diff --git a/AUTHORS b/AUTHORS
index c38eef76..f2751511 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -953,6 +953,7 @@
 Nagarjuna Atluri <nagarjuna.a@samsung.com>
 Naiem Shaik <naiem.shaik@gmail.com>
 Naman Kumar Narula <namankumarnarula@gmail.com>
+Naman Yadav <naman.yadav@samsung.com>
 Naoki Takano <takano.naoki@gmail.com>
 Naoto Ono <onoto1998@gmail.com>
 Nathan Mitchell <nathaniel.v.mitchell@gmail.com>
diff --git a/BUILD.gn b/BUILD.gn
index d51b075..64819b31 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -365,6 +365,7 @@
         "//chrome/android/webapk/shell_apk/prepare_upload_dir:prepare_webapk_shell_upload_dir",
         "//chrome/test:android_browsertests",
         "//components:components_junit_tests",
+        "//components/ip_protection_auth/javatests:ip_protection_auth_test_apk",
         "//content/public/android:content_junit_tests",
         "//content/shell/android:content_shell_apk",
         "//device:device_junit_tests",
diff --git a/DEPS b/DEPS
index 2c5a880..c0b0adc0 100644
--- a/DEPS
+++ b/DEPS
@@ -290,7 +290,7 @@
   'sysroots_json_path': 'build/linux/sysroot_scripts/sysroots.json',
 
   # siso CIPD package version.
-  'siso_version': 'git_revision:7c83de2b4db68a7b96e62a272a3b1eb40681fb81',
+  'siso_version': 'git_revision:23257e255f1d58d9f66923a1a40e746ac3a4b9d8',
 
   # download libaom test data
   'download_libaom_testdata': False,
@@ -310,19 +310,19 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'src_internal_revision': 'e22c0ea945eab1778664a95f0f7551c7cb075e80',
+  'src_internal_revision': '2eb505f90ae72ea13ed5cd2f8c2e3fbe72243748',
   # 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': '84b5116f98fbea7e625f0efc5ffa7943b1399746',
+  'skia_revision': 'a80c164ffb8a26aa29020be39993581a6761ab6c',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': 'aa8397922f9d7b4dadfab77f91d618cb0b7218d7',
+  'v8_revision': 'ec0c34c6196047f1e00f8a532222c29f1dd165e4',
   # 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': '66d3db3b9fdd1181c6879ca3dfef697ec53e6b43',
+  'angle_revision': '1cab871c220744402b887e73ff86aaf8f7f97fa0',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling SwiftShader
   # and whatever else without interference from each other.
@@ -385,11 +385,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling catapult
   # and whatever else without interference from each other.
-  'catapult_revision': '72a5f0b7ef081faf627913fb45d00c72b1cafc7d',
+  'catapult_revision': 'a217ecb5addbc06b125559679476960beb5e06d0',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling chromium_variations
   # and whatever else without interference from each other.
-  'chromium_variations_revision': '97af9cd3bd80748042952adc1aba9cb3ece2349e',
+  'chromium_variations_revision': 'c2e86526267a3e2f8c3a0c716a11d2b0866f7430',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling CrossBench
   # and whatever else without interference from each other.
@@ -445,11 +445,11 @@
   # 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': 'b7bd8d85b52e92ef2bf9aa901e7e6219dc9df3b0',
+  'dawn_revision': '6d8662dfd5e31ffd8bfe9cd9488a6a1864b429aa',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
-  'quiche_revision': '75266116f21731354fe44cea6a1aae7eae70441e',
+  'quiche_revision': '5cdf937c378cdf08ff55ea9e86cfbf05bec54df2',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling ios_webkit
   # and whatever else without interference from each other.
@@ -489,7 +489,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.
-  'cros_components_revision': 'e012464ddb8baeb541159efc7d6967c0481c298a',
+  'cros_components_revision': '66ea0cc1c267d9a8a969e80b0d45fb6bf9cfb33b',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling feed
   # and whatever else without interference from each other.
@@ -521,7 +521,7 @@
 
   # If you change this, also update the libc++ revision in
   # //buildtools/deps_revisions.gni.
-  'libcxx_revision':       '055d494c5c65957c94c58ac17cbf8b1f22ac11a6',
+  'libcxx_revision':       '8a241ea043bc9d3f0712f3a908da49b89495cd00',
 
   # GN CIPD package version.
   'gn_version': 'git_revision:991530ce394efb58fcd848195469022fa17ae126',
@@ -845,7 +845,7 @@
 
   'src/clank': {
     'url': Var('chrome_git') + '/clank/internal/apps.git' + '@' +
-    '5e96d16a206e29711039f968c564422b80df7786',
+    'e775e4966d9fa7e59540a94a7d87e2905a54b787',
     'condition': 'checkout_android and checkout_src_internal',
   },
 
@@ -854,7 +854,7 @@
   },
 
   'src/ios/third_party/earl_grey2/src': {
-      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + '806ee052db0c7e045f1fd510ea235abed3d746ee',
+      'url': Var('chromium_git') + '/external/github.com/google/EarlGrey.git' + '@' + 'bbf8b94d59079db24bc49ca3aa03ed860f53a3d0',
       'condition': 'checkout_ios',
   },
 
@@ -1051,7 +1051,7 @@
     'packages': [
       {
           'package': 'chromium/third_party/androidx',
-          'version': 'syeW3T24_byUAsix-0-Jt-Khv0alNAWG6iZvNgEjoxQC',
+          'version': 'i-hZT57mneCH8VFpEhCsyc9oIZo2nzGsBUvctDvsXyMC',
       },
     ],
     'condition': 'checkout_android',
@@ -1122,7 +1122,7 @@
       'packages': [
           {
                'package': 'chromium/third_party/android_build_tools/manifest_merger',
-               'version': 'EPmMtC5CNXQqxByKOxqF9Vk8LURwarA6qy5siWX1kRoC',
+               'version': '9gAaukznhLAAtANeZ_AT_9z8xXF5ZUzuc0h0TfMr7IIC',
           },
       ],
       'condition': 'checkout_android',
@@ -1295,13 +1295,13 @@
   },
 
   'src/third_party/depot_tools':
-    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '68318472db247d863355b56435d760fb1c102b35',
+    Var('chromium_git') + '/chromium/tools/depot_tools.git' + '@' + '4c1d6d90bc4326377ce670b74735029db9acde6a',
 
   'src/third_party/devtools-frontend/src':
     Var('chromium_git') + '/devtools/devtools-frontend' + '@' + Var('devtools_frontend_revision'),
 
   'src/third_party/devtools-frontend-internal': {
-      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + 'e9922f2f73a60016909a907d08a94bc77b64ab7d',
+      'url': Var('chrome_git') + '/devtools/devtools-internal.git' + '@' + '0f634419f74c7b73d8fef44d1a234dcd8072403c',
     'condition': 'checkout_src_internal',
   },
 
@@ -1790,7 +1790,7 @@
     Var('pdfium_git') + '/pdfium.git' + '@' +  Var('pdfium_revision'),
 
   'src/third_party/perfetto':
-    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'a38d6a9fc06768cd6a0923a7c8c6e3d62d8d582a',
+    Var('android_git') + '/platform/external/perfetto.git' + '@' + 'e879b2e2ee67ba3ce9bbb01bb7d776c8dff28897',
 
   'src/third_party/perl': {
       'url': Var('chromium_git') + '/chromium/deps/perl.git' + '@' + '6f3e5028eb65d0b4c5fdd792106ac4c84eee1eb3',
@@ -1935,7 +1935,7 @@
       'dep_type': 'cipd',
   },
 
-  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@34349287c87688eb8a0d704829032bbf6cfc7c0a',
+  'src/third_party/vulkan-deps': '{chromium_git}/vulkan-deps@4d31920a095aeda9fa83819cd17fdebc4d89220c',
 
   'src/third_party/vulkan_memory_allocator':
     Var('chromium_git') + '/external/github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git' + '@' + 'e87036508bb156f9986ea959323de1869e328f58',
@@ -1972,10 +1972,10 @@
     Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'f4bf599a8b575df685c31d9c4729a70a04e377ed',
 
   'src/third_party/webgpu-cts/src':
-    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'fc217e34b1792ab7600a42f0cf0aa5fa461efef3',
+    Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'f2b59e03621238d0d0fd6305be2c406ce3e45ac2',
 
   'src/third_party/webrtc':
-    Var('webrtc_git') + '/src.git' + '@' + '7d81e18ac0d3c91ce77e0eeee4f1baa783ed2306',
+    Var('webrtc_git') + '/src.git' + '@' + 'a61b334a12c7556c64887e1c553fd822d060881d',
 
   # 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.
@@ -2142,7 +2142,7 @@
     'packages': [
       {
         'package': 'chromeos_internal/apps/projector_app/app',
-        'version': 'lDiBnghDolpPhIu7MxcUoTIFQCi-Ev1A8TmT8CWeUfEC',
+        'version': '8C64N3Poa4cc9vHh0jYItH-oBDDrwzK10PlKI5z08c8C',
       },
     ],
     'condition': 'checkout_chromeos and checkout_src_internal',
@@ -4269,7 +4269,7 @@
 
   'src/ios_internal':  {
       'url': Var('chrome_git') + '/chrome/ios_internal.git' + '@' +
-        '5f154f8f111ecd416250348e8ef1c1a68b2582b1',
+        'e875678118f79bdac3c7bd75be7fb79360d77757',
       'condition': 'checkout_ios and checkout_src_internal',
   },
 
diff --git a/PRESUBMIT.py b/PRESUBMIT.py
index e78dbd4..e612700d 100644
--- a/PRESUBMIT.py
+++ b/PRESUBMIT.py
@@ -917,6 +917,19 @@
       [_THIRD_PARTY_EXCEPT_BLINK],  # Not an error in third_party folders.
     ),
     BanRule(
+      r'/\babsl::FixedArray\b',
+      (
+        'absl::FixedArray is banned. Use base::FixedArray instead.',
+      ),
+      True,
+      [
+        # base::FixedArray provides canonical access.
+        r'^base/types/fixed_array.h',
+        # Not an error in third_party folders.
+        _THIRD_PARTY_EXCEPT_BLINK,
+      ],
+    ),
+    BanRule(
       r'/\babsl::FunctionRef\b',
       (
         'absl::FunctionRef is banned. Use base::FunctionRef instead.',
diff --git a/android_webview/BUILD.gn b/android_webview/BUILD.gn
index 4b5b864..d3da167 100644
--- a/android_webview/BUILD.gn
+++ b/android_webview/BUILD.gn
@@ -475,6 +475,7 @@
     "java/src/org/chromium/android_webview/AwWebResourceInterceptResponse.java",
     "java/src/org/chromium/android_webview/JsReplyProxy.java",
     "java/src/org/chromium/android_webview/PopupTouchHandleDrawable.java",
+    "java/src/org/chromium/android_webview/StartupJavascriptInfo.java",
     "java/src/org/chromium/android_webview/SystemStateUtil.java",
     "java/src/org/chromium/android_webview/WebMessageListenerHolder.java",
     "java/src/org/chromium/android_webview/WebMessageListenerInfo.java",
@@ -569,6 +570,7 @@
     "java/src/org/chromium/android_webview/ScriptHandler.java",
     "java/src/org/chromium/android_webview/ScrollAccessibilityHelper.java",
     "java/src/org/chromium/android_webview/SslUtil.java",
+    "java/src/org/chromium/android_webview/StartupJavascriptInfo.java",
     "java/src/org/chromium/android_webview/SystemStateUtil.java",
     "java/src/org/chromium/android_webview/ViewPositionObserver.java",
     "java/src/org/chromium/android_webview/WebAddressParser.java",
diff --git a/android_webview/browser/aw_contents.cc b/android_webview/browser/aw_contents.cc
index 728c994..6440c221 100644
--- a/android_webview/browser/aw_contents.cc
+++ b/android_webview/browser/aw_contents.cc
@@ -35,6 +35,7 @@
 #include "android_webview/browser/permission/simple_permission_request.h"
 #include "android_webview/browser/state_serializer.h"
 #include "android_webview/browser_jni_headers/AwContents_jni.h"
+#include "android_webview/browser_jni_headers/StartupJavascriptInfo_jni.h"
 #include "android_webview/common/aw_switches.h"
 #include "android_webview/common/devtools_instrumentation.h"
 #include "android_webview/common/mojom/frame.mojom.h"
@@ -69,6 +70,7 @@
 #include "components/autofill/core/browser/browser_autofill_manager.h"
 #include "components/autofill/core/browser/webdata/autofill_webdata_service.h"
 #include "components/autofill/core/common/autofill_features.h"
+#include "components/js_injection/browser/js_communication_host.h"
 #include "components/navigation_interception/intercept_navigation_delegate.h"
 #include "components/safe_browsing/core/common/features.h"
 #include "components/security_interstitials/content/security_interstitial_tab_helper.h"
@@ -1371,7 +1373,8 @@
       ConvertJavaStringToUTF16(env, js_object_name));
 }
 
-base::android::ScopedJavaLocalRef<jobjectArray> AwContents::GetJsObjectsInfo(
+base::android::ScopedJavaLocalRef<jobjectArray>
+AwContents::GetWebMessageListenerInfos(
     JNIEnv* env,
     const base::android::JavaParamRef<jclass>& clazz) {
   if (js_communication_host_.get()) {
@@ -1381,6 +1384,31 @@
   return nullptr;
 }
 
+base::android::ScopedJavaLocalRef<jobjectArray>
+AwContents::GetDocumentStartupJavascripts(
+    JNIEnv* env,
+    const base::android::JavaParamRef<jclass>& clazz) {
+  if (!js_communication_host_.get()) {
+    return nullptr;
+  }
+
+  const std::vector<js_injection::DocumentStartJavaScript>& scripts =
+      GetJsCommunicationHost()->GetDocumentStartJavascripts();
+
+  std::vector<ScopedJavaLocalRef<jobject>> script_objects;
+  for (const auto& script : scripts) {
+    const std::vector<std::string> rules =
+        script.allowed_origin_rules_.Serialize();
+    script_objects.push_back(Java_StartupJavascriptInfo_create(
+        env, base::android::ConvertUTF16ToJavaString(env, script.script_),
+        base::android::ToJavaArrayOfStrings(env, rules)));
+  }
+
+  ScopedJavaLocalRef<jclass> clazz_ref(clazz);
+  return base::android::ToTypedJavaArrayOfObjects(env, script_objects,
+                                                  clazz_ref);
+}
+
 void AwContents::ClearView(JNIEnv* env) {
   DCHECK_CURRENTLY_ON(BrowserThread::UI);
   browser_view_renderer_.ClearView();
diff --git a/android_webview/browser/aw_contents.h b/android_webview/browser/aw_contents.h
index 3f7a3434..968586b 100644
--- a/android_webview/browser/aw_contents.h
+++ b/android_webview/browser/aw_contents.h
@@ -188,7 +188,11 @@
       JNIEnv* env,
       const base::android::JavaParamRef<jstring>& js_object_name);
 
-  base::android::ScopedJavaLocalRef<jobjectArray> GetJsObjectsInfo(
+  base::android::ScopedJavaLocalRef<jobjectArray> GetWebMessageListenerInfos(
+      JNIEnv* env,
+      const base::android::JavaParamRef<jclass>& clazz);
+
+  base::android::ScopedJavaLocalRef<jobjectArray> GetDocumentStartupJavascripts(
       JNIEnv* env,
       const base::android::JavaParamRef<jclass>& clazz);
 
diff --git a/android_webview/browser/aw_field_trials.cc b/android_webview/browser/aw_field_trials.cc
index f3470fb..1cc9255 100644
--- a/android_webview/browser/aw_field_trials.cc
+++ b/android_webview/browser/aw_field_trials.cc
@@ -75,5 +75,9 @@
   aw_feature_overrides.DisableFeature(
       ::features::kDefaultPassthroughCommandDecoder);
 
+  // Disable Reducing User Agent minor version on WebView.
+  aw_feature_overrides.DisableFeature(
+      blink::features::kReduceUserAgentMinorVersion);
+
   aw_feature_overrides.RegisterOverrides(feature_list);
 }
diff --git a/android_webview/browser/aw_field_trials_unittest.cc b/android_webview/browser/aw_field_trials_unittest.cc
index afd60302..dcd5a35e 100644
--- a/android_webview/browser/aw_field_trials_unittest.cc
+++ b/android_webview/browser/aw_field_trials_unittest.cc
@@ -107,6 +107,9 @@
 
   EXPECT_TRUE(
       base::FeatureList::IsEnabled(blink::features::kUserAgentClientHint));
+
+  EXPECT_FALSE(base::FeatureList::IsEnabled(
+      blink::features::kReduceUserAgentMinorVersion));
 }
 
 }  // namespace android_webview
diff --git a/android_webview/java/src/org/chromium/android_webview/AwContents.java b/android_webview/java/src/org/chromium/android_webview/AwContents.java
index 43596f43..3efaa99 100644
--- a/android_webview/java/src/org/chromium/android_webview/AwContents.java
+++ b/android_webview/java/src/org/chromium/android_webview/AwContents.java
@@ -1537,6 +1537,7 @@
         public final boolean wasWindowFocused;
         public final @NonNull Map<String, Pair<Object, Class>> javascriptInterfaces;
         public final @Nullable WebMessageListenerInfo[] webMessageListenerInfo;
+        public final @Nullable StartupJavascriptInfo[] startupJavascriptInfo;
 
         public StateSnapshot(@NonNull AwContents awContents) {
             wasAttached = awContents.mIsAttachedToWindow;
@@ -1553,8 +1554,10 @@
             }
 
             // Save injected WebMessageListeners.
-            webMessageListenerInfo = AwContentsJni.get().getJsObjectsInfo(
+            webMessageListenerInfo = AwContentsJni.get().getWebMessageListenerInfos(
                     awContents.mNativeAwContents, WebMessageListenerInfo.class);
+            startupJavascriptInfo = AwContentsJni.get().getDocumentStartupJavascripts(
+                    awContents.mNativeAwContents, StartupJavascriptInfo.class);
         }
     }
 
@@ -1833,6 +1836,13 @@
                         info.mObjectName, info.mAllowedOriginRules, info.mHolder.getListener());
             }
         }
+        StartupJavascriptInfo[] previousDocumentStartupJavascripts =
+                previousState.startupJavascriptInfo;
+        if (previousDocumentStartupJavascripts != null) {
+            for (StartupJavascriptInfo info : previousDocumentStartupJavascripts) {
+                addDocumentStartJavaScript(info.mScript, info.mAllowedOriginRules);
+            }
+        }
     }
 
     // Recap: supplyContentsForPopup() is called on the parent window's content, this method is
@@ -4866,7 +4876,10 @@
         String addWebMessageListener(long nativeAwContents, WebMessageListenerHolder listener,
                 String jsObjectName, String[] allowedOrigins);
         void removeWebMessageListener(long nativeAwContents, String jsObjectName);
-        WebMessageListenerInfo[] getJsObjectsInfo(long nativeAwContents, Class clazz);
+        WebMessageListenerInfo[] getWebMessageListenerInfos(long nativeAwContents, Class clazz);
+
+        StartupJavascriptInfo[] getDocumentStartupJavascripts(long nativeAwContents, Class clazz);
+
         void onConfigurationChanged(long nativeAwContents);
     }
 }
diff --git a/android_webview/java/src/org/chromium/android_webview/StartupJavascriptInfo.java b/android_webview/java/src/org/chromium/android_webview/StartupJavascriptInfo.java
new file mode 100644
index 0000000..caf50558
--- /dev/null
+++ b/android_webview/java/src/org/chromium/android_webview/StartupJavascriptInfo.java
@@ -0,0 +1,29 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.android_webview;
+
+import org.chromium.android_webview.common.Lifetime;
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.JNINamespace;
+
+/**
+ * For native to pass the information of a DocumentStartJavascript to Java.
+ */
+@Lifetime.Temporary
+@JNINamespace("android_webview")
+public class StartupJavascriptInfo {
+    public String mScript;
+    public String[] mAllowedOriginRules;
+
+    private StartupJavascriptInfo(String script, String[] allowedOriginRules) {
+        mScript = script;
+        mAllowedOriginRules = allowedOriginRules;
+    }
+
+    @CalledByNative
+    public static StartupJavascriptInfo create(String objectName, String[] allowedOriginRules) {
+        return new StartupJavascriptInfo(objectName, allowedOriginRules);
+    }
+}
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/ClientHintsTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/ClientHintsTest.java
index 0e19a79..b456e49 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/ClientHintsTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/ClientHintsTest.java
@@ -39,6 +39,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Test suite for user-agent client hints.
@@ -58,6 +60,8 @@
 
     private static final String ANDROID_WEBVIEW_BRAND_NAME = "Android WebView";
 
+    private static final String CHROME_PRODUCT_PATTERN = "Chrome/(\\d+).(\\d+).(\\d+).(\\d+)";
+
     private static class ClientHintsTestResult {
         public Map<String, String> mHttpHeaderClientHints;
         public JSONObject mJsClientHints;
@@ -805,6 +809,33 @@
         Assert.assertEquals("", jsClientHints.getString("platformVersion"));
     }
 
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView"})
+    public void testDefaultUserAgentDefaultReductionOverride() throws Throwable {
+        String defaultUserAgent = getDefaultUserAgent();
+        // Verify user-agent minor version not reduced.
+        Matcher uaMatcher = Pattern.compile(CHROME_PRODUCT_PATTERN).matcher(defaultUserAgent);
+        Assert.assertTrue(uaMatcher.find());
+        Assert.assertNotEquals("0.0.0",
+                String.format(
+                        "%s.%s.%s", uaMatcher.group(2), uaMatcher.group(3), uaMatcher.group(4)));
+    }
+
+    @Test
+    @SmallTest
+    @Feature({"AndroidWebView"})
+    @CommandLineFlags.Add({"enable-features=ReduceUserAgentMinorVersion"})
+    public void testDefaultUserAgentEnableReductionOverride() throws Throwable {
+        String defaultUserAgent = getDefaultUserAgent();
+        // Verify user-agent minor version is reduced.
+        Matcher uaMatcher = Pattern.compile(CHROME_PRODUCT_PATTERN).matcher(defaultUserAgent);
+        Assert.assertTrue(uaMatcher.find());
+        Assert.assertEquals("0.0.0",
+                String.format(
+                        "%s.%s.%s", uaMatcher.group(2), uaMatcher.group(3), uaMatcher.group(4)));
+    }
+
     private void verifyOverrideUaAndOverrideUaMetadata(String overrideUserAgent) throws Throwable {
         ClientHintsTestResult clientHintsResult = getClientHintsWithOverrides(
                 makeFakeMetadata(), /*overrideUserAgent=*/overrideUserAgent);
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/MultiProfileTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/MultiProfileTest.java
index 3dc0570..5f070869 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/MultiProfileTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/MultiProfileTest.java
@@ -24,7 +24,9 @@
 import org.chromium.android_webview.AwBrowserProcess;
 import org.chromium.android_webview.AwContents;
 import org.chromium.android_webview.AwCookieManager;
+import org.chromium.android_webview.WebMessageListener;
 import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.CallbackHelper;
 import org.chromium.base.test.util.DoNotBatch;
 import org.chromium.base.test.util.Feature;
 import org.chromium.content_public.browser.test.util.RenderProcessHostUtils;
@@ -45,16 +47,16 @@
     @Rule
     public AwActivityTestRule mActivityTestRule = new AwActivityTestRule();
 
-    private TestAwContentsClient mContentsClient = new TestAwContentsClient();
+    private final TestAwContentsClient mContentsClient = new TestAwContentsClient();
 
     private AwBrowserContext getContextSync(String name, boolean createIfNeeded) throws Throwable {
         return ThreadUtils.runOnUiThreadBlockingNoException(
                 () -> { return AwBrowserContext.getNamedContext(name, createIfNeeded); });
     }
 
-    private Object setBrowserContextSync(AwContents awContents, AwBrowserContext browserContext)
+    private void setBrowserContextSync(AwContents awContents, AwBrowserContext browserContext)
             throws Throwable {
-        return ThreadUtils.runOnUiThreadBlockingNoException(() -> {
+        ThreadUtils.runOnUiThreadBlockingNoException(() -> {
             awContents.setBrowserContext(browserContext);
             return null;
         });
@@ -454,4 +456,53 @@
         assertEquals("Found cookies list differs from expected list", expectedCookieNamesSet,
                 foundCookieNamesSet);
     }
+
+    @Test
+    @LargeTest
+    @OnlyRunIn(MULTI_PROCESS)
+    @Feature({"AndroidWebView"})
+    public void testInjectedJavascriptIsTransferredWhenProfileChanges() throws Throwable {
+        mActivityTestRule.startBrowserProcess();
+
+        String listenerName = "injectedListener";
+        String startupScript = listenerName + ".postMessage('success');";
+        String[] injectDomains = {"*"};
+
+        final AwContents webView =
+                mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient)
+                        .getAwContents();
+        AwActivityTestRule.enableJavaScriptOnUiThread(webView);
+
+        CallbackHelper testDoneHelper = new CallbackHelper();
+
+        final WebMessageListener injectedListener =
+                (payload, sourceOrigin, isMainFrame, jsReplyProxy, ports) -> {
+            Assert.assertEquals("success", payload.getAsString());
+            testDoneHelper.notifyCalled();
+        };
+
+        // Setup a message listener and a startup script to post on to the listener.
+        mActivityTestRule.runOnUiThread(() -> {
+            webView.addWebMessageListener(listenerName, injectDomains, injectedListener);
+            webView.addDocumentStartJavaScript(startupScript, injectDomains);
+        });
+
+        // Switch the profile after the JS objects have been injected, but before content is loaded.
+        AwBrowserContext otherProfile = getContextSync("other-profile", true);
+        setBrowserContextSync(webView, otherProfile);
+
+        // Load content using the new Context.
+        try (TestWebServer server = TestWebServer.start()) {
+            server.setResponse("/", "hello, world", new ArrayList<>());
+            mActivityTestRule.loadUrlSync(
+                    webView, mContentsClient.getOnPageFinishedHelper(), server.getBaseUrl());
+            Assert.assertEquals("Injected listener was missing", "true",
+                    mActivityTestRule.executeJavaScriptAndWaitForResult(
+                            webView, mContentsClient, listenerName + " != null"));
+        }
+
+        // Wait for the test to run (see injectedListener above).
+        testDoneHelper.waitForFirst(
+                "Did not receive post message triggered by injected javascript");
+    }
 }
diff --git a/android_webview/lib/aw_main_delegate.cc b/android_webview/lib/aw_main_delegate.cc
index 9624f99..2bad87c 100644
--- a/android_webview/lib/aw_main_delegate.cc
+++ b/android_webview/lib/aw_main_delegate.cc
@@ -264,9 +264,6 @@
 
     features.DisableIfNotSet(::features::kPeriodicBackgroundSync);
 
-    // Disable Reducing User Agent minor version on WebView.
-    features.DisableIfNotSet(blink::features::kReduceUserAgentMinorVersion);
-
     // Disabled until viz scheduling can be improved.
     features.DisableIfNotSet(::features::kUseSurfaceLayerForVideoDefault);
 
diff --git a/android_webview/support_library/boundary_interfaces/src/org/chromium/support_lib_boundary/util/Features.java b/android_webview/support_library/boundary_interfaces/src/org/chromium/support_lib_boundary/util/Features.java
index 2a2d5233..20200e1 100644
--- a/android_webview/support_library/boundary_interfaces/src/org/chromium/support_lib_boundary/util/Features.java
+++ b/android_webview/support_library/boundary_interfaces/src/org/chromium/support_lib_boundary/util/Features.java
@@ -243,10 +243,12 @@
     // DropDataContentProvider.call
     public static final String IMAGE_DRAG_DROP = "IMAGE_DRAG_DROP";
 
-    // ProfileStore.getOrCreateProfileAsync
-    // ProfileStore.getProfileAsync
-    // ProfileStore.getAllProfileNamesAsync
-    // ProfileStore.deleteProfileAsync
+    // ProfileStore.getInstance
+    // ProfileStore.getOrCreateProfile
+    // ProfileStore.getProfile
+    // ProfileStore.getAllProfileNames
+    // ProfileStore.deleteProfile
+    // Profile.getName
     // Profile.getCookieManager
     // Profile.getWebStorage
     // Profile.getGeolocationPermissions
diff --git a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibProfile.java b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibProfile.java
index 97a63070..c52d128 100644
--- a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibProfile.java
+++ b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibProfile.java
@@ -54,7 +54,7 @@
     @NonNull
     @Override
     public GeolocationPermissions getGeoLocationPermissions() {
-        recordApiCall(ApiCall.GET_PROFILE_GET_LOCATION_PERMISSIONS);
+        recordApiCall(ApiCall.GET_PROFILE_GEO_LOCATION_PERMISSIONS);
         return mProfileImpl.getGeolocationPermissions();
     }
 
diff --git a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebViewChromiumFactory.java b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebViewChromiumFactory.java
index 041f9fe..8805e1b6 100644
--- a/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebViewChromiumFactory.java
+++ b/android_webview/support_library/java/src/org/chromium/support_lib_glue/SupportLibWebViewChromiumFactory.java
@@ -92,7 +92,7 @@
                     Features.REQUESTED_WITH_HEADER_ALLOW_LIST,
                     Features.IMAGE_DRAG_DROP,
                     Features.USER_AGENT_METADATA,
-                    Features.MULTI_PROFILE + Features.DEV_SUFFIX,
+                    Features.MULTI_PROFILE,
                     Features.ATTRIBUTION_BEHAVIOR + Features.DEV_SUFFIX,
                     // Add new features above. New features must include `+ Features.DEV_SUFFIX`
                     // when they're initially added (this can be removed in a future CL). The final
@@ -191,7 +191,7 @@
             ApiCall.GET_PROFILE_NAME,
             ApiCall.GET_PROFILE_COOKIE_MANAGER,
             ApiCall.GET_PROFILE_WEB_STORAGE,
-            ApiCall.GET_PROFILE_GET_LOCATION_PERMISSIONS,
+            ApiCall.GET_PROFILE_GEO_LOCATION_PERMISSIONS,
             ApiCall.GET_PROFILE_SERVICE_WORKER_CONTROLLER,
             ApiCall.SET_WEBVIEW_PROFILE,
             ApiCall.GET_WEBVIEW_PROFILE,
@@ -298,7 +298,7 @@
         int GET_PROFILE_NAME = 88;
         int GET_PROFILE_COOKIE_MANAGER = 89;
         int GET_PROFILE_WEB_STORAGE = 90;
-        int GET_PROFILE_GET_LOCATION_PERMISSIONS = 91;
+        int GET_PROFILE_GEO_LOCATION_PERMISSIONS = 91;
         int GET_PROFILE_SERVICE_WORKER_CONTROLLER = 92;
 
         int SET_WEBVIEW_PROFILE = 93;
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 2bce068..76ba339 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -201,6 +201,8 @@
     "ambient/managed/screensaver_images_policy_handler.h",
     "ambient/metrics/ambient_animation_metrics_recorder.cc",
     "ambient/metrics/ambient_animation_metrics_recorder.h",
+    "ambient/metrics/ambient_consumer_session_metrics_delegate.cc",
+    "ambient/metrics/ambient_consumer_session_metrics_delegate.h",
     "ambient/metrics/ambient_metrics.cc",
     "ambient/metrics/ambient_metrics.h",
     "ambient/metrics/ambient_session_metrics_recorder.cc",
diff --git a/ash/accelerators/accelerator_alias_converter.cc b/ash/accelerators/accelerator_alias_converter.cc
index da13411e..a973f09 100644
--- a/ash/accelerators/accelerator_alias_converter.cc
+++ b/ash/accelerators/accelerator_alias_converter.cc
@@ -65,6 +65,8 @@
       case DeviceType::kDeviceExternalChromeOsKeyboard:
       case DeviceType::kDeviceExternalAppleKeyboard:
       case DeviceType::kDeviceExternalGenericKeyboard:
+      case ui::KeyboardCapability::DeviceType::
+          kDeviceExternalNullTopRowChromeOsKeyboard:
       case DeviceType::kDeviceExternalUnknown:
       case DeviceType::kDeviceHotrodRemote:
       case DeviceType::kDeviceVirtualCoreKeyboard:
@@ -98,6 +100,8 @@
       case DeviceType::kDeviceExternalChromeOsKeyboard:
       case DeviceType::kDeviceExternalAppleKeyboard:
       case DeviceType::kDeviceExternalGenericKeyboard:
+      case ui::KeyboardCapability::DeviceType::
+          kDeviceExternalNullTopRowChromeOsKeyboard:
       case DeviceType::kDeviceExternalUnknown:
       case DeviceType::kDeviceHotrodRemote:
       case DeviceType::kDeviceVirtualCoreKeyboard:
diff --git a/ash/ambient/ambient_controller.cc b/ash/ambient/ambient_controller.cc
index 40e39a9..2d4bc70 100644
--- a/ash/ambient/ambient_controller.cc
+++ b/ash/ambient/ambient_controller.cc
@@ -1258,8 +1258,8 @@
   // Add observer for assistant interaction model
   AssistantInteractionController::Get()->GetModel()->AddObserver(this);
 
-  session_metrics_recorder_ =
-      std::make_unique<AmbientSessionMetricsRecorder>(GetCurrentUiSettings());
+  session_metrics_recorder_ = std::make_unique<AmbientSessionMetricsRecorder>(
+      ambient_ui_launcher_->CreateMetricsDelegate(GetCurrentUiSettings()));
 
   SetUpPreTargetHandler();
 
diff --git a/ash/ambient/ambient_managed_slideshow_ui_launcher.cc b/ash/ambient/ambient_managed_slideshow_ui_launcher.cc
index 2a0c33150a..173e08fe 100644
--- a/ash/ambient/ambient_managed_slideshow_ui_launcher.cc
+++ b/ash/ambient/ambient_managed_slideshow_ui_launcher.cc
@@ -44,7 +44,6 @@
 void AmbientManagedSlideshowUiLauncher::OnImagesReady() {
   CHECK(initialization_callback_);
   std::move(initialization_callback_).Run(/*success=*/true);
-  metrics_recorder_.RecordSessionStartupTime();
 }
 
 void AmbientManagedSlideshowUiLauncher::OnErrorStateChanged() {
@@ -57,7 +56,6 @@
 
 void AmbientManagedSlideshowUiLauncher::Initialize(
     InitializationCallback on_done) {
-  metrics_recorder_.RecordSessionStart();
   initialization_callback_ = std::move(on_done);
   // TODO(b/281056480): Remove this line and add the login screen visible method
   // to session observer. This is required because if we compute the ready state
@@ -84,7 +82,6 @@
 
 void AmbientManagedSlideshowUiLauncher::Finalize() {
   photo_controller_.StopScreenUpdate();
-  metrics_recorder_.RecordSessionEnd();
 }
 
 AmbientBackendModel*
@@ -106,4 +103,9 @@
          !photo_controller_.HasScreenUpdateErrors();
 }
 
+std::unique_ptr<AmbientSessionMetricsRecorder::Delegate>
+AmbientManagedSlideshowUiLauncher::CreateMetricsDelegate(AmbientUiSettings) {
+  return std::make_unique<ManagedScreensaverMetricsDelegate>();
+}
+
 }  // namespace ash
diff --git a/ash/ambient/ambient_managed_slideshow_ui_launcher.h b/ash/ambient/ambient_managed_slideshow_ui_launcher.h
index 0e3561a..ec6f125 100644
--- a/ash/ambient/ambient_managed_slideshow_ui_launcher.h
+++ b/ash/ambient/ambient_managed_slideshow_ui_launcher.h
@@ -12,7 +12,6 @@
 #include "ash/ambient/ambient_ui_launcher.h"
 #include "ash/ambient/ambient_view_delegate_impl.h"
 #include "ash/ambient/managed/screensaver_images_policy_handler.h"
-#include "ash/ambient/metrics/managed_screensaver_metrics.h"
 #include "ash/ambient/model/ambient_backend_model_observer.h"
 #include "ash/public/cpp/session/session_observer.h"
 #include "base/files/file_path.h"
@@ -53,6 +52,8 @@
   AmbientBackendModel* GetAmbientBackendModel() override;
   AmbientPhotoController* GetAmbientPhotoController() override;
   bool IsActive() override;
+  std::unique_ptr<AmbientSessionMetricsRecorder::Delegate>
+  CreateMetricsDelegate(AmbientUiSettings current_ui_settings) override;
 
  private:
   friend class AmbientAshTestBase;
@@ -63,7 +64,6 @@
 
   bool ComputeReadyState();
 
-  ManagedScreensaverMetricsRecorder metrics_recorder_;
   AmbientManagedPhotoController photo_controller_;
   const raw_ptr<AmbientViewDelegateImpl> delegate_;
   InitializationCallback initialization_callback_;
diff --git a/ash/ambient/ambient_ui_launcher.cc b/ash/ambient/ambient_ui_launcher.cc
index 51918f8..01eafbc2 100644
--- a/ash/ambient/ambient_ui_launcher.cc
+++ b/ash/ambient/ambient_ui_launcher.cc
@@ -3,6 +3,8 @@
 // found in the LICENSE file.
 
 #include "ash/ambient/ambient_ui_launcher.h"
+#include "ash/ambient/ambient_ui_settings.h"
+#include "ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h"
 
 namespace ash {
 
@@ -26,4 +28,11 @@
   observer_ = observer;
 }
 
+std::unique_ptr<AmbientSessionMetricsRecorder::Delegate>
+AmbientUiLauncher::CreateMetricsDelegate(
+    AmbientUiSettings current_ui_settings) {
+  return std::make_unique<AmbientConsumerSessionMetricsDelegate>(
+      std::move(current_ui_settings));
+}
+
 }  // namespace ash
diff --git a/ash/ambient/ambient_ui_launcher.h b/ash/ambient/ambient_ui_launcher.h
index c852c7c..165783c 100644
--- a/ash/ambient/ambient_ui_launcher.h
+++ b/ash/ambient/ambient_ui_launcher.h
@@ -8,12 +8,15 @@
 #include <memory>
 
 #include "ash/ambient/ambient_photo_controller.h"
+#include "ash/ambient/metrics/ambient_session_metrics_recorder.h"
 #include "ash/ambient/model/ambient_backend_model.h"
 #include "base/functional/callback_forward.h"
 #include "ui/views/view.h"
 
 namespace ash {
 
+class AmbientUiSettings;
+
 // AmbientUiLauncher is used to start ambient UIs. Every implementation of
 // this abstract class is tied a particular UI (slideshow, animation etc) but it
 // is able to launch multiple ambient UI sessions.
@@ -66,6 +69,13 @@
 
   void SetObserver(Observer* observer);
 
+  // Always returns a non-null value. Defaults to
+  // `AmbientConsumerSessionMetricsDelegate`, but UI launchers that want to
+  // customize the standard set of metrics that recorded for all ambient UIs
+  // may override with their own implementation here.
+  virtual std::unique_ptr<AmbientSessionMetricsRecorder::Delegate>
+  CreateMetricsDelegate(AmbientUiSettings current_ui_settings);
+
  protected:
   // Sets the ready state and notifies the observer whenvever the reader state
   // changes.
diff --git a/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.cc b/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.cc
new file mode 100644
index 0000000..2d0636c
--- /dev/null
+++ b/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.cc
@@ -0,0 +1,57 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h"
+
+#include <utility>
+
+#include "ash/ambient/metrics/ambient_metrics.h"
+#include "ash/login/ui/lock_screen.h"
+#include "ash/public/cpp/ambient/ambient_ui_model.h"
+#include "ash/shell.h"
+#include "base/check.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+
+namespace ash {
+
+AmbientConsumerSessionMetricsDelegate::AmbientConsumerSessionMetricsDelegate(
+    AmbientUiSettings ui_settings)
+    : ui_settings_(std::move(ui_settings)) {}
+
+AmbientConsumerSessionMetricsDelegate::
+    ~AmbientConsumerSessionMetricsDelegate() = default;
+
+void AmbientConsumerSessionMetricsDelegate::RecordActivation() {
+  ambient::RecordAmbientModeActivation(
+      /*ui_mode=*/LockScreen::HasInstance() ? AmbientUiMode::kLockScreenUi
+                                            : AmbientUiMode::kInSessionUi,
+      /*tablet_mode=*/Shell::Get()->IsInTabletMode());
+}
+
+void AmbientConsumerSessionMetricsDelegate::RecordInitStatus(bool success) {
+  base::UmaHistogramBoolean(
+      base::StrCat({"Ash.AmbientMode.Init.", ui_settings_.ToString()}),
+      success);
+}
+
+void AmbientConsumerSessionMetricsDelegate::RecordStartupTime(
+    base::TimeDelta startup_time) {
+  ambient::RecordAmbientModeStartupTime(startup_time, ui_settings_);
+}
+
+void AmbientConsumerSessionMetricsDelegate::RecordEngagementTime(
+    base::TimeDelta engagement_time) {
+  ambient::RecordAmbientModeTimeElapsed(
+      engagement_time, Shell::Get()->IsInTabletMode(), ui_settings_);
+}
+
+void AmbientConsumerSessionMetricsDelegate::RecordScreenCount(int num_screens) {
+  base::UmaHistogramCounts100(
+      base::StrCat({"Ash.AmbientMode.ScreenCount.", ui_settings_.ToString()}),
+      num_screens);
+}
+
+}  // namespace ash
diff --git a/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h b/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h
new file mode 100644
index 0000000..eaa4c0b
--- /dev/null
+++ b/ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h
@@ -0,0 +1,39 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_AMBIENT_METRICS_AMBIENT_CONSUMER_SESSION_METRICS_DELEGATE_H_
+#define ASH_AMBIENT_METRICS_AMBIENT_CONSUMER_SESSION_METRICS_DELEGATE_H_
+
+#include "ash/ambient/ambient_ui_settings.h"
+#include "ash/ambient/metrics/ambient_session_metrics_recorder.h"
+#include "ash/ash_export.h"
+#include "base/time/time.h"
+
+namespace ash {
+
+// For all non-enterprise ambient sessions (the primary use case).
+class ASH_EXPORT AmbientConsumerSessionMetricsDelegate
+    : public AmbientSessionMetricsRecorder::Delegate {
+ public:
+  explicit AmbientConsumerSessionMetricsDelegate(AmbientUiSettings ui_settings);
+  AmbientConsumerSessionMetricsDelegate(
+      const AmbientConsumerSessionMetricsDelegate&) = delete;
+  AmbientConsumerSessionMetricsDelegate& operator=(
+      const AmbientConsumerSessionMetricsDelegate&) = delete;
+  ~AmbientConsumerSessionMetricsDelegate() override;
+
+  // AmbientSessionMetricsRecorder::Delegate:
+  void RecordActivation() override;
+  void RecordInitStatus(bool success) override;
+  void RecordStartupTime(base::TimeDelta startup_time) override;
+  void RecordEngagementTime(base::TimeDelta engagement_time) override;
+  void RecordScreenCount(int num_screens) override;
+
+ private:
+  const AmbientUiSettings ui_settings_;
+};
+
+}  // namespace ash
+
+#endif  // ASH_AMBIENT_METRICS_AMBIENT_CONSUMER_SESSION_METRICS_DELEGATE_H_
diff --git a/ash/ambient/metrics/ambient_session_metrics_recorder.cc b/ash/ambient/metrics/ambient_session_metrics_recorder.cc
index c983ef0..eb74c04f 100644
--- a/ash/ambient/metrics/ambient_session_metrics_recorder.cc
+++ b/ash/ambient/metrics/ambient_session_metrics_recorder.cc
@@ -18,46 +18,41 @@
 namespace ash {
 
 AmbientSessionMetricsRecorder::AmbientSessionMetricsRecorder(
-    AmbientUiSettings ui_settings)
-    : ui_settings_(std::move(ui_settings)),
+    std::unique_ptr<Delegate> delegate)
+    : delegate_(std::move(delegate)),
       session_start_time_(base::TimeTicks::Now()) {
+  CHECK(delegate_);
   // Don't record this metric for `kPreview` mode.
   if (AmbientUiModel::Get()->ui_visibility() ==
       AmbientUiVisibility::kShouldShow) {
-    ambient::RecordAmbientModeActivation(
-        /*ui_mode=*/LockScreen::HasInstance() ? AmbientUiMode::kLockScreenUi
-                                              : AmbientUiMode::kInSessionUi,
-        /*tablet_mode=*/Shell::Get()->IsInTabletMode());
+    delegate_->RecordActivation();
   }
 }
 
 AmbientSessionMetricsRecorder::~AmbientSessionMetricsRecorder() {
   auto elapsed = base::TimeTicks::Now() - session_start_time_;
   DVLOG(2) << "Exit ambient mode. Elapsed time: " << elapsed;
-  ambient::RecordAmbientModeTimeElapsed(elapsed, Shell::Get()->IsInTabletMode(),
-                                        ui_settings_);
+  delegate_->RecordEngagementTime(elapsed);
 
   bool ambient_ui_was_rendering = num_registered_screens_ > 0;
   if (!ambient_ui_was_rendering && elapsed >= ambient::kMetricsStartupTimeMax) {
     LOG(ERROR) << "Ambient UI completely failed to start";
-    ambient::RecordAmbientModeStartupTime(elapsed, ui_settings_);
+    delegate_->RecordStartupTime(elapsed);
     // If `AmbientUiLauncher::Initialize()` never ran the completion callback
     // within `kMetricsStartupTimeMax`, that still counts as a failure. It
     // should be completed (either successfully or unsuccessfully by then).
     if (!session_init_status_.has_value()) {
-      RecordInitStatus(false);
+      delegate_->RecordInitStatus(false);
     }
   }
 
-  base::UmaHistogramCounts100(
-      base::StrCat({"Ash.AmbientMode.ScreenCount.", ui_settings_.ToString()}),
-      num_registered_screens_);
+  delegate_->RecordScreenCount(num_registered_screens_);
 }
 
 void AmbientSessionMetricsRecorder::SetInitStatus(bool init_status) {
   CHECK(!session_init_status_.has_value());
   session_init_status_ = init_status;
-  RecordInitStatus(init_status);
+  delegate_->RecordInitStatus(init_status);
 }
 
 void AmbientSessionMetricsRecorder::RegisterScreen() {
@@ -68,15 +63,8 @@
   // initializing the required assets and is starting to render.
   if (num_registered_screens_ == 1 && AmbientUiModel::Get()->ui_visibility() ==
                                           AmbientUiVisibility::kShouldShow) {
-    ambient::RecordAmbientModeStartupTime(
-        base::TimeTicks::Now() - session_start_time_, ui_settings_);
+    delegate_->RecordStartupTime(base::TimeTicks::Now() - session_start_time_);
   }
 }
 
-void AmbientSessionMetricsRecorder::RecordInitStatus(bool init_status) {
-  base::UmaHistogramBoolean(
-      base::StrCat({"Ash.AmbientMode.Init.", ui_settings_.ToString()}),
-      init_status);
-}
-
 }  // namespace ash
diff --git a/ash/ambient/metrics/ambient_session_metrics_recorder.h b/ash/ambient/metrics/ambient_session_metrics_recorder.h
index a3ef3fd..7e4c517 100644
--- a/ash/ambient/metrics/ambient_session_metrics_recorder.h
+++ b/ash/ambient/metrics/ambient_session_metrics_recorder.h
@@ -5,6 +5,8 @@
 #ifndef ASH_AMBIENT_METRICS_AMBIENT_SESSION_METRICS_RECORDER_H_
 #define ASH_AMBIENT_METRICS_AMBIENT_SESSION_METRICS_RECORDER_H_
 
+#include <memory>
+
 #include "ash/ambient/ambient_ui_settings.h"
 #include "ash/ash_export.h"
 #include "base/time/time.h"
@@ -26,7 +28,29 @@
 // Metrics recorded apply to all `AmbientUiSettings`.
 class ASH_EXPORT AmbientSessionMetricsRecorder {
  public:
-  explicit AmbientSessionMetricsRecorder(AmbientUiSettings ui_settings);
+  // `AmbientSessionMetricsRecorder` dictates when to record a metric and what
+  // value to record, whereas the `Delegate` does the actual recording.
+  class Delegate {
+   public:
+    virtual ~Delegate() = default;
+
+    // Session has started.
+    virtual void RecordActivation() {}
+    // Session has finished initialization and will start rendering if
+    // successful.
+    virtual void RecordInitStatus(bool success) {}
+    // Amount of time initialization took.
+    virtual void RecordStartupTime(base::TimeDelta startup_time) {}
+    // Total duration of the session (initialization + rendering) before it
+    // was closed.
+    virtual void RecordEngagementTime(base::TimeDelta engagement_time) {}
+    // Total number of screens that were rendering the UI during this session.
+    // Note this may be 0 if the session never started rendering (initialization
+    // was pending or failed when the session closed).
+    virtual void RecordScreenCount(int num_screens) {}
+  };
+
+  explicit AmbientSessionMetricsRecorder(std::unique_ptr<Delegate> delegate);
   AmbientSessionMetricsRecorder(const AmbientSessionMetricsRecorder&) = delete;
   AmbientSessionMetricsRecorder& operator=(
       const AmbientSessionMetricsRecorder&) = delete;
@@ -41,9 +65,7 @@
   void RegisterScreen();
 
  private:
-  void RecordInitStatus(bool init_status);
-
-  const AmbientUiSettings ui_settings_;
+  const std::unique_ptr<Delegate> delegate_;
   const base::TimeTicks session_start_time_;
   int num_registered_screens_ = 0;
   absl::optional<bool> session_init_status_;
diff --git a/ash/ambient/metrics/ambient_session_metrics_recorder_unittest.cc b/ash/ambient/metrics/ambient_session_metrics_recorder_unittest.cc
index efc2d23..33218cbf 100644
--- a/ash/ambient/metrics/ambient_session_metrics_recorder_unittest.cc
+++ b/ash/ambient/metrics/ambient_session_metrics_recorder_unittest.cc
@@ -8,6 +8,7 @@
 #include <utility>
 
 #include "ash/ambient/ambient_ui_settings.h"
+#include "ash/ambient/metrics/ambient_consumer_session_metrics_delegate.h"
 #include "ash/constants/ambient_video.h"
 #include "ash/public/cpp/ambient/ambient_ui_model.h"
 #include "ash/shell.h"
@@ -47,6 +48,10 @@
   std::string GetMetricNameForTheme(base::StringPiece prefix) {
     return base::StrCat({prefix, GetParam().ToString()});
   }
+
+  std::unique_ptr<AmbientSessionMetricsRecorder::Delegate> CreateDelegate() {
+    return std::make_unique<AmbientConsumerSessionMetricsDelegate>(GetParam());
+  }
 };
 
 INSTANTIATE_TEST_SUITE_P(
@@ -66,7 +71,7 @@
   base::HistogramTester histogram_tester;
   Shell::Get()->tablet_mode_controller()->SetEnabledForTest(false);
   {
-    AmbientSessionMetricsRecorder recorder(GetParam());
+    AmbientSessionMetricsRecorder recorder(CreateDelegate());
     recorder.SetInitStatus(true);
     recorder.RegisterScreen();
     task_environment()->FastForwardBy(kExpectedEngagementTime);
@@ -82,7 +87,7 @@
   // Now do the same sequence in tablet mode.
   Shell::Get()->tablet_mode_controller()->SetEnabledForTest(true);
   {
-    AmbientSessionMetricsRecorder recorder(GetParam());
+    AmbientSessionMetricsRecorder recorder(CreateDelegate());
     recorder.SetInitStatus(true);
     recorder.RegisterScreen();
     task_environment()->FastForwardBy(kExpectedEngagementTime);
@@ -99,7 +104,7 @@
   constexpr base::TimeDelta kExpectedStartupTime = base::Seconds(5);
 
   base::HistogramTester histogram_tester;
-  AmbientSessionMetricsRecorder recorder(GetParam());
+  AmbientSessionMetricsRecorder recorder(CreateDelegate());
   task_environment()->FastForwardBy(kExpectedStartupTime);
   recorder.SetInitStatus(true);
   recorder.RegisterScreen();
@@ -116,7 +121,7 @@
   constexpr base::TimeDelta kFailedStartupTime = base::Minutes(1);
   base::HistogramTester histogram_tester;
   {
-    AmbientSessionMetricsRecorder recorder(GetParam());
+    AmbientSessionMetricsRecorder recorder(CreateDelegate());
     task_environment()->FastForwardBy(kFailedStartupTime);
   }
   histogram_tester.ExpectUniqueTimeSample(
@@ -129,7 +134,7 @@
 
 TEST_P(AmbientSessionMetricsRecorderTest, InitStatusSuccess) {
   base::HistogramTester histogram_tester;
-  AmbientSessionMetricsRecorder recorder(GetParam());
+  AmbientSessionMetricsRecorder recorder(CreateDelegate());
   recorder.SetInitStatus(true);
   recorder.RegisterScreen();
   recorder.RegisterScreen();
@@ -140,7 +145,7 @@
 
 TEST_P(AmbientSessionMetricsRecorderTest, InitStatusFailed) {
   base::HistogramTester histogram_tester;
-  AmbientSessionMetricsRecorder recorder(GetParam());
+  AmbientSessionMetricsRecorder recorder(CreateDelegate());
   recorder.SetInitStatus(false);
   histogram_tester.ExpectUniqueSample(
       base::StrCat({"Ash.AmbientMode.Init.", GetParam().ToString()}),
@@ -150,7 +155,7 @@
 TEST_P(AmbientSessionMetricsRecorderTest, RecordsScreenCount) {
   base::HistogramTester histogram_tester;
   {
-    AmbientSessionMetricsRecorder recorder(GetParam());
+    AmbientSessionMetricsRecorder recorder(CreateDelegate());
     recorder.SetInitStatus(true);
     recorder.RegisterScreen();
   }
@@ -158,7 +163,7 @@
       GetMetricNameForTheme("Ash.AmbientMode.ScreenCount."), /*sample=*/1,
       /*expected_bucket_count=*/1);
   {
-    AmbientSessionMetricsRecorder recorder(GetParam());
+    AmbientSessionMetricsRecorder recorder(CreateDelegate());
     recorder.SetInitStatus(true);
     recorder.RegisterScreen();
     recorder.RegisterScreen();
diff --git a/ash/ambient/metrics/managed_screensaver_metrics.cc b/ash/ambient/metrics/managed_screensaver_metrics.cc
index dc61194e..8a7d736c 100644
--- a/ash/ambient/metrics/managed_screensaver_metrics.cc
+++ b/ash/ambient/metrics/managed_screensaver_metrics.cc
@@ -52,43 +52,28 @@
       result);
 }
 
-ManagedScreensaverMetricsRecorder::ManagedScreensaverMetricsRecorder() =
+ManagedScreensaverMetricsDelegate::ManagedScreensaverMetricsDelegate() =
     default;
-ManagedScreensaverMetricsRecorder::~ManagedScreensaverMetricsRecorder() =
+ManagedScreensaverMetricsDelegate::~ManagedScreensaverMetricsDelegate() =
     default;
 
-void ManagedScreensaverMetricsRecorder::RecordSessionStart() {
-  session_elapsed_timer_ = std::make_unique<base::ElapsedTimer>();
-}
-
-void ManagedScreensaverMetricsRecorder::RecordSessionEnd() {
-  // The screensaver can transition to stopped/hidden state without ever being
-  // started when chrome starts up. That is why we add an early return here to
-  // make sure that we only record valid sessions.
-  if (!session_elapsed_timer_) {
-    return;
-  }
-
+void ManagedScreensaverMetricsDelegate::RecordEngagementTime(
+    base::TimeDelta engagement_time) {
   base::UmaHistogramCustomTimes(
       /*name=*/GetManagedScreensaverHistogram(
           kManagedScreensaverEngagementTimeSlideshowUMA),
-      /*sample=*/session_elapsed_timer_->Elapsed(),
+      /*sample=*/engagement_time,
       /*min=*/base::Seconds(1),
       /*max=*/base::Hours(24),
       /*buckets=*/kManagedScreensaverEngagemenTimeHistogramBuckets);
-
-  session_elapsed_timer_.reset();
 }
 
-void ManagedScreensaverMetricsRecorder::RecordSessionStartupTime() {
-  if (!session_elapsed_timer_) {
-    return;
-  }
-
+void ManagedScreensaverMetricsDelegate::RecordStartupTime(
+    base::TimeDelta startup_time) {
   base::UmaHistogramCustomTimes(
       /*name=*/GetManagedScreensaverHistogram(
           kManagedScreensaverStartupTimeSlideshowUMA),
-      /*sample=*/session_elapsed_timer_->Elapsed(),
+      /*sample=*/startup_time,
       /*min=*/base::Seconds(0),
       /*max=*/base::Seconds(1000),
       /*buckets=*/kManagedScreensaverStartupTimeHistogramBuckets);
diff --git a/ash/ambient/metrics/managed_screensaver_metrics.h b/ash/ambient/metrics/managed_screensaver_metrics.h
index 9eecff0..33ef9b83 100644
--- a/ash/ambient/metrics/managed_screensaver_metrics.h
+++ b/ash/ambient/metrics/managed_screensaver_metrics.h
@@ -7,6 +7,7 @@
 
 #include <string>
 
+#include "ash/ambient/metrics/ambient_session_metrics_recorder.h"
 #include "ash/ash_export.h"
 #include "base/strings/string_piece.h"
 #include "base/timer/elapsed_timer.h"
@@ -34,28 +35,18 @@
 ASH_EXPORT void RecordManagedScreensaverImageDownloadResult(
     ScreensaverImageDownloadResult result);
 
-class ManagedScreensaverMetricsRecorder {
+class ManagedScreensaverMetricsDelegate
+    : public AmbientSessionMetricsRecorder::Delegate {
  public:
-  ManagedScreensaverMetricsRecorder();
-  ~ManagedScreensaverMetricsRecorder();
-  ManagedScreensaverMetricsRecorder(const ManagedScreensaverMetricsRecorder&) =
+  ManagedScreensaverMetricsDelegate();
+  ~ManagedScreensaverMetricsDelegate() override;
+  ManagedScreensaverMetricsDelegate(const ManagedScreensaverMetricsDelegate&) =
       delete;
-  ManagedScreensaverMetricsRecorder& operator=(
-      const ManagedScreensaverMetricsRecorder&) = delete;
+  ManagedScreensaverMetricsDelegate& operator=(
+      const ManagedScreensaverMetricsDelegate&) = delete;
 
-  // Starts the session elapsed timer. This is used to keep track of the start
-  // of a session.
-  void RecordSessionStart();
-
-  // Records the amount of time it takes for the managed screensaver to start.
-  void RecordSessionStartupTime();
-
-  // Records the engagement time UMA.
-  void RecordSessionEnd();
-
- private:
-  // Timer use to keep track of ambient mode managed screensaver sessions.
-  std::unique_ptr<base::ElapsedTimer> session_elapsed_timer_;
+  void RecordStartupTime(base::TimeDelta startup_time) override;
+  void RecordEngagementTime(base::TimeDelta engagement_time) override;
 };
 
 }  // namespace ash
diff --git a/ash/display/screen_orientation_controller.cc b/ash/display/screen_orientation_controller.cc
index de1e941..66d467b 100644
--- a/ash/display/screen_orientation_controller.cc
+++ b/ash/display/screen_orientation_controller.cc
@@ -10,13 +10,14 @@
 #include "ash/constants/ash_switches.h"
 #include "ash/shell.h"
 #include "ash/wm/mru_window_tracker.h"
-#include "ash/wm/splitview/split_view_controller.h"
 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
 #include "ash/wm/window_state.h"
+#include "ash/wm/window_state_observer.h"
 #include "ash/wm/window_util.h"
 #include "base/auto_reset.h"
 #include "base/command_line.h"
 #include "base/containers/contains.h"
+#include "base/scoped_observation.h"
 #include "chromeos/ui/base/display_util.h"
 #include "ui/aura/client/aura_constants.h"
 #include "ui/display/display.h"
@@ -121,22 +122,88 @@
   return out;
 }
 
+// `WindowStateChangeNotifier` observes an active window state change, and
+// call `ApplyLockForTopMostWindowOnInternalDisplay` when the state changes.
+class ScreenOrientationController::WindowStateChangeNotifier
+    : public wm::ActivationChangeObserver,
+      public WindowStateObserver,
+      public aura::WindowObserver {
+ public:
+  explicit WindowStateChangeNotifier(ScreenOrientationController* controller)
+      : controller_(controller) {
+    activation_observation_.Observe(Shell::Get()->activation_client());
+  }
+  WindowStateChangeNotifier(const WindowStateChangeNotifier&) = delete;
+  WindowStateChangeNotifier& operator=(const WindowStateChangeNotifier&) =
+      delete;
+  ~WindowStateChangeNotifier() override = default;
+
+  // wm::ActivationChangeObserver:
+  void OnWindowActivated(wm::ActivationChangeObserver::ActivationReason reason,
+                         aura::Window* gained_active,
+                         aura::Window* lost_active) override {
+    StartObservingWindowIfNeeded(gained_active);
+  }
+
+  // WindowStateObserver:
+  void OnPostWindowStateTypeChange(
+      WindowState* window_state,
+      chromeos::WindowStateType old_type) override {
+    controller_->ApplyLockForTopMostWindowOnInternalDisplay();
+  }
+
+  // aura::WindowObserver:
+  void OnWindowDestroying(aura::Window* window) override {
+    window_observation_.Reset();
+    window_state_observation_.Reset();
+  }
+
+ private:
+  void StartObservingWindowIfNeeded(aura::Window* window) {
+    if (window == window_observation_.GetSource()) {
+      return;
+    }
+
+    window_observation_.Reset();
+    window_state_observation_.Reset();
+
+    // Orphan window can not have WindowState.
+    if (!window || !window->parent()) {
+      return;
+    }
+
+    if (auto* const window_state = WindowState::Get(window)) {
+      window_observation_.Observe(window);
+      window_state_observation_.Observe(window_state);
+    }
+  }
+
+  const raw_ptr<ScreenOrientationController> controller_;
+
+  base::ScopedObservation<WindowState, WindowStateObserver>
+      window_state_observation_{this};
+  base::ScopedObservation<aura::Window, aura::WindowObserver>
+      window_observation_{this};
+  base::ScopedObservation<wm::ActivationClient, wm::ActivationChangeObserver>
+      activation_observation_{this};
+};
+
 ScreenOrientationController::ScreenOrientationController()
     : natural_orientation_(GetInternalDisplayNaturalOrientation()),
       ignore_display_configuration_updates_(false),
       rotation_locked_(false),
       rotation_locked_orientation_(chromeos::OrientationType::kAny),
       user_rotation_(display::Display::ROTATE_0),
-      current_rotation_(display::Display::ROTATE_0) {
+      current_rotation_(display::Display::ROTATE_0),
+      window_state_change_notifier_(
+          std::make_unique<WindowStateChangeNotifier>(this)) {
   Shell::Get()->tablet_mode_controller()->AddObserver(this);
-  SplitViewController::Get(Shell::GetPrimaryRootWindow())->AddObserver(this);
   Shell::Get()->window_tree_host_manager()->AddObserver(this);
   AccelerometerReader::GetInstance()->AddObserver(this);
 }
 
 ScreenOrientationController::~ScreenOrientationController() {
   Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
-  SplitViewController::Get(Shell::GetPrimaryRootWindow())->RemoveObserver(this);
   Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
   AccelerometerReader::GetInstance()->RemoveObserver(this);
   Shell::Get()->window_tree_host_manager()->RemoveObserver(this);
@@ -390,15 +457,6 @@
     observer.OnUserRotationLockChanged();
 }
 
-void ScreenOrientationController::OnSplitViewStateChanged(
-    SplitViewController::State previous_state,
-    SplitViewController::State state) {
-  if (previous_state == SplitViewController::State::kNoSnap ||
-      state == SplitViewController::State::kNoSnap) {
-    ApplyLockForTopMostWindowOnInternalDisplay();
-  }
-}
-
 void ScreenOrientationController::OnWillProcessDisplayChanges() {
   suspend_orientation_lock_refreshes_ = true;
 }
@@ -620,13 +678,6 @@
     return;
   }
 
-  if (SplitViewController::Get(internal_display_root)
-          ->InTabletSplitViewMode()) {
-    // While split view is enabled, ignore rotation lock set by windows.
-    LockRotationToOrientation(user_locked_orientation_);
-    return;
-  }
-
   MruWindowTracker::WindowList mru_windows(
       Shell::Get()->mru_window_tracker()->BuildWindowListIgnoreModal(
           kActiveDesk));
@@ -638,6 +689,22 @@
     if (!window->TargetVisibility())
       continue;
 
+    auto* const window_state = WindowState::Get(window);
+    // If the top visible window is snapped, we ignore any window-requested
+    // rotation. Here we shouldn't rely on `SplitViewController::state()` which
+    // can be updated in `WindowStateObserver::OnPostWindowStateTypeChange()`
+    // because `ApplyLockForTopMostWindowOnInternalDisplay()` is also triggered
+    // by the same callback.
+    if (window_state->IsSnapped()) {
+      break;
+    }
+
+    // We don't respect a window-requested rotation when the window is not
+    // maximized/fullscreen.
+    if (!window_state->IsMaximized() && !window_state->IsFullscreen()) {
+      continue;
+    }
+
     if (ApplyLockForWindowIfPossible(window))
       return;
   }
diff --git a/ash/display/screen_orientation_controller.h b/ash/display/screen_orientation_controller.h
index a20b56a5..69773924 100644
--- a/ash/display/screen_orientation_controller.h
+++ b/ash/display/screen_orientation_controller.h
@@ -5,6 +5,7 @@
 #ifndef ASH_DISPLAY_SCREEN_ORIENTATION_CONTROLLER_H_
 #define ASH_DISPLAY_SCREEN_ORIENTATION_CONTROLLER_H_
 
+#include <memory>
 #include <unordered_map>
 
 #include "ash/accelerometer/accelerometer_reader.h"
@@ -44,7 +45,6 @@
       public AccelerometerReader::Observer,
       public WindowTreeHostManager::Observer,
       public TabletModeObserver,
-      public SplitViewObserver,
       public display::DisplayObserver {
  public:
   // Observer that reports changes to the state of ScreenOrientationProvider's
@@ -156,16 +156,13 @@
   void OnTabletModeEnded() override;
   void OnTabletPhysicalStateChanged() override;
 
-  // SplitViewObserver:
-  void OnSplitViewStateChanged(SplitViewController::State previous_state,
-                               SplitViewController::State state) override;
-
   // display::DisplayObserver:
   void OnWillProcessDisplayChanges() override;
   void OnDidProcessDisplayChanges() override;
 
  private:
   friend class ScreenOrientationControllerTestApi;
+  class WindowStateChangeNotifier;
 
   struct LockInfo {
     LockInfo(chromeos::OrientationType lock, aura::Window* root)
@@ -291,6 +288,8 @@
 
   // Register for DisplayObserver callbacks.
   display::ScopedDisplayObserver display_observer_{this};
+
+  std::unique_ptr<WindowStateChangeNotifier> window_state_change_notifier_;
 };
 
 }  // namespace ash
diff --git a/ash/display/screen_orientation_controller_unittest.cc b/ash/display/screen_orientation_controller_unittest.cc
index 0fbfe32..72f796a 100644
--- a/ash/display/screen_orientation_controller_unittest.cc
+++ b/ash/display/screen_orientation_controller_unittest.cc
@@ -938,6 +938,28 @@
   EXPECT_EQ(chromeos::OrientationType::kAny, UserLockedOrientation());
 }
 
+// Tests that the controller ignores the app-requested orientation of floated
+// windows.
+TEST_F(ScreenOrientationControllerTest, IgnoreFloatWindowOrientationLock) {
+  EnableTabletMode(true);
+
+  std::unique_ptr<aura::Window> child_window = CreateControlWindow();
+  std::unique_ptr<aura::Window> focus_window(CreateAppWindow());
+  ASSERT_EQ(display::Display::ROTATE_0, GetCurrentInternalDisplayRotation());
+  ASSERT_FALSE(RotationLocked());
+
+  AddWindowAndActivateParent(child_window.get(), focus_window.get());
+  Lock(child_window.get(), chromeos::OrientationType::kPortrait);
+  EXPECT_TRUE(RotationLocked());
+
+  // Float `focus_window`.
+  const WindowFloatWMEvent float_event(
+      chromeos::FloatStartLocation::kBottomRight);
+  WindowState::Get(focus_window.get())->OnWMEvent(&float_event);
+
+  EXPECT_FALSE(RotationLocked());
+}
+
 class SupportsClamshellAutoRotation : public ScreenOrientationControllerTest {
  public:
   SupportsClamshellAutoRotation() = default;
diff --git a/ash/events/OWNERS b/ash/events/OWNERS
index 5c6747d..35f1e123 100644
--- a/ash/events/OWNERS
+++ b/ash/events/OWNERS
@@ -1,4 +1,4 @@
 per-file accessibility_event_rewriter*=file://ash/accessibility/OWNERS
 per-file select_to_speak_event_handler*=file://ash/accessibility/OWNERS
 per-file keyboard_capability*=zentaro@chromium.org,dpad@google.com
-per-file peripheral_customization*=zentaro@chromium.org
+per-file peripheral_customization*=zentaro@chromium.org,dpad@google.com
diff --git a/ash/events/keyboard_capability_unittest.cc b/ash/events/keyboard_capability_unittest.cc
index 83ec01f..cc6cefe 100644
--- a/ash/events/keyboard_capability_unittest.cc
+++ b/ash/events/keyboard_capability_unittest.cc
@@ -996,6 +996,18 @@
   }
 }
 
+TEST_F(KeyboardCapabilityTest, NullTopRowDescriptor) {
+  ui::KeyboardDevice input_device(kDeviceId1, ui::INPUT_DEVICE_BLUETOOTH,
+                                  "External Keyboard");
+  fake_keyboard_manager_->AddFakeKeyboard(input_device,
+                                          "C0000 C0000 C0000 C0000",
+                                          /*has_custom_top_row=*/true);
+  EXPECT_EQ(ui::KeyboardCapability::DeviceType::
+                kDeviceExternalNullTopRowChromeOsKeyboard,
+            keyboard_capability_->GetDeviceType(input_device));
+  EXPECT_TRUE(keyboard_capability_->HasCapsLockKey(input_device));
+}
+
 class TopRowLayoutCustomTest
     : public KeyboardCapabilityTest,
       public testing::WithParamInterface<std::vector<ui::TopRowActionKey>> {
diff --git a/ash/events/peripheral_customization_event_rewriter.cc b/ash/events/peripheral_customization_event_rewriter.cc
index 6a5ff04..c3a360f 100644
--- a/ash/events/peripheral_customization_event_rewriter.cc
+++ b/ash/events/peripheral_customization_event_rewriter.cc
@@ -36,16 +36,16 @@
     ui::EF_RIGHT_MOUSE_BUTTON | ui::EF_BACK_MOUSE_BUTTON |
     ui::EF_FORWARD_MOUSE_BUTTON | ui::EF_MIDDLE_MOUSE_BUTTON;
 
-mojom::KeyEvent GetHardCodedAction(mojom::HardCodedAction action) {
+mojom::KeyEvent GetStaticShortcutAction(mojom::StaticShortcutAction action) {
   mojom::KeyEvent key_event;
   switch (action) {
-    case mojom::HardCodedAction::kCopy:
+    case mojom::StaticShortcutAction::kCopy:
       key_event = mojom::KeyEvent(
           ui::VKEY_C, static_cast<int>(ui::DomCode::US_C),
           static_cast<int>(ui::DomKey::Constant<'c'>::Character),
           ui::EF_CONTROL_DOWN);
       break;
-    case mojom::HardCodedAction::kPaste:
+    case mojom::StaticShortcutAction::kPaste:
       key_event = mojom::KeyEvent(
           ui::VKEY_V, static_cast<int>(ui::DomCode::US_V),
           static_cast<int>(ui::DomKey::Constant<'v'>::Character),
@@ -365,14 +365,14 @@
     return false;
   }
 
-  if (remapping_action->is_action()) {
+  if (remapping_action->is_accelerator_action()) {
     if (event.type() == ui::ET_KEY_PRESSED ||
         event.type() == ui::ET_MOUSE_PRESSED) {
       // Every accelerator supported by peripheral customization is not impacted
       // by the accelerator passed. Therefore, passing an empty accelerator will
       // cause no issues.
       Shell::Get()->accelerator_controller()->PerformActionIfEnabled(
-          remapping_action->get_action(), /*accelerator=*/{});
+          remapping_action->get_accelerator_action(), /*accelerator=*/{});
     }
 
     return true;
@@ -383,9 +383,10 @@
     rewritten_event = RewriteEventToKeyEvent(event, *key_event);
   }
 
-  if (remapping_action->is_hardcoded_action()) {
+  if (remapping_action->is_static_shortcut_action()) {
     rewritten_event = RewriteEventToKeyEvent(
-        event, GetHardCodedAction(remapping_action->get_hardcoded_action()));
+        event, GetStaticShortcutAction(
+                   remapping_action->get_static_shortcut_action()));
   }
 
   return false;
diff --git a/ash/events/peripheral_customization_event_rewriter_unittest.cc b/ash/events/peripheral_customization_event_rewriter_unittest.cc
index 27c009d..e1435c37 100644
--- a/ash/events/peripheral_customization_event_rewriter_unittest.cc
+++ b/ash/events/peripheral_customization_event_rewriter_unittest.cc
@@ -343,9 +343,10 @@
   TestAcceleratorObserver accelerator_observer;
   TestEventRewriterContinuation continuation;
 
-  mouse_settings_->button_remappings.push_back(mojom::ButtonRemapping::New(
-      "", mojom::Button::NewVkey(ui::VKEY_A),
-      mojom::RemappingAction::NewAction(AcceleratorAction::kBrightnessDown)));
+  mouse_settings_->button_remappings.push_back(
+      mojom::ButtonRemapping::New("", mojom::Button::NewVkey(ui::VKEY_A),
+                                  mojom::RemappingAction::NewAcceleratorAction(
+                                      AcceleratorAction::kBrightnessDown)));
 
   rewriter_->RewriteEvent(CreateKeyButtonEvent(ui::ET_KEY_PRESSED, ui::VKEY_A),
                           continuation.weak_ptr_factory_.GetWeakPtr());
@@ -369,7 +370,8 @@
   mouse_settings_->button_remappings.push_back(mojom::ButtonRemapping::New(
       "",
       mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kMiddle),
-      mojom::RemappingAction::NewAction(AcceleratorAction::kLaunchApp0)));
+      mojom::RemappingAction::NewAcceleratorAction(
+          AcceleratorAction::kLaunchApp0)));
 
   rewriter_->RewriteEvent(
       CreateMouseButtonEvent(ui::ET_MOUSE_PRESSED, ui::EF_MIDDLE_MOUSE_BUTTON,
@@ -1054,23 +1056,23 @@
             ConvertToString(*continuation.passthrough_event));
 }
 
-class HardcodedActionRewritingTest
+class StaticShortcutActionRewritingTest
     : public PeripheralCustomizationEventRewriterTest,
       public testing::WithParamInterface<
-          std::tuple<mojom::HardCodedAction, ui::KeyEvent>> {};
+          std::tuple<mojom::StaticShortcutAction, ui::KeyEvent>> {};
 
 INSTANTIATE_TEST_SUITE_P(
     All,
-    HardcodedActionRewritingTest,
+    StaticShortcutActionRewritingTest,
     testing::ValuesIn(
-        std::vector<std::tuple<mojom::HardCodedAction, ui::KeyEvent>>({
-            {mojom::HardCodedAction::kCopy,
+        std::vector<std::tuple<mojom::StaticShortcutAction, ui::KeyEvent>>({
+            {mojom::StaticShortcutAction::kCopy,
              CreateKeyButtonEvent(ui::ET_KEY_PRESSED,
                                   ui::VKEY_C,
                                   ui::EF_CONTROL_DOWN,
                                   ui::DomCode::US_C,
                                   ui::DomKey::Constant<'c'>::Character)},
-            {mojom::HardCodedAction::kPaste,
+            {mojom::StaticShortcutAction::kPaste,
              CreateKeyButtonEvent(ui::ET_KEY_PRESSED,
                                   ui::VKEY_V,
                                   ui::EF_CONTROL_DOWN,
@@ -1078,13 +1080,13 @@
                                   ui::DomKey::Constant<'v'>::Character)},
         })));
 
-TEST_P(HardcodedActionRewritingTest, HardcodedMouseRewriting) {
-  const auto& [hardcoded_action, expected_key_event] = GetParam();
+TEST_P(StaticShortcutActionRewritingTest, StaticShortcutMouseRewriting) {
+  const auto& [static_shortcut_action, expected_key_event] = GetParam();
 
   mouse_settings_->button_remappings.push_back(mojom::ButtonRemapping::New(
       "",
       mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kForward),
-      mojom::RemappingAction::NewHardcodedAction(hardcoded_action)));
+      mojom::RemappingAction::NewStaticShortcutAction(static_shortcut_action)));
 
   TestEventRewriterContinuation continuation;
   ui::MouseEvent mouse_pressed_event =
@@ -1117,20 +1119,23 @@
             ConvertToString(*continuation.passthrough_event));
 }
 
-TEST_P(HardcodedActionRewritingTest, HardcodedGraphicsTabletRewriting) {
-  const auto& [hardcoded_action, expected_key_event] = GetParam();
+TEST_P(StaticShortcutActionRewritingTest,
+       StaticShortcutGraphicsTabletRewriting) {
+  const auto& [static_shortcut_action, expected_key_event] = GetParam();
   graphics_tablet_settings_->pen_button_remappings.push_back(
       mojom::ButtonRemapping::New(
           "",
           mojom::Button::NewCustomizableButton(
               mojom::CustomizableButton::kForward),
-          mojom::RemappingAction::NewHardcodedAction(hardcoded_action)));
+          mojom::RemappingAction::NewStaticShortcutAction(
+              static_shortcut_action)));
   graphics_tablet_settings_->tablet_button_remappings.push_back(
       mojom::ButtonRemapping::New(
           "",
           mojom::Button::NewCustomizableButton(
               mojom::CustomizableButton::kBack),
-          mojom::RemappingAction::NewHardcodedAction(hardcoded_action)));
+          mojom::RemappingAction::NewStaticShortcutAction(
+              static_shortcut_action)));
 
   TestEventRewriterContinuation continuation;
 
diff --git a/ash/login/ui/lock_screen_media_controls_view.cc b/ash/login/ui/lock_screen_media_controls_view.cc
index 404e03b..958f5f1 100644
--- a/ash/login/ui/lock_screen_media_controls_view.cc
+++ b/ash/login/ui/lock_screen_media_controls_view.cc
@@ -506,13 +506,7 @@
     return;
   }
 
-  // We only consider the session as sensitive if its metadata comes from an OTR
-  // session and the kHideIncognitoMediaMetadata flag is off. When the flag is
-  // on, the session's metadata is obscured (in Incognito mode), so don't need
-  // to hide the media controls.
-  bool is_sensitive =
-      !base::FeatureList::IsEnabled(media::kHideIncognitoMediaMetadata) &&
-      session_info->is_sensitive;
+  bool is_sensitive = session_info->is_sensitive;
 
   // If the session is marked as sensitive then don't show the controls.
   if (is_sensitive && !IsDrawn()) {
diff --git a/ash/login/ui/lock_screen_media_controls_view_unittest.cc b/ash/login/ui/lock_screen_media_controls_view_unittest.cc
index 0c38186fc..92ca1e51f 100644
--- a/ash/login/ui/lock_screen_media_controls_view_unittest.cc
+++ b/ash/login/ui/lock_screen_media_controls_view_unittest.cc
@@ -19,7 +19,6 @@
 #include "base/test/scoped_feature_list.h"
 #include "base/timer/mock_timer.h"
 #include "components/media_message_center/media_controls_progress_view.h"
-#include "media/base/media_switches.h"
 #include "services/media_session/public/cpp/test/test_media_controller.h"
 #include "services/media_session/public/mojom/media_session.mojom.h"
 #include "ui/accessibility/ax_enums.mojom.h"
@@ -528,11 +527,7 @@
   EXPECT_FALSE(CloseButtonHasImage());
 }
 
-TEST_F(LockScreenMediaControlsViewTest,
-       MediaControlsNotShownIfSensitiveWithHideMetadataFeatureFlagDisabled) {
-  base::test::ScopedFeatureList scoped_feature_list_;
-  scoped_feature_list_.InitAndDisableFeature(
-      media::kHideIncognitoMediaMetadata);
+TEST_F(LockScreenMediaControlsViewTest, MediaControlsNotShownIfSensitive) {
   SimulateMediaSessionChanged(
       media_session::mojom::MediaPlaybackState::kPlaying,
       /*is_sensitive=*/true);
@@ -540,13 +535,10 @@
   EXPECT_FALSE(media_controls_view_->IsDrawn());
 }
 
-TEST_F(LockScreenMediaControlsViewTest,
-       MediaControlsShownIfSensitiveWithHideMetadataFeatureFlagEnabled) {
-  base::test::ScopedFeatureList scoped_feature_list_;
-  scoped_feature_list_.InitAndEnableFeature(media::kHideIncognitoMediaMetadata);
+TEST_F(LockScreenMediaControlsViewTest, MediaControlsShownIfNotSensitive) {
   SimulateMediaSessionChanged(
       media_session::mojom::MediaPlaybackState::kPlaying,
-      /*is_sensitive=*/true);
+      /*is_sensitive=*/false);
 
   EXPECT_TRUE(media_controls_view_->IsDrawn());
 }
diff --git a/ash/public/cpp/system_tray_client.h b/ash/public/cpp/system_tray_client.h
index a12c702..f29285c8 100644
--- a/ash/public/cpp/system_tray_client.h
+++ b/ash/public/cpp/system_tray_client.h
@@ -129,6 +129,9 @@
   // Opens SIM unlock dialog in OS Settings.
   virtual void ShowSettingsSimUnlock() = 0;
 
+  // Opens the APN subpage for network with guid |network_id|.
+  virtual void ShowApnSubpage(const std::string& network_id) = 0;
+
   // Shows the "add network" UI to create a third-party extension-backed VPN
   // connection (e.g. Cisco AnyConnect).
   virtual void ShowThirdPartyVpnCreate(const std::string& extension_id) = 0;
diff --git a/ash/public/cpp/test/test_system_tray_client.cc b/ash/public/cpp/test/test_system_tray_client.cc
index 8ac5be4..f8546d86 100644
--- a/ash/public/cpp/test/test_system_tray_client.cc
+++ b/ash/public/cpp/test/test_system_tray_client.cc
@@ -106,6 +106,11 @@
   ++show_sim_unlock_settings_count_;
 }
 
+void TestSystemTrayClient::ShowApnSubpage(const std::string& network_id) {
+  ++show_apn_subpage_count_;
+  last_apn_subpage_network_id_ = network_id;
+}
+
 void TestSystemTrayClient::ShowThirdPartyVpnCreate(
     const std::string& extension_id) {
   ++show_third_party_vpn_create_count_;
diff --git a/ash/public/cpp/test/test_system_tray_client.h b/ash/public/cpp/test/test_system_tray_client.h
index 0b68828..d08cc000 100644
--- a/ash/public/cpp/test/test_system_tray_client.h
+++ b/ash/public/cpp/test/test_system_tray_client.h
@@ -58,6 +58,7 @@
   void ShowNetworkCreate(const std::string& type) override;
   void ShowSettingsCellularSetup(bool show_psim_flow) override;
   void ShowSettingsSimUnlock() override;
+  void ShowApnSubpage(const std::string& network_id) override;
   void ShowThirdPartyVpnCreate(const std::string& extension_id) override;
   void ShowArcVpnCreate(const std::string& app_id) override;
   void ShowNetworkSettings(const std::string& network_id) override;
@@ -131,6 +132,12 @@
     return show_sim_unlock_settings_count_;
   }
 
+  int show_apn_subpage_count() const { return show_apn_subpage_count_; }
+
+  const std::string& last_apn_subpage_network_id() const {
+    return last_apn_subpage_network_id_;
+  }
+
   int show_third_party_vpn_create_count() const {
     return show_third_party_vpn_create_count_;
   }
@@ -211,6 +218,7 @@
   int show_os_smart_privacy_settings_count_ = 0;
   int show_wifi_sync_settings_count_ = 0;
   int show_sim_unlock_settings_count_ = 0;
+  int show_apn_subpage_count_ = 0;
   int show_third_party_vpn_create_count_ = 0;
   std::string last_third_party_vpn_extension_id_;
   int show_arc_vpn_create_count_ = 0;
@@ -221,6 +229,7 @@
   int show_calendar_event_count_ = 0;
   int show_video_conference_count_ = 0;
   std::string last_bluetooth_settings_device_id_;
+  std::string last_apn_subpage_network_id_;
   std::string last_network_settings_network_id_;
   std::string last_network_type_;
   int show_channel_info_additional_details_count_ = 0;
diff --git a/ash/public/mojom/input_device_settings.mojom b/ash/public/mojom/input_device_settings.mojom
index 901a2174..1c940417 100644
--- a/ash/public/mojom/input_device_settings.mojom
+++ b/ash/public/mojom/input_device_settings.mojom
@@ -279,7 +279,8 @@
 };
 
 // Contains all information needed to apply remappings from a button to
-// either an action or key event and display it within the settings app.
+// either an acceleration action or key event or static shortcut action
+// and display it within the settings app.
 struct ButtonRemapping {
   // Human-readable label of the button remapping which user can rename.
   string name;
@@ -287,12 +288,12 @@
   RemappingAction? remapping_action;
 };
 
-// Represents the remapping action which can be either a key event or action
-// or hardcoded action.
+// Represents the remapping action which can be either a key event or
+// acceleration action or static shortcut action.
 union RemappingAction {
-  ash.mojom.AcceleratorAction action;
+  ash.mojom.AcceleratorAction accelerator_action;
   KeyEvent key_event;
-  HardCodedAction hardcoded_action;
+  StaticShortcutAction static_shortcut_action;
 };
 
 // Represents the key event the button remaps to.
@@ -321,8 +322,8 @@
   kSide = 6,
 };
 
-// Contains the valid set of hardcoded actions.
-enum HardCodedAction {
+// Contains the valid set of static shortcut actions.
+enum StaticShortcutAction {
   kCopy = 0,
   kPaste = 1,
 };
diff --git a/ash/sensor_info/sensor_provider.cc b/ash/sensor_info/sensor_provider.cc
index e94a660..df108d8 100644
--- a/ash/sensor_info/sensor_provider.cc
+++ b/ash/sensor_info/sensor_provider.cc
@@ -571,11 +571,11 @@
   GetLidAngleUpdate();
 }
 
-void SensorProvider::AddObserver(Observer* observer) {
+void SensorProvider::AddObserver(SensorObserver* observer) {
   observers_.AddObserver(observer);
 }
 
-void SensorProvider::RemoveObserver(Observer* observer) {
+void SensorProvider::RemoveObserver(SensorObserver* observer) {
   observers_.RemoveObserver(observer);
 }
 }  // namespace ash
diff --git a/ash/sensor_info/sensor_provider.h b/ash/sensor_info/sensor_provider.h
index 0a2c358..cefe2ac 100644
--- a/ash/sensor_info/sensor_provider.h
+++ b/ash/sensor_info/sensor_provider.h
@@ -100,9 +100,9 @@
       int32_t iio_device_id,
       const std::vector<chromeos::sensors::mojom::DeviceType>& types) override;
 
-  // Adds/Removes observers.
-  void AddObserver(Observer* observer);
-  void RemoveObserver(Observer* observer);
+  // Adds/Removes SensorObservers.
+  void AddObserver(SensorObserver* observer);
+  void RemoveObserver(SensorObserver* observer);
 
   // Starts/Stops sensor reading.
   // Changes 'sensor_read_on_' and call EnableSensorReadingInternal.
@@ -228,7 +228,7 @@
   bool sensor_read_on_ = false;
 
   // List of all external observers.
-  base::ObserverList<Observer> observers_;
+  base::ObserverList<SensorObserver> observers_;
 
   base::WeakPtrFactory<SensorProvider> weak_ptr_factory_{this};
 };
diff --git a/ash/sensor_info/sensor_provider_unittest.cc b/ash/sensor_info/sensor_provider_unittest.cc
index edb1a125..9346176d 100644
--- a/ash/sensor_info/sensor_provider_unittest.cc
+++ b/ash/sensor_info/sensor_provider_unittest.cc
@@ -38,7 +38,7 @@
 
 constexpr int64_t kFakeSampleData[] = {1, 2, 3};
 
-class FakeObserver : public Observer {
+class FakeObserver : public SensorObserver {
  public:
   void OnSensorUpdated(const SensorUpdate& update) override {
     for (int index = 0; index < static_cast<int>(SensorType::kSensorTypeCount);
diff --git a/ash/sensor_info/sensor_types.h b/ash/sensor_info/sensor_types.h
index 73752a5..4748301 100644
--- a/ash/sensor_info/sensor_types.h
+++ b/ash/sensor_info/sensor_types.h
@@ -77,11 +77,11 @@
       data_[static_cast<int>(SensorType::kSensorTypeCount)];
 };
 
-// Class for all potential observer.
-class ASH_EXPORT Observer : public base::CheckedObserver {
+// Class for all potential observers for sensor updates.
+class ASH_EXPORT SensorObserver : public base::CheckedObserver {
  public:
   // SensorProvider will gather updates from AccelGyroSamplesObserver. Then
-  // SensorProvider will call OnSensorUpdated to notify Observer.
+  // SensorProvider will call OnSensorUpdated to notify SensorObserver.
   virtual void OnSensorUpdated(const SensorUpdate& update) = 0;
 };
 
diff --git a/ash/system/input_device_settings/input_device_settings_controller_impl.cc b/ash/system/input_device_settings/input_device_settings_controller_impl.cc
index 7af5bd30..88d0ec0 100644
--- a/ash/system/input_device_settings/input_device_settings_controller_impl.cc
+++ b/ash/system/input_device_settings/input_device_settings_controller_impl.cc
@@ -72,6 +72,8 @@
     case ui::KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
     case ui::KeyboardCapability::DeviceType::kDeviceExternalUnknown:
     case ui::KeyboardCapability::DeviceType::kDeviceInternalRevenKeyboard:
+    case ui::KeyboardCapability::DeviceType::
+        kDeviceExternalNullTopRowChromeOsKeyboard:
       return mojom::MetaKey::kExternalMeta;
   };
 }
diff --git a/ash/system/input_device_settings/input_device_settings_controller_unittest.cc b/ash/system/input_device_settings/input_device_settings_controller_unittest.cc
index 72a7263..7d94c38 100644
--- a/ash/system/input_device_settings/input_device_settings_controller_unittest.cc
+++ b/ash/system/input_device_settings/input_device_settings_controller_unittest.cc
@@ -751,7 +751,7 @@
       /*button=*/
       button->Clone(),
       /*remapping_action=*/
-      mojom::RemappingAction::NewAction(
+      mojom::RemappingAction::NewAcceleratorAction(
           ash::AcceleratorAction::kBrightnessDown));
   std::vector<mojom::ButtonRemappingPtr> tablet_button_remappings;
   std::vector<mojom::ButtonRemappingPtr> pen_button_remappings;
@@ -889,7 +889,7 @@
       /*button=*/
       mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kBack),
       /*remapping_action=*/
-      mojom::RemappingAction::NewAction(
+      mojom::RemappingAction::NewAcceleratorAction(
           ash::AcceleratorAction::kBrightnessDown));
   std::vector<mojom::ButtonRemappingPtr> button_remappings;
   button_remappings.push_back(button_remapping.Clone());
diff --git a/ash/system/input_device_settings/input_device_settings_metrics_manager_unittest.cc b/ash/system/input_device_settings/input_device_settings_metrics_manager_unittest.cc
index 1a56714f..35917de 100644
--- a/ash/system/input_device_settings/input_device_settings_metrics_manager_unittest.cc
+++ b/ash/system/input_device_settings/input_device_settings_metrics_manager_unittest.cc
@@ -276,13 +276,15 @@
   mouse.device_key = kExternalMouseId;
   mouse.settings = mojom::MouseSettings::New();
   mouse.settings->sensitivity = kSampleSensitivity;
-  mouse.settings->button_remappings.push_back(mojom::ButtonRemapping::New(
-      "my-vkey", mojom::Button::NewVkey(ui::VKEY_A),
-      mojom::RemappingAction::NewAction(AcceleratorAction::kBrightnessDown)));
+  mouse.settings->button_remappings.push_back(
+      mojom::ButtonRemapping::New("my-vkey", mojom::Button::NewVkey(ui::VKEY_A),
+                                  mojom::RemappingAction::NewAcceleratorAction(
+                                      AcceleratorAction::kBrightnessDown)));
   mouse.settings->button_remappings.push_back(mojom::ButtonRemapping::New(
       "middle-button",
       mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kMiddle),
-      mojom::RemappingAction::NewAction(AcceleratorAction::kMediaPlay)));
+      mojom::RemappingAction::NewAcceleratorAction(
+          AcceleratorAction::kMediaPlay)));
 
   base::HistogramTester histogram_tester;
   SimulateUserLogin(kUser1);
@@ -502,25 +504,25 @@
   graphics_tablet.settings->pen_button_remappings.push_back(
       mojom::ButtonRemapping::New("pen-vkey",
                                   mojom::Button::NewVkey(ui::VKEY_C),
-                                  mojom::RemappingAction::NewAction(
+                                  mojom::RemappingAction::NewAcceleratorAction(
                                       AcceleratorAction::kBrightnessDown)));
   graphics_tablet.settings->pen_button_remappings.push_back(
-      mojom::ButtonRemapping::New(
-          "pen-middle-button",
-          mojom::Button::NewCustomizableButton(
-              mojom::CustomizableButton::kMiddle),
-          mojom::RemappingAction::NewAction(AcceleratorAction::kMediaPlay)));
+      mojom::ButtonRemapping::New("pen-middle-button",
+                                  mojom::Button::NewCustomizableButton(
+                                      mojom::CustomizableButton::kMiddle),
+                                  mojom::RemappingAction::NewAcceleratorAction(
+                                      AcceleratorAction::kMediaPlay)));
   graphics_tablet.settings->tablet_button_remappings.push_back(
       mojom::ButtonRemapping::New("tablet-vkey",
                                   mojom::Button::NewVkey(ui::VKEY_B),
-                                  mojom::RemappingAction::NewAction(
+                                  mojom::RemappingAction::NewAcceleratorAction(
                                       AcceleratorAction::kBrightnessDown)));
   graphics_tablet.settings->tablet_button_remappings.push_back(
-      mojom::ButtonRemapping::New(
-          "tablet-right-button",
-          mojom::Button::NewCustomizableButton(
-              mojom::CustomizableButton::kRight),
-          mojom::RemappingAction::NewAction(AcceleratorAction::kMediaPlay)));
+      mojom::ButtonRemapping::New("tablet-right-button",
+                                  mojom::Button::NewCustomizableButton(
+                                      mojom::CustomizableButton::kRight),
+                                  mojom::RemappingAction::NewAcceleratorAction(
+                                      AcceleratorAction::kMediaPlay)));
 
   base::HistogramTester histogram_tester;
   SimulateUserLogin(kUser1);
diff --git a/ash/system/input_device_settings/input_device_settings_pref_names.h b/ash/system/input_device_settings/input_device_settings_pref_names.h
index aa840f7..a7b75b0 100644
--- a/ash/system/input_device_settings/input_device_settings_pref_names.h
+++ b/ash/system/input_device_settings/input_device_settings_pref_names.h
@@ -120,12 +120,13 @@
 constexpr char kButtonRemappingName[] = "name";
 constexpr char kButtonRemappingCustomizableButton[] = "customizable_button";
 constexpr char kButtonRemappingKeyboardCode[] = "vkey";
-constexpr char kButtonRemappingAction[] = "action";
+constexpr char kButtonRemappingAcceleratorAction[] = "accelerator_action";
 constexpr char kButtonRemappingKeyEvent[] = "key_event";
 constexpr char kButtonRemappingDomCode[] = "dom_code";
 constexpr char kButtonRemappingDomKey[] = "dom_key";
 constexpr char kButtonRemappingModifiers[] = "modifiers";
-constexpr char kButtonRemappingHardCodedAction[] = "hardcoded_action";
+constexpr char kButtonRemappingStaticShortcutAction[] =
+    "static_shortcut_action";
 
 }  // namespace ash::prefs
 
diff --git a/ash/system/input_device_settings/input_device_settings_utils.cc b/ash/system/input_device_settings/input_device_settings_utils.cc
index 15cadf0..2fa7893d 100644
--- a/ash/system/input_device_settings/input_device_settings_utils.cc
+++ b/ash/system/input_device_settings/input_device_settings_utils.cc
@@ -215,13 +215,14 @@
         prefs::kButtonRemappingKeyboardCode,
         static_cast<int>(remapping.remapping_action->get_key_event()->vkey));
     dict.Set(prefs::kButtonRemappingKeyEvent, std::move(key_event));
-  } else if (remapping.remapping_action->is_action()) {
-    dict.Set(prefs::kButtonRemappingAction,
-             static_cast<int>(remapping.remapping_action->get_action()));
-  } else if (remapping.remapping_action->is_hardcoded_action()) {
+  } else if (remapping.remapping_action->is_accelerator_action()) {
     dict.Set(
-        prefs::kButtonRemappingHardCodedAction,
-        static_cast<int>(remapping.remapping_action->get_hardcoded_action()));
+        prefs::kButtonRemappingAcceleratorAction,
+        static_cast<int>(remapping.remapping_action->get_accelerator_action()));
+  } else if (remapping.remapping_action->is_static_shortcut_action()) {
+    dict.Set(prefs::kButtonRemappingStaticShortcutAction,
+             static_cast<int>(
+                 remapping.remapping_action->get_static_shortcut_action()));
   }
 
   return dict;
@@ -286,17 +287,18 @@
   mojom::RemappingActionPtr remapping_action;
   const base::Value::Dict* key_event =
       dict.FindDict(prefs::kButtonRemappingKeyEvent);
-  const absl::optional<int> action =
-      dict.FindInt(prefs::kButtonRemappingAction);
-  const absl::optional<int> hardcoded_action =
-      dict.FindInt(prefs::kButtonRemappingHardCodedAction);
+  const absl::optional<int> accelerator_action =
+      dict.FindInt(prefs::kButtonRemappingAcceleratorAction);
+  const absl::optional<int> static_shortcut_action =
+      dict.FindInt(prefs::kButtonRemappingStaticShortcutAction);
   // Remapping action can only have one value at most.
-  if ((key_event && action) || (key_event && hardcoded_action) ||
-      (action && hardcoded_action)) {
+  if ((key_event && accelerator_action) ||
+      (key_event && static_shortcut_action) ||
+      (accelerator_action && static_shortcut_action)) {
     return nullptr;
   }
-  // Remapping action can be either a keyboard event or an action
-  // or hardcoded action or null.
+  // Remapping action can be either a keyboard event or an accelerator action
+  // or static shortcut action or null.
   if (key_event) {
     const absl::optional<int> dom_code =
         key_event->FindInt(prefs::kButtonRemappingDomCode);
@@ -315,12 +317,12 @@
                              /*dom_code=*/*dom_code,
                              /*dom_key=*/*dom_key,
                              /*modifiers=*/*modifiers));
-  } else if (action) {
-    remapping_action = mojom::RemappingAction::NewAction(
-        static_cast<ash::AcceleratorAction>(*action));
-  } else if (hardcoded_action) {
-    remapping_action = mojom::RemappingAction::NewHardcodedAction(
-        static_cast<mojom::HardCodedAction>(*hardcoded_action));
+  } else if (accelerator_action) {
+    remapping_action = mojom::RemappingAction::NewAcceleratorAction(
+        static_cast<ash::AcceleratorAction>(*accelerator_action));
+  } else if (static_shortcut_action) {
+    remapping_action = mojom::RemappingAction::NewStaticShortcutAction(
+        static_cast<mojom::StaticShortcutAction>(*static_shortcut_action));
   }
 
   return mojom::ButtonRemapping::New(*name, std::move(button),
diff --git a/ash/system/input_device_settings/input_device_settings_utils_unittest.cc b/ash/system/input_device_settings/input_device_settings_utils_unittest.cc
index 7654ad29..927b22f 100644
--- a/ash/system/input_device_settings/input_device_settings_utils_unittest.cc
+++ b/ash/system/input_device_settings/input_device_settings_utils_unittest.cc
@@ -29,7 +29,8 @@
     /*button=*/
     mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kBack),
     /*remapping_action=*/
-    mojom::RemappingAction::NewAction(ash::AcceleratorAction::kBrightnessDown));
+    mojom::RemappingAction::NewAcceleratorAction(
+        ash::AcceleratorAction::kBrightnessDown));
 const mojom::ButtonRemapping button_remapping2(
     /*name=*/"test2",
     /*button=*/
@@ -51,7 +52,8 @@
     /*name=*/"test5",
     /*button=*/mojom::Button::NewVkey(::ui::KeyboardCode::VKEY_3),
     /*remapping_action=*/
-    mojom::RemappingAction::NewHardcodedAction(mojom::HardCodedAction::kCopy));
+    mojom::RemappingAction::NewStaticShortcutAction(
+        mojom::StaticShortcutAction::kCopy));
 }  // namespace
 
 class DeviceKeyTest : public testing::TestWithParam<
@@ -116,8 +118,9 @@
   EXPECT_EQ(
       static_cast<int>(button_remapping1.button->get_customizable_button()),
       *dict1.FindInt(prefs::kButtonRemappingCustomizableButton));
-  EXPECT_EQ(static_cast<int>(button_remapping1.remapping_action->get_action()),
-            *dict1.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()),
+            *dict1.FindInt(prefs::kButtonRemappingAcceleratorAction));
 
   const base::Value::Dict dict2 =
       ConvertButtonRemappingToDict(button_remapping2);
@@ -175,7 +178,8 @@
   EXPECT_EQ(static_cast<int>(button_remapping4.button->get_vkey()),
             *dict4.FindInt(prefs::kButtonRemappingKeyboardCode));
   EXPECT_EQ(nullptr, dict4.FindDict(prefs::kButtonRemappingKeyEvent));
-  EXPECT_EQ(absl::nullopt, dict4.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(absl::nullopt,
+            dict4.FindInt(prefs::kButtonRemappingAcceleratorAction));
 
   const base::Value::Dict dict5 =
       ConvertButtonRemappingToDict(button_remapping5);
@@ -184,21 +188,24 @@
   EXPECT_EQ(static_cast<int>(button_remapping5.button->get_vkey()),
             *dict5.FindInt(prefs::kButtonRemappingKeyboardCode));
   EXPECT_EQ(nullptr, dict5.FindDict(prefs::kButtonRemappingKeyEvent));
-  EXPECT_EQ(absl::nullopt, dict5.FindInt(prefs::kButtonRemappingAction));
-  EXPECT_EQ(static_cast<int>(
-                button_remapping5.remapping_action->get_hardcoded_action()),
-            dict5.FindInt(prefs::kButtonRemappingHardCodedAction));
+  EXPECT_EQ(absl::nullopt,
+            dict5.FindInt(prefs::kButtonRemappingAcceleratorAction));
+  EXPECT_EQ(
+      static_cast<int>(
+          button_remapping5.remapping_action->get_static_shortcut_action()),
+      dict5.FindInt(prefs::kButtonRemappingStaticShortcutAction));
 }
 
 TEST(ConvertDictToButtonRemapping, ConvertDictToButtonRemapping) {
-  // Valid dict with name, customizable button and action fields.
+  // Valid dict with name, customizable button and accelerator action fields.
   base::Value::Dict dict1;
   dict1.Set(prefs::kButtonRemappingName, button_remapping1.name);
   dict1.Set(
       prefs::kButtonRemappingCustomizableButton,
       static_cast<int>(button_remapping1.button->get_customizable_button()));
-  dict1.Set(prefs::kButtonRemappingAction,
-            static_cast<int>(button_remapping1.remapping_action->get_action()));
+  dict1.Set(prefs::kButtonRemappingAcceleratorAction,
+            static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()));
 
   mojom::ButtonRemappingPtr remapping1 = ConvertDictToButtonRemapping(dict1);
   EXPECT_EQ(*dict1.FindString(prefs::kButtonRemappingName), remapping1->name);
@@ -206,9 +213,10 @@
   EXPECT_EQ(static_cast<mojom::CustomizableButton>(
                 *dict1.FindInt(prefs::kButtonRemappingCustomizableButton)),
             remapping1->button->get_customizable_button());
-  EXPECT_TRUE(remapping1->remapping_action->is_action());
-  EXPECT_EQ(static_cast<uint>(*dict1.FindInt(prefs::kButtonRemappingAction)),
-            remapping1->remapping_action->get_action());
+  EXPECT_TRUE(remapping1->remapping_action->is_accelerator_action());
+  EXPECT_EQ(static_cast<uint>(
+                *dict1.FindInt(prefs::kButtonRemappingAcceleratorAction)),
+            remapping1->remapping_action->get_accelerator_action());
 
   // Valid dict with name, customizable button and key event fields.
   base::Value::Dict dict2;
@@ -317,7 +325,8 @@
   EXPECT_EQ(static_cast<::ui::KeyboardCode>(
                 *dict4.FindInt(prefs::kButtonRemappingKeyboardCode)),
             remapping4->button->get_vkey());
-  EXPECT_EQ(absl::nullopt, dict4.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(absl::nullopt,
+            dict4.FindInt(prefs::kButtonRemappingAcceleratorAction));
   EXPECT_EQ(nullptr, dict4.FindDict(prefs::kButtonRemappingKeyEvent));
 
   // Invalid dict with customizable button and vkey fields.
@@ -344,25 +353,27 @@
   mojom::ButtonRemappingPtr remapping7 = ConvertDictToButtonRemapping(dict7);
   EXPECT_FALSE(remapping7);
 
-  // Invalid dict with key event and action fields.
+  // Invalid dict with key event and accelerator action fields.
   base::Value::Dict dict8;
   dict8.Set(prefs::kButtonRemappingName, button_remapping3.name);
   dict8.Set(prefs::kButtonRemappingKeyboardCode,
             static_cast<int>(button_remapping3.button->get_vkey()));
   dict8.Set(prefs::kButtonRemappingKeyEvent, std::move(dict3_key_event));
-  dict8.Set(prefs::kButtonRemappingAction,
-            static_cast<int>(button_remapping1.remapping_action->get_action()));
+  dict8.Set(prefs::kButtonRemappingAcceleratorAction,
+            static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()));
   mojom::ButtonRemappingPtr remapping8 = ConvertDictToButtonRemapping(dict8);
   EXPECT_FALSE(remapping8);
 
-  // Valid dict with name, vkey and hardcoded action fields.
+  // Valid dict with name, vkey and static shortcut action fields.
   base::Value::Dict dict9;
   dict9.Set(prefs::kButtonRemappingName, button_remapping5.name);
   dict9.Set(prefs::kButtonRemappingKeyboardCode,
             static_cast<int>(button_remapping5.button->get_vkey()));
-  dict9.Set(prefs::kButtonRemappingHardCodedAction,
-            static_cast<int>(
-                button_remapping5.remapping_action->get_hardcoded_action()));
+  dict9.Set(
+      prefs::kButtonRemappingStaticShortcutAction,
+      static_cast<int>(
+          button_remapping5.remapping_action->get_static_shortcut_action()));
 
   mojom::ButtonRemappingPtr remapping9 = ConvertDictToButtonRemapping(dict9);
   EXPECT_EQ(*dict9.FindString(prefs::kButtonRemappingName), remapping9->name);
@@ -370,10 +381,10 @@
   EXPECT_EQ(static_cast<::ui::KeyboardCode>(
                 *dict9.FindInt(prefs::kButtonRemappingKeyboardCode)),
             remapping9->button->get_vkey());
-  EXPECT_TRUE(remapping9->remapping_action->is_hardcoded_action());
-  EXPECT_EQ(static_cast<mojom::HardCodedAction>(
-                *dict9.FindInt(prefs::kButtonRemappingHardCodedAction)),
-            remapping9->remapping_action->get_hardcoded_action());
+  EXPECT_TRUE(remapping9->remapping_action->is_static_shortcut_action());
+  EXPECT_EQ(static_cast<mojom::StaticShortcutAction>(
+                *dict9.FindInt(prefs::kButtonRemappingStaticShortcutAction)),
+            remapping9->remapping_action->get_static_shortcut_action());
 }
 
 TEST(ConvertButtonRemappingArrayToList, ConvertButtonRemappingArrayToList) {
@@ -390,8 +401,9 @@
   EXPECT_EQ(
       static_cast<int>(button_remapping1.button->get_customizable_button()),
       *dict1.FindInt(prefs::kButtonRemappingCustomizableButton));
-  EXPECT_EQ(static_cast<int>(button_remapping1.remapping_action->get_action()),
-            *dict1.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()),
+            *dict1.FindInt(prefs::kButtonRemappingAcceleratorAction));
 
   ASSERT_TRUE(list[1].is_dict());
   const auto& dict2 = list[1].GetDict();
@@ -420,14 +432,15 @@
 }
 
 TEST(ConvertListToButtonRemappingArray, ConvertListToButtonRemappingArray) {
-  // Valid dict with name, customizable button and action fields.
+  // Valid dict with name, customizable button and accelerator action fields.
   base::Value::Dict dict1;
   dict1.Set(prefs::kButtonRemappingName, button_remapping1.name);
   dict1.Set(
       prefs::kButtonRemappingCustomizableButton,
       static_cast<int>(button_remapping1.button->get_customizable_button()));
-  dict1.Set(prefs::kButtonRemappingAction,
-            static_cast<int>(button_remapping1.remapping_action->get_action()));
+  dict1.Set(prefs::kButtonRemappingAcceleratorAction,
+            static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()));
 
   // Invalid dict without name field.
   base::Value::Dict dict2;
@@ -475,9 +488,10 @@
   EXPECT_EQ(static_cast<mojom::CustomizableButton>(
                 *dict1.FindInt(prefs::kButtonRemappingCustomizableButton)),
             remapping1->button->get_customizable_button());
-  EXPECT_TRUE(remapping1->remapping_action->is_action());
-  EXPECT_EQ(static_cast<uint>(*dict1.FindInt(prefs::kButtonRemappingAction)),
-            remapping1->remapping_action->get_action());
+  EXPECT_TRUE(remapping1->remapping_action->is_accelerator_action());
+  EXPECT_EQ(static_cast<uint>(
+                *dict1.FindInt(prefs::kButtonRemappingAcceleratorAction)),
+            remapping1->remapping_action->get_accelerator_action());
 
   mojom::ButtonRemappingPtr remapping2 = std::move(array[1]);
   EXPECT_EQ(*dict3.FindString(prefs::kButtonRemappingName), remapping2->name);
diff --git a/ash/system/input_device_settings/pref_handlers/graphics_tablet_pref_handler_unittest.cc b/ash/system/input_device_settings/pref_handlers/graphics_tablet_pref_handler_unittest.cc
index 58c10ef..f3036b86 100644
--- a/ash/system/input_device_settings/pref_handlers/graphics_tablet_pref_handler_unittest.cc
+++ b/ash/system/input_device_settings/pref_handlers/graphics_tablet_pref_handler_unittest.cc
@@ -32,7 +32,8 @@
     /*button=*/
     mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kBack),
     /*remapping_action=*/
-    mojom::RemappingAction::NewAction(ash::AcceleratorAction::kBrightnessDown));
+    mojom::RemappingAction::NewAcceleratorAction(
+        ash::AcceleratorAction::kBrightnessDown));
 const mojom::ButtonRemapping button_remapping2(
     /*name=*/"test2",
     /*button=*/
@@ -201,8 +202,10 @@
       static_cast<int>(button_remapping1.button->get_customizable_button()),
       *tablet_button_remapping.FindInt(
           prefs::kButtonRemappingCustomizableButton));
-  EXPECT_EQ(static_cast<int>(button_remapping1.remapping_action->get_action()),
-            *tablet_button_remapping.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(static_cast<int>(
+                button_remapping1.remapping_action->get_accelerator_action()),
+            *tablet_button_remapping.FindInt(
+                prefs::kButtonRemappingAcceleratorAction));
   ASSERT_NE(nullptr, updated_pen_button_remapping_list);
   ASSERT_EQ(0u, updated_pen_button_remapping_list->size());
 }
@@ -264,7 +267,7 @@
   updated_button_remapping1->button =
       mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kExtra);
   updated_button_remapping1->remapping_action =
-      mojom::RemappingAction::NewAction(
+      mojom::RemappingAction::NewAcceleratorAction(
           ash::AcceleratorAction::kCycleBackwardMru);
   std::vector<mojom::ButtonRemappingPtr> updated_tablet_button_remappings1;
   std::vector<mojom::ButtonRemappingPtr> updated_pen_button_remappings1;
@@ -295,9 +298,9 @@
   EXPECT_EQ(static_cast<int>(
                 updated_button_remapping1->button->get_customizable_button()),
             *updated_dict.FindInt(prefs::kButtonRemappingCustomizableButton));
-  EXPECT_EQ(static_cast<int>(
-                updated_button_remapping1->remapping_action->get_action()),
-            *updated_dict.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(static_cast<int>(updated_button_remapping1->remapping_action
+                                 ->get_accelerator_action()),
+            *updated_dict.FindInt(prefs::kButtonRemappingAcceleratorAction));
 
   // Verify if the graphics tablet1 pen button remappings are updated.
   auto* updated_graphics_tablet1_pen_button_remappings =
diff --git a/ash/system/input_device_settings/pref_handlers/mouse_pref_handler_unittest.cc b/ash/system/input_device_settings/pref_handlers/mouse_pref_handler_unittest.cc
index b527643..caeac33 100644
--- a/ash/system/input_device_settings/pref_handlers/mouse_pref_handler_unittest.cc
+++ b/ash/system/input_device_settings/pref_handlers/mouse_pref_handler_unittest.cc
@@ -46,7 +46,8 @@
     /*button=*/
     mojom::Button::NewCustomizableButton(mojom::CustomizableButton::kBack),
     /*remapping_action=*/
-    mojom::RemappingAction::NewAction(ash::AcceleratorAction::kBrightnessDown));
+    mojom::RemappingAction::NewAcceleratorAction(
+        ash::AcceleratorAction::kBrightnessDown));
 
 const mojom::MouseSettings kMouseSettingsDefault(
     /*swap_right=*/kDefaultSwapRight,
@@ -350,8 +351,10 @@
   EXPECT_EQ(
       static_cast<int>(button_remapping1.button->get_customizable_button()),
       *button_remapping.FindInt(prefs::kButtonRemappingCustomizableButton));
-  EXPECT_EQ(static_cast<int>(button_remapping1.remapping_action->get_action()),
-            *button_remapping.FindInt(prefs::kButtonRemappingAction));
+  EXPECT_EQ(
+      static_cast<int>(
+          button_remapping1.remapping_action->get_accelerator_action()),
+      *button_remapping.FindInt(prefs::kButtonRemappingAcceleratorAction));
 }
 
 TEST_F(MousePrefHandlerTest, UpdateLoginScreenMouseSettings) {
@@ -747,8 +750,10 @@
       *updated_button_remapping_dict.FindInt(
           prefs::kButtonRemappingCustomizableButton));
   EXPECT_EQ(
-      static_cast<int>(button_remappings[0]->remapping_action->get_action()),
-      *updated_button_remapping_dict.FindInt(prefs::kButtonRemappingAction));
+      static_cast<int>(
+          button_remappings[0]->remapping_action->get_accelerator_action()),
+      *updated_button_remapping_dict.FindInt(
+          prefs::kButtonRemappingAcceleratorAction));
 }
 
 TEST_F(MousePrefHandlerTest, InitializeButtonRemappings) {
diff --git a/ash/system/message_center/ash_message_popup_collection.cc b/ash/system/message_center/ash_message_popup_collection.cc
index ae2b10a..925a16d 100644
--- a/ash/system/message_center/ash_message_popup_collection.cc
+++ b/ash/system/message_center/ash_message_popup_collection.cc
@@ -4,6 +4,7 @@
 
 #include "ash/system/message_center/ash_message_popup_collection.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <memory>
 
@@ -66,10 +67,14 @@
     AshMessagePopupCollection* popup_collection)
     : popup_collection_(popup_collection) {
   Shell::Get()->system_tray_notifier()->AddSystemTrayObserver(this);
+  Shell::Get()->tablet_mode_controller()->AddObserver(this);
+  popup_collection_->shelf_->AddObserver(this);
 }
 
 AshMessagePopupCollection::NotifierCollisionHandler::
     ~NotifierCollisionHandler() {
+  popup_collection_->shelf_->RemoveObserver(this);
+  Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
   Shell::Get()->system_tray_notifier()->RemoveSystemTrayObserver(this);
 }
 
@@ -96,14 +101,18 @@
 
     // Reset bounds so popup baseline is updated.
     popup_collection_->ResetBounds();
+  } else {
+    // Record metrics if the bubble stays open.
+    RecordOnTopOfSurfacesPopupCount();
   }
 }
 
 int AshMessagePopupCollection::NotifierCollisionHandler::
-    CalculateBaselineOffset() const {
+    CalculateBaselineOffset() {
   // Baseline pre-notifier collision does not consider corner anchored shelf pod
   // bubbles or slider bubbles to set its offset.
   if (!features::IsNotifierCollisionEnabled()) {
+    surface_type_ = NotifierCollisionSurfaceType::kExtendedHotseat;
     return CalculateExtendedHotseatOffset();
   }
 
@@ -116,13 +125,30 @@
       current_open_shelf_pod_bubble->IsAnchoredToShelfCorner()) {
     // Offset is calculated based on the height of the corner anchored shelf pod
     // bubble, if one is open.
-    return current_open_shelf_pod_bubble->height() +
-           message_center::kMarginBetweenPopups;
+    baseline_offset_ = current_open_shelf_pod_bubble->height() +
+                       message_center::kMarginBetweenPopups;
+    surface_type_ = NotifierCollisionSurfaceType::kShelfPodBubble;
   } else {
+    int slider_offset = CalculateSliderOffset();
+    int hotseat_offset = CalculateExtendedHotseatOffset();
+
     // If no corner anchored shelf pod bubble is open, the offset is calculated
     // based on the visibility of slider bubbles and the extended hotseat.
-    return CalculateSliderOffset() + CalculateExtendedHotseatOffset();
+    baseline_offset_ = slider_offset + hotseat_offset;
+
+    if (slider_offset != 0 && hotseat_offset != 0) {
+      surface_type_ =
+          NotifierCollisionSurfaceType::kSliderBubbleAndExtendedHotseat;
+    } else if (slider_offset != 0) {
+      surface_type_ = NotifierCollisionSurfaceType::kSliderBubble;
+    } else if (hotseat_offset != 0) {
+      surface_type_ = NotifierCollisionSurfaceType::kExtendedHotseat;
+    } else {
+      surface_type_ = NotifierCollisionSurfaceType::kNone;
+    }
   }
+
+  return baseline_offset_;
 }
 
 void AshMessagePopupCollection::NotifierCollisionHandler::
@@ -142,6 +168,8 @@
     return;
   }
 
+  int previous_baseline_offset = baseline_offset_;
+
   // If the popup collection does not fit in the available space when opening a
   // bubble or updating its height, close all popups.
   if (popup_collection_->popup_collection_bounds().height() >
@@ -151,6 +179,11 @@
 
   // Reset bounds so popup baseline is updated.
   popup_collection_->ResetBounds();
+
+  if (baseline_offset_ != previous_baseline_offset && baseline_offset_ != 0) {
+    RecordOnTopOfSurfacesPopupCount();
+    RecordSurfaceType();
+  }
 }
 
 int AshMessagePopupCollection::NotifierCollisionHandler::
@@ -183,16 +216,56 @@
              : 0;
 }
 
-// TODO(b/300003350): Record whenever a bubble or a slider shifts the pop-up
-// baseline up.
 void AshMessagePopupCollection::NotifierCollisionHandler::
-    RecordBaselineShiftedUp(int popup_count) {
+    RecordOnTopOfSurfacesPopupCount() {
+  size_t popup_count = popup_collection_->popup_items().size();
   if (popup_count != 0) {
-    base::UmaHistogramCounts100("Ash.NotificationPopup.BaselineShiftedUp",
-                                popup_count);
+    base::UmaHistogramCounts100(
+        "Ash.NotificationPopup.OnTopOfSurfacesPopupCount", popup_count);
   }
 }
 
+void AshMessagePopupCollection::NotifierCollisionHandler::RecordSurfaceType() {
+  if (popup_collection_->popup_items().size() != 0) {
+    base::UmaHistogramEnumeration("Ash.NotificationPopup.OnTopOfSurfacesType",
+                                  surface_type_);
+  }
+}
+
+void AshMessagePopupCollection::NotifierCollisionHandler::
+    OnTabletModeStarted() {
+  // Reset bounds so pop-up baseline is updated.
+  popup_collection_->ResetBounds();
+}
+
+void AshMessagePopupCollection::NotifierCollisionHandler::OnTabletModeEnded() {
+  // Reset bounds so pop-up baseline is updated.
+  popup_collection_->ResetBounds();
+}
+
+void AshMessagePopupCollection::NotifierCollisionHandler::
+    OnBackgroundTypeChanged(ShelfBackgroundType background_type,
+                            AnimationChangeType change_type) {
+  popup_collection_->ResetBounds();
+}
+
+void AshMessagePopupCollection::NotifierCollisionHandler::
+    OnShelfWorkAreaInsetsChanged() {
+  popup_collection_->UpdateWorkArea();
+}
+
+void AshMessagePopupCollection::NotifierCollisionHandler::OnHotseatStateChanged(
+    HotseatState old_state,
+    HotseatState new_state) {
+  // We only need to take care of `HotseatState::kExtended` state.
+  if (old_state != HotseatState::kExtended &&
+      new_state != HotseatState::kExtended) {
+    return;
+  }
+  popup_collection_->ResetBounds();
+  RecordSurfaceType();
+}
+
 ///////////////////////////////////////////////////////////////////////////////
 // AshMessagePopupCollection:
 
@@ -200,17 +273,16 @@
     : screen_(nullptr), shelf_(shelf) {
   notifier_collision_handler_ =
       std::make_unique<NotifierCollisionHandler>(this);
-
-  shelf_->AddObserver(this);
-  Shell::Get()->tablet_mode_controller()->AddObserver(this);
 }
 
 AshMessagePopupCollection::~AshMessagePopupCollection() {
-  Shell::Get()->tablet_mode_controller()->RemoveObserver(this);
-  shelf_->RemoveObserver(this);
   for (views::Widget* widget : tracked_widgets_)
     widget->RemoveObserver(this);
   CHECK(!views::WidgetObserver::IsInObserverList());
+
+  // Should destruct `notifier_collision_handler_` before all other instances of
+  // this class since the handler depends on some of them.
+  notifier_collision_handler_.reset();
 }
 
 void AshMessagePopupCollection::StartObserving(
@@ -360,16 +432,6 @@
   message_center::MessagePopupCollection::ClosePopupItem(item);
 }
 
-void AshMessagePopupCollection::OnTabletModeStarted() {
-  // Reset bounds so pop-up baseline is updated.
-  ResetBounds();
-}
-
-void AshMessagePopupCollection::OnTabletModeEnded() {
-  // Reset bounds so pop-up baseline is updated.
-  ResetBounds();
-}
-
 bool AshMessagePopupCollection::IsWidgetAPopupNotification(
     views::Widget* widget) {
   for (auto* popup_widget : tracked_widgets_) {
@@ -430,27 +492,6 @@
   ResetBounds();
 }
 
-///////////////////////////////////////////////////////////////////////////////
-// ShelfObserver:
-
-void AshMessagePopupCollection::OnBackgroundTypeChanged(
-    ShelfBackgroundType background_type,
-    AnimationChangeType change_type) {
-  ResetBounds();
-}
-
-void AshMessagePopupCollection::OnShelfWorkAreaInsetsChanged() {
-  UpdateWorkArea();
-}
-
-void AshMessagePopupCollection::OnHotseatStateChanged(HotseatState old_state,
-                                                      HotseatState new_state) {
-  ResetBounds();
-}
-
-///////////////////////////////////////////////////////////////////////////////
-// display::DisplayObserver:
-
 void AshMessagePopupCollection::OnDisplayMetricsChanged(
     const display::Display& display,
     uint32_t metrics) {
diff --git a/ash/system/message_center/ash_message_popup_collection.h b/ash/system/message_center/ash_message_popup_collection.h
index 9064280..f68167a8 100644
--- a/ash/system/message_center/ash_message_popup_collection.h
+++ b/ash/system/message_center/ash_message_popup_collection.h
@@ -45,8 +45,6 @@
     : public display::DisplayObserver,
       public message_center::MessagePopupCollection,
       public message_center::MessageView::Observer,
-      public ShelfObserver,
-      public TabletModeObserver,
       public views::WidgetObserver {
  public:
   // The name that will set for the message popup widget in
@@ -54,6 +52,21 @@
   // message popup widget.
   static const char kMessagePopupWidgetName[];
 
+  // All the types of surfaces that can make popup collection shift up. Used
+  // inside of `NotifierCollisionHandler` for metrics collection. Make sure to
+  // keep this in sync with `NotifierCollisionSurfaceType` in
+  // tools/metrics/histograms/enums.xml.
+  enum class NotifierCollisionSurfaceType {
+    // Default value. Ideally this should never be recorded in the metrics.
+    kNone = 0,
+
+    kShelfPodBubble = 1,
+    kSliderBubble = 2,
+    kExtendedHotseat = 3,
+    kSliderBubbleAndExtendedHotseat = 4,
+    kMaxValue = kSliderBubbleAndExtendedHotseat
+  };
+
   explicit AshMessagePopupCollection(Shelf* shelf);
 
   AshMessagePopupCollection(const AshMessagePopupCollection&) = delete;
@@ -87,10 +100,6 @@
       const message_center::Notification& notification) override;
   void ClosePopupItem(const PopupItem& item) override;
 
-  // TabletModeObserver:
-  void OnTabletModeStarted() override;
-  void OnTabletModeEnded() override;
-
   // Returns true if `widget` is a popup widget belongs to this popup
   // collection.
   bool IsWidgetAPopupNotification(views::Widget* widget);
@@ -106,10 +115,14 @@
   friend class TrayEventFilterTest;
 
   // Handles the collision of popup notifications with corner anchored shelf pod
-  // bubbles, sliders and the extended hotseat by updating the popup baseline.
-  class NotifierCollisionHandler : public SystemTrayObserver {
+  // bubbles, sliders, shelf, and the extended hotseat by updating the popup
+  // baseline.
+  class NotifierCollisionHandler : public ShelfObserver,
+                                   public SystemTrayObserver,
+                                   public TabletModeObserver {
    public:
-    NotifierCollisionHandler(AshMessagePopupCollection* popup_collection);
+    explicit NotifierCollisionHandler(
+        AshMessagePopupCollection* popup_collection);
 
     NotifierCollisionHandler(const NotifierCollisionHandler&) = delete;
     NotifierCollisionHandler& operator=(const NotifierCollisionHandler&) =
@@ -123,7 +136,7 @@
     // Calculates the offset that is applied to the popup collection's baseline.
     // It considers the extended hotseat, corner anchored shelf pod bubbles and
     // slider bubbles.
-    int CalculateBaselineOffset() const;
+    int CalculateBaselineOffset();
 
     // SystemTrayObserver:
     void OnFocusLeavingSystemTray(bool reverse) override {}
@@ -144,11 +157,31 @@
     // corner anchored shelf pod bubble is not open.
     int CalculateSliderOffset() const;
 
-    // Records the number of popups whose baseline was shifted up every time
-    // every time a baseline offset is applied.
-    void RecordBaselineShiftedUp(int popup_count);
+    // Records metrics for the count of popups when it is put on top of a
+    // surface.
+    void RecordOnTopOfSurfacesPopupCount();
+
+    // Records surface type when there are popup(s) on top of that surface.
+    void RecordSurfaceType();
+
+    // TabletModeObserver:
+    void OnTabletModeStarted() override;
+    void OnTabletModeEnded() override;
+
+    // ShelfObserver:
+    void OnBackgroundTypeChanged(ShelfBackgroundType background_type,
+                                 AnimationChangeType change_type) override;
+    void OnShelfWorkAreaInsetsChanged() override;
+    void OnHotseatStateChanged(HotseatState old_state,
+                               HotseatState new_state) override;
 
     raw_ptr<AshMessagePopupCollection> const popup_collection_;
+
+    // Keeps track of the current baseline offset and surface type for metrics
+    // collection purpose.
+    int baseline_offset_ = 0;
+    NotifierCollisionSurfaceType surface_type_ =
+        NotifierCollisionSurfaceType::kNone;
   };
 
   // message_center::MessageView::Observer:
@@ -166,13 +199,6 @@
   // Computes the new work area.
   void UpdateWorkArea();
 
-  // ShelfObserver:
-  void OnBackgroundTypeChanged(ShelfBackgroundType background_type,
-                               AnimationChangeType change_type) override;
-  void OnShelfWorkAreaInsetsChanged() override;
-  void OnHotseatStateChanged(HotseatState old_state,
-                             HotseatState new_state) override;
-
   // display::DisplayObserver:
   void OnDisplayMetricsChanged(const display::Display& display,
                                uint32_t metrics) override;
diff --git a/ash/system/message_center/ash_message_popup_collection_unittest.cc b/ash/system/message_center/ash_message_popup_collection_unittest.cc
index 379d952..e328ac40 100644
--- a/ash/system/message_center/ash_message_popup_collection_unittest.cc
+++ b/ash/system/message_center/ash_message_popup_collection_unittest.cc
@@ -34,6 +34,7 @@
 #include "ash/wm/desks/desks_util.h"
 #include "ash/wm/overview/overview_controller.h"
 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
+#include "ash/wm/tablet_mode/tablet_mode_controller_test_api.h"
 #include "base/command_line.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
@@ -54,6 +55,7 @@
 #include "ui/message_center/views/notification_control_buttons_view.h"
 #include "ui/message_center/views/notification_view_base.h"
 #include "ui/views/controls/button/label_button.h"
+#include "ui/wm/core/window_util.h"
 #include "url/gurl.h"
 
 namespace ash {
@@ -1019,11 +1021,14 @@
   }
 }
 
-// TODO(b/300003350): Reenable test when metric is fixed.
-TEST_P(AshMessagePopupCollectionTest,
-       DISABLED_BaselineShiftedUpHistogramRecorded) {
+TEST_P(AshMessagePopupCollectionTest, HistogramRecordedForShelfPodBubble) {
+  using SurfaceType = AshMessagePopupCollection::NotifierCollisionSurfaceType;
+
   base::HistogramTester histogram_tester;
-  const std::string histogram_name = "Ash.NotificationPopup.BaselineShiftedUp";
+  const std::string popup_count_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesPopupCount";
+  const std::string surface_type_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesType";
 
   AddNotification();
 
@@ -1032,16 +1037,20 @@
 
   // Notification popups will be closed on QS bubble open pre-QS revamp.
   if (!IsQsRevampEnabled()) {
-    histogram_tester.ExpectBucketCount(histogram_name, 1, 0);
+    histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 0);
     return;
   }
 
   if (IsNotifierCollisionEnabled()) {
     // The popup should appear on top of the bubble and histogram is recorded.
-    histogram_tester.ExpectBucketCount(histogram_name, 1, 1);
+    histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 1);
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kShelfPodBubble, 1);
   } else {
     // The popup stays the same if the feature is disabled.
-    histogram_tester.ExpectBucketCount(histogram_name, 1, 0);
+    histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 0);
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kShelfPodBubble, 0);
   }
 
   // Add another notification. Histogram should also be recorded with the
@@ -1049,7 +1058,7 @@
   AddNotification();
   AnimateUntilIdle();
 
-  histogram_tester.ExpectBucketCount(histogram_name, 2,
+  histogram_tester.ExpectBucketCount(popup_count_histogram_name, 2,
                                      IsNotifierCollisionEnabled() ? 1 : 0);
 
   // Close and re-open the bubble. Histogram should be recorded again.
@@ -1057,8 +1066,123 @@
   bubble_widget->CloseNow();
   unified_system_tray->ShowBubble();
 
-  histogram_tester.ExpectBucketCount(histogram_name, 2,
+  histogram_tester.ExpectBucketCount(popup_count_histogram_name, 2,
                                      IsNotifierCollisionEnabled() ? 2 : 0);
+  histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                     SurfaceType::kShelfPodBubble,
+                                     IsNotifierCollisionEnabled() ? 2 : 0);
+}
+
+TEST_P(AshMessagePopupCollectionTest, HistogramRecordedForSliderAndHotseat) {
+  if (!IsQsRevampEnabled()) {
+    return;
+  }
+
+  using SurfaceType = AshMessagePopupCollection::NotifierCollisionSurfaceType;
+
+  base::HistogramTester histogram_tester;
+  const std::string popup_count_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesPopupCount";
+  const std::string surface_type_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesType";
+
+  AddNotification();
+
+  auto* unified_system_tray = GetPrimaryUnifiedSystemTray();
+  unified_system_tray->ShowVolumeSliderBubble();
+
+  if (IsNotifierCollisionEnabled()) {
+    // The popup should appear on top of the bubble and histogram is recorded.
+    histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 1);
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kSliderBubble, 1);
+  } else {
+    // The popup stays the same if the feature is disabled.
+    histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 0);
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kSliderBubble, 0);
+  }
+
+  TabletModeControllerTestApi().EnterTabletMode();
+  std::unique_ptr<aura::Window> window =
+      CreateTestWindow(gfx::Rect(0, 0, 400, 400));
+  wm::ActivateWindow(window.get());
+  ASSERT_EQ(HotseatState::kHidden,
+            GetPrimaryShelf()->shelf_layout_manager()->hotseat_state());
+
+  // Dragging up to show the hotseat.
+  gfx::Rect display_bounds =
+      display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
+  const gfx::Point start = display_bounds.bottom_center();
+  const gfx::Point end = start + gfx::Vector2d(0, -80);
+  GetEventGenerator()->GestureScrollSequence(
+      start, end, /*duration=*/base::Milliseconds(100),
+      /*steps=*/4);
+  ASSERT_EQ(HotseatState::kExtended,
+            GetPrimaryShelf()->shelf_layout_manager()->hotseat_state());
+
+  // Histogram should be recorded accordingly to the adjusted baseline.
+  if (IsNotifierCollisionEnabled()) {
+    histogram_tester.ExpectBucketCount(
+        surface_type_histogram_name,
+        SurfaceType::kSliderBubbleAndExtendedHotseat, 1);
+  } else {
+    histogram_tester.ExpectBucketCount(
+        surface_type_histogram_name,
+        SurfaceType::kSliderBubbleAndExtendedHotseat, 0);
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kExtendedHotseat, 1);
+  }
+
+  unified_system_tray->CloseSecondaryBubbles();
+  if (IsNotifierCollisionEnabled()) {
+    histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                       SurfaceType::kExtendedHotseat, 1);
+  }
+}
+
+TEST_P(AshMessagePopupCollectionTest, HistogramNotRecordedWhenAllPopupsClosed) {
+  if (!IsQsRevampEnabled() || !IsNotifierCollisionEnabled()) {
+    return;
+  }
+
+  using SurfaceType = AshMessagePopupCollection::NotifierCollisionSurfaceType;
+
+  base::HistogramTester histogram_tester;
+  const std::string popup_count_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesPopupCount";
+  const std::string surface_type_histogram_name =
+      "Ash.NotificationPopup.OnTopOfSurfacesType";
+
+  UpdateDisplay("801x800");
+
+  AddNotification(/*has_image=*/true);
+  ASSERT_TRUE(GetLastPopUpAdded());
+
+  auto* unified_system_tray = GetPrimaryUnifiedSystemTray();
+  unified_system_tray->ShowBubble();
+
+  // Histogram is recorded when open the bubble.
+  histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 1);
+  histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                     SurfaceType::kShelfPodBubble, 1);
+
+  // Increase the bubble height so that there's not enough space to display the
+  // popups on top of it. Note that this only works with screen height of 800
+  // (set above), and the test might fail if we change the height of bubble
+  // width or notification width in the future.
+  auto* bubble_widget = unified_system_tray->bubble()->GetBubbleWidget();
+  auto bubble_bounds = bubble_widget->GetWindowBoundsInScreen();
+  bubble_widget->SetBounds(gfx::Rect(bubble_bounds.x(), bubble_bounds.y() - 100,
+                                     bubble_bounds.width(),
+                                     bubble_bounds.height() + 100));
+
+  ASSERT_FALSE(GetLastPopUpAdded());
+
+  // Histogram should not be recorded in this case.
+  histogram_tester.ExpectBucketCount(popup_count_histogram_name, 1, 1);
+  histogram_tester.ExpectBucketCount(surface_type_histogram_name,
+                                     SurfaceType::kShelfPodBubble, 1);
 }
 
 TEST_P(AshMessagePopupCollectionTest, NotificationAddedOnTrayBubbleOpen) {
diff --git a/ash/system/power/power_status.cc b/ash/system/power/power_status.cc
index abf5aaf..3a6e0df 100644
--- a/ash/system/power/power_status.cc
+++ b/ash/system/power/power_status.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 #include <cmath>
 
+#include "ash/constants/ash_features.h"
 #include "ash/public/cpp/power_utils.h"
 #include "ash/resources/vector_icons/vector_icons.h"
 #include "ash/strings/grit/ash_strings.h"
@@ -292,7 +293,12 @@
 }
 
 void PowerStatus::CalculateBatteryImageInfo(BatteryImageInfo* info) const {
-  info->alert_if_low = !IsLinePowerConnected();
+  // We only alert if we are on battery, and battery saver mode is disabled.
+  if (features::IsBatterySaverAvailable()) {
+    info->alert_if_low = !IsLinePowerConnected() && !IsBatterySaverActive();
+  } else {
+    info->alert_if_low = !IsLinePowerConnected();
+  }
 
   if (!IsUsbChargerConnected() && !IsBatteryPresent()) {
     info->icon_badge = &kUnifiedMenuBatteryXIcon;
diff --git a/ash/system/tray/tray_bubble_wrapper.cc b/ash/system/tray/tray_bubble_wrapper.cc
index e46dc40..34c733c 100644
--- a/ash/system/tray/tray_bubble_wrapper.cc
+++ b/ash/system/tray/tray_bubble_wrapper.cc
@@ -10,10 +10,8 @@
 #include "ash/system/tray/tray_background_view.h"
 #include "ash/system/tray/tray_bubble_view.h"
 #include "ash/system/tray/tray_event_filter.h"
-#include "ui/aura/window.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
 #include "ui/views/widget/widget.h"
-#include "ui/wm/core/transient_window_manager.h"
 
 namespace ash {
 
@@ -23,12 +21,6 @@
 
 TrayBubbleWrapper::~TrayBubbleWrapper() {
   if (bubble_widget_) {
-    auto* transient_manager = ::wm::TransientWindowManager::GetOrCreate(
-        bubble_widget_->GetNativeWindow());
-    if (transient_manager) {
-      for (auto* window : transient_manager->transient_children())
-        transient_manager->RemoveTransientChild(window);
-    }
     bubble_widget_->RemoveObserver(this);
     bubble_widget_->Close();
   }
@@ -49,6 +41,7 @@
   TrayBackgroundView::InitializeBubbleAnimations(bubble_widget_);
   bubble_view_->InitializeAndShowBubble();
 
+  // We need to explicitly dismiss app list bubble here due to b/1186479.
   if (!Shell::Get()->tablet_mode_controller()->InTabletMode())
     Shell::Get()->app_list_controller()->DismissAppList();
 
@@ -75,12 +68,6 @@
   bubble_widget_->RemoveObserver(this);
   bubble_widget_ = nullptr;
 
-  // Although the bubble is already closed, the next mouse release event
-  // will invoke PerformAction which reopens the bubble again. To prevent the
-  // reopen, the mouse capture of |tray_| has to be released.
-  // See crbug.com/177075
-  tray_->GetWidget()->GetNativeWindow()->ReleaseCapture();
-
   tray_->HideBubbleWithView(bubble_view_);  // May destroy |bubble_view_|
 }
 
diff --git a/ash/system/tray/tray_event_filter.cc b/ash/system/tray/tray_event_filter.cc
index 8b1f3f9..3a5c6e8 100644
--- a/ash/system/tray/tray_event_filter.cc
+++ b/ash/system/tray/tray_event_filter.cc
@@ -136,13 +136,7 @@
   auto* gained_active_widget =
       views::Widget::GetWidgetForNativeView(gained_active);
 
-  // Don't close the bubble if a transient child is gaining or losing
-  // activation.
-  if (bubble_widget == gained_active_widget ||
-      ::wm::HasTransientAncestor(gained_active,
-                                 bubble_widget->GetNativeWindow()) ||
-      (lost_active && ::wm::HasTransientAncestor(
-                          lost_active, bubble_widget->GetNativeWindow()))) {
+  if (bubble_widget == gained_active_widget) {
     return;
   }
 
diff --git a/ash/system/unified/unified_system_tray_bubble.cc b/ash/system/unified/unified_system_tray_bubble.cc
index 84b4f4d..410b0a6 100644
--- a/ash/system/unified/unified_system_tray_bubble.cc
+++ b/ash/system/unified/unified_system_tray_bubble.cc
@@ -351,11 +351,7 @@
 
   // Don't close the bubble if a transient child is gaining or losing
   // activation.
-  if (bubble_widget_ == gained_active_widget ||
-      ::wm::HasTransientAncestor(gained_active,
-                                 bubble_widget_->GetNativeWindow()) ||
-      (lost_active && ::wm::HasTransientAncestor(
-                          lost_active, bubble_widget_->GetNativeWindow()))) {
+  if (bubble_widget_ == gained_active_widget) {
     return;
   }
 
diff --git a/ash/webui/camera_app_ui/resources/js/mojo/type.ts b/ash/webui/camera_app_ui/resources/js/mojo/type.ts
index 79c374f..4968288 100644
--- a/ash/webui/camera_app_ui/resources/js/mojo/type.ts
+++ b/ash/webui/camera_app_ui/resources/js/mojo/type.ts
@@ -5,7 +5,7 @@
 // This file contains many long export lines that exceed the max-len limit */
 /* eslint-disable max-len */
 
-export {
+export type {
   PointF,
 } from 'chrome://resources/mojo/ui/gfx/geometry/mojom/geometry.mojom-webui.js';
 export {
diff --git a/ash/webui/common/mojom/BUILD.gn b/ash/webui/common/mojom/BUILD.gn
index a1fccad..3fc3564 100644
--- a/ash/webui/common/mojom/BUILD.gn
+++ b/ash/webui/common/mojom/BUILD.gn
@@ -12,5 +12,7 @@
 
   webui_module_path = "/ash/webui/common"
 
+  use_typescript_sources = true
+
   public_deps = [ "//mojo/public/mojom/base" ]
 }
diff --git a/ash/webui/os_feedback_ui/os_feedback_ui.cc b/ash/webui/os_feedback_ui/os_feedback_ui.cc
index 82824a9..60483cc 100644
--- a/ash/webui/os_feedback_ui/os_feedback_ui.cc
+++ b/ash/webui/os_feedback_ui/os_feedback_ui.cc
@@ -57,6 +57,7 @@
       {"feedbackHelpLinkLabel", IDS_FEEDBACK_TOOL_FEEDBACK_HELP_LINK_LABEL},
       {"pageTitle", IDS_FEEDBACK_TOOL_PAGE_TITLE},
       {"privacyNote", IDS_FEEDBACK_TOOL_PRIVACY_NOTE},
+      {"privacyNoteLoggedOut", IDS_FEEDBACK_TOOL_PRIVACY_NOTE_LOGGED_OUT},
       {"mayBeShareWithPartnerNote", IDS_FEEDBACK_TOOL_MAY_BE_SHARED_NOTE},
       {"sendButtonLabel", IDS_FEEDBACK_TOOL_SEND_BUTTON_LABEL},
       // The help content strings are needed for browser tests.
diff --git a/ash/webui/os_feedback_ui/resources/share_data_page.html b/ash/webui/os_feedback_ui/resources/share_data_page.html
index b5ea394..7c987d2 100644
--- a/ash/webui/os_feedback_ui/resources/share_data_page.html
+++ b/ash/webui/os_feedback_ui/resources/share_data_page.html
@@ -343,7 +343,8 @@
       </div>
     </div>
     <!-- Privacy note -->
-    <div id="privacyNote" inner-h-t-m-l="[[privacyNote_]]" class="privacy-note">
+    <div id="privacyNote" inner-h-t-m-l="[[privacyNote_]]"
+        class="privacy-note">
     </div>
     <div id="shareWithPartnerNote" class="privacy-note">
       [[i18n('mayBeShareWithPartnerNote')]]</div>
diff --git a/ash/webui/os_feedback_ui/resources/share_data_page.js b/ash/webui/os_feedback_ui/resources/share_data_page.js
index 39a5572..4a171d686 100644
--- a/ash/webui/os_feedback_ui/resources/share_data_page.js
+++ b/ash/webui/os_feedback_ui/resources/share_data_page.js
@@ -489,12 +489,44 @@
    */
   openLinkInNewWindow_(linkSelector, linkUrl) {
     const linkElement = this.shadowRoot.querySelector(linkSelector);
-    linkElement.setAttribute('href', linkUrl);
-    linkElement.setAttribute('target', '_blank');
+    if (linkElement) {
+      linkElement.setAttribute('href', linkUrl);
+      linkElement.setAttribute('target', '_blank');
+    }
+  }
+
+  /**
+   * When the feedback app is launched from OOBE or the login screen, the
+   * categoryTag is set to "Login".
+   * @returns {boolean} True if the categoryTag is Login.
+   * @private
+   */
+  isUserLoggedOut_() {
+    return this.feedbackContext && this.feedbackContext.categoryTag === 'Login';
   }
 
   /** @private */
   setPrivacyNote_() {
+    if (this.isUserLoggedOut_()) {
+      this.setPrivacyNoteForLoggedOutUsers_();
+    } else {
+      this.setPrivacyNoteForLoggedInUsers_();
+    }
+  }
+
+  /** @private */
+  setPrivacyNoteForLoggedOutUsers_() {
+    this.privacyNote_ = this.i18nAdvanced('privacyNoteLoggedOut', {
+      substitutions: [
+        FEEDBACK_PRIVACY_POLICY_URL,
+        FEEDBACK_TERMS_OF_SERVICE_URL,
+        FEEDBACK_LEGAL_HELP_URL,
+      ],
+    });
+  }
+
+  /** @private */
+  setPrivacyNoteForLoggedInUsers_() {
     this.privacyNote_ = this.i18nAdvanced('privacyNote', {attrs: ['id']});
 
     this.openLinkInNewWindow_('#legalHelpPageUrl', FEEDBACK_LEGAL_HELP_URL);
@@ -597,6 +629,8 @@
           '#performanceTraceLink',
           `chrome://slow_trace/tracing.zip#${this.feedbackContext.traceId}`);
     }
+    // Update the privacy note when the feedback context changed.
+    this.setPrivacyNote_();
   }
 
   /**
diff --git a/ash/webui/scanning/mojom/BUILD.gn b/ash/webui/scanning/mojom/BUILD.gn
index 95be449b..8e61c12 100644
--- a/ash/webui/scanning/mojom/BUILD.gn
+++ b/ash/webui/scanning/mojom/BUILD.gn
@@ -12,6 +12,8 @@
 
   webui_module_path = "/ash/webui/scanning"
 
+  use_typescript_sources = true
+
   public_deps = [ "//mojo/public/mojom/base" ]
 
   cpp_typemaps = [
diff --git a/ash/webui/scanning/resources/BUILD.gn b/ash/webui/scanning/resources/BUILD.gn
index 0fab77d..e09b553 100644
--- a/ash/webui/scanning/resources/BUILD.gn
+++ b/ash/webui/scanning/resources/BUILD.gn
@@ -62,13 +62,13 @@
   ]
 
   mojo_files = [
-    "$root_gen_dir/mojom-webui/ash/webui/common/mojom/accessibility_features.mojom-webui.js",
-    "$root_gen_dir/mojom-webui/ash/webui/scanning/mojom/scanning.mojom-webui.js",
+    "$root_gen_dir/ash/webui/common/mojom/accessibility_features.mojom-webui.ts",
+    "$root_gen_dir/ash/webui/scanning/mojom/scanning.mojom-webui.ts",
   ]
 
   mojo_files_deps = [
-    "//ash/webui/common/mojom:mojom_webui_js",
-    "//ash/webui/scanning/mojom:mojom_webui_js",
+    "//ash/webui/common/mojom:mojom_ts__generator",
+    "//ash/webui/scanning/mojom:mojom_ts__generator",
   ]
 
   ts_tsconfig_base = "tsconfig.json"
diff --git a/ash/wm/overview/overview_utils.cc b/ash/wm/overview/overview_utils.cc
index 3628c37..aaad0931 100644
--- a/ash/wm/overview/overview_utils.cc
+++ b/ash/wm/overview/overview_utils.cc
@@ -219,10 +219,8 @@
   if (auto* split_view_overview_session =
           RootWindowController::ForWindow(target_root)
               ->split_view_overview_session()) {
-    auto* window_state =
-        WindowState::Get(split_view_overview_session->window());
-    CHECK(window_state->IsSnapped());
-    chromeos::WindowStateType window_state_type = window_state->GetStateType();
+    const chromeos::WindowStateType window_state_type =
+        split_view_overview_session->GetWindowStateType();
     state = window_state_type == chromeos::WindowStateType::kPrimarySnapped
                 ? SplitViewController::State::kPrimarySnapped
                 : SplitViewController::State::kSecondarySnapped;
diff --git a/ash/wm/snap_group/snap_group_unittest.cc b/ash/wm/snap_group/snap_group_unittest.cc
index 5b8fe8e..fc117212 100644
--- a/ash/wm/snap_group/snap_group_unittest.cc
+++ b/ash/wm/snap_group/snap_group_unittest.cc
@@ -10,6 +10,7 @@
 #include "ash/public/cpp/test/shell_test_api.h"
 #include "ash/public/cpp/window_properties.h"
 #include "ash/resources/vector_icons/vector_icons.h"
+#include "ash/root_window_controller.h"
 #include "ash/screen_util.h"
 #include "ash/shell.h"
 #include "ash/strings/grit/ash_strings.h"
@@ -59,7 +60,9 @@
 #include "ui/base/hit_test.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/compositor/layer.h"
+#include "ui/display/test/display_manager_test_api.h"
 #include "ui/events/event_constants.h"
+#include "ui/events/keycodes/keyboard_codes_posix.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/point_conversions.h"
 #include "ui/gfx/geometry/rect.h"
@@ -564,6 +567,51 @@
   EXPECT_FALSE(split_view_controller()->InSplitViewMode());
 }
 
+// Tests that when there is one snapped window and overview open, creating a new
+// window, i.e. by clicking the shelf icon, will auto-snap it.
+TEST_F(SnapGroupEntryPointArm1Test, AutoSnapNewWindow) {
+  // Snap `w1` to start split view overview session.
+  std::unique_ptr<aura::Window> w1(CreateAppWindow());
+  SnapOneTestWindow(w1.get(),
+                    /*state_type=*/chromeos::WindowStateType::kPrimarySnapped);
+  EXPECT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
+  EXPECT_TRUE(
+      RootWindowController::ForWindow(w1.get())->split_view_overview_session());
+
+  // Create a new `w2`. Test it auto-snaps and forms a snap group with `w1`.
+  std::unique_ptr<aura::Window> w2(CreateAppWindow());
+  EXPECT_EQ(chromeos::WindowStateType::kSecondarySnapped,
+            WindowState::Get(w2.get())->GetStateType());
+  EXPECT_TRUE(
+      SnapGroupController::Get()->AreWindowsInSnapGroup(w1.get(), w2.get()));
+}
+
+// Tests that removing a display during split view overview session doesn't
+// crash.
+TEST_F(SnapGroupEntryPointArm1Test, RemoveDisplay) {
+  UpdateDisplay("800x600,800x600");
+  display::test::DisplayManagerTestApi display_manager_test(display_manager());
+
+  // Snap `window` on the second display to start split view overview session.
+  std::unique_ptr<aura::Window> window(
+      CreateTestWindowInShellWithBounds(gfx::Rect(900, 0, 100, 100)));
+  WindowState* window_state = WindowState::Get(window.get());
+  const WindowSnapWMEvent snap_type(WM_EVENT_SNAP_PRIMARY);
+  window_state->OnWMEvent(&snap_type);
+  ASSERT_EQ(
+      display_manager_test.GetSecondaryDisplay().id(),
+      display::Screen::GetScreen()->GetDisplayNearestWindow(window.get()).id());
+  EXPECT_EQ(chromeos::WindowStateType::kPrimarySnapped,
+            window_state->GetStateType());
+  EXPECT_TRUE(Shell::Get()->overview_controller()->InOverviewSession());
+  EXPECT_TRUE(RootWindowController::ForWindow(window.get())
+                  ->split_view_overview_session());
+
+  // Disconnect the second display. Test no crash.
+  UpdateDisplay("800x600");
+  base::RunLoop().RunUntilIdle();
+}
+
 // Tests the snap ratio is updated correctly when resizing the windows in a snap
 // group with the split view divider.
 TEST_F(SnapGroupEntryPointArm1Test, SnapRatioTest) {
@@ -1521,6 +1569,7 @@
   SnapOneTestWindow(w1.get(), chromeos::WindowStateType::kPrimarySnapped);
   OverviewController* overview_controller = Shell::Get()->overview_controller();
   EXPECT_TRUE(overview_controller->InOverviewSession());
+  EXPECT_TRUE(GetOverviewSession()->IsWindowInOverview(w2.get()));
   EXPECT_EQ(WindowState::Get(w1.get())->GetStateType(),
             chromeos::WindowStateType::kPrimarySnapped);
   ASSERT_EQ(1u, GetOverviewSession()->grid_list().size());
diff --git a/ash/wm/splitview/auto_snap_controller.cc b/ash/wm/splitview/auto_snap_controller.cc
index 85b9b1c8..cb8f2297 100644
--- a/ash/wm/splitview/auto_snap_controller.cc
+++ b/ash/wm/splitview/auto_snap_controller.cc
@@ -4,23 +4,25 @@
 
 #include "ash/wm/splitview/auto_snap_controller.h"
 
+#include "ash/root_window_controller.h"
 #include "ash/shell.h"
 #include "ash/wm/desks/desks_controller.h"
 #include "ash/wm/mru_window_tracker.h"
 #include "ash/wm/overview/overview_controller.h"
 #include "ash/wm/overview/overview_metrics.h"
+#include "ash/wm/overview/overview_session.h"
 #include "ash/wm/splitview/split_view_controller.h"
+#include "ash/wm/splitview/split_view_overview_session.h"
 #include "ash/wm/splitview/split_view_utils.h"
 #include "ash/wm/window_properties.h"
 #include "ui/wm/public/activation_client.h"
 
 namespace ash {
 
-AutoSnapController::AutoSnapController(
-    SplitViewController* split_view_controller)
-    : split_view_controller_(split_view_controller) {
+AutoSnapController::AutoSnapController(aura::Window* root_window)
+    : root_window_(root_window) {
   Shell::Get()->activation_client()->AddObserver(this);
-  AddWindow(split_view_controller->root_window());
+  AddWindow(root_window);
   for (auto* window :
        Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk)) {
     AddWindow(window);
@@ -93,17 +95,60 @@
 
 void AutoSnapController::OnWindowDestroying(aura::Window* window) {
   RemoveWindow(window);
+  if (window == root_window_) {
+    // The root window is destroyed asynchronously with RootWindowController,
+    // which owns SplitViewController, which owns `this`, so `root_window_` must
+    // be reset earlier, in `OnWindowDestroying()`.
+    root_window_ = nullptr;
+  }
 }
 
 void AutoSnapController::AutoSnapWindowIfNeeded(aura::Window* window) {
   CHECK(window);
-
-  if (window->GetRootWindow() != split_view_controller_->root_window()) {
+  if (window->GetRootWindow() != root_window_) {
     return;
   }
 
-  // We perform an "auto" snapping only if split view mode is active.
-  if (!split_view_controller_->InSplitViewMode()) {
+  if (auto* overview_session =
+          Shell::Get()->overview_controller()->overview_session();
+      overview_session && overview_session->is_shutting_down()) {
+    // `OverviewSession::Shutdown()` may restore window activation and trigger
+    // this; do not auto snap in this case.
+    return;
+  }
+
+  if (auto* split_view_overview_session =
+          RootWindowController::ForWindow(window)
+              ->split_view_overview_session();
+      IsSnapGroupEnabledInClamshellMode() && split_view_overview_session &&
+      split_view_overview_session->window() != window) {
+    WindowState* window_state = WindowState::Get(window);
+    if (!window_state->CanSnap()) {
+      // TODO(b/302212206): Consider showing a toast if the window can't snap.
+      return;
+    }
+    // If `IsSnapGroupEnabledInClamshellMode()` is true, snap via
+    // `WindowState::OnWMEvent()` instead of
+    // `SplitViewController::SnapWindow()`.
+    // Snap to the opposite side of `split_view_overview_session->window()`.
+    const float snap_ratio =
+        1.f - WindowState::Get(split_view_overview_session->window())
+                  ->snap_ratio()
+                  .value_or(chromeos::kDefaultSnapRatio);
+    const WindowSnapWMEvent event(
+        split_view_overview_session->GetWindowStateType() ==
+                chromeos::WindowStateType::kPrimarySnapped
+            ? WM_EVENT_SNAP_SECONDARY
+            : WM_EVENT_SNAP_PRIMARY,
+        snap_ratio, WindowSnapActionSource::kAutoSnapInSplitView);
+    window_state->OnWMEvent(&event);
+    return;
+  }
+
+  auto* split_view_controller = SplitViewController::Get(window);
+  if (!split_view_controller->InSplitViewMode()) {
+    // A window may be activated during mid-drag, during which split view is not
+    // active yet.
     return;
   }
 
@@ -111,7 +156,7 @@
   // happen after floating a window, starting split view, and activating
   // an unfloated window from overview), don't snap.
   if (WindowState::Get(window)->IsFloated() &&
-      split_view_controller_->BothSnapped()) {
+      split_view_controller->BothSnapped()) {
     return;
   }
 
@@ -123,9 +168,9 @@
     return;
   }
 
-  // Only windows that are in the MRU list and are not already in split view
-  // can be auto-snapped.
-  if (split_view_controller_->IsWindowInSplitView(window) ||
+  // Only windows that are in the MRU list and are not already in tablet split
+  // view can be auto-snapped.
+  if (split_view_controller->IsWindowInSplitView(window) ||
       !base::Contains(
           Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk),
           window)) {
@@ -134,12 +179,8 @@
 
   // We do not auto snap windows in clamshell splitview mode if a new window
   // is activated when clamshell splitview mode is active.
-  // TODO(xdai): Handle this logic in `OverviewSession::OnWindowActivating()`.
-  // TODO(michelefan): Considering to auto-snap the second window with one
-  // window snapped when
-  // `SnapGroupController::IsArm1AutomaticallyLockEnabled()`.
-  if (split_view_controller_->InClamshellSplitViewMode()) {
-    if (split_view_controller_->IsWindowInTransitionalState(window)) {
+  if (split_view_controller->InClamshellSplitViewMode()) {
+    if (split_view_controller->IsWindowInTransitionalState(window)) {
       // If `window` is the transitional state (i.e. it's going to be snapped
       // very soon), no need to end overview mode here because
       // `OverviewGrid::OnSplitViewStateChanged()` will handle it when the
@@ -153,7 +194,7 @@
     return;
   }
 
-  CHECK(split_view_controller_->InTabletSplitViewMode());
+  CHECK(split_view_controller->InTabletSplitViewMode());
 
   // Do not snap the window if the activation change is caused by dragging a
   // window.
@@ -166,22 +207,22 @@
   // now). Then if `window` is user-positionable, we should end split view
   // mode, but the cannot snap toast would be inappropriate because the user
   // still might be able to snap `window`.
-  if (split_view_controller_->IsDividerAnimating()) {
+  if (split_view_controller->IsDividerAnimating()) {
     if (WindowState::Get(window)->IsUserPositionable()) {
-      split_view_controller_->EndSplitView(
+      split_view_controller->EndSplitView(
           SplitViewController::EndReason::kUnsnappableWindowActivated);
     }
     return;
   }
 
   absl::optional<float> snap_ratio =
-      split_view_controller_->ComputeSnapRatio(window);
+      split_view_controller->ComputeSnapRatio(window);
 
   // If it's a user positionable window but can't be snapped, end split view
   // mode and show the cannot snap toast.
   if (!snap_ratio) {
     if (WindowState::Get(window)->IsUserPositionable()) {
-      split_view_controller_->EndSplitView(
+      split_view_controller->EndSplitView(
           SplitViewController::EndReason::kUnsnappableWindowActivated);
       ShowAppCannotSnapToast();
     }
@@ -190,9 +231,9 @@
 
   // Snap the window on the non-default side of the screen if split view mode
   // is active.
-  split_view_controller_->SnapWindow(
+  split_view_controller->SnapWindow(
       window,
-      (split_view_controller_->default_snap_position() ==
+      (split_view_controller->default_snap_position() ==
        SplitViewController::SnapPosition::kPrimary)
           ? SplitViewController::SnapPosition::kSecondary
           : SplitViewController::SnapPosition::kPrimary,
@@ -201,7 +242,7 @@
 }
 
 void AutoSnapController::AddWindow(aura::Window* window) {
-  if (split_view_controller_->root_window() != window->GetRootWindow()) {
+  if (window->GetRootWindow() != root_window_) {
     return;
   }
 
diff --git a/ash/wm/splitview/auto_snap_controller.h b/ash/wm/splitview/auto_snap_controller.h
index 5d0892a1..ccf5e20 100644
--- a/ash/wm/splitview/auto_snap_controller.h
+++ b/ash/wm/splitview/auto_snap_controller.h
@@ -11,8 +11,6 @@
 
 namespace ash {
 
-class SplitViewController;
-
 // The controller that observes the window state and performs auto snapping
 // for the window if needed. When it's created, it observes the root window
 // and all windows in a current active desk. When 1) an observed window is
@@ -21,7 +19,7 @@
 class AutoSnapController : public wm::ActivationChangeObserver,
                            public aura::WindowObserver {
  public:
-  explicit AutoSnapController(SplitViewController* split_view_controller);
+  explicit AutoSnapController(aura::Window* root_window);
 
   AutoSnapController(const AutoSnapController&) = delete;
   AutoSnapController& operator=(const AutoSnapController&) = delete;
@@ -47,7 +45,7 @@
   void AddWindow(aura::Window* window);
   void RemoveWindow(aura::Window* window);
 
-  raw_ptr<SplitViewController, ExperimentalAsh> split_view_controller_;
+  raw_ptr<aura::Window> root_window_;
 
   // Tracks observed windows.
   base::ScopedMultiSourceObservation<aura::Window, aura::WindowObserver>
diff --git a/ash/wm/splitview/split_view_controller.cc b/ash/wm/splitview/split_view_controller.cc
index b031b0a..a1da1dcb 100644
--- a/ash/wm/splitview/split_view_controller.cc
+++ b/ash/wm/splitview/split_view_controller.cc
@@ -830,7 +830,15 @@
       Shell::Get()->activation_client()->AddObserver(this);
     }
 
-    auto_snap_controller_ = std::make_unique<AutoSnapController>(this);
+    if (!IsSnapGroupEnabledInClamshellMode()) {
+      // AutoSnapController will end overview in clamshell split view if a
+      // window is not in transitional state. See
+      // `AutoSnapController::AutoSnapWindowIfNeeded()`.
+      // TODO(b/302397864): Handle this logic in
+      // `OverviewSession::OnWindowActivating()`.
+      auto_snap_controller_ =
+          std::make_unique<AutoSnapController>(root_window_);
+    }
 
     if (!IsInTabletMode() && IsInOverviewSession()) {
       // Start the clamshell split overview session. It is too late to create
@@ -1638,12 +1646,6 @@
 
 void SplitViewController::OnDisplayRemoved(
     const display::Display& old_display) {
-  // Display removal always triggers a window activation which ends overview,
-  // and therefore ends clamshell split view, before `OnDisplayRemoved is
-  // called. In clamshell mode, `OverviewController::CanEndOverview` always
-  // returns true, meaning that overview is guaranteed to end successfully.
-  DCHECK(!(InClamshellSplitViewMode() && IsSnapGroupEnabledInClamshellMode()));
-
   // If the `root_window_`is the root window of the display which is going to
   // be removed, there's no need to start overview.
   if (GetRootWindowSettings(root_window_)->display_id ==
diff --git a/ash/wm/splitview/split_view_overview_session.cc b/ash/wm/splitview/split_view_overview_session.cc
index bff3616b..f30f21e 100644
--- a/ash/wm/splitview/split_view_overview_session.cc
+++ b/ash/wm/splitview/split_view_overview_session.cc
@@ -9,6 +9,7 @@
 #include "ash/wm/overview/overview_controller.h"
 #include "ash/wm/overview/overview_grid.h"
 #include "ash/wm/overview/overview_utils.h"
+#include "ash/wm/splitview/auto_snap_controller.h"
 #include "ash/wm/splitview/split_view_divider.h"
 #include "ash/wm/splitview/split_view_utils.h"
 #include "ash/wm/window_resizer.h"
@@ -52,12 +53,23 @@
   auto* window_state = WindowState::Get(window);
   CHECK(window_state && window_state->IsSnapped());
   window_state->AddObserver(this);
+
+  if (IsSnapGroupEnabledInClamshellMode()) {
+    auto_snap_controller_ =
+        std::make_unique<AutoSnapController>(window->GetRootWindow());
+  }
 }
 
 SplitViewOverviewSession::~SplitViewOverviewSession() {
   WindowState::Get(window_)->RemoveObserver(this);
 }
 
+chromeos::WindowStateType SplitViewOverviewSession::GetWindowStateType() const {
+  WindowState* window_state = WindowState::Get(window_);
+  CHECK(window_state->IsSnapped());
+  return window_state->GetStateType();
+}
+
 void SplitViewOverviewSession::OnResizeLoopStarted(aura::Window* window) {
   auto* split_view_controller =
       SplitViewController::Get(window->GetRootWindow());
diff --git a/ash/wm/splitview/split_view_overview_session.h b/ash/wm/splitview/split_view_overview_session.h
index a244070..fb7e35d 100644
--- a/ash/wm/splitview/split_view_overview_session.h
+++ b/ash/wm/splitview/split_view_overview_session.h
@@ -14,6 +14,8 @@
 
 namespace ash {
 
+class AutoSnapController;
+
 // Encapsulates the split view state with a single snapped window and
 // overview, also known as intermediate split view or the snap group creation
 // session.
@@ -37,6 +39,7 @@
   ~SplitViewOverviewSession() override;
 
   const aura::Window* window() const { return window_; }
+  chromeos::WindowStateType GetWindowStateType() const;
 
   // aura::WindowObserver:
   void OnResizeLoopStarted(aura::Window* window) override;
@@ -56,6 +59,9 @@
   // mode.
   std::unique_ptr<ui::PresentationTimeRecorder> presentation_time_recorder_;
 
+  // Observes windows and performs auto snapping if needed in clamshell mode.
+  std::unique_ptr<AutoSnapController> auto_snap_controller_;
+
   // The single snapped window in intermediate split view, with overview on
   // the opposite side.
   const raw_ptr<aura::Window> window_;
diff --git a/base/BUILD.gn b/base/BUILD.gn
index 04513de..4a227a6 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -917,6 +917,7 @@
     "types/expected.h",
     "types/expected_internal.h",
     "types/expected_macros.h",
+    "types/fixed_array.h",
     "types/id_type.h",
     "types/optional_ref.h",
     "types/optional_util.h",
@@ -3426,6 +3427,7 @@
     "types/cxx23_to_underlying_unittest.cc",
     "types/expected_macros_unittest.cc",
     "types/expected_unittest.cc",
+    "types/fixed_array_unittest.cc",
     "types/id_type_unittest.cc",
     "types/optional_ref_unittest.cc",
     "types/optional_unittest.cc",
diff --git a/base/allocator/partition_allocator/BUILD.gn b/base/allocator/partition_allocator/BUILD.gn
index 43ed49a..23e7510 100644
--- a/base/allocator/partition_allocator/BUILD.gn
+++ b/base/allocator/partition_allocator/BUILD.gn
@@ -286,6 +286,7 @@
   configs += [
     ":partition_alloc_implementation",
     ":memory_tagging",
+    "//build/config/compiler:wexit_time_destructors",
   ]
   deps = [ ":allocator_base" ]
   public_configs = []
@@ -322,7 +323,6 @@
     ]
   }
 
-  configs += [ "//build/config/compiler:wexit_time_destructors" ]
   configs -= _remove_configs
   configs += _add_configs
 
@@ -504,7 +504,10 @@
     ":partition_alloc_buildflags",
   ]
 
-  configs += [ ":partition_alloc_implementation" ]
+  configs += [
+    ":partition_alloc_implementation",
+    "//build/config/compiler:wexit_time_destructors",
+  ]
 
   deps = []
   if (is_fuchsia) {
@@ -529,7 +532,10 @@
   sources = []
   deps = [ ":allocator_base" ]
   all_dependent_configs = []
-  configs += [ ":partition_alloc_implementation" ]
+  configs += [
+    ":partition_alloc_implementation",
+    "//build/config/compiler:wexit_time_destructors",
+  ]
 
   configs -= _remove_configs
   configs += _add_configs
@@ -633,6 +639,8 @@
     "pointers/raw_ref.h",
   ]
   sources = []
+  configs += [ "//build/config/compiler:wexit_time_destructors" ]
+
   if (enable_backup_ref_ptr_support) {
     sources += [
       "pointers/raw_ptr_backup_ref_impl.cc",
diff --git a/base/allocator/partition_allocator/dot/address-space.dot b/base/allocator/partition_allocator/dot/address-space.dot
index 456794f..2293e55 100644
--- a/base/allocator/partition_allocator/dot/address-space.dot
+++ b/base/allocator/partition_allocator/dot/address-space.dot
@@ -2,7 +2,6 @@
   node[shape=box]
   edge[dir=both]
   compound = true
-  bgcolor = transparent
   dpi = 192
   nodesep = 0.91
   // Allows aligning nodes in different subgraphs.
diff --git a/base/allocator/partition_allocator/dot/address-space.png b/base/allocator/partition_allocator/dot/address-space.png
index 07b45ba..aee032a 100644
--- a/base/allocator/partition_allocator/dot/address-space.png
+++ b/base/allocator/partition_allocator/dot/address-space.png
Binary files differ
diff --git a/base/allocator/partition_allocator/dot/bucket.dot b/base/allocator/partition_allocator/dot/bucket.dot
index c4d60060..d40d38c 100644
--- a/base/allocator/partition_allocator/dot/bucket.dot
+++ b/base/allocator/partition_allocator/dot/bucket.dot
@@ -1,5 +1,4 @@
 digraph {
-  graph[bgcolor=transparent]
   node[shape=plaintext]
   edge[style=dashed, color=crimson]
 
diff --git a/base/allocator/partition_allocator/dot/bucket.png b/base/allocator/partition_allocator/dot/bucket.png
index bf7374b1b..4ed7b75 100644
--- a/base/allocator/partition_allocator/dot/bucket.png
+++ b/base/allocator/partition_allocator/dot/bucket.png
Binary files differ
diff --git a/base/allocator/partition_allocator/dot/layers.dot b/base/allocator/partition_allocator/dot/layers.dot
index 27ea7c6c..bf6eea5f 100644
--- a/base/allocator/partition_allocator/dot/layers.dot
+++ b/base/allocator/partition_allocator/dot/layers.dot
@@ -1,5 +1,4 @@
 digraph G {
-  graph[bgcolor=transparent]
   node[shape=box,style="filled,rounded",color=deepskyblue]
 
   subgraph cluster_tc {
diff --git a/base/allocator/partition_allocator/dot/layers.png b/base/allocator/partition_allocator/dot/layers.png
index 80c78e2..c2794f1 100644
--- a/base/allocator/partition_allocator/dot/layers.png
+++ b/base/allocator/partition_allocator/dot/layers.png
Binary files differ
diff --git a/base/allocator/partition_allocator/dot/super-page.dot b/base/allocator/partition_allocator/dot/super-page.dot
index 068392d8..7f82c9ca 100644
--- a/base/allocator/partition_allocator/dot/super-page.dot
+++ b/base/allocator/partition_allocator/dot/super-page.dot
@@ -1,5 +1,4 @@
 digraph G {
-  graph[bgcolor=transparent]
   node[shape=plaintext]
   edge[style=dashed]
 
diff --git a/base/allocator/partition_allocator/dot/super-page.png b/base/allocator/partition_allocator/dot/super-page.png
index 0bfd69ab..0d1a6962 100644
--- a/base/allocator/partition_allocator/dot/super-page.png
+++ b/base/allocator/partition_allocator/dot/super-page.png
Binary files differ
diff --git a/base/allocator/partition_allocator/flags.h b/base/allocator/partition_allocator/flags.h
index ec70f76..7914229 100644
--- a/base/allocator/partition_allocator/flags.h
+++ b/base/allocator/partition_allocator/flags.h
@@ -46,6 +46,12 @@
   return (superset & subset) == subset;
 }
 
+// Removes flags `target` from `from`.
+template <typename EnumType>
+constexpr inline IfEnum<EnumType> RemoveFlags(EnumType from, EnumType target) {
+  return from & ~target;
+}
+
 // A macro to define binary arithmetic over `EnumType`.
 // Use inside `namespace partition_alloc::internal`.
 #define PA_DEFINE_OPERATORS_FOR_FLAGS(EnumType)                              \
diff --git a/base/allocator/partition_allocator/pointers/raw_ptr.h b/base/allocator/partition_allocator/pointers/raw_ptr.h
index 713cb44c..ff073ec2 100644
--- a/base/allocator/partition_allocator/pointers/raw_ptr.h
+++ b/base/allocator/partition_allocator/pointers/raw_ptr.h
@@ -13,6 +13,7 @@
 #include <type_traits>
 #include <utility>
 
+#include "base/allocator/partition_allocator/flags.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/compiler_specific.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/component_export.h"
 #include "base/allocator/partition_allocator/partition_alloc_base/cxx20_is_constant_evaluated.h"
@@ -68,7 +69,7 @@
 class Calculator;
 }
 
-namespace base {
+namespace partition_alloc::internal {
 
 // NOTE: All methods should be `PA_ALWAYS_INLINE`. raw_ptr is meant to be a
 // lightweight replacement of a raw pointer, hence performance is critical.
@@ -121,40 +122,23 @@
   //
   // Test only.
   kDummyForTest = (1 << 11),
-};
 
-// Used to combine RawPtrTraits:
-constexpr RawPtrTraits operator|(RawPtrTraits a, RawPtrTraits b) {
-  return static_cast<RawPtrTraits>(static_cast<unsigned>(a) |
-                                   static_cast<unsigned>(b));
-}
-constexpr RawPtrTraits operator&(RawPtrTraits a, RawPtrTraits b) {
-  return static_cast<RawPtrTraits>(static_cast<unsigned>(a) &
-                                   static_cast<unsigned>(b));
-}
-constexpr RawPtrTraits operator~(RawPtrTraits a) {
-  return static_cast<RawPtrTraits>(~static_cast<unsigned>(a));
-}
+  kAllMask = kMayDangle | kDisableHooks | kAllowPtrArithmetic |
+             kExperimentalAsh | kUseCountingWrapperForTest | kDummyForTest,
+};
+// Template specialization to use |PA_DEFINE_OPERATORS_FOR_FLAGS| without
+// |kMaxValue| declaration.
+template <>
+constexpr inline RawPtrTraits kAllFlags<RawPtrTraits> = RawPtrTraits::kAllMask;
+PA_DEFINE_OPERATORS_FOR_FLAGS(RawPtrTraits);
+
+}  // namespace partition_alloc::internal
+
+namespace base {
+using partition_alloc::internal::RawPtrTraits;
 
 namespace raw_ptr_traits {
 
-constexpr bool Contains(RawPtrTraits a, RawPtrTraits b) {
-  return (a & b) != RawPtrTraits::kEmpty;
-}
-
-constexpr RawPtrTraits Remove(RawPtrTraits a, RawPtrTraits b) {
-  return a & ~b;
-}
-
-constexpr bool AreValid(RawPtrTraits traits) {
-  return Remove(traits, RawPtrTraits::kMayDangle | RawPtrTraits::kDisableHooks |
-                            RawPtrTraits::kAllowPtrArithmetic |
-                            RawPtrTraits::kExperimentalAsh |
-                            RawPtrTraits::kUseCountingWrapperForTest |
-                            RawPtrTraits::kDummyForTest) ==
-         RawPtrTraits::kEmpty;
-}
-
 // IsSupportedType<T>::value answers whether raw_ptr<T> 1) compiles and 2) is
 // always safe at runtime.  Templates that may end up using `raw_ptr<T>` should
 // use IsSupportedType to ensure that raw_ptr is not used with unsupported
@@ -231,20 +215,19 @@
 #if BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
 template <RawPtrTraits Traits>
 using UnderlyingImplForTraits = internal::RawPtrBackupRefImpl<
-    /*AllowDangling=*/Contains(Traits, RawPtrTraits::kMayDangle),
-    /*ExperimentalAsh=*/Contains(Traits, RawPtrTraits::kExperimentalAsh)>;
+    /*AllowDangling=*/ContainsFlags(Traits, RawPtrTraits::kMayDangle),
+    /*ExperimentalAsh=*/ContainsFlags(Traits, RawPtrTraits::kExperimentalAsh)>;
 
 #elif BUILDFLAG(USE_ASAN_UNOWNED_PTR)
 template <RawPtrTraits Traits>
-using UnderlyingImplForTraits =
-    internal::RawPtrAsanUnownedImpl<Contains(Traits,
-                                             RawPtrTraits::kAllowPtrArithmetic),
-                                    Contains(Traits, RawPtrTraits::kMayDangle)>;
+using UnderlyingImplForTraits = internal::RawPtrAsanUnownedImpl<
+    ContainsFlags(Traits, RawPtrTraits::kAllowPtrArithmetic),
+    ContainsFlags(Traits, RawPtrTraits::kMayDangle)>;
 
 #elif BUILDFLAG(USE_HOOKABLE_RAW_PTR)
 template <RawPtrTraits Traits>
 using UnderlyingImplForTraits = internal::RawPtrHookableImpl<
-    /*EnableHooks=*/!Contains(Traits, RawPtrTraits::kDisableHooks)>;
+    /*EnableHooks=*/!ContainsFlags(Traits, RawPtrTraits::kDisableHooks)>;
 
 #else
 template <RawPtrTraits Traits>
@@ -253,7 +236,7 @@
 
 constexpr bool IsPtrArithmeticAllowed(RawPtrTraits Traits) {
 #if BUILDFLAG(ENABLE_POINTER_ARITHMETIC_TRAIT_CHECK)
-  return Contains(Traits, RawPtrTraits::kAllowPtrArithmetic);
+  return ContainsFlags(Traits, RawPtrTraits::kAllowPtrArithmetic);
 #else
   return true;
 #endif
@@ -276,9 +259,9 @@
 // wrapper.
 template <RawPtrTraits Traits>
 using ImplForTraits = std::conditional_t<
-    Contains(Traits, RawPtrTraits::kUseCountingWrapperForTest),
+    ContainsFlags(Traits, RawPtrTraits::kUseCountingWrapperForTest),
     test::RawPtrCountingImplWrapperForTest<
-        Remove(Traits, RawPtrTraits::kUseCountingWrapperForTest)>,
+        RemoveFlags(Traits, RawPtrTraits::kUseCountingWrapperForTest)>,
     UnderlyingImplForTraits<Traits>>;
 
 }  // namespace raw_ptr_traits
@@ -306,7 +289,7 @@
   static_assert(std::is_same_v<Impl, internal::RawPtrNoOpImpl>);
 #endif  // !BUILDFLAG(USE_PARTITION_ALLOC)
 
-  static_assert(raw_ptr_traits::AreValid(Traits), "Unknown raw_ptr trait(s)");
+  static_assert(AreValidFlags(Traits), "Unknown raw_ptr trait(s)");
   static_assert(raw_ptr_traits::IsSupportedType<T>::value,
                 "raw_ptr<T> doesn't work with this kind of pointee type T");
 
@@ -916,7 +899,7 @@
 
 template <typename T, RawPtrTraits Traits>
 inline constexpr bool IsRawPtrMayDangleV<raw_ptr<T, Traits>> =
-    raw_ptr_traits::Contains(Traits, RawPtrTraits::kMayDangle);
+    ContainsFlags(Traits, RawPtrTraits::kMayDangle);
 
 // Template helpers for working with T* or raw_ptr<T>.
 template <typename T>
@@ -1015,6 +998,13 @@
 // This is not meant to be added manually. You can ignore this flag.
 constexpr auto LeakedDanglingUntriaged = base::RawPtrTraits::kMayDangle;
 
+// Temporary annotation for new pointers added during the renderer rewrite.
+// TODO(crbug.com/1444624): Find pre-existing dangling pointers and remove
+// this annotation.
+//
+// DO NOT ADD new occurrences of this.
+constexpr auto ExperimentalRenderer = base::RawPtrTraits::kMayDangle;
+
 // Public verson used in callbacks arguments when it is known that they might
 // receive dangling pointers. In any other cases, please
 // use one of:
diff --git a/base/allocator/partition_allocator/pointers/raw_ptr_counting_impl_wrapper_for_test.h b/base/allocator/partition_allocator/pointers/raw_ptr_counting_impl_wrapper_for_test.h
index 74fbdbaf..93b0d7d 100644
--- a/base/allocator/partition_allocator/pointers/raw_ptr_counting_impl_wrapper_for_test.h
+++ b/base/allocator/partition_allocator/pointers/raw_ptr_counting_impl_wrapper_for_test.h
@@ -20,9 +20,8 @@
 template <RawPtrTraits Traits>
 struct RawPtrCountingImplWrapperForTest
     : public raw_ptr_traits::ImplForTraits<Traits> {
-  static_assert(
-      !raw_ptr_traits::Contains(Traits,
-                                RawPtrTraits::kUseCountingWrapperForTest));
+  static_assert(!ContainsFlags(Traits,
+                               RawPtrTraits::kUseCountingWrapperForTest));
 
   using SuperImpl = typename raw_ptr_traits::ImplForTraits<Traits>;
 
diff --git a/base/allocator/partition_allocator/pointers/raw_ptr_unittest.nc b/base/allocator/partition_allocator/pointers/raw_ptr_unittest.nc
index 77767717..8ab2d90 100644
--- a/base/allocator/partition_allocator/pointers/raw_ptr_unittest.nc
+++ b/base/allocator/partition_allocator/pointers/raw_ptr_unittest.nc
@@ -25,15 +25,14 @@
 #if defined(NCTEST_INVALID_RAW_PTR_TRAIT)  // [r"Unknown raw_ptr trait\(s\)"]
 
 void WontCompile() {
-  constexpr auto InvalidRawPtrTrait =
-      ~base::RawPtrTraits::kEmpty;
+  constexpr auto InvalidRawPtrTrait = static_cast<base::RawPtrTraits>(-1);
   raw_ptr<int, InvalidRawPtrTrait> p;
 }
 
 #elif defined(NCTEST_INVALID_RAW_PTR_TRAIT_OF_MANY)  // [r"Unknown raw_ptr trait\(s\)"]
 
 void WontCompile() {
-  constexpr auto InvalidRawPtrTrait = ~base::RawPtrTraits::kEmpty;
+  constexpr auto InvalidRawPtrTrait = static_cast<base::RawPtrTraits>(-1);
   raw_ptr<int, DisableDanglingPtrDetection | InvalidRawPtrTrait>
       p;
 }
@@ -212,35 +211,35 @@
 #endif  // !BUILDFLAG(HAS_64_BIT_POINTERS)
 }
 
-#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)0U == \(\(base::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)0U == \(\(partition_alloc::internal::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr = new int(3);
   [[maybe_unused]] raw_ptr<int> ptr2(ptr);
 }
 
-#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)1U == \(\(base::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)1U == \(\(partition_alloc::internal::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kDummyForTest> ptr = new int(3);
   [[maybe_unused]] raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr2(ptr);
 }
 
-#elif defined(NCTEST_CROSS_KIND_MOVE_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)0U == \(\(base::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_MOVE_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)0U == \(\(partition_alloc::internal::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr = new int(3);
   [[maybe_unused]] raw_ptr<int> ptr2(std::move(ptr));
 }
 
-#elif defined(NCTEST_CROSS_KIND_MOVE_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)1U == \(\(base::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_MOVE_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)1U == \(\(partition_alloc::internal::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kDummyForTest> ptr = new int(3);
   [[maybe_unused]] raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr2(std::move(ptr));
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)0U == \(\(base::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)0U == \(\(partition_alloc::internal::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr = new int(3);
@@ -248,7 +247,7 @@
   ptr2 = ptr;
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)1U == \(\(base::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)1U == \(\(partition_alloc::internal::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kDummyForTest> ptr = new int(3);
@@ -256,7 +255,7 @@
   ptr2 = ptr;
 }
 
-#elif defined(NCTEST_CROSS_KIND_MOVE_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)0U == \(\(base::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_MOVE_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)0U == \(\(partition_alloc::internal::RawPtrTraits\)1U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kMayDangle> ptr = new int(3);
@@ -264,7 +263,7 @@
   ptr2 = std::move(ptr);
 }
 
-#elif defined(NCTEST_CROSS_KIND_MOVE_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)1U == \(\(base::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_MOVE_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)1U == \(\(partition_alloc::internal::RawPtrTraits\)2048U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   raw_ptr<int, base::RawPtrTraits::kDummyForTest> ptr = new int(3);
diff --git a/base/allocator/partition_allocator/pointers/raw_ref_unittest.nc b/base/allocator/partition_allocator/pointers/raw_ref_unittest.nc
index f60709f..9cbdd1b4 100644
--- a/base/allocator/partition_allocator/pointers/raw_ref_unittest.nc
+++ b/base/allocator/partition_allocator/pointers/raw_ref_unittest.nc
@@ -9,7 +9,7 @@
 
 namespace {
 
-#if defined(NCTEST_CROSS_KIND_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)4U == \(\(base::RawPtrTraits\)5U | RawPtrTraits::kMayDangle\)'"]
+#if defined(NCTEST_CROSS_KIND_CONVERSION_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)4U == \(\(partition_alloc::internal::RawPtrTraits\)5U | RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -17,7 +17,7 @@
   [[maybe_unused]] raw_ref<int> ref2(ref);
 }
 
-#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)5U == \(\(base::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_CONVERSION_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)5U == \(\(partition_alloc::internal::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -25,7 +25,7 @@
   [[maybe_unused]] raw_ref<int, base::RawPtrTraits::kMayDangle> ref2(ref);
 }
 
-#elif defined(NCTEST_CROSS_KIND_CONVERSION_MOVE_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)4U == \(\(base::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_CONVERSION_MOVE_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)4U == \(\(partition_alloc::internal::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -33,7 +33,7 @@
   [[maybe_unused]] raw_ref<int> ref2(std::move(ref));
 }
 
-#elif defined(NCTEST_CROSS_KIND_CONVERSION_MOVE_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)5U == \(\(base::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_CONVERSION_MOVE_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)5U == \(\(partition_alloc::internal::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -41,7 +41,7 @@
   [[maybe_unused]] raw_ref<int, base::RawPtrTraits::kMayDangle> ref2(std::move(ref));
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)4U == \(\(base::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)4U == \(\(partition_alloc::internal::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -50,7 +50,7 @@
   ref2 = ref;
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)5U == \(\(base::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)5U == \(\(partition_alloc::internal::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -59,7 +59,7 @@
   ref2 = ref;
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_MOVE_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)4U == \(\(base::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_MOVE_FROM_MAY_DANGLE) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)4U == \(\(partition_alloc::internal::RawPtrTraits\)5U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
@@ -68,7 +68,7 @@
   ref2 = std::move(ref);
 }
 
-#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_MOVE_FROM_DUMMY) // [r"static assertion failed due to requirement '\(base::RawPtrTraits\)5U == \(\(base::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
+#elif defined(NCTEST_CROSS_KIND_ASSIGNMENT_MOVE_FROM_DUMMY) // [r"static assertion failed due to requirement '\(partition_alloc::internal::RawPtrTraits\)5U == \(\(partition_alloc::internal::RawPtrTraits\)2052U \| RawPtrTraits::kMayDangle\)'"]
 
 void WontCompile() {
   int x = 123;
diff --git a/base/metrics/dummy_histogram.cc b/base/metrics/dummy_histogram.cc
index 1de4814..dd19905 100644
--- a/base/metrics/dummy_histogram.cc
+++ b/base/metrics/dummy_histogram.cc
@@ -56,6 +56,10 @@
   std::unique_ptr<SampleCountIterator> ExtractingIterator() override {
     return std::make_unique<DummySampleCountIterator>();
   }
+  bool IsDefinitelyEmpty() const override {
+    NOTREACHED();
+    return true;
+  }
   bool AddSubtractImpl(SampleCountIterator* iter, Operator op) override {
     return true;
   }
diff --git a/base/metrics/histogram_samples.cc b/base/metrics/histogram_samples.cc
index 58069b3..76aa633 100644
--- a/base/metrics/histogram_samples.cc
+++ b/base/metrics/histogram_samples.cc
@@ -293,6 +293,10 @@
   DCHECK(success);
 }
 
+bool HistogramSamples::IsDefinitelyEmpty() const {
+  return sum() == 0 && redundant_count() == 0;
+}
+
 void HistogramSamples::Serialize(Pickle* pickle) const {
   pickle->WriteInt64(sum());
   pickle->WriteInt(redundant_count());
diff --git a/base/metrics/histogram_samples.h b/base/metrics/histogram_samples.h
index 3fe25047..a2f4a28a 100644
--- a/base/metrics/histogram_samples.h
+++ b/base/metrics/histogram_samples.h
@@ -165,6 +165,22 @@
   // enforced by a DCHECK in the destructor).
   virtual std::unique_ptr<SampleCountIterator> ExtractingIterator() = 0;
 
+  // Returns true if |this| is empty (has no samples, has a |sum| of zero, and
+  // has a |redundant_count| of zero), which is indicative that the caller does
+  // not need to process |this|.
+  // - Note 1: This should only be called when |this| is only manipulated on one
+  // thread at a time (e.g., the underlying data does not change on another
+  // thread). If this is not the case, then the returned value cannot be trusted
+  // at all.
+  // - Note 2: For performance reasons, this is not guaranteed to return the
+  // correct value. If false is returned, |this| may or may not be empty.
+  // However, if true is returned, then |this| is guaranteed to be empty (no
+  // false positives). Of course, this assumes that "Note 1" is respected.
+  //  - Note 3: The base implementation of this method checks for |sum| and
+  // |redundant_count|, but the child implementations should also check for
+  // samples.
+  virtual bool IsDefinitelyEmpty() const;
+
   void Serialize(Pickle* pickle) const;
 
   // Returns ASCII representation of histograms data for histogram samples.
diff --git a/base/metrics/histogram_unittest.cc b/base/metrics/histogram_unittest.cc
index 626909f..5cde264 100644
--- a/base/metrics/histogram_unittest.cc
+++ b/base/metrics/histogram_unittest.cc
@@ -307,6 +307,29 @@
   EXPECT_EQ(samples->TotalCount(), samples->redundant_count());
 }
 
+// Check that IsDefinitelyEmpty() works with the results of SnapshotDelta().
+TEST_P(HistogramTest, IsDefinitelyEmpty_SnapshotDelta) {
+  HistogramBase* histogram = Histogram::FactoryGet("DeltaHistogram", 1, 64, 8,
+                                                   HistogramBase::kNoFlags);
+  // No samples initially.
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+
+  // Verify when |histogram| is using SingleSample.
+  histogram->Add(1);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  histogram->Add(10);
+  histogram->Add(10);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+
+  // Verify when |histogram| uses a counts array instead of SingleSample.
+  histogram->Add(1);
+  histogram->Add(50);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+}
+
 TEST_P(HistogramTest, ExponentialRangesTest) {
   // Check that we got a nice exponential when there was enough room.
   BucketRanges ranges(9);
diff --git a/base/metrics/persistent_histogram_allocator.cc b/base/metrics/persistent_histogram_allocator.cc
index d30f105..dcf26dc 100644
--- a/base/metrics/persistent_histogram_allocator.cc
+++ b/base/metrics/persistent_histogram_allocator.cc
@@ -447,24 +447,14 @@
     HistogramBase* histogram) {
   DCHECK(histogram);
 
-  HistogramBase* existing = GetOrCreateStatisticsRecorderHistogram(histogram);
-  if (!existing) {
-    // The above should never fail but if it does, no real harm is done.
-    // The data won't be merged but it also won't be recorded as merged
-    // so a future try, if successful, will get what was missed. If it
-    // continues to fail, some metric data will be lost but that is better
-    // than crashing.
+  // Return immediately if the histogram has no samples since the last delta
+  // snapshot. This is to prevent looking up or registering the histogram with
+  // the StatisticsRecorder, which requires acquiring a lock.
+  std::unique_ptr<HistogramSamples> samples = histogram->SnapshotDelta();
+  if (samples->IsDefinitelyEmpty()) {
     return;
   }
 
-  // Merge the delta from the passed object to the one in the SR.
-  existing->AddSamples(*histogram->SnapshotDelta());
-}
-
-void PersistentHistogramAllocator::MergeHistogramFinalDeltaToStatisticsRecorder(
-    const HistogramBase* histogram) {
-  DCHECK(histogram);
-
   HistogramBase* existing = GetOrCreateStatisticsRecorderHistogram(histogram);
   if (!existing) {
     // The above should never fail but if it does, no real harm is done.
@@ -473,7 +463,30 @@
   }
 
   // Merge the delta from the passed object to the one in the SR.
-  existing->AddSamples(*histogram->SnapshotFinalDelta());
+  existing->AddSamples(*samples);
+}
+
+void PersistentHistogramAllocator::MergeHistogramFinalDeltaToStatisticsRecorder(
+    const HistogramBase* histogram) {
+  DCHECK(histogram);
+
+  // Return immediately if the histogram has no samples. This is to prevent
+  // looking up or registering the histogram with the StatisticsRecorder, which
+  // requires acquiring a lock.
+  std::unique_ptr<HistogramSamples> samples = histogram->SnapshotFinalDelta();
+  if (samples->IsDefinitelyEmpty()) {
+    return;
+  }
+
+  HistogramBase* existing = GetOrCreateStatisticsRecorderHistogram(histogram);
+  if (!existing) {
+    // The above should never fail but if it does, no real harm is done.
+    // Some metric data will be lost but that is better than crashing.
+    return;
+  }
+
+  // Merge the delta from the passed object to the one in the SR.
+  existing->AddSamples(*samples);
 }
 
 std::unique_ptr<PersistentSampleMapRecords>
diff --git a/base/metrics/persistent_histogram_allocator_unittest.cc b/base/metrics/persistent_histogram_allocator_unittest.cc
index 8baf666..fa5bd06d 100644
--- a/base/metrics/persistent_histogram_allocator_unittest.cc
+++ b/base/metrics/persistent_histogram_allocator_unittest.cc
@@ -358,6 +358,113 @@
   EXPECT_EQ(1, snapshot->GetCount(7));
 }
 
+// Verify that when merging histograms from an allocator with the global
+// StatisticsRecorder, if the histogram has no samples to be merged, then it
+// is skipped (no lookup/registration of the histogram with the SR).
+TEST_F(PersistentHistogramAllocatorTest,
+       StatisticsRecorderMerge_IsDefinitelyEmpty) {
+  const size_t global_sr_initial_histogram_count =
+      StatisticsRecorder::GetHistogramCount();
+  const size_t global_sr_initial_bucket_ranges_count =
+      StatisticsRecorder::GetBucketRanges().size();
+
+  // Create a local StatisticsRecorder in which the newly created histogram
+  // will be recorded. The global allocator must be replaced after because the
+  // act of releasing will cause the active SR to forget about all histograms
+  // in the released memory.
+  std::unique_ptr<StatisticsRecorder> local_sr =
+      StatisticsRecorder::CreateTemporaryForTesting();
+  EXPECT_EQ(0U, StatisticsRecorder::GetHistogramCount());
+  std::unique_ptr<GlobalHistogramAllocator> old_allocator =
+      GlobalHistogramAllocator::ReleaseForTesting();
+  GlobalHistogramAllocator::CreateWithLocalMemory(kAllocatorMemorySize, 0, "");
+  ASSERT_TRUE(GlobalHistogramAllocator::Get());
+
+  // Create a bunch of histograms, and call SnapshotDelta() on all of them so
+  // that their next SnapshotDelta() calls return an empty HistogramSamples.
+  LinearHistogram::FactoryGet("SRTLinearHistogram1", 1, 10, 10, 0);
+  HistogramBase* histogram2 =
+      LinearHistogram::FactoryGet("SRTLinearHistogram2", 1, 10, 10, 0);
+  histogram2->Add(3);
+  histogram2->SnapshotDelta();
+  HistogramBase* histogram3 =
+      LinearHistogram::FactoryGet("SRTLinearHistogram3", 1, 10, 10, 0);
+  histogram3->Add(1);
+  histogram3->Add(10);
+  histogram3->SnapshotDelta();
+  SparseHistogram::FactoryGet("SRTSparseHistogram1", 0);
+  HistogramBase* sparse_histogram2 =
+      SparseHistogram::FactoryGet("SRTSparseHistogram2", 0);
+  sparse_histogram2->Add(3);
+  sparse_histogram2->SnapshotDelta();
+  HistogramBase* sparse_histogram3 =
+      SparseHistogram::FactoryGet("SRTSparseHistogram3", 0);
+  sparse_histogram3->Add(1);
+  sparse_histogram3->Add(10);
+  sparse_histogram3->SnapshotDelta();
+
+  EXPECT_EQ(6U, StatisticsRecorder::GetHistogramCount());
+
+  // Destroy the local SR and ensure that we're back to the initial state and
+  // restore the global allocator. Histograms created in the local SR will
+  // become unmanaged.
+  std::unique_ptr<GlobalHistogramAllocator> new_allocator =
+      GlobalHistogramAllocator::ReleaseForTesting();
+  local_sr.reset();
+  EXPECT_EQ(global_sr_initial_histogram_count,
+            StatisticsRecorder::GetHistogramCount());
+  EXPECT_EQ(global_sr_initial_bucket_ranges_count,
+            StatisticsRecorder::GetBucketRanges().size());
+  GlobalHistogramAllocator::Set(std::move(old_allocator));
+
+  // Create a "recovery" allocator using the same memory as the local one.
+  PersistentHistogramAllocator recovery1(
+      std::make_unique<PersistentMemoryAllocator>(
+          const_cast<void*>(new_allocator->memory_allocator()->data()),
+          new_allocator->memory_allocator()->size(), 0, 0, "",
+          PersistentMemoryAllocator::kReadWrite));
+  PersistentHistogramAllocator::Iterator histogram_iter1(&recovery1);
+
+  // Get the histograms that were created locally (and forgotten) and attempt
+  // to merge them into the global SR. Since their delta are all empty, nothing
+  // should end up being registered with the SR.
+  while (true) {
+    std::unique_ptr<HistogramBase> recovered = histogram_iter1.GetNext();
+    if (!recovered) {
+      break;
+    }
+
+    recovery1.MergeHistogramDeltaToStatisticsRecorder(recovered.get());
+    HistogramBase* found =
+        StatisticsRecorder::FindHistogram(recovered->histogram_name());
+    EXPECT_FALSE(found);
+  }
+  EXPECT_EQ(global_sr_initial_histogram_count,
+            StatisticsRecorder::GetHistogramCount());
+
+  // Same as above, but with MergeHistogramFinalDeltaToStatisticsRecorder()
+  // instead of MergeHistogramDeltaToStatisticsRecorder().
+  PersistentHistogramAllocator recovery2(
+      std::make_unique<PersistentMemoryAllocator>(
+          const_cast<void*>(new_allocator->memory_allocator()->data()),
+          new_allocator->memory_allocator()->size(), 0, 0, "",
+          PersistentMemoryAllocator::kReadWrite));
+  PersistentHistogramAllocator::Iterator histogram_iter2(&recovery2);
+  while (true) {
+    std::unique_ptr<HistogramBase> recovered = histogram_iter2.GetNext();
+    if (!recovered) {
+      break;
+    }
+
+    recovery2.MergeHistogramFinalDeltaToStatisticsRecorder(recovered.get());
+    HistogramBase* found =
+        StatisticsRecorder::FindHistogram(recovered->histogram_name());
+    EXPECT_FALSE(found);
+  }
+  EXPECT_EQ(global_sr_initial_histogram_count,
+            StatisticsRecorder::GetHistogramCount());
+}
+
 TEST_F(PersistentHistogramAllocatorTest, MultipleSameSparseHistograms) {
   const std::string kSparseHistogramName = "SRTSparseHistogram";
 
diff --git a/base/metrics/persistent_sample_map.cc b/base/metrics/persistent_sample_map.cc
index 04d9fe6..adf9367 100644
--- a/base/metrics/persistent_sample_map.cc
+++ b/base/metrics/persistent_sample_map.cc
@@ -172,6 +172,15 @@
       sample_counts_);
 }
 
+bool PersistentSampleMap::IsDefinitelyEmpty() const {
+  // Not implemented.
+  NOTREACHED();
+
+  // Always return false. If we are wrong, this will just make the caller
+  // perform some extra work thinking that |this| is non-empty.
+  return false;
+}
+
 // static
 PersistentMemoryAllocator::Reference
 PersistentSampleMap::GetNextPersistentRecord(
diff --git a/base/metrics/persistent_sample_map.h b/base/metrics/persistent_sample_map.h
index 19c1aae..4661154 100644
--- a/base/metrics/persistent_sample_map.h
+++ b/base/metrics/persistent_sample_map.h
@@ -49,6 +49,7 @@
   HistogramBase::Count TotalCount() const override;
   std::unique_ptr<SampleCountIterator> Iterator() const override;
   std::unique_ptr<SampleCountIterator> ExtractingIterator() override;
+  bool IsDefinitelyEmpty() const override;
 
   // Uses a persistent-memory |iterator| to locate and return information about
   // the next record holding information for a PersistentSampleMap (in
diff --git a/base/metrics/sample_map.cc b/base/metrics/sample_map.cc
index f0eed32..c18d153e 100644
--- a/base/metrics/sample_map.cc
+++ b/base/metrics/sample_map.cc
@@ -138,14 +138,27 @@
   return std::make_unique<ExtractingSampleMapIterator>(sample_counts_);
 }
 
+bool SampleMap::IsDefinitelyEmpty() const {
+  // If |sample_counts_| is empty (no entry was ever inserted), then return
+  // true. If it does contain some entries, then it may or may not have samples
+  // (e.g. it's possible all entries have a bucket count of 0). Just return
+  // false in this case. If we are wrong, this will just make the caller perform
+  // some extra work thinking that |this| is non-empty.
+  return HistogramSamples::IsDefinitelyEmpty() && sample_counts_.empty();
+}
+
 bool SampleMap::AddSubtractImpl(SampleCountIterator* iter, Operator op) {
   Sample min;
   int64_t max;
   Count count;
   for (; !iter->Done(); iter->Next()) {
     iter->Get(&min, &max, &count);
-    if (strict_cast<int64_t>(min) + 1 != max)
+    if (strict_cast<int64_t>(min) + 1 != max) {
       return false;  // SparseHistogram only supports bucket with size 1.
+    }
+
+    // Note that we do not need to check that count != 0, since Next() above
+    // will skip empty buckets.
 
     // We do not have to do the following atomically -- if the caller needs
     // thread safety, they should use a lock. And since this is in local memory,
diff --git a/base/metrics/sample_map.h b/base/metrics/sample_map.h
index 7caf976..e62e6a7 100644
--- a/base/metrics/sample_map.h
+++ b/base/metrics/sample_map.h
@@ -39,6 +39,7 @@
   HistogramBase::Count TotalCount() const override;
   std::unique_ptr<SampleCountIterator> Iterator() const override;
   std::unique_ptr<SampleCountIterator> ExtractingIterator() override;
+  bool IsDefinitelyEmpty() const override;
 
  protected:
   // Performs arithemetic. |op| is ADD or SUBTRACT.
diff --git a/base/metrics/sample_vector.cc b/base/metrics/sample_vector.cc
index 9f38d05..b9f6fda0 100644
--- a/base/metrics/sample_vector.cc
+++ b/base/metrics/sample_vector.cc
@@ -462,6 +462,18 @@
 
 SampleVector::~SampleVector() = default;
 
+bool SampleVector::IsDefinitelyEmpty() const {
+  // If we are still using SingleSample, and it has a count of 0, then |this|
+  // has no samples. If we are not using SingleSample, always return false, even
+  // though it is possible that |this| has no samples (e.g. we are using a
+  // counts array and all the bucket counts are 0). If we are wrong, this will
+  // just make the caller perform some extra work thinking that |this| is
+  // non-empty.
+  AtomicSingleSample sample = single_sample();
+  return HistogramSamples::IsDefinitelyEmpty() && !sample.IsDisabled() &&
+         sample.Load().count == 0;
+}
+
 bool SampleVector::MountExistingCountsStorage() const {
   // There is never any existing storage other than what is already in use.
   return counts() != nullptr;
@@ -592,6 +604,15 @@
 
 PersistentSampleVector::~PersistentSampleVector() = default;
 
+bool PersistentSampleVector::IsDefinitelyEmpty() const {
+  // Not implemented.
+  NOTREACHED();
+
+  // Always return false. If we are wrong, this will just make the caller
+  // perform some extra work thinking that |this| is non-empty.
+  return false;
+}
+
 bool PersistentSampleVector::MountExistingCountsStorage() const {
   // There is no early exit if counts is not yet mounted because, given that
   // this is a virtual function, it's more efficient to do that at the call-
diff --git a/base/metrics/sample_vector.h b/base/metrics/sample_vector.h
index 0160f59..b1059ec 100644
--- a/base/metrics/sample_vector.h
+++ b/base/metrics/sample_vector.h
@@ -125,6 +125,9 @@
   SampleVector& operator=(const SampleVector&) = delete;
   ~SampleVector() override;
 
+  // HistogramSamples:
+  bool IsDefinitelyEmpty() const override;
+
  private:
   FRIEND_TEST_ALL_PREFIXES(SampleVectorTest, GetPeakBucketSize);
 
@@ -165,6 +168,9 @@
   PersistentSampleVector& operator=(const PersistentSampleVector&) = delete;
   ~PersistentSampleVector() override;
 
+  // HistogramSamples:
+  bool IsDefinitelyEmpty() const override;
+
  private:
   // SampleVectorBase:
   bool MountExistingCountsStorage() const override;
diff --git a/base/metrics/sparse_histogram_unittest.cc b/base/metrics/sparse_histogram_unittest.cc
index 7bd3775..835f8af3 100644
--- a/base/metrics/sparse_histogram_unittest.cc
+++ b/base/metrics/sparse_histogram_unittest.cc
@@ -195,6 +195,26 @@
   EXPECT_EQ(10, samples->sum());
 }
 
+// Check that IsDefinitelyEmpty() works with the results of SnapshotDelta().
+TEST_P(SparseHistogramTest, IsDefinitelyEmpty_SnapshotDelta) {
+  std::unique_ptr<SparseHistogram> histogram(NewSparseHistogram("Sparse"));
+
+  // No samples initially.
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+
+  histogram->Add(1);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  histogram->Add(10);
+  histogram->Add(10);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  histogram->Add(1);
+  histogram->Add(50);
+  EXPECT_FALSE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+  EXPECT_TRUE(histogram->SnapshotDelta()->IsDefinitelyEmpty());
+}
+
 TEST_P(SparseHistogramTest, AddCount_LargeValuesDontOverflow) {
   std::unique_ptr<SparseHistogram> histogram(NewSparseHistogram("Sparse"));
   std::unique_ptr<HistogramSamples> snapshot(histogram->SnapshotSamples());
diff --git a/base/numerics/safe_conversions.h b/base/numerics/safe_conversions.h
index 2fc20098..7d1e305 100644
--- a/base/numerics/safe_conversions.h
+++ b/base/numerics/safe_conversions.h
@@ -357,8 +357,18 @@
 using SizeT = StrictNumeric<size_t>;
 
 // floating -> integral conversions that saturate and thus can actually return
-// an integral type.  In most cases, these should be preferred over the std::
-// versions.
+// an integral type.
+//
+// Generally, what you want is saturated_cast<Dst>(std::nearbyint(x)), which
+// rounds correctly according to IEEE-754 (round to nearest, ties go to nearest
+// even number; this avoids bias). If your code is performance-critical
+// and you are sure that you will never overflow, you can use std::lrint()
+// or std::llrint(), which return a long or long long directly.
+//
+// Below are convenience functions around similar patterns, except that
+// they round in nonstandard directions and will generally be slower.
+
+// Rounds towards negative infinity (i.e., down).
 template <typename Dst = int,
           typename Src,
           typename = std::enable_if_t<std::is_integral<Dst>::value &&
@@ -366,6 +376,8 @@
 Dst ClampFloor(Src value) {
   return saturated_cast<Dst>(std::floor(value));
 }
+
+// Rounds towards positive infinity (i.e., up).
 template <typename Dst = int,
           typename Src,
           typename = std::enable_if_t<std::is_integral<Dst>::value &&
@@ -373,13 +385,22 @@
 Dst ClampCeil(Src value) {
   return saturated_cast<Dst>(std::ceil(value));
 }
+
+// Rounds towards nearest integer, with ties away from zero.
+// This means that 0.5 will be rounded to 1 and 1.5 will be rounded to 2.
+// Similarly, -0.5 will be rounded to -1 and -1.5 will be rounded to -2.
+//
+// This is normally not what you want accuracy-wise (it introduces a small bias
+// away from zero), and it is not the fastest option, but it is frequently what
+// existing code expects. Compare with saturated_cast<Dst>(std::nearbyint(x))
+// or std::lrint(x), which would round 0.5 and -0.5 to 0 but 1.5 to 2 and
+// -1.5 to -2.
 template <typename Dst = int,
           typename Src,
           typename = std::enable_if_t<std::is_integral<Dst>::value &&
                                       std::is_floating_point<Src>::value>>
 Dst ClampRound(Src value) {
-  const Src rounded =
-      (value >= 0.0f) ? std::floor(value + 0.5f) : std::ceil(value - 0.5f);
+  const Src rounded = std::round(value);
   return saturated_cast<Dst>(rounded);
 }
 
diff --git a/base/profiler/stack_copier_signal.cc b/base/profiler/stack_copier_signal.cc
index 5c749dc..73b3907 100644
--- a/base/profiler/stack_copier_signal.cc
+++ b/base/profiler/stack_copier_signal.cc
@@ -15,10 +15,13 @@
 #include <cstring>
 
 #include "base/memory/raw_ptr.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/notreached.h"
 #include "base/profiler/register_context.h"
 #include "base/profiler/stack_buffer.h"
 #include "base/profiler/suspendable_thread_delegate.h"
+#include "base/time/default_tick_clock.h"
+#include "base/time/time.h"
 #include "base/time/time_override.h"
 #include "base/trace_event/base_tracing.h"
 #include "build/build_config.h"
@@ -95,11 +98,17 @@
 // destructor.
 class ScopedEventSignaller {
  public:
-  ScopedEventSignaller(AsyncSafeWaitableEvent* event) : event_(event) {}
-  ~ScopedEventSignaller() { event_->Signal(); }
+  ScopedEventSignaller(AsyncSafeWaitableEvent* event,
+                       absl::optional<TimeTicks>* signal_time)
+      : event_(event), signal_time_(signal_time) {}
+  ~ScopedEventSignaller() {
+    *signal_time_ = subtle::MaybeTimeTicksNowIgnoringOverride();
+    event_->Signal();
+  }
 
  private:
   raw_ptr<AsyncSafeWaitableEvent> event_;
+  raw_ptr<absl::optional<TimeTicks>> signal_time_;
 };
 
 // Struct to store the arguments to the signal handler.
@@ -124,6 +133,9 @@
   // The timestamp when the stack was copied.
   raw_ptr<absl::optional<TimeTicks>> maybe_timestamp;
 
+  // The timestamp when |event| was signaled.
+  raw_ptr<absl::optional<TimeTicks>> maybe_timestamp_signaled;
+
   // The delegate provided to the StackCopier.
   raw_ptr<StackCopier::Delegate> stack_copier_delegate;
 };
@@ -146,7 +158,7 @@
   // to always succeed and is thus not signal-safe.
   *params->maybe_timestamp = subtle::MaybeTimeTicksNowIgnoringOverride();
 
-  ScopedEventSignaller e(params->event);
+  ScopedEventSignaller e(params->event, params->maybe_timestamp_signaled);
   *params->success = false;
 
   const ucontext_t* ucontext = static_cast<ucontext_t*>(sigcontext);
@@ -214,7 +226,8 @@
 
 StackCopierSignal::StackCopierSignal(
     std::unique_ptr<ThreadDelegate> thread_delegate)
-    : thread_delegate_(std::move(thread_delegate)) {}
+    : thread_delegate_(std::move(thread_delegate)),
+      clock_(DefaultTickClock::GetInstance()) {}
 
 StackCopierSignal::~StackCopierSignal() = default;
 
@@ -228,9 +241,21 @@
   const uint8_t* stack_copy_bottom = nullptr;
   const uintptr_t stack_base_address = thread_delegate_->GetStackBaseAddress();
   absl::optional<TimeTicks> maybe_timestamp;
-  HandlerParams params = {stack_base_address, &wait_event,  &copied,
-                          thread_context,     stack_buffer, &stack_copy_bottom,
-                          &maybe_timestamp,   delegate};
+  absl::optional<TimeTicks> maybe_timestamp_signaled;
+  HandlerParams params = {stack_base_address,
+                          &wait_event,
+                          &copied,
+                          thread_context,
+                          stack_buffer,
+                          &stack_copy_bottom,
+                          &maybe_timestamp,
+                          &maybe_timestamp_signaled,
+                          delegate};
+  TimeTicks signal_time;
+  TimeTicks wait_start_time;
+  TimeTicks wait_end_time;
+
+  RecordEvent(CopyStackEvent::kStarted);
   {
     ScopedSetSignalHandlerParams scoped_handler_params(&params);
 
@@ -246,21 +271,30 @@
     // SIGURG is chosen here because we observe no crashes with this signal and
     // neither Chrome or the AOSP sets up a special handler for this signal.
     ScopedSigaction scoped_sigaction(SIGURG, &action, &original_action);
-    if (!scoped_sigaction.succeeded())
+    if (!scoped_sigaction.succeeded()) {
+      RecordEvent(CopyStackEvent::kSigactionFailed);
       return false;
+    }
+
+    signal_time = clock_->NowTicks();
 
     if (syscall(SYS_tgkill, getpid(), thread_delegate_->GetThreadId(),
                 SIGURG) != 0) {
+      RecordEvent(CopyStackEvent::kTgkillFailed);
       NOTREACHED();
       return false;
     }
+
+    wait_start_time = clock_->NowTicks();
     bool finished_waiting = wait_event.Wait();
     TRACE_EVENT_END0(TRACE_DISABLED_BY_DEFAULT("cpu_profiler.debug"),
                      "StackCopierSignal copy stack");
     if (!finished_waiting) {
+      RecordEvent(CopyStackEvent::kWaitFailed);
       NOTREACHED();
       return false;
     }
+    wait_end_time = clock_->NowTicks();
     // Ideally, an accurate timestamp is captured while the sampled thread is
     // paused. In rare cases, this may fail, in which case we resort to
     // capturing an delayed timestamp here instead.
@@ -269,10 +303,45 @@
     else {
       TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cpu_profiler.debug"),
                    "Fallback on TimeTicks::Now()");
-      *timestamp = TimeTicks::Now();
+      *timestamp = clock_->NowTicks();
     }
   }
 
+  RecordEvent(CopyStackEvent::kSucceeded);
+  // Record UMA stats about how long everything took. Since the profiler can't
+  // profile the profiler, this is our only way to make sure the profiler isn't
+  // taking excessively long.
+  //
+  // All times are recorded in microseconds since TimeTicks::IsHighResolution()
+  // is always true on Posix systems, and we expect these to be very short
+  // times.
+  constexpr TimeDelta kMin = Microseconds(1);
+  constexpr TimeDelta kMax = Microseconds(200 * 1000);
+  constexpr int kBuckets = 100;
+
+  UmaHistogramCustomMicrosecondsTimes(
+      "UMA.StackProfiler.CopyStack.TotalCrossThreadTime",
+      wait_end_time - signal_time, kMin, kMax, kBuckets);
+  UmaHistogramCustomMicrosecondsTimes(
+      "UMA.StackProfiler.CopyStack.ProfileThreadTotalWaitTime",
+      wait_end_time - wait_start_time, kMin, kMax, kBuckets);
+  if (maybe_timestamp) {
+    UmaHistogramCustomMicrosecondsTimes(
+        "UMA.StackProfiler.CopyStack.SignalToHandlerTime",
+        *maybe_timestamp - signal_time, kMin, kMax, kBuckets);
+
+    if (maybe_timestamp_signaled) {
+      UmaHistogramCustomMicrosecondsTimes(
+          "UMA.StackProfiler.CopyStack.HandlerRunTime",
+          *maybe_timestamp_signaled - *maybe_timestamp, kMin, kMax, kBuckets);
+    }
+  }
+  if (maybe_timestamp_signaled) {
+    UmaHistogramCustomMicrosecondsTimes(
+        "UMA.StackProfiler.CopyStack.EventSignalToWaitEndTime",
+        wait_end_time - *maybe_timestamp_signaled, kMin, kMax, kBuckets);
+  }
+
   const uintptr_t bottom = RegisterContextStackPointer(params.context);
   for (uintptr_t* reg :
        thread_delegate_->GetRegistersToRewrite(thread_context)) {
@@ -288,4 +357,9 @@
   return copied;
 }
 
+// static
+void StackCopierSignal::RecordEvent(CopyStackEvent event) {
+  UmaHistogramEnumeration("UMA.StackProfiler.CopyStack.Event", event);
+}
+
 }  // namespace base
diff --git a/base/profiler/stack_copier_signal.h b/base/profiler/stack_copier_signal.h
index 5b3e778..27ceea68 100644
--- a/base/profiler/stack_copier_signal.h
+++ b/base/profiler/stack_copier_signal.h
@@ -8,7 +8,9 @@
 #include <memory>
 
 #include "base/base_export.h"
+#include "base/memory/raw_ptr.h"
 #include "base/profiler/stack_copier.h"
+#include "base/time/tick_clock.h"
 
 namespace base {
 
@@ -30,8 +32,30 @@
 
   using StackCopier::CopyStackContentsAndRewritePointers;
 
+  void set_clock_for_testing(const TickClock* clock) { clock_ = clock; }
+
+  // Events that happen during CopyStack; used for the
+  // UMA.StackProfiler.CopyStack.Event histogram. Public for use by tests.
+  // These values are persisted to logs. Entries should not be renumbered and
+  // numeric values should never be reused.
+  enum class CopyStackEvent {
+    kStarted = 0,
+    kSucceeded = 1,
+    kSigactionFailed = 2,
+    kTgkillFailed = 3,
+    kWaitFailed = 4,
+    kMaxValue = kWaitFailed
+  };
+
  private:
+  // Records an event during a run of CopyStack to the
+  // UMA.StackProfiler.CopyStack.Event histogram.
+  static void RecordEvent(CopyStackEvent event);
+
   std::unique_ptr<ThreadDelegate> thread_delegate_;
+  // Clock used for time inside CopyStack. NOT used for getting the time in the
+  // signal handler, which always uses the real system tick clock.
+  raw_ptr<const TickClock> clock_;
 };
 
 }  // namespace base
diff --git a/base/profiler/stack_copier_signal_unittest.cc b/base/profiler/stack_copier_signal_unittest.cc
index 495edef..7876745 100644
--- a/base/profiler/stack_copier_signal_unittest.cc
+++ b/base/profiler/stack_copier_signal_unittest.cc
@@ -5,6 +5,7 @@
 #include <string.h>
 #include <algorithm>
 #include <utility>
+#include <vector>
 
 #include "base/debug/alias.h"
 #include "base/profiler/sampling_profiler_thread_token.h"
@@ -12,16 +13,21 @@
 #include "base/profiler/stack_copier_signal.h"
 #include "base/profiler/thread_delegate_posix.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/test/metrics/histogram_tester.h"
 #include "base/threading/platform_thread.h"
 #include "base/threading/simple_thread.h"
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace base {
 
 namespace {
 
+using ::testing::ElementsAre;
+using ::testing::Return;
+
 // Values to write to the stack and look for in the copy.
 static const uint32_t kStackSentinels[] = {0xf312ecd9, 0x1fcd7f19, 0xe69e617d,
                                            0x8245f94f};
@@ -73,6 +79,11 @@
   bool on_stack_copy_was_invoked_ = false;
 };
 
+class MockTickClock : public TickClock {
+ public:
+  MOCK_METHOD(TimeTicks, NowTicks, (), (const, override));
+};
+
 }  // namespace
 
 // ASAN moves local variables outside of the stack extents, which breaks the
@@ -185,6 +196,98 @@
   EXPECT_TRUE(stack_copier_delegate.on_stack_copy_was_invoked());
 }
 
+// TSAN hangs on the AsyncSafeWaitableEvent FUTEX_WAIT call.
+#if defined(THREAD_SANITIZER)
+#define MAYBE_CopyStackUMAStats DISABLED_CopyStackUMAStats
+#elif BUILDFLAG(IS_LINUX)
+// We don't support getting the stack base address on Linux, and thus can't
+// copy the stack. // https://crbug.com/1394278
+#define MAYBE_CopyStackUMAStats DISABLED_CopyStackUMAStats
+#else
+#define MAYBE_CopyStackUMAStats CopyStackUMAStats
+#endif
+TEST(StackCopierSignalTest, MAYBE_CopyStackUMAStats) {
+  HistogramTester histograms;
+  StackBuffer stack_buffer(/* buffer_size = */ 1 << 20);
+  memset(stack_buffer.buffer(), 0, stack_buffer.size());
+  uintptr_t stack_top = 0;
+  TimeTicks timestamp;
+  RegisterContext context;
+  TestStackCopierDelegate stack_copier_delegate;
+  MockTickClock clock;
+  TimeTicks real_now = TimeTicks::Now();
+  EXPECT_CALL(clock, NowTicks())
+      // signal_time
+      .WillOnce(Return(real_now - Microseconds(1000)))
+      // wait_start_time
+      .WillOnce(Return(real_now - Microseconds(600)))
+      // wait_end_time. Also covers the fallback if needed.
+      .WillRepeatedly(Return(real_now));
+
+  auto thread_delegate =
+      ThreadDelegatePosix::Create(GetSamplingProfilerCurrentThreadToken());
+  ASSERT_TRUE(thread_delegate);
+  StackCopierSignal copier(std::move(thread_delegate));
+  copier.set_clock_for_testing(&clock);
+
+  bool result = copier.CopyStack(&stack_buffer, &stack_top, &timestamp,
+                                 &context, &stack_copier_delegate);
+  ASSERT_TRUE(result);
+
+  EXPECT_THAT(
+      histograms.GetAllSamples("UMA.StackProfiler.CopyStack.Event"),
+      ElementsAre(Bucket(StackCopierSignal::CopyStackEvent::kStarted, 1),
+                  Bucket(StackCopierSignal::CopyStackEvent::kSucceeded, 1)));
+
+  // Do not use HistogramTester::ExpectUniqueTimeSample which assumes the
+  // histogram has units of milliseconds.
+  histograms.ExpectUniqueSample(
+      "UMA.StackProfiler.CopyStack.TotalCrossThreadTime",
+      // signal_time to wait_end_time should be 1000 microseconds.
+      1000, 1);
+  histograms.ExpectUniqueSample(
+      "UMA.StackProfiler.CopyStack.ProfileThreadTotalWaitTime",
+      // start_wait_time to end_wait_time should be 600 microseconds.
+      600, 1);
+  // Since we can't override the times returned from the signal handler, we
+  // can't use the normal ExpectUniqueSample. HistogramTester doesn't give us
+  // enough information to check if a sample was in a range. So the best we can
+  // do is check that we got a sample for SignalToHandlerTime, HandlerRunTime,
+  // and EventSignalToWaitEndTime. However, we might not even get a sample
+  // for those if the signal handler couldn't get a time, and we don't want to
+  // fail the test for that. So all we can verify is that:
+  // 1. We have at most one sample for each and
+  // 2. If SignalToHandlerTime and EventSignalToWaitEndTime both have a sample
+  //    (meaning both clock fetches succeeded), then HandlerRunTime does as
+  //    well.
+  HistogramTester::CountsMap counts =
+      histograms.GetTotalCountsForPrefix("UMA.StackProfiler.CopyStack.");
+
+  int signal_to_handler_sample_count = 0;
+  if (auto it = counts.find("UMA.StackProfiler.CopyStack.SignalToHandlerTime");
+      it != counts.end()) {
+    signal_to_handler_sample_count = it->second;
+    EXPECT_EQ(signal_to_handler_sample_count, 1);
+  }
+  int handler_run_time_sample_count = 0;
+  if (auto it = counts.find("UMA.StackProfiler.CopyStack.HandlerRunTime");
+      it != counts.end()) {
+    handler_run_time_sample_count = it->second;
+    EXPECT_EQ(handler_run_time_sample_count, 1);
+  }
+  int event_signal_to_wait_end_time_sample_count = 0;
+  if (auto it =
+          counts.find("UMA.StackProfiler.CopyStack.EventSignalToWaitEndTime");
+      it != counts.end()) {
+    event_signal_to_wait_end_time_sample_count = it->second;
+    EXPECT_EQ(event_signal_to_wait_end_time_sample_count, 1);
+  }
+
+  EXPECT_EQ(handler_run_time_sample_count != 0,
+            signal_to_handler_sample_count != 0 &&
+                event_signal_to_wait_end_time_sample_count != 0);
+}
+
 // Limit to 32-bit Android, which is the platform we care about for this
 // functionality. The test is broken on too many other varied platforms to try
 // to selectively disable.
diff --git a/base/safe_numerics_unittest.cc b/base/safe_numerics_unittest.cc
index cb92cf3e3..0d1f6723 100644
--- a/base/safe_numerics_unittest.cc
+++ b/base/safe_numerics_unittest.cc
@@ -1838,7 +1838,9 @@
   EXPECT_EQ(-100, ClampRound(-100.1f));
   EXPECT_EQ(-101, ClampRound(-100.5f));
   EXPECT_EQ(-101, ClampRound(-100.9f));
+  EXPECT_EQ(0, ClampRound(std::nextafter(-0.5f, 0.0f)));
   EXPECT_EQ(0, ClampRound(0.0f));
+  EXPECT_EQ(0, ClampRound(std::nextafter(0.5f, 0.0f)));
   EXPECT_EQ(100, ClampRound(100.1f));
   EXPECT_EQ(101, ClampRound(100.5f));
   EXPECT_EQ(101, ClampRound(100.9f));
diff --git a/base/types/DEPS b/base/types/DEPS
new file mode 100644
index 0000000..2d16b79
--- /dev/null
+++ b/base/types/DEPS
@@ -0,0 +1,6 @@
+specific_include_rules = {
+  # Provides the canonical access point for this type
+  "fixed_array.h": [
+    "+third_party/abseil-cpp/absl/container/fixed_array.h",
+  ],
+}
diff --git a/base/types/fixed_array.h b/base/types/fixed_array.h
new file mode 100644
index 0000000..1ca3956f
--- /dev/null
+++ b/base/types/fixed_array.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_TYPES_FIXED_ARRAY_H_
+#define BASE_TYPES_FIXED_ARRAY_H_
+
+#include <stddef.h>
+
+#include <memory>
+#include <type_traits>
+
+#include "third_party/abseil-cpp/absl/container/fixed_array.h"
+
+namespace base {
+
+// `FixedArray` provides `absl::FixedArray` in Chromium, but when `T` is
+// trivially-default-constructible, forces the no-default-value constructor to
+// initialize the elements to `T()`, instead of leaving them uninitialized. This
+// makes `base::FixedArray` behave like `std::vector` instead of `std::array`
+// and avoids the risk of UB.
+
+// Trivially-default-constructible case: no-value constructor should init
+template <typename T,
+          size_t N = absl::kFixedArrayUseDefault,
+          typename A = std::allocator<T>,
+          typename = void>
+class FixedArray : public absl::FixedArray<T, N, A> {
+ public:
+  using absl::FixedArray<T, N, A>::FixedArray;
+  explicit FixedArray(absl::FixedArray<T, N, A>::size_type n,
+                      const absl::FixedArray<T, N, A>::allocator_type& a =
+                          typename absl::FixedArray<T, N, A>::allocator_type())
+      : FixedArray(n, T(), a) {}
+};
+
+// Non-trivially-default-constructible case: Pass through all constructors
+template <typename T, size_t N, typename A>
+struct FixedArray<
+    T,
+    N,
+    A,
+    std::enable_if_t<!std::is_trivially_default_constructible_v<T>>>
+    : public absl::FixedArray<T, N, A> {
+  using absl::FixedArray<T, N, A>::FixedArray;
+};
+
+}  // namespace base
+
+#endif  // BASE_TYPES_FIXED_ARRAY_H_
diff --git a/base/types/fixed_array_unittest.cc b/base/types/fixed_array_unittest.cc
new file mode 100644
index 0000000..4d0b5f29
--- /dev/null
+++ b/base/types/fixed_array_unittest.cc
@@ -0,0 +1,42 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/types/fixed_array.h"
+
+#include <stddef.h>
+
+#include <cstring>
+#include <memory>
+#include <type_traits>
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace base {
+namespace {
+
+TEST(FixedArrayTest, TriviallyDefaultConstructibleInitializes) {
+  using T = int;
+  static_assert(std::is_trivially_default_constructible_v<T>);
+  using Array = FixedArray<T, 1>;
+
+  // First try an array on the stack.
+  Array stack_array(1);
+  // This read and the one below are UB if `FixedArray` does not initialize the
+  // elements, but hopefully even if the compiler chooses to zero memory anyway,
+  // the test will fail under the memory sanitizer.
+  EXPECT_EQ(0, stack_array[0]);
+
+  // Now try an array on the heap, where we've purposefully written a non-zero
+  // bitpattern in hopes of increasing the chance of catching incorrect
+  // behavior.
+  constexpr size_t kSize = sizeof(Array);
+  alignas(Array) char storage[kSize];
+  std::memset(storage, 0xAA, kSize);
+  Array* placement_new_array = new (storage) Array(1);
+  EXPECT_EQ(0, (*placement_new_array)[0]);
+  placement_new_array->~Array();
+}
+
+}  // namespace
+}  // namespace base
diff --git a/build/config/siso/PRESUBMIT.py b/build/config/siso/PRESUBMIT.py
index 0216dd5..ae6ed39c 100644
--- a/build/config/siso/PRESUBMIT.py
+++ b/build/config/siso/PRESUBMIT.py
@@ -16,6 +16,7 @@
       "Missing 'Cq-Include-Trybots:' field required for Siso config changes"
       "\nPlease add the following fields to run Siso tryjobs.\n\n"
       "Cq-Include-Trybots: luci.chromium.try:android-arm64-siso-rel\n"
+      "Cq-Include-Trybots: luci.chromium.try:ios-simulator-siso\n"
       "Cq-Include-Trybots: luci.chromium.try:mac-siso-rel\n"
       "Cq-Include-Trybots: luci.chromium.try:linux-chromeos-siso-rel\n"
       "Cq-Include-Trybots: luci.chromium.try:linux-lacros-siso-rel\n"
diff --git a/build/config/siso/linux.star b/build/config/siso/linux.star
index 86249a5c..9b0e9ed 100644
--- a/build/config/siso/linux.star
+++ b/build/config/siso/linux.star
@@ -68,20 +68,6 @@
     step_config["rules"].insert(0, rule)
     return step_config
 
-def __disable_remote_crbug1484474(ctx, step_config):
-    rule = {
-        # TODO(crbug.com/1484474): they timed out and never cache hit.
-        "name": "crbug1484474/timeout",
-        "action_outs": [
-            "./obj/third_party/abseil-cpp/absl/functional/any_invocable_test/any_invocable_test.o",
-        ],
-        "remote": False,
-    }
-    if reproxy.enabled(ctx):
-        rule["handler"] = "strip_rewrapper"
-    step_config["rules"].insert(0, rule)
-    return step_config
-
 def __step_config(ctx, step_config):
     config.check(ctx)
     step_config["platforms"].update({
@@ -93,7 +79,6 @@
     })
 
     step_config = __disable_remote_b289968566(ctx, step_config)
-    step_config = __disable_remote_crbug1484474(ctx, step_config)
 
     if android.enabled(ctx):
         step_config = android.step_config(ctx, step_config)
diff --git a/build/config/siso/reproxy.star b/build/config/siso/reproxy.star
index b84c8c5..af78bc9b 100644
--- a/build/config/siso/reproxy.star
+++ b/build/config/siso/reproxy.star
@@ -92,9 +92,15 @@
             return
     if not cfg_file:
         fail("couldn't find rewrapper cfg file in %s" % str(cmd.args))
+    reproxy_config = rewrapper_cfg.parse(ctx, cfg_file)
+    if cmd.outputs[0] == ctx.fs.canonpath("./obj/third_party/abseil-cpp/absl/functional/any_invocable_test/any_invocable_test.o"):
+        # need longer timeout for any_invocable_test.o crbug.com/1484474
+        reproxy_config.update({
+            "exec_timeout": "4m",
+        })
     ctx.actions.fix(
         args = args,
-        reproxy_config = json.encode(rewrapper_cfg.parse(ctx, cfg_file)),
+        reproxy_config = json.encode(reproxy_config)
     )
 
 def __strip_rewrapper(ctx, cmd):
diff --git a/buildtools/deps_revisions.gni b/buildtools/deps_revisions.gni
index f75eb9a..2d0f23f 100644
--- a/buildtools/deps_revisions.gni
+++ b/buildtools/deps_revisions.gni
@@ -5,5 +5,5 @@
 declare_args() {
   # Used to cause full rebuilds on libc++ rolls. This should be kept in sync
   # with the libcxx_revision vars in //DEPS.
-  libcxx_revision = "055d494c5c65957c94c58ac17cbf8b1f22ac11a6"
+  libcxx_revision = "8a241ea043bc9d3f0712f3a908da49b89495cd00"
 }
diff --git a/cc/base/features.cc b/cc/base/features.cc
index 984dcef4..b220aba 100644
--- a/cc/base/features.cc
+++ b/cc/base/features.cc
@@ -40,11 +40,6 @@
              "RemoveMobileViewportDoubleTap",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-// Design doc: bit.ly/scrollunification
-BASE_FEATURE(kScrollUnification,
-             "ScrollUnification",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 BASE_FEATURE(kScrollSnapCoveringAvoidNestedSnapAreas,
              "ScrollSnapCoveringAvoidNestedSnapAreas",
              base::FEATURE_ENABLED_BY_DEFAULT);
diff --git a/cc/base/features.h b/cc/base/features.h
index fb11d0a3..804c4e6 100644
--- a/cc/base/features.h
+++ b/cc/base/features.h
@@ -21,14 +21,6 @@
 // or content=initial-scale=1.0
 CC_BASE_EXPORT BASE_DECLARE_FEATURE(kRemoveMobileViewportDoubleTap);
 
-// When enabled, all scrolling is performed on the compositor thread -
-// delegating only the hit test to Blink. This causes Blink to send additional
-// information in the scroll property tree. When a scroll can't be hit tested
-// on the compositor, it will post a hit test task to Blink and continue the
-// scroll when that resolves. For details, see:
-// https://docs.google.com/document/d/1smLAXs-DSLLmkEt4FIPP7PVglJXOcwRc7A5G0SEwxaY/edit
-CC_BASE_EXPORT BASE_DECLARE_FEATURE(kScrollUnification);
-
 // When enabled, scrolling within a covering snap area avoids or snaps to inner
 // nested areas, avoiding resting on positions which do not snap the inner area.
 // E.g. when scrolling within snap area A, it will stop either before/after
diff --git a/cc/input/input_handler.cc b/cc/input/input_handler.cc
index 2dcba57..1bdf1b3 100644
--- a/cc/input/input_handler.cc
+++ b/cc/input/input_handler.cc
@@ -103,7 +103,6 @@
   }
 
   ScrollNode* scrolling_node = nullptr;
-  bool scroll_on_main_thread = false;
 
   // TODO(bokan): ClearCurrentlyScrollingNode shouldn't happen in ScrollBegin,
   // this should only happen in ScrollEnd. We should DCHECK here that the state
@@ -112,8 +111,6 @@
 
   ElementId target_element_id = scroll_state->target_element_id();
   ScrollTree& scroll_tree = GetScrollTree();
-  bool unification_enabled =
-      base::FeatureList::IsEnabled(features::kScrollUnification);
 
   if (target_element_id && (!scroll_state->main_thread_hit_tested_reasons() ||
                             scroll_state->is_scrollbar_interaction())) {
@@ -122,17 +119,6 @@
     // If the caller passed in an element_id we can skip all the hit-testing
     // bits and provide a node straight-away.
     scrolling_node = scroll_tree.FindNodeFromElementId(target_element_id);
-
-    // In unified scrolling, if we found a node we get to scroll it.
-    if (!unification_enabled) {
-      // We still need to confirm the targeted node exists and can scroll on
-      // the compositor.
-      if (scrolling_node) {
-        scroll_status = TryScroll(scroll_tree, scrolling_node);
-        if (IsMainThreadScrolling(scroll_status, scrolling_node))
-          scroll_on_main_thread = true;
-      }
-    }
   } else {
     ScrollNode* starting_node = nullptr;
     if (target_element_id) {
@@ -144,7 +130,6 @@
       // unification is enabled and the targeted scroller comes back from a
       // main thread hit test.
       DCHECK(scroll_state->main_thread_hit_tested_reasons());
-      DCHECK(unification_enabled);
       starting_node = scroll_tree.FindNodeFromElementId(target_element_id);
 
       if (!starting_node) {
@@ -165,74 +150,34 @@
           gfx::ScalePoint(gfx::PointF(viewport_point),
                           compositor_delegate_->DeviceScaleFactor());
 
-      if (unification_enabled) {
-        if (scroll_state->main_thread_hit_tested_reasons()) {
-          // The client should have discarded the scroll when the hit test came
-          // back with an invalid element id. If we somehow get here, we should
-          // drop the scroll as continuing could cause us to infinitely bounce
-          // back and forth between here and hit testing on the main thread.
-          NOTREACHED();
-          scroll_status.thread = InputHandler::ScrollThread::kScrollIgnored;
-          return scroll_status;
-        }
-
-        ScrollHitTestResult scroll_hit_test =
-            HitTestScrollNode(device_viewport_point);
-
-        if (!scroll_hit_test.hit_test_successful) {
-          // This result tells the client that the compositor doesn't have
-          // enough information to target this scroll. The client should
-          // perform a hit test in Blink and call this method again, with the
-          // ElementId of the hit-tested scroll node.
-          TRACE_EVENT_INSTANT0("cc", "Request Main Thread Hit Test",
-                               TRACE_EVENT_SCOPE_THREAD);
-          scroll_status.thread =
-              InputHandler::ScrollThread::kScrollOnImplThread;
-          DCHECK(scroll_hit_test.main_thread_hit_test_reasons);
-          scroll_status.main_thread_hit_test_reasons =
-              scroll_hit_test.main_thread_hit_test_reasons;
-          return scroll_status;
-        }
-
-        starting_node = scroll_hit_test.scroll_node;
-      } else {  // !unification_enabled
-        LayerImpl* layer_impl =
-            ActiveTree().FindLayerThatIsHitByPoint(device_viewport_point);
-
-        if (layer_impl) {
-          LayerImpl* first_scrolling_layer_or_scrollbar =
-              ActiveTree().FindFirstScrollingLayerOrScrollbarThatIsHitByPoint(
-                  device_viewport_point);
-
-          // Touch dragging the scrollbar requires falling back to main-thread
-          // scrolling.
-          if (IsTouchDraggingScrollbar(first_scrolling_layer_or_scrollbar,
-                                       type)) {
-            TRACE_EVENT_INSTANT0("cc", "Scrollbar Scrolling",
-                                 TRACE_EVENT_SCOPE_THREAD);
-            scroll_status.thread =
-                InputHandler::ScrollThread::kScrollOnMainThread;
-            scroll_status.main_thread_scrolling_reasons =
-                MainThreadScrollingReason::kScrollbarScrolling;
-            return scroll_status;
-          }
-          ScrollNode* unused = nullptr;
-          if (!IsInitialScrollHitTestReliable(
-                  layer_impl, first_scrolling_layer_or_scrollbar, unused)) {
-            TRACE_EVENT_INSTANT0("cc", "Failed Hit Test",
-                                 TRACE_EVENT_SCOPE_THREAD);
-            scroll_status.thread =
-                InputHandler::ScrollThread::kScrollOnMainThread;
-            scroll_status.main_thread_scrolling_reasons =
-                MainThreadScrollingReason::kFailedHitTest;
-            return scroll_status;
-          }
-        }
-
-        starting_node = FindScrollNodeForCompositedScrolling(
-            device_viewport_point, layer_impl, &scroll_on_main_thread,
-            &scroll_status.main_thread_scrolling_reasons);
+      if (scroll_state->main_thread_hit_tested_reasons()) {
+        // The client should have discarded the scroll when the hit test came
+        // back with an invalid element id. If we somehow get here, we should
+        // drop the scroll as continuing could cause us to infinitely bounce
+        // back and forth between here and hit testing on the main thread.
+        NOTREACHED();
+        scroll_status.thread = InputHandler::ScrollThread::kScrollIgnored;
+        return scroll_status;
       }
+
+      ScrollHitTestResult scroll_hit_test =
+          HitTestScrollNode(device_viewport_point);
+
+      if (!scroll_hit_test.hit_test_successful) {
+        // This result tells the client that the compositor doesn't have
+        // enough information to target this scroll. The client should
+        // perform a hit test in Blink and call this method again, with the
+        // ElementId of the hit-tested scroll node.
+        TRACE_EVENT_INSTANT0("cc", "Request Main Thread Hit Test",
+                             TRACE_EVENT_SCOPE_THREAD);
+        scroll_status.thread = InputHandler::ScrollThread::kScrollOnImplThread;
+        DCHECK(scroll_hit_test.main_thread_hit_test_reasons);
+        scroll_status.main_thread_hit_test_reasons =
+            scroll_hit_test.main_thread_hit_test_reasons;
+        return scroll_status;
+      }
+
+      starting_node = scroll_hit_test.scroll_node;
     }
 
     // The above finds the ScrollNode that's hit by the given point but we
@@ -241,14 +186,7 @@
     scrolling_node = FindNodeToLatch(scroll_state, starting_node, type);
   }
 
-  if (scroll_on_main_thread) {
-    // Under scroll unification we can request a main thread hit test, but we
-    // should never send scrolls to the main thread.
-    DCHECK(!unification_enabled);
-
-    scroll_status.thread = InputHandler::ScrollThread::kScrollOnMainThread;
-    return scroll_status;
-  } else if (!scrolling_node) {
+  if (!scrolling_node) {
     if (compositor_delegate_->GetSettings().is_for_embedded_frame) {
       // OOPIFs or fenced frames never have a viewport scroll node so if we
       // can't scroll we need to be bubble up to the parent frame. This happens
@@ -277,9 +215,8 @@
   DCHECK(scrolling_node);
 
   ActiveTree().SetCurrentlyScrollingNode(scrolling_node);
-  if (unification_enabled)
-    scroll_status.main_thread_repaint_reasons =
-        scroll_tree.GetMainThreadRepaintReasons(*scrolling_node);
+  scroll_status.main_thread_repaint_reasons =
+      scroll_tree.GetMainThreadRepaintReasons(*scrolling_node);
 
   DidLatchToScroller(*scroll_state, type);
 
@@ -435,8 +372,7 @@
   float scale_factor = ActiveTree().page_scale_factor_for_scroll();
   scroll_result.current_visual_offset.Scale(scale_factor);
 
-  if (base::FeatureList::IsEnabled(features::kScrollUnification) &&
-      !GetScrollTree().CanRealizeScrollsOnCompositor(scroll_node)) {
+  if (!GetScrollTree().CanRealizeScrollsOnCompositor(scroll_node)) {
     scroll_result.needs_main_thread_repaint = true;
   }
 
@@ -885,23 +821,6 @@
   return true;
 }
 
-bool InputHandler::ScrollingShouldSwitchtoMainThread() {
-  DCHECK(!base::FeatureList::IsEnabled(features::kScrollUnification));
-  ScrollTree& scroll_tree = GetScrollTree();
-  ScrollNode* scroll_node = scroll_tree.CurrentlyScrollingNode();
-
-  if (!scroll_node)
-    return true;
-
-  for (; scroll_tree.parent(scroll_node);
-       scroll_node = scroll_tree.parent(scroll_node)) {
-    if (!!scroll_node->main_thread_scrolling_reasons)
-      return true;
-  }
-
-  return false;
-}
-
 absl::optional<gfx::PointF> InputHandler::ConstrainFling(gfx::PointF original) {
   gfx::PointF fling = original;
   if (fling_snap_constrain_x_) {
@@ -1259,22 +1178,6 @@
   }
 }
 
-bool InputHandler::IsMainThreadScrolling(
-    const InputHandler::ScrollStatus& status,
-    const ScrollNode* scroll_node) const {
-  if (status.thread == InputHandler::ScrollThread::kScrollOnMainThread) {
-    if (!!scroll_node->main_thread_scrolling_reasons) {
-      DCHECK(MainThreadScrollingReason::MainThreadCanSetScrollReasons(
-          status.main_thread_scrolling_reasons));
-    } else {
-      DCHECK(MainThreadScrollingReason::CompositorCanSetScrollReasons(
-          status.main_thread_scrolling_reasons));
-    }
-    return true;
-  }
-  return false;
-}
-
 float InputHandler::LineStep() const {
   return kPixelsPerLineStep * ActiveTree().painted_device_scale_factor();
 }
@@ -1318,163 +1221,6 @@
   return pixel_delta;
 }
 
-InputHandler::ScrollStatus InputHandler::TryScroll(
-    const ScrollTree& scroll_tree,
-    ScrollNode* scroll_node) const {
-  DCHECK(!base::FeatureList::IsEnabled(features::kScrollUnification));
-  DCHECK(scroll_node->transform_id != kInvalidPropertyNodeId);
-
-  InputHandler::ScrollStatus scroll_status;
-  scroll_status.main_thread_scrolling_reasons =
-      MainThreadScrollingReason::kNotScrollingOnMain;
-  if (scroll_node->main_thread_scrolling_reasons) {
-    TRACE_EVENT1("cc", "LayerImpl::TryScroll: Failed ShouldScrollOnMainThread",
-                 "MainThreadScrollingReason",
-                 scroll_node->main_thread_scrolling_reasons);
-    scroll_status.thread = InputHandler::ScrollThread::kScrollOnMainThread;
-    scroll_status.main_thread_scrolling_reasons =
-        scroll_node->main_thread_scrolling_reasons;
-    return scroll_status;
-  }
-
-  gfx::Transform screen_space_transform =
-      scroll_tree.ScreenSpaceTransform(scroll_node->id);
-  if (!screen_space_transform.IsInvertible()) {
-    TRACE_EVENT0("cc", "LayerImpl::TryScroll: Ignored NonInvertibleTransform");
-    scroll_status.thread = InputHandler::ScrollThread::kScrollIgnored;
-    return scroll_status;
-  }
-
-  if (!scroll_node->scrollable) {
-    TRACE_EVENT0("cc", "LayerImpl::tryScroll: Ignored not scrollable");
-    scroll_status.thread = InputHandler::ScrollThread::kScrollIgnored;
-    return scroll_status;
-  }
-
-  // If an associated scrolling layer is not found, the scroll node must not
-  // support impl-scrolling. The root, secondary root, and inner viewports
-  // are all exceptions to this and may not have a layer because it is not
-  // required for hit testing.
-  if (scroll_node->id != kRootPropertyNodeId &&
-      scroll_node->id != kSecondaryRootPropertyNodeId &&
-      !scroll_node->scrolls_inner_viewport &&
-      !ActiveTree().LayerByElementId(scroll_node->element_id)) {
-    TRACE_EVENT0("cc",
-                 "LayerImpl::tryScroll: Failed due to no scrolling layer");
-    scroll_status.thread = InputHandler::ScrollThread::kScrollOnMainThread;
-    scroll_status.main_thread_scrolling_reasons =
-        MainThreadScrollingReason::kNoScrollingLayer;
-    return scroll_status;
-  }
-
-  // The a viewport node should be scrolled even if it has no scroll extent
-  // since it'll scroll using the Viewport class which will generate browser
-  // controls movement and overscroll delta.
-  gfx::PointF max_scroll_offset = scroll_tree.MaxScrollOffset(scroll_node->id);
-  if (max_scroll_offset.x() <= 0 && max_scroll_offset.y() <= 0 &&
-      !GetViewport().ShouldScroll(*scroll_node)) {
-    TRACE_EVENT0("cc",
-                 "LayerImpl::tryScroll: Ignored. Technically scrollable,"
-                 " but has no affordance in either direction.");
-    scroll_status.thread = InputHandler::ScrollThread::kScrollIgnored;
-    return scroll_status;
-  }
-
-  scroll_status.thread = InputHandler::ScrollThread::kScrollOnImplThread;
-  return scroll_status;
-}
-
-base::flat_set<int> InputHandler::NonFastScrollableNodes(
-    const gfx::PointF& device_viewport_point) const {
-  base::flat_set<int> non_fast_scrollable_nodes;
-
-  const auto& non_fast_layers =
-      ActiveTree().FindLayersHitByPointInNonFastScrollableRegion(
-          device_viewport_point);
-  for (const auto* layer : non_fast_layers)
-    non_fast_scrollable_nodes.insert(layer->scroll_tree_index());
-
-  return non_fast_scrollable_nodes;
-}
-
-ScrollNode* InputHandler::FindScrollNodeForCompositedScrolling(
-    const gfx::PointF& device_viewport_point,
-    LayerImpl* layer_impl,
-    bool* scroll_on_main_thread,
-    uint32_t* main_thread_scrolling_reasons) {
-  DCHECK(!base::FeatureList::IsEnabled(features::kScrollUnification));
-  DCHECK(scroll_on_main_thread);
-  DCHECK(main_thread_scrolling_reasons);
-  *main_thread_scrolling_reasons =
-      MainThreadScrollingReason::kNotScrollingOnMain;
-
-  const auto& non_fast_scrollable_nodes =
-      NonFastScrollableNodes(device_viewport_point);
-
-  // If we hit a scrollbar layer, get the ScrollNode from its associated
-  // scrolling layer, rather than directly from the scrollbar layer. The latter
-  // would return the parent scroller's ScrollNode.
-  if (layer_impl && layer_impl->IsScrollbarLayer()) {
-    layer_impl = ActiveTree().LayerByElementId(
-        ToScrollbarLayer(layer_impl)->scroll_element_id());
-  }
-
-  // Walk up the hierarchy and look for a scrollable layer.
-  ScrollTree& scroll_tree = GetScrollTree();
-  ScrollNode* impl_scroll_node = nullptr;
-  if (layer_impl) {
-    ScrollNode* scroll_node = scroll_tree.Node(layer_impl->scroll_tree_index());
-    for (; scroll_tree.parent(scroll_node);
-         scroll_node = scroll_tree.parent(scroll_node)) {
-      // The content layer can also block attempts to scroll outside the main
-      // thread.
-      InputHandler::ScrollStatus status = TryScroll(scroll_tree, scroll_node);
-      if (IsMainThreadScrolling(status, scroll_node)) {
-        *scroll_on_main_thread = true;
-        *main_thread_scrolling_reasons = status.main_thread_scrolling_reasons;
-        return scroll_node;
-      }
-
-      if (non_fast_scrollable_nodes.contains(scroll_node->id)) {
-        *scroll_on_main_thread = true;
-        *main_thread_scrolling_reasons =
-            MainThreadScrollingReason::kNonFastScrollableRegion;
-        return scroll_node;
-      }
-
-      if (status.thread == InputHandler::ScrollThread::kScrollOnImplThread &&
-          !impl_scroll_node) {
-        impl_scroll_node = scroll_node;
-      }
-    }
-  }
-
-  // TODO(bokan): We shouldn't need this - ordinarily all scrolls should pass
-  // through the outer viewport. If we aren't able to find a scroller we should
-  // return nullptr here and ignore the scroll. However, it looks like on some
-  // pages (reddit.com) we start scrolling from the inner node.
-  if (!impl_scroll_node)
-    impl_scroll_node = InnerViewportScrollNode();
-
-  if (!impl_scroll_node)
-    return nullptr;
-
-  impl_scroll_node = GetNodeToScroll(impl_scroll_node);
-
-  // Ensure that final scroll node scrolls on impl thread (crbug.com/625100)
-  InputHandler::ScrollStatus status = TryScroll(scroll_tree, impl_scroll_node);
-  if (IsMainThreadScrolling(status, impl_scroll_node)) {
-    *scroll_on_main_thread = true;
-    *main_thread_scrolling_reasons = status.main_thread_scrolling_reasons;
-  } else if (non_fast_scrollable_nodes.contains(impl_scroll_node->id)) {
-    *scroll_on_main_thread = true;
-    *main_thread_scrolling_reasons =
-        MainThreadScrollingReason::kNonFastScrollableRegion;
-  }
-
-  return impl_scroll_node;
-}
-
 InputHandler::ScrollHitTestResult InputHandler::HitTestScrollNode(
     const gfx::PointF& device_viewport_point) const {
   ScrollHitTestResult result;
@@ -1534,16 +1280,6 @@
   return result;
 }
 
-// Requires falling back to main thread scrolling when it hit tests in scrollbar
-// from touch.
-bool InputHandler::IsTouchDraggingScrollbar(
-    LayerImpl* first_scrolling_layer_or_scrollbar,
-    ui::ScrollInputType type) {
-  return first_scrolling_layer_or_scrollbar &&
-         first_scrolling_layer_or_scrollbar->IsScrollbarLayer() &&
-         type == ui::ScrollInputType::kTouchscreen;
-}
-
 ScrollNode* InputHandler::GetNodeToScroll(ScrollNode* node) const {
   // Blink has a notion of a "root scroller", which is the scroller in a page
   // that is considered to host the main content. Typically this will be the
diff --git a/cc/input/input_handler.h b/cc/input/input_handler.h
index fe0cfb5..aca93994 100644
--- a/cc/input/input_handler.h
+++ b/cc/input/input_handler.h
@@ -384,8 +384,6 @@
                                        gfx::PointF* offset);
   virtual bool ScrollLayerTo(ElementId element_id, const gfx::PointF& offset);
 
-  virtual bool ScrollingShouldSwitchtoMainThread();
-
   // Sets the initial and target offset for scroll snapping for the currently
   // scrolling node and the given natural displacement. Also sets the target
   // element of the snap's scrolling animation.
@@ -522,13 +520,6 @@
   LayerTreeImpl& ActiveTree();
   LayerTreeImpl& ActiveTree() const;
 
-  bool IsMainThreadScrolling(const InputHandler::ScrollStatus& status,
-                             const ScrollNode* scroll_node) const;
-
-  bool IsTouchDraggingScrollbar(
-      LayerImpl* first_scrolling_layer_or_drawn_scrollbar,
-      ui::ScrollInputType type);
-
   void UpdateRootLayerStateForSynchronousInputHandler();
 
   // Called during ScrollBegin once a scroller was successfully latched to
@@ -554,16 +545,6 @@
   void ScrollLatchedScroller(ScrollState* scroll_state,
                              base::TimeDelta delayed_by);
 
-  // Determines whether the given scroll node can scroll on the compositor
-  // thread or if there are any reasons it must be scrolled on the main thread
-  // or not at all. Note: in general, this is not sufficient to determine if a
-  // scroll can occur on the compositor thread. If hit testing to a scroll
-  // node, the caller must also check whether the hit point intersects a
-  // non-fast-scrolling-region of any ancestor scrolling layers. Can be removed
-  // after scroll unification https://crbug.com/476553.
-  InputHandler::ScrollStatus TryScroll(const ScrollTree& scroll_tree,
-                                       ScrollNode* scroll_node) const;
-
   enum class SnapReason { kGestureScrollEnd, kScrollOffsetAnimationFinished };
 
   // Creates an animation curve and returns true if we need to update the
@@ -585,24 +566,6 @@
       const LayerImpl* first_layer_scrollable_or_opaque_to_hit_test,
       ScrollNode*& out_node_to_scroll) const;
 
-  // Similar to above but includes complicated logic to determine whether the
-  // ScrollNode is able to be scrolled on the compositor or requires main
-  // thread scrolling. If main thread scrolling is required
-  // |scroll_on_main_thread| is set to true and the reason is given in
-  // |main_thread_scrolling_reason| to on of the enum values in
-  // main_thread_scrolling_reason.h. Can be removed after scroll unification
-  // https://crbug.com/476553.
-  ScrollNode* FindScrollNodeForCompositedScrolling(
-      const gfx::PointF& device_viewport_point,
-      LayerImpl* layer_hit_by_point,
-      bool* scroll_on_main_thread,
-      uint32_t* main_thread_scrolling_reason);
-
-  // Return all ScrollNode indices that have an associated layer with a non-fast
-  // region that intersects the point.
-  base::flat_set<int> NonFastScrollableNodes(
-      const gfx::PointF& device_viewport_point) const;
-
   // Returns the ScrollNode we should use to scroll, accounting for viewport
   // scroll chaining rules.
   ScrollNode* GetNodeToScroll(ScrollNode* node) const;
diff --git a/cc/input/main_thread_scrolling_reason.h b/cc/input/main_thread_scrolling_reason.h
index 79f8ad42..7a12664 100644
--- a/cc/input/main_thread_scrolling_reason.h
+++ b/cc/input/main_thread_scrolling_reason.h
@@ -76,25 +76,6 @@
       kPreferNonCompositedScrolling | kNotOpaqueForTextAndLCDText |
       kCantPaintScrollingBackgroundAndLCDText;
 
-  // Returns true if the given MainThreadScrollingReason can be set by the main
-  // thread.
-  static bool MainThreadCanSetScrollReasons(uint32_t reasons) {
-    constexpr uint32_t reasons_set_by_main_thread =
-        kHasBackgroundAttachmentFixedObjects | kPopupNoThreadedInput |
-        kPreferNonCompositedScrolling | kBackgroundNeedsRepaintOnScroll |
-        kNotOpaqueForTextAndLCDText | kCantPaintScrollingBackgroundAndLCDText;
-    return (reasons & reasons_set_by_main_thread) == reasons;
-  }
-
-  // Returns true if the given MainThreadScrollingReason can be set by the
-  // compositor.
-  static bool CompositorCanSetScrollReasons(uint32_t reasons) {
-    constexpr uint32_t reasons_set_by_compositor =
-        kNonFastScrollableRegion | kFailedHitTest | kNoScrollingLayer |
-        kWheelEventHandlerRegion | kTouchEventHandlerRegion;
-    return (reasons & reasons_set_by_compositor) == reasons;
-  }
-
   // Returns true if there are any reasons that prevented the scroller
   // from being composited.
   static bool HasNonCompositedScrollReasons(uint32_t reasons) {
diff --git a/cc/input/scrollbar_controller.cc b/cc/input/scrollbar_controller.cc
index c4d9d73..e548164 100644
--- a/cc/input/scrollbar_controller.cc
+++ b/cc/input/scrollbar_controller.cc
@@ -68,18 +68,6 @@
   if (scrollbar->OverlayScrollbarOpacity() == 0.f)
     return PointerResultType::kUnhandled;
 
-  if (!base::FeatureList::IsEnabled(features::kScrollUnification)) {
-    // If the scroll_node has a main_thread_scrolling_reason, don't initiate a
-    // scroll.
-    const ScrollNode* target_node =
-        layer_tree_host_impl_->active_tree()
-            ->property_trees()
-            ->scroll_tree()
-            .FindNodeFromElementId(scrollbar->scroll_element_id());
-    if (target_node->main_thread_scrolling_reasons) {
-      return PointerResultType::kUnhandled;
-    }
-  }
   return PointerResultType::kScrollbarScroll;
 }
 
diff --git a/cc/raster/one_copy_raster_buffer_provider.cc b/cc/raster/one_copy_raster_buffer_provider.cc
index cbf2a3b..1d1ad9e 100644
--- a/cc/raster/one_copy_raster_buffer_provider.cc
+++ b/cc/raster/one_copy_raster_buffer_provider.cc
@@ -52,10 +52,6 @@
              "OneCopyRasterBufferPlaybackNormalThreadPriority",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
-BASE_FEATURE(kAlwaysUseMappableSIForOneCopyRaster,
-             "AlwaysUseMappableSIForOneCopyRaster",
-             base::FEATURE_DISABLED_BY_DEFAULT);
-
 }  // namespace
 
 // Subclass for InUsePoolResource that holds ownership of a one-copy backing
@@ -341,12 +337,6 @@
     const RasterSource::PlaybackSettings& playback_settings,
     uint64_t previous_content_id,
     uint64_t new_content_id) {
-  is_shared_memory_ = false;
-  std::unique_ptr<gpu::SharedImageInterface::ScopedMapping> mapping;
-  gfx::GpuMemoryBuffer* buffer = nullptr;
-  void* memory = nullptr;
-  size_t stride = 0;
-
   gfx::Rect playback_rect = raster_full_rect;
   if (use_partial_raster_ && previous_content_id) {
     // Reduce playback rect to dirty region if the content id of the staging
@@ -358,77 +348,46 @@
 
   float full_rect_size = raster_full_rect.size().GetArea();
 
-  if (base::FeatureList::IsEnabled(kAlwaysUseMappableSIForOneCopyRaster)) {
-    CHECK(!staging_buffer->gpu_memory_buffer);
-
-    auto* sii = worker_context_provider_->SharedImageInterface();
-
-    // Allocate MappableSharedImage if necessary.
-    if (staging_buffer->mailbox.IsZero()) {
-      staging_buffer->mailbox = sii->CreateSharedImage(
-          format, staging_buffer->size, dst_color_space,
-          kTopLeft_GrSurfaceOrigin, kPremul_SkAlphaType,
-          gpu::SHARED_IMAGE_USAGE_CPU_WRITE, "OneCopyRasterStaging",
-          gpu::kNullSurfaceHandle, gfx::BufferUsage::GPU_READ_CPU_READ_WRITE);
-    }
-
-    mapping = sii->MapSharedImage(staging_buffer->mailbox);
-    if (!mapping) {
-      LOG(ERROR) << "MapSharedImage Failed.";
-      return false;
-    }
-    memory = mapping->Memory(0);
-    stride = mapping->Stride(0);
-    is_shared_memory_ = mapping->IsSharedMemory();
-  } else {
-    // Allocate GpuMemoryBuffer if necessary.
-    if (!staging_buffer->gpu_memory_buffer) {
-      staging_buffer->gpu_memory_buffer =
-          gpu_memory_buffer_manager_->CreateGpuMemoryBuffer(
-              staging_buffer->size,
-              viz::SinglePlaneSharedImageFormatToBufferFormat(format),
-              gfx::BufferUsage::GPU_READ_CPU_READ_WRITE,
-              gpu::kNullSurfaceHandle, shutdown_event_);
-    }
-
-    buffer = staging_buffer->gpu_memory_buffer.get();
-    if (!buffer) {
-      return false;
-    }
-
-    CHECK_EQ(1u, gfx::NumberOfPlanesForLinearBufferFormat(buffer->GetFormat()));
-    bool rv = buffer->Map();
-    CHECK(rv);
-    CHECK(buffer->memory(0));
-    // RasterBufferProvider::PlaybackToMemory only supports unsigned strides.
-    CHECK_GE(buffer->stride(0), 0);
-
-    // TODO(https://crbug.com/870663): Temporary diagnostics.
-    base::debug::Alias(&playback_rect);
-    base::debug::Alias(&full_rect_size);
-    base::debug::Alias(&rv);
-    void* buffer_memory = buffer->memory(0);
-    base::debug::Alias(&buffer_memory);
-    gfx::Size staging_buffer_size = staging_buffer->size;
-    base::debug::Alias(&staging_buffer_size);
-    gfx::Size buffer_size = buffer->GetSize();
-    base::debug::Alias(&buffer_size);
-
-    memory = buffer->memory(0);
-    stride = buffer->stride(0);
-    is_shared_memory_ =
-        buffer->GetType() == gfx::GpuMemoryBufferType::SHARED_MEMORY_BUFFER;
+  // Allocate GpuMemoryBuffer if necessary.
+  if (!staging_buffer->gpu_memory_buffer) {
+    staging_buffer->gpu_memory_buffer =
+        gpu_memory_buffer_manager_->CreateGpuMemoryBuffer(
+            staging_buffer->size,
+            viz::SinglePlaneSharedImageFormatToBufferFormat(format),
+            gfx::BufferUsage::GPU_READ_CPU_READ_WRITE, gpu::kNullSurfaceHandle,
+            shutdown_event_);
   }
 
+  if (!staging_buffer->gpu_memory_buffer) {
+    return false;
+  }
+
+  gfx::GpuMemoryBuffer* buffer = staging_buffer->gpu_memory_buffer.get();
+  CHECK_EQ(1u, gfx::NumberOfPlanesForLinearBufferFormat(buffer->GetFormat()));
+  bool rv = buffer->Map();
+  CHECK(rv);
+  CHECK(buffer->memory(0));
+  // RasterBufferProvider::PlaybackToMemory only supports unsigned strides.
+  CHECK_GE(buffer->stride(0), 0);
+
+  // TODO(https://crbug.com/870663): Temporary diagnostics.
+  base::debug::Alias(&playback_rect);
+  base::debug::Alias(&full_rect_size);
+  base::debug::Alias(&rv);
+  void* buffer_memory = buffer->memory(0);
+  base::debug::Alias(&buffer_memory);
+  gfx::Size staging_buffer_size = staging_buffer->size;
+  base::debug::Alias(&staging_buffer_size);
+  gfx::Size buffer_size = buffer->GetSize();
+  base::debug::Alias(&buffer_size);
+
   DCHECK(!playback_rect.IsEmpty())
       << "Why are we rastering a tile that's not dirty?";
   RasterBufferProvider::PlaybackToMemory(
-      memory, format, staging_buffer->size, stride, raster_source,
-      raster_full_rect, playback_rect, transform, dst_color_space,
-      /*gpu_compositing=*/true, playback_settings);
-  base::FeatureList::IsEnabled(kAlwaysUseMappableSIForOneCopyRaster)
-      ? mapping.reset()
-      : buffer->Unmap();
+      buffer->memory(0), format, staging_buffer->size, buffer->stride(0),
+      raster_source, raster_full_rect, playback_rect, transform,
+      dst_color_space, /*gpu_compositing=*/true, playback_settings);
+  buffer->Unmap();
   staging_buffer->content_id = new_content_id;
 
   return true;
@@ -448,11 +407,7 @@
   auto* sii = worker_context_provider_->SharedImageInterface();
   DCHECK(sii);
 
-  if (base::FeatureList::IsEnabled(kAlwaysUseMappableSIForOneCopyRaster)) {
-    CHECK(!staging_buffer->mailbox.IsZero());
-  } else {
-    CHECK(staging_buffer->gpu_memory_buffer.get());
-  }
+  CHECK(staging_buffer->gpu_memory_buffer.get());
 
   bool needs_clear = false;
 
@@ -505,8 +460,10 @@
   query_target = GL_COMMANDS_ISSUED_CHROMIUM;
 #endif
 
-  // COMMANDS_ISSUED is sufficient for shared memory resources.
-  if (is_shared_memory_) {
+  // COMMANDS_ISSUED is sufficient for shared memory GpuMemoryBuffers.
+  const auto* buffer = staging_buffer->gpu_memory_buffer.get();
+  if (buffer &&
+      buffer->GetType() == gfx::GpuMemoryBufferType::SHARED_MEMORY_BUFFER) {
     query_target = GL_COMMANDS_ISSUED_CHROMIUM;
   }
 
diff --git a/cc/raster/one_copy_raster_buffer_provider.h b/cc/raster/one_copy_raster_buffer_provider.h
index a187a37..978ef70 100644
--- a/cc/raster/one_copy_raster_buffer_provider.h
+++ b/cc/raster/one_copy_raster_buffer_provider.h
@@ -172,10 +172,6 @@
   const bool tile_overlay_candidate_;
   const uint32_t tile_texture_target_;
 
-  // Whether the current data in the staging buffer came from a shared memory
-  // resource.
-  bool is_shared_memory_ = false;
-
   StagingBufferPool staging_pool_;
 };
 
diff --git a/cc/trees/layer_tree_host.cc b/cc/trees/layer_tree_host.cc
index ac8fac7..9f2a1976 100644
--- a/cc/trees/layer_tree_host.cc
+++ b/cc/trees/layer_tree_host.cc
@@ -1078,12 +1078,7 @@
         //
         // But if the scroll was NOT realized on the compositor, we need a
         // commit to push the transform change.
-        //
-        // Skip this if scroll unification is disabled as we will not set
-        // ScrollNode::is_composited in that case.
-        //
-        if (base::FeatureList::IsEnabled(features::kScrollUnification) &&
-            !scroll_tree.CanRealizeScrollsOnCompositor(*scroll_node)) {
+        if (!scroll_tree.CanRealizeScrollsOnCompositor(*scroll_node)) {
           SetNeedsCommit();
         }
       }
diff --git a/cc/trees/layer_tree_impl.cc b/cc/trees/layer_tree_impl.cc
index d2dc97b..667d564 100644
--- a/cc/trees/layer_tree_impl.cc
+++ b/cc/trees/layer_tree_impl.cc
@@ -225,16 +225,13 @@
   }
 
   // This bit controls whether we'll update the transform node based on a
-  // changed scroll offset. If scroll unification is off, we always do this
-  // because the scroll handling code will only invoke a scroll update on nodes
-  // that can compositor scroll. However, with scroll unification, we can
-  // mutate scroll nodes which have main thread scrolling reasons, or aren't
-  // backed by a layer at all. In those cases, we don't want to produce any
-  // immediate changes in the compositor, we want the scroll to propagate
-  // through Blink in a commit and have Blink update properties, paint,
-  // compositing, etc. Thus, we avoid mutating the transform tree in this case.
+  // changed scroll offset. We can mutate scroll nodes which have main thread
+  // scrolling reasons, or aren't backed by a layer at all, but in those cases
+  // we don't want to produce any immediate changes in the compositor. Instead
+  // we want the scroll to propagate through Blink in a commit and have Blink
+  // update properties, paint, compositing, etc. Thus, we avoid mutating the
+  // transform tree in this case.
   bool should_realize_scroll_on_compositor =
-      !base::FeatureList::IsEnabled(features::kScrollUnification) ||
       scroll_tree.CanRealizeScrollsOnCompositor(*scroll_node);
 
   // A ScrollNode may have an invalid transform_id if its scroller is
@@ -2431,21 +2428,6 @@
   }
 }
 
-LayerImpl* LayerTreeImpl::FindFirstScrollingLayerOrScrollbarThatIsHitByPoint(
-    const gfx::PointF& screen_space_point) {
-  if (layer_list_.empty())
-    return nullptr;
-
-  FindClosestMatchingLayerState state;
-  LayerImpl* root_layer = layer_list_[0].get();
-  auto HitTestScrollingLayerOrScrollbarFunctor = [](const LayerImpl* layer) {
-    return layer->HitTestable() && layer->IsScrollerOrScrollbar();
-  };
-  FindClosestMatchingLayer(screen_space_point, root_layer,
-                           HitTestScrollingLayerOrScrollbarFunctor, &state);
-  return state.closest_match;
-}
-
 struct HitTestVisibleScrollableOrTouchableFunctor {
   bool operator()(LayerImpl* layer) const { return layer->HitTestable(); }
 };
@@ -2510,28 +2492,6 @@
 }
 
 std::vector<const LayerImpl*>
-LayerTreeImpl::FindLayersHitByPointInNonFastScrollableRegion(
-    const gfx::PointF& screen_space_point) {
-  std::vector<const LayerImpl*> layers;
-  if (layer_list_.empty())
-    return layers;
-  if (!UpdateDrawProperties())
-    return layers;
-  for (const auto* layer : *this) {
-    if (layer->non_fast_scrollable_region().IsEmpty())
-      continue;
-    if (!PointHitsLayer(layer, screen_space_point, nullptr))
-      continue;
-    if (PointHitsRegion(screen_space_point, layer->ScreenSpaceTransform(),
-                        layer->non_fast_scrollable_region(), layer)) {
-      layers.push_back(layer);
-    }
-  }
-
-  return layers;
-}
-
-std::vector<const LayerImpl*>
 LayerTreeImpl::FindLayersUpToFirstScrollableOrOpaqueToHitTest(
     const gfx::PointF& screen_space_point) {
   std::vector<const LayerImpl*> layers;
diff --git a/cc/trees/layer_tree_impl.h b/cc/trees/layer_tree_impl.h
index 99daa32..43c7e36 100644
--- a/cc/trees/layer_tree_impl.h
+++ b/cc/trees/layer_tree_impl.h
@@ -612,9 +612,6 @@
   void UnregisterScrollbar(ScrollbarLayerImplBase* scrollbar_layer);
   ScrollbarSet ScrollbarsFor(ElementId scroll_element_id) const;
 
-  LayerImpl* FindFirstScrollingLayerOrScrollbarThatIsHitByPoint(
-      const gfx::PointF& screen_space_point);
-
   LayerImpl* FindLayerThatIsHitByPoint(const gfx::PointF& screen_space_point);
 
   LayerImpl* FindLayerThatIsHitByPointInTouchHandlerRegion(
@@ -623,9 +620,6 @@
   LayerImpl* FindLayerThatIsHitByPointInWheelEventHandlerRegion(
       const gfx::PointF& screen_space_point);
 
-  // Return all layers with a hit non-fast scrollable region.
-  std::vector<const LayerImpl*> FindLayersHitByPointInNonFastScrollableRegion(
-      const gfx::PointF& screen_space_point);
   // Returns all layers up to the first scroller, scrollbar layer or a layer
   // opaque to hit test, inclusive. The returned vector is sorted in order of
   // top most come first. The back of the vector will be the scrollable layer
diff --git a/cc/trees/property_tree.cc b/cc/trees/property_tree.cc
index 2e391d3..b7dfc04d 100644
--- a/cc/trees/property_tree.cc
+++ b/cc/trees/property_tree.cc
@@ -1601,7 +1601,6 @@
     const ScrollNode& scroll_node) const {
   gfx::PointF offset = current_scroll_offset(scroll_node.element_id);
   if (!property_trees()->is_main_thread() &&
-      base::FeatureList::IsEnabled(features::kScrollUnification) &&
       !CanRealizeScrollsOnCompositor(scroll_node)) {
     // Ignore impl-thread scroll delta if the scroll can't be realized on
     // compositor because the main thread is the source of truth in the case.
diff --git a/chrome/VERSION b/chrome/VERSION
index 4b1678d5..097a261f2 100644
--- a/chrome/VERSION
+++ b/chrome/VERSION
@@ -1,4 +1,4 @@
 MAJOR=119
 MINOR=0
-BUILD=6037
+BUILD=6038
 PATCH=0
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index cbdcc43..3db397b3 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -2543,6 +2543,7 @@
       "java/src/org/chromium/chrome/browser/DeferredStartupHandler.java",
       "java/src/org/chromium/chrome/browser/app/bluetooth/BluetoothNotificationService.java",
       "java/src/org/chromium/chrome/browser/app/usb/UsbNotificationService.java",
+      "java/src/org/chromium/chrome/browser/base/ColdStartTracker.java",
       "java/src/org/chromium/chrome/browser/base/DexFixer.java",
       "java/src/org/chromium/chrome/browser/base/DexFixerReason.java",
       "java/src/org/chromium/chrome/browser/base/ServiceTracingProxyProvider.java",
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
index 144a5afe..4ee9e382 100644
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -216,6 +216,7 @@
   "java/src/org/chromium/chrome/browser/bookmarks/ImprovedBookmarkSaveFlowViewBinder.java",
   "java/src/org/chromium/chrome/browser/bookmarks/LegacyBookmarkQueryHandler.java",
   "java/src/org/chromium/chrome/browser/bookmarks/PageImageServiceQueue.java",
+  "java/src/org/chromium/chrome/browser/bookmarks/PendingRunnable.java",
   "java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkMetrics.java",
   "java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkShoppingItemRow.java",
   "java/src/org/chromium/chrome/browser/bookmarks/PowerBookmarkTagChipList.java",
diff --git a/chrome/android/chrome_junit_test_java_sources.gni b/chrome/android/chrome_junit_test_java_sources.gni
index d629657..af430e4f 100644
--- a/chrome/android/chrome_junit_test_java_sources.gni
+++ b/chrome/android/chrome_junit_test_java_sources.gni
@@ -75,6 +75,7 @@
   "junit/src/org/chromium/chrome/browser/bookmarks/ImprovedBookmarkRowTest.java",
   "junit/src/org/chromium/chrome/browser/bookmarks/LegacyBookmarkQueryHandlerTest.java",
   "junit/src/org/chromium/chrome/browser/bookmarks/PageImageServiceQueueTest.java",
+  "junit/src/org/chromium/chrome/browser/bookmarks/PendingRunnableTest.java",
   "junit/src/org/chromium/chrome/browser/bookmarks/ReadingListSectionHeaderTest.java",
   "junit/src/org/chromium/chrome/browser/bookmarks/SharedBookmarkModelMocks.java",
   "junit/src/org/chromium/chrome/browser/bookmarks/ShoppingAccessoryCoordinatorTest.java",
@@ -353,6 +354,7 @@
   "junit/src/org/chromium/chrome/browser/webapps/WebappDirectoryManagerTest.java",
   "junit/src/org/chromium/chrome/browser/webapps/WebappLauncherActivityTest.java",
   "junit/src/org/chromium/chrome/browser/webapps/WebappRegistryTest.java",
+  "junit/src/org/chromium/chrome/browser/webauth/BarrierTest.java",
   "junit/src/org/chromium/chrome/browser/webauth/CredManHelperRobolectricTest.java",
   "junit/src/org/chromium/chrome/browser/webauth/FakeAndroidCredManCreateRequest.java",
   "junit/src/org/chromium/chrome/browser/webauth/FakeAndroidCredManCreateResponse.java",
diff --git a/chrome/android/java/res/xml/main_preferences.xml b/chrome/android/java/res/xml/main_preferences.xml
index c14807c..e287c3e6 100644
--- a/chrome/android/java/res/xml/main_preferences.xml
+++ b/chrome/android/java/res/xml/main_preferences.xml
@@ -92,7 +92,7 @@
         android:order="16"
         android:title="@string/options_homepage_title"/>
     <Preference
-        android:fragment="org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarPreferenceFragment"
+        android:fragment="org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarSettingsFragment"
         android:key="toolbar_shortcut"
         android:order="17"
         android:title="@string/toolbar_shortcut"/>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
index d32c952..a9fb9d7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeTabbedActivity.java
@@ -117,6 +117,7 @@
 import org.chromium.chrome.browser.metrics.LaunchMetrics;
 import org.chromium.chrome.browser.metrics.MainIntentBehaviorMetrics;
 import org.chromium.chrome.browser.modaldialog.ChromeTabModalPresenter;
+import org.chromium.chrome.browser.modaldialog.TabModalLifetimeHandler;
 import org.chromium.chrome.browser.multiwindow.MultiInstanceChromeTabbedActivity;
 import org.chromium.chrome.browser.multiwindow.MultiInstanceManager;
 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
@@ -224,6 +225,7 @@
 import org.chromium.content_public.common.ContentSwitches;
 import org.chromium.ui.base.DeviceFormFactor;
 import org.chromium.ui.base.PageTransition;
+import org.chromium.ui.modaldialog.ModalDialogManager;
 import org.chromium.ui.widget.Toast;
 import org.chromium.url.GURL;
 
@@ -304,6 +306,7 @@
     private HistoricalTabModelObserver mHistoricalTabModelObserver;
 
     private BrowserControlsVisibilityDelegate mVrBrowserControlsVisibilityDelegate;
+    private TabModalLifetimeHandler mTabModalHandler;
 
     private boolean mUIWithNativeInitialized;
 
@@ -2332,7 +2335,7 @@
     }
 
     private void onOmniboxFocusChanged(boolean hasFocus) {
-        getTabModalLifetimeHandler().onOmniboxFocusChanged(hasFocus);
+        mTabModalHandler.onOmniboxFocusChanged(hasFocus);
     }
 
     private void recordLauncherShortcutAction(boolean isIncognito) {
@@ -2360,7 +2363,7 @@
             return true;
         }
 
-        if (getTabModalLifetimeHandler().onBackPressed()) {
+        if (mTabModalHandler.onBackPressed()) {
             BackPressManager.record(BackPressHandler.Type.TAB_MODAL_HANDLER);
             return true;
         }
@@ -2968,7 +2971,21 @@
     }
 
     private ComposedBrowserControlsVisibilityDelegate getAppBrowserControlsVisibilityDelegate() {
-        return mRootUiCoordinator.getAppBrowserControlsVisibilityDelegate();
+        // TODO(jinsukkim): Move this to RootUiCoordinator.
+        return ((TabbedRootUiCoordinator) mRootUiCoordinator)
+                .getAppBrowserControlsVisibilityDelegate();
+    }
+
+    @Override
+    protected ModalDialogManager createModalDialogManager() {
+        ModalDialogManager manager = super.createModalDialogManager();
+        // TODO(crbug.com/1157310): Transition this::method refs to dedicated suppliers.
+        mTabModalHandler = new TabModalLifetimeHandler(this, getLifecycleDispatcher(), manager,
+                this::getAppBrowserControlsVisibilityDelegate, this::getTabObscuringHandler,
+                this::getToolbarManager, getContextualSearchManagerSupplier(),
+                getTabModelSelectorSupplier(), this::getBrowserControlsManager,
+                this::getFullscreenManager, mBackPressManager);
+        return manager;
     }
 
     // App Menu related code -----------------------------------------------------------------------
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
index faae8f2..98aff99 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/app/ChromeActivity.java
@@ -126,7 +126,6 @@
 import org.chromium.chrome.browser.metrics.ActivityTabStartupMetricsTracker;
 import org.chromium.chrome.browser.metrics.LaunchMetrics;
 import org.chromium.chrome.browser.metrics.UmaSessionStats;
-import org.chromium.chrome.browser.modaldialog.TabModalLifetimeHandler;
 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
 import org.chromium.chrome.browser.night_mode.SystemNightModeMonitor;
 import org.chromium.chrome.browser.night_mode.WebContentsDarkModeController;
@@ -399,8 +398,6 @@
     private boolean mBlockingDrawForAppRestart;
     private Runnable mShowContentRunnable;
     private boolean mIsRecreatingForTabletModeChange;
-    // Handling the dismissal of tab modal dialog.
-    private TabModalLifetimeHandler mTabModalLifetimeHandler;
     // This is only used on automotive.
     private @Nullable MissingDeviceLockLauncher mMissingDeviceLockLauncher;
 
@@ -1118,8 +1115,7 @@
     private static void reportSyncStatus(@Nullable SyncService syncService) {
         if (syncService == null || !syncService.isEngineInitialized()) {
             ContextReporter.reportStatus(ContextReporter.STATUS_SYNC_NOT_INITIALIZED);
-        } else if (!syncService.getActiveDataTypes().contains(ModelType.TYPED_URLS)
-                && !syncService.getActiveDataTypes().contains(ModelType.HISTORY)) {
+        } else if (!syncService.getActiveDataTypes().contains(ModelType.HISTORY)) {
             ContextReporter.reportStatus(ContextReporter.STATUS_SYNC_NOT_SYNCING_URLS);
         } else if (syncService.getPassphraseType() != PassphraseType.KEYSTORE_PASSPHRASE
                 && syncService.getPassphraseType() != PassphraseType.TRUSTED_VAULT_PASSPHRASE) {
@@ -1650,21 +1646,8 @@
 
     @Override
     protected ModalDialogManager createModalDialogManager() {
-        var dialogManager = new ModalDialogManager(
+        return new ModalDialogManager(
                 new AppModalPresenter(this), ModalDialogManager.ModalDialogType.APP);
-        // TODO(crbug.com/1157310): Transition this::method refs to dedicated suppliers.
-        mTabModalLifetimeHandler = new TabModalLifetimeHandler(this, getLifecycleDispatcher(),
-                dialogManager,
-                ()
-                        -> mRootUiCoordinator.getAppBrowserControlsVisibilityDelegate(),
-                this::getTabObscuringHandler, this::getToolbarManager,
-                getContextualSearchManagerSupplier(), getTabModelSelectorSupplier(),
-                this::getBrowserControlsManager, this::getFullscreenManager, mBackPressManager);
-        return dialogManager;
-    }
-
-    protected TabModalLifetimeHandler getTabModalLifetimeHandler() {
-        return mTabModalLifetimeHandler;
     }
 
     protected Drawable getBackgroundDrawable() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/base/ColdStartTracker.java b/chrome/android/java/src/org/chromium/chrome/browser/base/ColdStartTracker.java
new file mode 100644
index 0000000..5a43f97
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/base/ColdStartTracker.java
@@ -0,0 +1,134 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.base;
+
+import android.app.Activity;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Process;
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+
+import org.chromium.base.ActivityState;
+import org.chromium.base.ApplicationStatus;
+import org.chromium.base.ApplicationStatus.ActivityStateListener;
+import org.chromium.build.BuildConfig;
+import org.chromium.chrome.browser.base.SplitCompatAppComponentFactory.ProcessCreationReason;
+import org.chromium.chrome.browser.metrics.SimpleStartupForegroundSessionDetector;
+
+/**
+ * Utility class to guess whether the first Activity was created during application cold start.
+ *
+ * <p>
+ * Cold startup is most important in cases when an activity is created and needs to become
+ * interactive as fast as possible, while the browser is still initializing. Android OS can
+ * create the app process in many ways without an Activity and keep it "cached" for a long time
+ * before the first Activity gets created. This quite common course of events runs faster and is
+ * therefore considered to be a "warm" start.
+ *
+ * <p>
+ * For this class, the "cold start" is a time interval closely related to the lifecycle of the first
+ * Activity created in the app process lifetime.
+ *
+ * <p>
+ * The cold start interval is _empty_ if the process creation was not _caused_ by the creation of an
+ * Activity. In other words, when the process is instantiated due to receiving a broadcast,
+ * instantiating a service or a content provider, Chrome gets initialized during that time and can
+ * remain idle (frozen) for a long time before the first Activity is created. Otherwise, the cold
+ * start interval starts at process creation and ends when the first activity returns from
+ * onCreate().
+ *
+ * <p>
+ * It is possible to confirm that it was cold start when the first Activity started. Detecting that
+ * no other Activities intervened since then is more difficult and is not always possible using the
+ * ApplicationStatus because activity state changes arrive when the activity calls corresponding
+ * methods of the superclass (e.g. performs super.onPause()).
+ *
+ * <p>
+ * Note: This class is not ideal for use when recording UMA metrics filtered by cold startup because
+ * it applies a time-based heuristic on Android releases prior to P. On these systems the app may
+ * sometimes appear as cold even if it has been fully initialized a little bit in advance. UMA
+ * histograms based on this heuristic may feature an unwanted population-biased hump.
+ */
+public class ColdStartTracker implements ActivityStateListener {
+    private static ColdStartTracker sColdStartTracker;
+
+    private Boolean mStartedAsCold;
+
+    private ColdStartTracker() {
+        assert ApplicationStatus.isInitialized();
+        ApplicationStatus.registerStateListenerForAllActivities(this);
+    }
+
+    /**
+     * Must be called after {@link ApplicationStatus} is initialized.
+     */
+    public static void initialize() {
+        assert sColdStartTracker == null;
+        sColdStartTracker = new ColdStartTracker();
+    }
+
+    @Override
+    public void onActivityStateChange(Activity activity, @ActivityState int newState) {
+        if (newState == ActivityState.CREATED) {
+            detectStartedAsCold();
+            ApplicationStatus.unregisterActivityStateListener(this);
+        }
+    }
+
+    // Must be called during onCreate() (or earlier) of the first activity created in the process.
+    private void detectStartedAsCold() {
+        if (mStartedAsCold != null) return;
+        if (VERSION.SDK_INT >= VERSION_CODES.P) {
+            mStartedAsCold = isColdStartupOnP();
+            return;
+        }
+
+        // Fallback: treat recently started process as cold.
+        mStartedAsCold = (SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime() < 500);
+    }
+
+    @RequiresApi(Build.VERSION_CODES.P)
+    private boolean isColdStartupOnP() {
+        @ProcessCreationReason
+        int creationReason = SplitCompatAppComponentFactory.getProcessCreationReason();
+        if (creationReason <= ProcessCreationReason.PENDING) {
+            assert BuildConfig.IS_FOR_TEST;
+            // The process creation hooks have not been run, therefore this is a cold start.
+            // This condition was observed in robolectric tests. Unlikely to happen on real devices.
+            return true;
+        }
+
+        // Service connections, content providers and broadcast receivers could have created the
+        // process long before the activity was created. These other process creation reasons likely
+        // make it a warm start.
+        return creationReason == ProcessCreationReason.ACTIVITY;
+    }
+
+    /**
+     * Tells whether it was cold start when the first Activity was created. If called before any
+     * Activity is fully created, considers that the call is made from the first Activity.onCreate()
+     * and tells whether it is starting as cold.
+     *
+     * <p>
+     * It is highly recommended to combine this heuristic with
+     * {@link SimpleStartupForegroundSessionDetector#runningCleanForegroundSession()} to filter out
+     * cases when another Activity was created.
+     *
+     * <p>
+     * This method must *not* be called from code running before the first Activity (such as in
+     * Application).
+     */
+    public static boolean wasColdOnFirstActivityCreationOrNow() {
+        return sColdStartTracker.firstActivityWasColdOrDidNotGetCreatedYet();
+    }
+
+    private boolean firstActivityWasColdOrDidNotGetCreatedYet() {
+        detectStartedAsCold();
+        return mStartedAsCold;
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java b/chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java
index 886ea9c..ce612b6 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/base/SplitCompatApplication.java
@@ -218,6 +218,7 @@
             // created and is needed only by processes that use the ApplicationStatus api (which
             // for Chrome is just the browser process).
             ApplicationStatus.initialize(this);
+            ColdStartTracker.initialize();
 
             // Register and initialize application status listener for crashes, this needs to be
             // done as early as possible so that this value is set before any crashes are
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
index c800cb1f6..8015edd3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/BookmarkManagerMediator.java
@@ -20,7 +20,6 @@
 import org.chromium.base.ObserverList;
 import org.chromium.base.metrics.RecordUserAction;
 import org.chromium.base.supplier.ObservableSupplierImpl;
-import org.chromium.base.task.PostTask;
 import org.chromium.base.task.TaskTraits;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.app.bookmarks.BookmarkFolderSelectActivity;
@@ -115,12 +114,13 @@
     }
 
     private final BookmarkModelObserver mBookmarkModelObserver = new BookmarkModelObserver() {
-        private boolean mPendingRefresh;
+        private final PendingRunnable mPendingRefresh =
+                new PendingRunnable(TaskTraits.UI_DEFAULT, BookmarkManagerMediator.this::refresh);
 
         @Override
         public void bookmarkNodeChildrenReordered(BookmarkItem node) {
             if (!mIsBookmarkModelReorderingInProgress) {
-                postPendingRefresh();
+                mPendingRefresh.post();
             }
             mIsBookmarkModelReorderingInProgress = false;
         }
@@ -148,7 +148,7 @@
                     // If the position couldn't be found, then do a full refresh. Otherwise be
                     // smart and remove only the index of the removed bookmark.
                     if (position == -1) {
-                        postPendingRefresh();
+                        mPendingRefresh.post();
                     } else {
                         mModelList.removeAt(position);
                         // If the deleted node was selection, unselect it.
@@ -161,7 +161,7 @@
                 // We cannot rely on removing the specific list item that corresponds to the
                 // removed node because the node might be a parent with children also shown
                 // in the list.
-                postPendingRefresh();
+                mPendingRefresh.post();
             }
         }
 
@@ -176,7 +176,7 @@
 
             if (getCurrentUiMode() == BookmarkUiMode.FOLDER
                     && Objects.equals(id, getCurrentFolderId())) {
-                postPendingRefresh();
+                mPendingRefresh.post();
             } else {
                 super.bookmarkNodeChanged(item);
             }
@@ -190,21 +190,9 @@
                     && TextUtils.isEmpty(getCurrentSearchText())) {
                 onEndSearch();
             } else {
-                postPendingRefresh();
+                mPendingRefresh.post();
             }
         }
-
-        private void postPendingRefresh() {
-            if (!mPendingRefresh) {
-                mPendingRefresh = true;
-                PostTask.postTask(TaskTraits.UI_DEFAULT, this::runPendingRefresh);
-            }
-        }
-
-        private void runPendingRefresh() {
-            mPendingRefresh = false;
-            refresh();
-        }
     };
 
     private final Stack<BookmarkUiState> mStateStack = new Stack<>() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PendingRunnable.java b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PendingRunnable.java
new file mode 100644
index 0000000..45ee4ca9
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/bookmarks/PendingRunnable.java
@@ -0,0 +1,45 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.bookmarks;
+
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.task.PostTask;
+import org.chromium.base.task.TaskTraits;
+
+/**
+ * Simple interface to collapse multiple posts of the same runnable into a single deduplicated
+ * execution.
+ */
+public class PendingRunnable {
+    private final ThreadUtils.ThreadChecker mThreadChecker = new ThreadUtils.ThreadChecker();
+    private final @TaskTraits int mTaskTraits;
+    private final Runnable mRunnable;
+
+    private boolean mIsPending;
+
+    /**
+     * @param taskTraits Traits to run with.
+     * @param runnable The actual task to be executed.
+     */
+    public PendingRunnable(@TaskTraits int taskTraits, Runnable runnable) {
+        mTaskTraits = taskTraits;
+        mRunnable = runnable;
+    }
+
+    /** Posts a task to run the runnable this object holds. */
+    public void post() {
+        mThreadChecker.assertOnValidThread();
+        if (!mIsPending) {
+            mIsPending = true;
+            PostTask.postTask(mTaskTraits, this::onRun);
+        }
+    }
+
+    private void onRun() {
+        mThreadChecker.assertOnValidThread();
+        mIsPending = false;
+        mRunnable.run();
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
index 4084f3c..13f0d75 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/BaseCustomTabActivity.java
@@ -447,11 +447,6 @@
 
     @Override
     protected boolean handleBackPressed() {
-        if (!BackPressManager.isEnabled() && getTabModalLifetimeHandler() != null
-                && getTabModalLifetimeHandler().onBackPressed()) {
-            BackPressManager.record(BackPressHandler.Type.TAB_MODAL_HANDLER);
-            return true;
-        }
         if (BackPressManager.correctTabNavigationOnFallback()) {
             if (getToolbarManager() != null && getToolbarManager().back()) {
                 return true;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabObserver.java
index 8392f88..2c1d7214 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabObserver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabObserver.java
@@ -10,7 +10,6 @@
 import android.content.Intent;
 import android.graphics.Rect;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Process;
 import android.os.SystemClock;
 import android.text.TextUtils;
@@ -22,8 +21,7 @@
 
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.chrome.R;
-import org.chromium.chrome.browser.base.SplitCompatAppComponentFactory;
-import org.chromium.chrome.browser.base.SplitCompatAppComponentFactory.ProcessCreationReason;
+import org.chromium.chrome.browser.base.ColdStartTracker;
 import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
 import org.chromium.chrome.browser.customtabs.ClientManager.CalledWarmup;
 import org.chromium.chrome.browser.customtabs.features.TabInteractionRecorder;
@@ -250,6 +248,7 @@
     }
 
     private boolean wasWarmedUp() {
+        if (mCustomTabsConnection == null) return false;
         @CalledWarmup
         int warmedState = mCustomTabsConnection.getWarmupState(mSession);
         return warmedState == CalledWarmup.SESSION_NO_WARMUP_ALREADY_CALLED
@@ -270,14 +269,9 @@
     }
 
     private void recordFirstCommitNavigation(long firstCommitRealtimeMillis) {
-        // ProcessCreationReason is only available on P+.
-        if (mCustomTabsConnection == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return;
-
+        if (mCustomTabsConnection == null) return;
         String histogram = null;
         long duration = 0;
-        @ProcessCreationReason
-        int processCreationReason = SplitCompatAppComponentFactory.getProcessCreationReason();
-
         // Note that this will exclude Webapp launches in all cases due to either
         // mUsedHiddenTabSpeculation being null, or mIntentReceivedTimestamp being 0.
         if (mUsedHiddenTabSpeculation != null && mUsedHiddenTabSpeculation) {
@@ -290,7 +284,7 @@
             if (wasWarmedUp()) {
                 duration = firstCommitRealtimeMillis - mIntentReceivedRealtimeMillis;
                 histogram = "CustomTabs.Startup.TimeToFirstCommitNavigation2.WarmedUp";
-            } else if (processCreationReason == ProcessCreationReason.ACTIVITY
+            } else if (ColdStartTracker.wasColdOnFirstActivityCreationOrNow()
                     && SimpleStartupForegroundSessionDetector.runningCleanForegroundSession()) {
                 duration = firstCommitRealtimeMillis - Process.getStartElapsedRealtime();
                 histogram = "CustomTabs.Startup.TimeToFirstCommitNavigation2.Cold";
@@ -306,14 +300,9 @@
     }
 
     public void recordLargestContentfulPaint(long lcpUptimeMillis) {
-        // ProcessCreationReason is only available on P+.
-        if (mCustomTabsConnection == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return;
-
+        if (mCustomTabsConnection == null) return;
         String histogram = null;
         long duration = 0;
-        @ProcessCreationReason
-        int processCreationReason = SplitCompatAppComponentFactory.getProcessCreationReason();
-
         // Note that this will exclude Webapp launches in all cases due to either
         // mUsedHiddenTabSpeculation being null, or mIntentReceivedTimestamp being 0.
         if (mUsedHiddenTabSpeculation != null && mUsedHiddenTabSpeculation) {
@@ -326,7 +315,7 @@
             if (wasWarmedUp()) {
                 duration = lcpUptimeMillis - mIntentReceivedUptimeMillis;
                 histogram = "CustomTabs.Startup.TimeToLargestContentfulPaint2.WarmedUp";
-            } else if (processCreationReason == ProcessCreationReason.ACTIVITY
+            } else if (ColdStartTracker.wasColdOnFirstActivityCreationOrNow()
                     && SimpleStartupForegroundSessionDetector.runningCleanForegroundSession()) {
                 duration = lcpUptimeMillis - Process.getStartUptimeMillis();
                 histogram = "CustomTabs.Startup.TimeToLargestContentfulPaint2.Cold";
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
index 49baa35..aba8e61 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tabbed_mode/TabbedRootUiCoordinator.java
@@ -111,6 +111,7 @@
 import org.chromium.chrome.features.start_surface.StartSurfaceUserData;
 import org.chromium.components.browser_ui.accessibility.PageZoomCoordinator;
 import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
+import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
 import org.chromium.components.browser_ui.widget.InsetObserverView;
 import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
 import org.chromium.components.browser_ui.widget.TouchEventObserver;
@@ -153,6 +154,7 @@
     private NotificationPermissionController mNotificationPermissionController;
     private HistoryNavigationCoordinator mHistoryNavigationCoordinator;
     private NavigationSheet mNavigationSheet;
+    private ComposedBrowserControlsVisibilityDelegate mAppBrowserControlsVisibilityDelegate;
     private LayoutManagerImpl mLayoutManager;
     private CommerceSubscriptionsService mCommerceSubscriptionsService;
     private UndoGroupSnackbarController mUndoGroupSnackbarController;
@@ -877,6 +879,16 @@
         }
     }
 
+    /**
+     * @return {@link ComposedBrowserControlsVisibilityDelegate} object for tabbed activity.
+     */
+    public ComposedBrowserControlsVisibilityDelegate getAppBrowserControlsVisibilityDelegate() {
+        if (mAppBrowserControlsVisibilityDelegate == null) {
+            mAppBrowserControlsVisibilityDelegate = new ComposedBrowserControlsVisibilityDelegate();
+        }
+        return mAppBrowserControlsVisibilityDelegate;
+    }
+
     public StatusIndicatorCoordinator getStatusIndicatorCoordinatorForTesting() {
         return mStatusIndicatorCoordinator;
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
index f3361c0d..97f2d13 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
@@ -165,7 +165,6 @@
 import org.chromium.components.browser_ui.bottomsheet.ManagedBottomSheetController;
 import org.chromium.components.browser_ui.device_lock.DeviceLockActivityLauncher;
 import org.chromium.components.browser_ui.device_lock.DeviceLockActivityLauncherSupplier;
-import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
 import org.chromium.components.browser_ui.widget.MenuOrKeyboardActionController;
 import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
 import org.chromium.components.browser_ui.widget.scrim.ScrimCoordinator;
@@ -338,7 +337,6 @@
             new OneshotSupplierImpl<>();
     private FoldTransitionController mFoldTransitionController;
     private RestoreTabsFeatureHelper mRestoreTabsFeatureHelper;
-    private ComposedBrowserControlsVisibilityDelegate mAppBrowserControlsVisibilityDelegate;
     private @Nullable EdgeToEdgeController mE2eController;
 
     /**
@@ -1617,16 +1615,6 @@
     }
 
     /**
-     * @return {@link ComposedBrowserControlsVisibilityDelegate} object for tabbed activity.
-     */
-    public ComposedBrowserControlsVisibilityDelegate getAppBrowserControlsVisibilityDelegate() {
-        if (mAppBrowserControlsVisibilityDelegate == null) {
-            mAppBrowserControlsVisibilityDelegate = new ComposedBrowserControlsVisibilityDelegate();
-        }
-        return mAppBrowserControlsVisibilityDelegate;
-    }
-
-    /**
      * Gets the browser controls manager, creates it unless already created.
      * @deprecated Instead, inject this directly to your constructor. If that's not possible, then
      *         use {@link BrowserControlsManagerSupplier}.
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
index 402ff5ab..b91fab09 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabActivityTest.java
@@ -178,9 +178,6 @@
 import org.chromium.content_public.common.ContentSwitches;
 import org.chromium.net.test.EmbeddedTestServer;
 import org.chromium.net.test.util.TestWebServer;
-import org.chromium.ui.modaldialog.ModalDialogManager;
-import org.chromium.ui.modaldialog.ModalDialogProperties;
-import org.chromium.ui.modelutil.PropertyModel;
 import org.chromium.ui.mojom.WindowOpenDisposition;
 import org.chromium.ui.test.util.BlankUiTestActivity;
 import org.chromium.ui.test.util.DeviceRestriction;
@@ -2435,50 +2432,6 @@
 
     @Test
     @SmallTest
-    public void testNavigationDismissTabModalDialog() {
-        Context context = getInstrumentation().getTargetContext().getApplicationContext();
-        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
-        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
-        final Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
-
-        ModalDialogManager dialogManager =
-                mCustomTabActivityTestRule.getActivity().getModalDialogManagerSupplier().get();
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PropertyModel dialog =
-                    new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
-                            .with(ModalDialogProperties.TITLE, "test")
-                            .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT,
-                                    context.getString(R.string.delete))
-                            .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
-                                    context.getString(R.string.cancel))
-                            .with(ModalDialogProperties.CONTROLLER,
-                                    new ModalDialogProperties.Controller() {
-                                        @Override
-                                        public void onClick(PropertyModel model, int buttonType) {}
-
-                                        @Override
-                                        public void onDismiss(
-                                                PropertyModel model, int dismissalCause) {}
-                                    })
-                            .build();
-
-            dialogManager.showDialog(dialog, ModalDialogManager.ModalDialogType.TAB);
-        });
-
-        CriteriaHelper.pollUiThread(() -> dialogManager.isShowing());
-
-        TestThreadUtils.runOnUiThreadBlocking(
-                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
-        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);
-
-        Assert.assertTrue(tab.canGoBack());
-        Assert.assertFalse(tab.canGoForward());
-
-        CriteriaHelper.pollUiThread(() -> !dialogManager.isShowing());
-    }
-
-    @Test
-    @SmallTest
     @EnableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
     public void testBackPressNavigationFailure_WithRecover() {
         Context context = getInstrumentation().getTargetContext().getApplicationContext();
@@ -2557,74 +2510,6 @@
         });
     }
 
-    @Test
-    @SmallTest
-    @DisableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
-    public void testBackPressDismissTabModalDialog() {
-        Context context = getInstrumentation().getTargetContext().getApplicationContext();
-        Intent intent = CustomTabsIntentTestUtils.createMinimalCustomTabIntent(context, mTestPage);
-        mCustomTabActivityTestRule.startCustomTabActivityWithIntent(intent);
-        final Tab tab = mCustomTabActivityTestRule.getActivity().getActivityTab();
-
-        ModalDialogManager dialogManager =
-                mCustomTabActivityTestRule.getActivity().getModalDialogManagerSupplier().get();
-
-        TestThreadUtils.runOnUiThreadBlocking(
-                (Runnable) () -> tab.loadUrl(new LoadUrlParams(mTestPage2)));
-        ChromeTabUtils.waitForTabPageLoaded(tab, mTestPage2);
-
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            PropertyModel dialog =
-                    new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
-                            .with(ModalDialogProperties.TITLE, "test")
-                            .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT,
-                                    context.getString(R.string.delete))
-                            .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
-                                    context.getString(R.string.cancel))
-                            .with(ModalDialogProperties.CONTROLLER,
-                                    new ModalDialogProperties.Controller() {
-                                        @Override
-                                        public void onClick(PropertyModel model, int buttonType) {}
-
-                                        @Override
-                                        public void onDismiss(
-                                                PropertyModel model, int dismissalCause) {}
-                                    })
-                            .build();
-
-            dialogManager.showDialog(dialog, ModalDialogManager.ModalDialogType.TAB);
-        });
-        CriteriaHelper.pollUiThread(() -> dialogManager.isShowing(), "Dialog should be displayed");
-
-        HistogramWatcher histogramWatcher =
-                HistogramWatcher.newSingleRecordWatcher("Android.BackPress.Intercept",
-                        BackPressManager.getHistogramValueForTesting(
-                                BackPressHandler.Type.TAB_MODAL_HANDLER));
-
-        TestThreadUtils.runOnUiThreadBlocking(() -> {
-            mCustomTabActivityTestRule.getActivity().getOnBackPressedDispatcher().onBackPressed();
-        });
-
-        Assert.assertTrue("Should be able to navigate back after navigation", tab.canGoBack());
-        Assert.assertFalse("Should be unable to navigate forward", tab.canGoForward());
-        CriteriaHelper.pollInstrumentationThread(() -> {
-            Criteria.checkThat("Tab should not be navigated when dialog is dismissed",
-                    ChromeTabUtils.getUrlStringOnUiThread(getActivity().getActivityTab()),
-                    is(mTestPage2));
-        });
-
-        histogramWatcher.assertExpected("Dialog should be dismissed by back press");
-        CriteriaHelper.pollUiThread(
-                () -> !dialogManager.isShowing(), "Dialog should be dismissed by back press");
-    }
-
-    @Test
-    @SmallTest
-    @EnableFeatures(ChromeFeatureList.BACK_GESTURE_REFACTOR)
-    public void testBackPressDismissTabModalDialog_BackGestureRefactor() {
-        testBackPressDismissTabModalDialog();
-    }
-
     private void rotateCustomTabActivity(CustomTabActivity activity, int orientation) {
         ActivityTestUtils.rotateActivityToOrientation(activity, orientation);
         CriteriaHelper.pollUiThread(() -> {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/PermissionInfoTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/PermissionInfoTest.java
index 10d9eb6..6fdfbba 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/PermissionInfoTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/PermissionInfoTest.java
@@ -96,7 +96,7 @@
     private void setSettingAndExpectValue(@ContentSettingsType int type, String origin,
             String embedder, @ContentSettingValues int setting, Profile profile,
             @ContentSettingValues int expectedSetting) {
-        PermissionInfo info = new PermissionInfo(type, origin, embedder);
+        PermissionInfo info = new PermissionInfo(type, origin, embedder, /*isEmbargoed=*/false);
 
         TestThreadUtils.runOnUiThreadBlocking(() -> info.setContentSetting(profile, setting));
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/SingleWebsiteSettingsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/SingleWebsiteSettingsTest.java
index ecb6f518..bd4d454 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/SingleWebsiteSettingsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/site_settings/SingleWebsiteSettingsTest.java
@@ -4,8 +4,18 @@
 
 package org.chromium.chrome.browser.site_settings;
 
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+
 import android.os.Build;
 
 import androidx.test.filters.SmallTest;
@@ -26,6 +36,7 @@
 import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.DisableIf;
 import org.chromium.chrome.browser.flags.ChromeSwitches;
+import org.chromium.chrome.browser.profiles.Profile;
 import org.chromium.chrome.browser.settings.SettingsActivity;
 import org.chromium.chrome.test.ChromeJUnit4RunnerDelegate;
 import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
@@ -33,15 +44,16 @@
 import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
 import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
 import org.chromium.components.browser_ui.site_settings.ContentSettingException;
-import org.chromium.components.browser_ui.site_settings.PermissionInfo;
 import org.chromium.components.browser_ui.site_settings.SingleWebsiteSettings;
 import org.chromium.components.browser_ui.site_settings.SiteSettingsUtil;
 import org.chromium.components.browser_ui.site_settings.Website;
 import org.chromium.components.browser_ui.site_settings.WebsiteAddress;
+import org.chromium.components.browser_ui.site_settings.WebsitePreferenceBridge;
 import org.chromium.components.content_settings.ContentSettingValues;
 import org.chromium.components.content_settings.ContentSettingsType;
 import org.chromium.components.permissions.PermissionsAndroidFeatureList;
 import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.url.GURL;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -141,21 +153,53 @@
     @Test
     @SmallTest
     @EnableFeatures(PermissionsAndroidFeatureList.PERMISSION_STORAGE_ACCESS)
-    public void testStorageAccessePermission() {
-        Website website = createWebsiteWithStorageAccessPermission(
-                "https://[*.]embedded.com", "https://[*.]example.com");
-        Website other = createWebsiteWithStorageAccessPermission(
-                "https://[*.]embedded.com", "https://[*.]foo.com");
-        Website merged = SingleWebsiteSettings.mergePermissionAndStorageInfoForTopLevelOrigin(
-                WebsiteAddress.create(EXAMPLE_ADDRESS), List.of(website, other));
-        assertThat(merged.getEmbeddedPermissionInfos())
-                .isEqualTo(website.getEmbeddedPermissionInfos());
+    public void testStorageAccessPermission() {
+        int type = ContentSettingsType.STORAGE_ACCESS;
+        GURL example = new GURL("https://example.com");
+        GURL embedded2 = new GURL("https://embedded2.com");
 
+        Website website = createWebsiteWithStorageAccessPermission(
+                "https://[*.]embedded.com", "https://[*.]example.com", ContentSettingValues.ALLOW);
+        Website website2 = createWebsiteWithStorageAccessPermission(
+                "https://[*.]embedded2.com", "https://[*.]example.com", ContentSettingValues.BLOCK);
+        Website other = createWebsiteWithStorageAccessPermission(
+                "https://[*.]embedded.com", "https://[*.]foo.com", ContentSettingValues.BLOCK);
+        Website merged = SingleWebsiteSettings.mergePermissionAndStorageInfoForTopLevelOrigin(
+                WebsiteAddress.create(EXAMPLE_ADDRESS), List.of(website, website2, other));
+
+        var exceptions = merged.getEmbeddedPermissions().get(type);
+        assertThat(exceptions.size()).isEqualTo(2);
+        assertThat(exceptions.get(0).getContentSetting()).isEqualTo(ContentSettingValues.ALLOW);
+        assertThat(exceptions.get(1).getContentSetting()).isEqualTo(ContentSettingValues.BLOCK);
+        assertEquals(ContentSettingValues.BLOCK, getStorageAccessSetting(type, embedded2, example));
+
+        // Open site settings.
         SettingsActivity activity = SiteSettingsTestUtils.startSingleWebsitePreferences(merged);
-        // TODO(crbug.com/1478113): Test UI when it is implemented.
+        onView(withText("embedded.com")).check(matches(isDisplayed()));
+
+        // Toggle the second permission.
+        onView(withText("embedded2.com")).check(matches(isDisplayed())).perform(click());
+        assertEquals(ContentSettingValues.ALLOW, getStorageAccessSetting(type, embedded2, example));
+
+        // Reset permission.
+        onView(withText(containsString("reset"))).perform(click());
+        onView(withText("Delete & reset")).perform(click());
+        onView(withText("Embedded content")).check(doesNotExist());
+        assertEquals(ContentSettingValues.ASK, getStorageAccessSetting(type, embedded2, example));
         activity.finish();
     }
 
+    private static int getStorageAccessSetting(
+            @ContentSettingsType int contentSettingType, GURL primaryUrl, GURL secondaryUrl) {
+        int[] result = {0};
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            result[0] =
+                    WebsitePreferenceBridge.getContentSetting(Profile.getLastUsedRegularProfile(),
+                            contentSettingType, primaryUrl, secondaryUrl);
+        });
+        return result[0];
+    }
+
     /**
      * Helper function for creating a {@link ParameterSet} for {@link SingleWebsiteSettingsParams}.
      */
@@ -205,7 +249,7 @@
             String prefKey = SingleWebsiteSettings.getPreferenceKey(mContentSettingsType);
             ChromeSwitchPreference switchPref = websitePreferences.findPreference(prefKey);
             Assert.assertNotNull("Preference cannot be found on screen.", switchPref);
-            Assert.assertEquals("Switch check state is different than test setting.",
+            assertEquals("Switch check state is different than test setting.",
                     mContentSettingValue == ContentSettingValues.ALLOW, switchPref.isChecked());
         }
     }
@@ -222,11 +266,16 @@
     }
 
     private static Website createWebsiteWithStorageAccessPermission(
-            String origin, String embedder) {
+            String origin, String embedder, @ContentSettingValues int setting) {
         Website website =
                 new Website(WebsiteAddress.create(origin), WebsiteAddress.create(embedder));
-        website.addEmbeddedPermissionInfo(new PermissionInfo(
-                ContentSettingsType.STORAGE_ACCESS, origin, embedder, /*isEmbargoed=*/false));
+        ContentSettingException info = new ContentSettingException(
+                ContentSettingsType.STORAGE_ACCESS, origin, embedder, ContentSettingValues.ASK,
+                /*source=*/"", /*expiration=*/0, /*isEmbargoed=*/false);
+        // Set setting explicitly to write it to prefs.
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { info.setContentSetting(Profile.getLastUsedRegularProfile(), setting); });
+        website.addEmbeddedPermission(info);
         return website;
     }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/PendingRunnableTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/PendingRunnableTest.java
new file mode 100644
index 0000000..96d8cc6f
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/bookmarks/PendingRunnableTest.java
@@ -0,0 +1,52 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.bookmarks;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowLooper;
+
+import org.chromium.base.task.TaskTraits;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.base.test.util.Batch;
+
+/** Unit tests for {@link PendingRunnable}. */
+@Batch(Batch.UNIT_TESTS)
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class PendingRunnableTest {
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock
+    private Runnable mRunnable;
+
+    @Test
+    public void testPost() {
+        PendingRunnable pendingRunnable = new PendingRunnable(TaskTraits.UI_DEFAULT, mRunnable);
+        Mockito.verify(mRunnable, never()).run();
+
+        pendingRunnable.post();
+        Mockito.verify(mRunnable, never()).run();
+
+        pendingRunnable.post();
+        Mockito.verify(mRunnable, never()).run();
+
+        ShadowLooper.idleMainLooper();
+        Mockito.verify(mRunnable, times(1)).run();
+
+        ShadowLooper.idleMainLooper();
+        Mockito.verify(mRunnable, times(1)).run();
+    }
+}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
index 415bf82..f8d23ff 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionUnitTest.java
@@ -149,7 +149,6 @@
     @Test
     @EnableFeatures(ChromeFeatureList.CCT_RESIZABLE_SIDE_SHEET)
     public void onActivityLayout_CallbackIsCalledForNamedMethod() {
-        int uid = 123;
         int left = 0;
         int top = 0;
         int right = 100;
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webauth/BarrierTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/BarrierTest.java
new file mode 100644
index 0000000..a70e24a
--- /dev/null
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/BarrierTest.java
@@ -0,0 +1,226 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.webauth;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
+
+import org.chromium.base.Callback;
+import org.chromium.base.test.BaseRobolectricTestRule;
+import org.chromium.components.webauthn.Barrier;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+@RunWith(ParameterizedRobolectricTestRunner.class)
+public class BarrierTest {
+    public enum ApiCallType {
+        NONE,
+        CRED_MAN,
+        FIDO_2_API,
+    }
+    public enum ApiCallStatus {
+        NONE,
+        SUCCESS,
+        FAILURE,
+    }
+    public enum Expectation {
+        NONE,
+        CRED_MAN_RAN,
+        FIDO_2_API_RAN,
+        BOTH_RAN,
+        ERROR_RAN,
+    }
+    @Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(new Object[][] {{/*mode=*/Barrier.Mode.ONLY_CRED_MAN,
+                                                     /*firstCompletedApi=*/ApiCallType.CRED_MAN,
+                                                     /*firstCompletedStatus=*/ApiCallStatus.SUCCESS,
+                                                     /*secondCompletion=*/ApiCallType.NONE,
+                                                     /*secondCompletionType=*/ApiCallStatus.NONE,
+                                                     /*expectation=*/Expectation.CRED_MAN_RAN},
+                {/*mode=*/Barrier.Mode.ONLY_CRED_MAN,
+                        /*firstCompletedApi=*/ApiCallType.CRED_MAN,
+                        /*firstCompletedStatus=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.NONE,
+                        /*secondCompletionType=*/ApiCallStatus.NONE,
+                        /*expectation=*/Expectation.ERROR_RAN},
+
+                {/*mode=*/Barrier.Mode.ONLY_FIDO_2_API,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.NONE,
+                        /*secondCompletionType=*/ApiCallStatus.NONE,
+                        /*expectation=*/Expectation.FIDO_2_API_RAN},
+                {/*mode=*/Barrier.Mode.ONLY_FIDO_2_API,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.NONE,
+                        /*secondCompletionType=*/ApiCallStatus.NONE,
+                        /*expectation=*/Expectation.ERROR_RAN},
+
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.CRED_MAN,
+                        /*secondCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*expectation=*/Expectation.BOTH_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.CRED_MAN,
+                        /*secondCompletionType=*/ApiCallStatus.FAILURE,
+                        /*expectation=*/Expectation.FIDO_2_API_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.NONE,
+                        /*secondCompletionType=*/ApiCallStatus.NONE,
+                        /*expectation=*/Expectation.NONE},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.CRED_MAN,
+                        /*secondCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*expectation=*/Expectation.CRED_MAN_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.FIDO_2_API,
+                        /*firstCompletionType=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.CRED_MAN,
+                        /*secondCompletionType=*/ApiCallStatus.FAILURE,
+                        /*expectation=*/Expectation.ERROR_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.CRED_MAN,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.FIDO_2_API,
+                        /*secondCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*expectation=*/Expectation.BOTH_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.CRED_MAN,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.FIDO_2_API,
+                        /*secondCompletionType=*/ApiCallStatus.FAILURE,
+                        /*expectation=*/Expectation.CRED_MAN_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.CRED_MAN,
+                        /*firstCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*secondCompletion=*/ApiCallType.NONE,
+                        /*secondCompletionType=*/ApiCallStatus.NONE,
+                        /*expectation=*/Expectation.NONE},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.CRED_MAN,
+                        /*firstCompletionType=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.FIDO_2_API,
+                        /*secondCompletionType=*/ApiCallStatus.SUCCESS,
+                        /*expectation=*/Expectation.FIDO_2_API_RAN},
+                {/*mode=*/Barrier.Mode.BOTH,
+                        /*firstCompletion=*/ApiCallType.CRED_MAN,
+                        /*firstCompletionType=*/ApiCallStatus.FAILURE,
+                        /*secondCompletion=*/ApiCallType.FIDO_2_API,
+                        /*secondCompletionType=*/ApiCallStatus.FAILURE,
+                        /*expectation=*/Expectation.ERROR_RAN}});
+    }
+    @Parameter(0)
+    public Barrier.Mode mMode;
+    @Parameter(1)
+    public ApiCallType mFirstCompletedApi;
+    @Parameter(2)
+    public ApiCallStatus mFirstCompletedStatus;
+    @Parameter(3)
+    public ApiCallType mSecondCompletedApi;
+    @Parameter(4)
+    public ApiCallStatus mSecondCompletedStatus;
+    @Parameter(5)
+    public Expectation mExpectation;
+
+    @Rule(order = -2)
+    public BaseRobolectricTestRule mBaseRule = new BaseRobolectricTestRule();
+
+    @Mock
+    Runnable mCredManSuccesfulRunnable;
+    @Mock
+    Runnable mFido2ApiSuccessfulRunnable;
+    @Mock
+    Callback<Integer> mErrorCallback;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.openMocks(this);
+    }
+
+    @Test
+    public void testScenarios() {
+        Barrier barrier = new Barrier(mErrorCallback);
+        barrier.resetAndSetWaitStatus(mMode);
+
+        if (mFirstCompletedApi == ApiCallType.CRED_MAN
+                && mFirstCompletedStatus == ApiCallStatus.SUCCESS) {
+            barrier.onCredManSuccessful(mCredManSuccesfulRunnable);
+        } else if (mFirstCompletedApi == ApiCallType.CRED_MAN
+                && mFirstCompletedStatus == ApiCallStatus.FAILURE) {
+            barrier.onCredManFailed(0);
+        } else if (mFirstCompletedApi == ApiCallType.FIDO_2_API
+                && mFirstCompletedStatus == ApiCallStatus.SUCCESS) {
+            barrier.onFido2ApiSuccessful(mFido2ApiSuccessfulRunnable);
+        } else {
+            barrier.onFido2ApiFailed(0);
+        }
+
+        if (mSecondCompletedApi == ApiCallType.CRED_MAN
+                && mSecondCompletedStatus == ApiCallStatus.SUCCESS) {
+            barrier.onCredManSuccessful(mCredManSuccesfulRunnable);
+        } else if (mSecondCompletedApi == ApiCallType.CRED_MAN
+                && mSecondCompletedStatus == ApiCallStatus.FAILURE) {
+            barrier.onCredManFailed(0);
+        } else if (mSecondCompletedApi == ApiCallType.FIDO_2_API
+                && mSecondCompletedStatus == ApiCallStatus.SUCCESS) {
+            barrier.onFido2ApiSuccessful(mFido2ApiSuccessfulRunnable);
+        } else if (mSecondCompletedApi == ApiCallType.FIDO_2_API
+                && mSecondCompletedStatus == ApiCallStatus.FAILURE) {
+            barrier.onFido2ApiFailed(0);
+        }
+
+        switch (mExpectation) {
+            case BOTH_RAN:
+                verify(mFido2ApiSuccessfulRunnable, times(1)).run();
+                verify(mCredManSuccesfulRunnable, times(1)).run();
+                verify(mErrorCallback, times(0)).onResult(anyInt());
+                break;
+            case ERROR_RAN:
+                verify(mFido2ApiSuccessfulRunnable, times(0)).run();
+                verify(mCredManSuccesfulRunnable, times(0)).run();
+                verify(mErrorCallback, times(1)).onResult(anyInt());
+                break;
+            case FIDO_2_API_RAN:
+                verify(mFido2ApiSuccessfulRunnable, times(1)).run();
+                verify(mCredManSuccesfulRunnable, times(0)).run();
+                verify(mErrorCallback, times(0)).onResult(anyInt());
+                break;
+            case CRED_MAN_RAN:
+                verify(mFido2ApiSuccessfulRunnable, times(0)).run();
+                verify(mCredManSuccesfulRunnable, times(1)).run();
+                verify(mErrorCallback, times(0)).onResult(anyInt());
+                break;
+            case NONE:
+                verify(mFido2ApiSuccessfulRunnable, times(0)).run();
+                verify(mCredManSuccesfulRunnable, times(0)).run();
+                verify(mErrorCallback, times(0)).onResult(anyInt());
+                break;
+            default:
+                assert false : "Unhandled expectation " + mExpectation;
+        }
+    }
+}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webauth/CredManHelperRobolectricTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/CredManHelperRobolectricTest.java
index c1865465..683f549 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/webauth/CredManHelperRobolectricTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/CredManHelperRobolectricTest.java
@@ -38,6 +38,7 @@
 import org.chromium.blink.mojom.PublicKeyCredentialDescriptor;
 import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
 import org.chromium.blink.mojom.ResidentKeyRequirement;
+import org.chromium.components.webauthn.Barrier;
 import org.chromium.components.webauthn.CredManHelper;
 import org.chromium.components.webauthn.CredManMetricsHelper;
 import org.chromium.components.webauthn.CredManMetricsHelper.CredManCreateRequestEnum;
@@ -69,6 +70,8 @@
     private WebAuthnBrowserBridge mBrowserBridge;
     @Mock
     private Callback<Integer> mErrorCallback;
+    @Mock
+    private Barrier mBarrier;
 
     private CredManHelper.BridgeProvider mBridgeProvider = new CredManHelper.BridgeProvider() {
         @Override
@@ -201,11 +204,14 @@
         int result =
                 mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                         /*isCrossOrigin=*/false, /*maybeClientDataHash=*/null,
-                        mCallback::onSignResponse, mErrorCallback);
+                        mCallback::onSignResponse, mErrorCallback, /*ignoreGpm=*/false);
 
         assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
         FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
         assertThat(credManRequest).isNotNull();
+        assertThat(credManRequest.getData().containsKey(
+                           "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME"))
+                .isTrue();
         assertThat(credManRequest.getOrigin()).isEqualTo(mOriginString);
         assertThat(credManRequest.getCredentialOptions()).hasSize(1);
         FakeAndroidCredentialOption option = credManRequest.getCredentialOptions().get(0);
@@ -216,6 +222,9 @@
                 .isEqualTo("{serialized_get_request}");
         assertThat(option.getCandidateQueryData().containsKey("com.android.chrome.CHANNEL"))
                 .isTrue();
+        assertThat(
+                option.getCandidateQueryData().getBoolean("com.android.chrome.GPM_IGNORE", false))
+                .isFalse();
         assertThat(option.isSystemProviderRequired()).isFalse();
         assertThat(mCallback.getStatus()).isEqualTo(Integer.valueOf(AuthenticatorStatus.SUCCESS));
         verify(mBrowserBridge, never()).onCredManUiClosed(any(), anyBoolean());
@@ -231,7 +240,7 @@
         int result =
                 mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                         /*isCrossOrigin=*/false, mMaybeClientDataHash, mCallback::onSignResponse,
-                        mErrorCallback);
+                        mErrorCallback, /*ignoreGpm=*/false);
 
         assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
         FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
@@ -254,7 +263,7 @@
         int result =
                 mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                         /*isCrossOrigin=*/false, mMaybeClientDataHash, mCallback::onSignResponse,
-                        mErrorCallback);
+                        mErrorCallback, /*ignoreGpm=*/false);
 
         assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
         FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
@@ -276,7 +285,7 @@
         int result =
                 mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                         /*isCrossOrigin=*/false, mMaybeClientDataHash, mCallback::onSignResponse,
-                        mErrorCallback);
+                        mErrorCallback, /*ignoreGpm=*/false);
 
         assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
         verify(mErrorCallback, times(1)).onResult(AuthenticatorStatus.NOT_ALLOWED_ERROR);
@@ -294,7 +303,7 @@
         int result =
                 mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                         /*isCrossOrigin=*/false, mMaybeClientDataHash, mCallback::onSignResponse,
-                        mErrorCallback);
+                        mErrorCallback, /*ignoreGpm=*/false);
 
         assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
         verify(mErrorCallback, times(1)).onResult(AuthenticatorStatus.UNKNOWN_ERROR);
@@ -308,12 +317,17 @@
     public void testStartPrefetchRequest_default_success() {
         mRequestOptions.isConditional = true;
 
-        int result = mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions,
-                mOriginString,
+        mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                 /*isCrossOrigin=*/false,
-                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback);
+                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback, mBarrier,
+                /*ignoreGpm=*/false);
 
-        assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
+        verify(mBarrier, never()).onCredManFailed(anyInt());
+        ArgumentCaptor<Runnable> credManCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrier).onCredManSuccessful(credManCallSuccessfulRunback.capture());
+        credManCallSuccessfulRunback.getValue().run();
+
         FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
         assertThat(credManRequest).isNotNull();
         assertThat(credManRequest.getOrigin()).isEqualTo(mOriginString);
@@ -342,13 +356,13 @@
         mCredentialManager.setErrorResponse(new FakeAndroidCredManException(
                 "android.credentials.GetCredentialException.TYPE_UNKNOWN", "Message"));
 
-        int result = mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions,
-                mOriginString,
+        mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                 /*isCrossOrigin=*/false,
-                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback);
+                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback, mBarrier,
+                /*ignoreGpm=*/false);
 
-        assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
-        verify(mErrorCallback, times(1)).onResult(AuthenticatorStatus.UNKNOWN_ERROR);
+        verify(mBarrier, never()).onCredManFailed(eq(0));
+        verify(mBarrier, times(1)).onCredManFailed(eq(AuthenticatorStatus.UNKNOWN_ERROR));
         verify(mMetricsHelper, times(1))
                 .recordCredmanPrepareRequestHistogram(eq(CredManPrepareRequestEnum.SENT_REQUEST));
         verify(mMetricsHelper, times(1))
@@ -363,10 +377,16 @@
 
         mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                 /*isCrossOrigin=*/false,
-                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback);
+                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback, mBarrier,
+                /*ignoreGpm=*/false);
+        ArgumentCaptor<Runnable> credManCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrier).onCredManSuccessful(credManCallSuccessfulRunback.capture());
+        credManCallSuccessfulRunback.getValue().run();
+
         mCredManHelper.cancelConditionalGetAssertion(mFrameHost);
 
-        verify(mErrorCallback, times(1)).onResult(AuthenticatorStatus.ABORT_ERROR);
+        verify(mBarrier, times(1)).onCredManCancelled();
         verify(mBrowserBridge, times(1)).cleanupCredManRequest(any());
         verify(mBrowserBridge, never()).onCredManUiClosed(any(), anyBoolean());
         verify(mMetricsHelper, never()).reportGetCredentialMetrics(anyInt(), any());
@@ -379,12 +399,17 @@
         ArgumentCaptor<Callback<Boolean>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
         mRequestOptions.isConditional = true;
 
-        int result = mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions,
-                mOriginString,
+        mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                 /*isCrossOrigin=*/false,
-                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback);
+                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback, mBarrier,
+                /*ignoreGpm=*/false);
 
-        assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
+        verify(mBarrier, never()).onCredManFailed(anyInt());
+        ArgumentCaptor<Runnable> credManCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrier).onCredManSuccessful(credManCallSuccessfulRunback.capture());
+        credManCallSuccessfulRunback.getValue().run();
+
         verify(mMetricsHelper, times(1)).recordCredmanPrepareRequestDuration(anyLong());
 
         // Setup the test for startGetRequest:
@@ -411,12 +436,17 @@
         ArgumentCaptor<Callback<Boolean>> callbackCaptor = ArgumentCaptor.forClass(Callback.class);
         mRequestOptions.isConditional = true;
 
-        int result = mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions,
-                mOriginString,
+        mCredManHelper.startPrefetchRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
                 /*isCrossOrigin=*/false,
-                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback);
+                /*maybeClientDataHash=*/null, mCallback::onSignResponse, mErrorCallback, mBarrier,
+                /*ignoreGpm=*/false);
 
-        assertThat(result).isEqualTo(AuthenticatorStatus.SUCCESS);
+        verify(mBarrier, never()).onCredManFailed(anyInt());
+        ArgumentCaptor<Runnable> credManCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrier).onCredManSuccessful(credManCallSuccessfulRunback.capture());
+        credManCallSuccessfulRunback.getValue().run();
+
         verify(mMetricsHelper, times(1)).recordCredmanPrepareRequestDuration(anyLong());
         FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
         assertThat(credManRequest).isNotNull();
@@ -454,4 +484,26 @@
         verify(mMetricsHelper, times(1))
                 .reportGetCredentialMetrics(eq(CredManGetRequestEnum.SUCCESS_PASSWORD), any());
     }
+
+    @Test
+    @SmallTest
+    public void testStartGetRequest_ignoreGpm_DisablesBrandingAndHasBooleanInBundle() {
+        mCredManHelper.startGetRequest(mContext, mFrameHost, mRequestOptions, mOriginString,
+                /*isCrossOrigin=*/false, /*maybeClientDataHash=*/null, mCallback::onSignResponse,
+                mErrorCallback, /*ignoreGpm=*/true);
+
+        FakeAndroidCredManGetRequest credManRequest = mCredentialManager.getGetRequest();
+        assertThat(credManRequest).isNotNull();
+        assertThat(credManRequest.getData().containsKey(
+                           "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME"))
+                .isFalse();
+        assertThat(credManRequest.getCredentialOptions()).hasSize(1);
+        FakeAndroidCredentialOption option = credManRequest.getCredentialOptions().get(0);
+        assertThat(option).isNotNull();
+        assertThat(option.getType()).isEqualTo("androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL");
+        assertThat(option.getCandidateQueryData().containsKey("com.android.chrome.GPM_IGNORE"))
+                .isTrue();
+        assertThat(option.getCandidateQueryData().getBoolean("com.android.chrome.GPM_IGNORE"))
+                .isTrue();
+    }
 }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/webauth/Fido2CredentialRequestRobolectricTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/Fido2CredentialRequestRobolectricTest.java
index 928a193..67178320 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/webauth/Fido2CredentialRequestRobolectricTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/webauth/Fido2CredentialRequestRobolectricTest.java
@@ -42,6 +42,7 @@
 import org.chromium.blink.mojom.PublicKeyCredentialDescriptor;
 import org.chromium.blink.mojom.PublicKeyCredentialRequestOptions;
 import org.chromium.blink.mojom.ResidentKeyRequirement;
+import org.chromium.components.webauthn.Barrier;
 import org.chromium.components.webauthn.CredManHelper;
 import org.chromium.components.webauthn.Fido2ApiCallHelper;
 import org.chromium.components.webauthn.Fido2ApiTestHelper;
@@ -82,6 +83,8 @@
     WebAuthnBrowserBridge mBrowserBridgeMock;
     @Mock
     CredManHelper mCredManHelperMock;
+    @Mock
+    Barrier mBarrierMock;
 
     @Rule
     public JniMocker mMocker = new JniMocker();
@@ -90,6 +93,8 @@
     public void setUp() throws Exception {
         FeatureList.TestValues testValues = new FeatureList.TestValues();
         testValues.addFeatureFlagOverride(DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, true);
+        testValues.addFieldTrialParamOverride(
+                DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, "gpm_in_cred_man", "true");
         FeatureList.setTestValues(testValues);
 
         MockitoAnnotations.initMocks(this);
@@ -135,6 +140,7 @@
         mRequest.setOverrideVersionCheckForTesting(true);
         mRequest.overrideBrowserBridgeForTesting(mBrowserBridgeMock);
         mRequest.setCredManHelperForTesting(mCredManHelperMock);
+        mRequest.setBarrierForTesting(mBarrierMock);
     }
 
     @Test
@@ -274,7 +280,7 @@
 
     @Test
     @SmallTest
-    public void testGetAssertion_credManEnabled_success() {
+    public void testGetAssertion_credManEnabledWithGpmInCredManFlag_success() {
         // Calls to `context.getMainExecutor()` require API level 28 or higher.
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
 
@@ -288,7 +294,38 @@
                         eq(originString),
                         /*isCrossOrigin=*/eq(false), /*maybeClientDataHash=*/eq(null),
                         /*getCallback=*/any(),
-                        /*errorCallback=*/any());
+                        /*errorCallback=*/any(), /*ignoreGpm=*/eq(false));
+    }
+
+    @Test
+    @SmallTest
+    public void testGetAssertion_credManEnabledWithGpmNotInCredManFlag_success() {
+        // Calls to `context.getMainExecutor()` require API level 28 or higher.
+        Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
+
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.addFeatureFlagOverride(DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, true);
+        testValues.addFieldTrialParamOverride(
+                DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, "gpm_in_cred_man", "false");
+        FeatureList.setTestValues(testValues);
+
+        mRequest.handleGetAssertionRequest(mActivity, mRequestOptions, mFrameHost,
+                /*maybeClientDataHash=*/null, mOrigin, mOrigin, /*payment=*/null,
+                mCallback::onSignResponse, errorStatus -> mCallback.onError(errorStatus));
+
+        ArgumentCaptor<Runnable> fido2ApiCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrierMock).onFido2ApiSuccessful(fido2ApiCallSuccessfulRunback.capture());
+        fido2ApiCallSuccessfulRunback.getValue().run();
+
+        String originString = Fido2CredentialRequest.convertOriginToString(mOrigin);
+        verify(mCredManHelperMock)
+                .startPrefetchRequest(eq(mActivity), eq(mFrameHost), eq(mRequestOptions),
+                        eq(originString),
+                        /*isCrossOrigin=*/eq(false), /*maybeClientDataHash=*/eq(null),
+                        /*getCallback=*/any(),
+                        /*errorCallback=*/any(), /*barrier=*/any(), /*ignoreGpm=*/eq(true));
+        assertThat(mFido2ApiCallHelper.mGetAssertionCalled).isTrue();
     }
 
     @Test
@@ -334,7 +371,7 @@
                         /*originString=*/any(),
                         /*isCrossOrigin=*/eq(false), /*maybeClientDataHash=*/eq(null),
                         /*getCallback=*/any(),
-                        /*errorCallback=*/any());
+                        /*errorCallback=*/any(), /*ignoreGpm=*/eq(false));
     }
 
     @Test
@@ -385,7 +422,7 @@
 
     @Test
     @SmallTest
-    public void testGetAssertion_credManNoCredentials_fallbackToPlayServices() {
+    public void testGetAssertion_credManNoCredentialsWithGpmInCredManFlag_fallbackToPlayServices() {
         // Calls to `context.getMainExecutor()` require API level 28 or higher.
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
 
@@ -400,7 +437,8 @@
                 ArgumentCaptor.forClass(Runnable.class);
         verify(mCredManHelperMock).setNoCredentialsFallback(setNoCredentialsParamCaptor.capture());
         verify(mCredManHelperMock)
-                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any());
+                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any(),
+                        /*ignoreGpm=*/eq(false));
 
         // Now run the no credentials fallback action:
         setNoCredentialsParamCaptor.getValue().run();
@@ -430,7 +468,8 @@
                 errorStatus -> mCallback.onError(errorStatus));
 
         verify(mCredManHelperMock)
-                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any());
+                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any(),
+                        /*ignoreGpm=*/eq(false));
         assertThat(mFido2ApiCallHelper.mGetAssertionCalled).isFalse();
     }
 
@@ -455,7 +494,8 @@
                 errorStatus -> mCallback.onError(errorStatus));
 
         verify(mCredManHelperMock)
-                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any());
+                .startGetRequest(any(), any(), any(), any(), anyBoolean(), any(), any(), any(),
+                        /*ignoreGpm=*/eq(false));
         assertThat(mFido2ApiCallHelper.mGetAssertionCalled).isFalse();
     }
 
@@ -488,7 +528,7 @@
 
     @Test
     @SmallTest
-    public void testConditionalGetAssertion_credManEnabledSuccess_success() {
+    public void testConditionalGetAssertion_credManEnabledSuccessWithGpmInCredManFlag_success() {
         // Calls to `context.getMainExecutor()` require API level 28 or higher.
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
         mRequestOptions.isConditional = true;
@@ -503,13 +543,48 @@
                         eq(originString),
                         /*isCrossOrigin=*/eq(false), /*maybeClientDataHash=*/eq(null),
                         /*getCallback=*/any(),
-                        /*errorCallback=*/any());
+                        /*errorCallback=*/any(), /*barrier=*/any(), /*ignoreGpm=*/eq(false));
         verify(mBrowserBridgeMock, never()).onCredManUiClosed(any(), anyBoolean());
     }
 
     @Test
     @SmallTest
-    public void testConditionalGetAssertion_credManEnabledRpCancelWhileIdle_notAllowedError() {
+    public void testConditionalGetAssertion_credManEnabledSuccessWithGpmNotInCredManFlag_success() {
+        // Calls to `context.getMainExecutor()` require API level 28 or higher.
+        Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
+        mRequestOptions.isConditional = true;
+
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.addFeatureFlagOverride(DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, true);
+        testValues.addFieldTrialParamOverride(
+                DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, "gpm_in_cred_man", "false");
+        FeatureList.setTestValues(testValues);
+
+        mRequest.handleGetAssertionRequest(mActivity, mRequestOptions, mFrameHost,
+                /*maybeClientDataHash=*/null, mOrigin, mOrigin, /*payment=*/null,
+                mCallback::onSignResponse, errorStatus -> mCallback.onError(errorStatus));
+
+        ArgumentCaptor<Runnable> fido2ApiCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrierMock).onFido2ApiSuccessful(fido2ApiCallSuccessfulRunback.capture());
+        fido2ApiCallSuccessfulRunback.getValue().run();
+
+        String originString = Fido2CredentialRequest.convertOriginToString(mOrigin);
+        verify(mCredManHelperMock, times(1))
+                .startPrefetchRequest(eq(mActivity), eq(mFrameHost), eq(mRequestOptions),
+                        eq(originString),
+                        /*isCrossOrigin=*/eq(false), /*maybeClientDataHash=*/eq(null),
+                        /*getCallback=*/any(),
+                        /*errorCallback=*/any(), /*barrier=*/any(), /*ignoreGpm=*/eq(true));
+        verify(mBrowserBridgeMock, times(1))
+                .onCredentialsDetailsListReceived(any(), any(), eq(true), any(), any());
+        verify(mBrowserBridgeMock, never()).onCredManUiClosed(any(), anyBoolean());
+    }
+
+    @Test
+    @SmallTest
+    public void
+    testConditionalGetAssertion_credManEnabledRpCancelWhileIdleWithGpmInCredManFlag_notAllowedError() {
         // Calls to `context.getMainExecutor()` require API level 28 or higher.
         Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
         mRequestOptions.isConditional = true;
@@ -527,6 +602,37 @@
         verify(mBrowserBridgeMock, never()).onCredManUiClosed(any(), anyBoolean());
     }
 
+    @Test
+    @SmallTest
+    public void
+    testConditionalGetAssertion_credManEnabledRpCancelWhileIdleWithGpmNotInCredManFlag_notAllowedError() {
+        // Calls to `context.getMainExecutor()` require API level 28 or higher.
+        Assume.assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P);
+        mRequestOptions.isConditional = true;
+
+        FeatureList.TestValues testValues = new FeatureList.TestValues();
+        testValues.addFeatureFlagOverride(DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, true);
+        testValues.addFieldTrialParamOverride(
+                DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, "gpm_in_cred_man", "false");
+        FeatureList.setTestValues(testValues);
+
+        mRequest.handleGetAssertionRequest(mActivity, mRequestOptions, mFrameHost,
+                /*maybeClientDataHash=*/null, mOrigin, mOrigin, /*payment=*/null,
+                mCallback::onSignResponse, errorStatus -> mCallback.onError(errorStatus));
+
+        ArgumentCaptor<Runnable> fido2ApiCallSuccessfulRunback =
+                ArgumentCaptor.forClass(Runnable.class);
+        verify(mBarrierMock).onFido2ApiSuccessful(fido2ApiCallSuccessfulRunback.capture());
+        fido2ApiCallSuccessfulRunback.getValue().run();
+
+        mRequest.cancelConditionalGetAssertion(mFrameHost);
+
+        verify(mBarrierMock, times(1)).onFido2ApiCancelled();
+        verify(mCredManHelperMock, times(1)).cancelConditionalGetAssertion(eq(mFrameHost));
+        verify(mBrowserBridgeMock, times(1)).cleanupRequest(any());
+        verify(mBrowserBridgeMock, never()).onCredManUiClosed(any(), anyBoolean());
+    }
+
     static class FakeFido2ApiCallHelper extends Fido2ApiCallHelper {
         public boolean mMakeCredentialCalled;
         public boolean mGetAssertionCalled;
diff --git a/chrome/app/chrome_command_ids.h b/chrome/app/chrome_command_ids.h
index 4a6d30b..60cd5ee 100644
--- a/chrome/app/chrome_command_ids.h
+++ b/chrome/app/chrome_command_ids.h
@@ -500,8 +500,6 @@
 
 // PDF OCR
 #define IDC_CONTENT_CONTEXT_PDF_OCR 52421
-#define IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS 52422
-#define IDC_CONTENT_CONTEXT_PDF_OCR_ONCE 52423
 
 // Tab Search
 #define IDC_TAB_SEARCH 52500
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index e133a5dc..b9d114d 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -260,10 +260,10 @@
     Invalid code. Please try again.
   </message>
   <message name="IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_SUBTITLE" desc="Subtitle for input informing the user that the code used to install an eSIM profile must follow a specific format.">
-    Your entry should have the format LPA:1$&lt;smdp address&gt;$&lt;activation code&gt;
+      Your entry should have the format <ph name="LPA_0">LPA:1</ph>&#36;<ph name="LPA_1">&lt;</ph>SM-DP+ address<ph name="LPA_2">&gt;</ph>&#36;<ph name="LPA_3">&lt;</ph>optional matching id<ph name="LPA_4">&gt;</ph>
   </message>
   <message name="IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_ERROR" desc="Error label for input informing the user that the code used to install an eSIM profile is invalid and must follow a specific format.">
-    Invalid code. Your entry should have the format LPA:1$&lt;smdp address&gt;$&lt;activation code&gt;
+      Invalid code. Your entry should have the format <ph name="LPA_0">LPA:1</ph>&#36;<ph name="LPA_1">&lt;</ph>SM-DP+ address<ph name="LPA_2">&gt;</ph>&#36;<ph name="LPA_3">&lt;</ph>optional matching id<ph name="LPA_4">&gt;</ph>
   </message>
   <message name="IDS_CELLULAR_SETUP_ESTABLISH_NETWORK_CONNECTION" desc="Message, informing user that a network connection is being established during cellular setup">
     Establishing network connection ...
diff --git a/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_ERROR.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_ERROR.png.sha1
index 589925a5..15a00e6 100644
--- a/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_ERROR.png.sha1
+++ b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_ERROR.png.sha1
@@ -1 +1 @@
-287e5e5412ffc53650d57ebe270d465788b59625
\ No newline at end of file
+b7b4038bca6c79df7ec3b145e12754eaa93bbb7a
\ No newline at end of file
diff --git a/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_SUBTITLE.png.sha1 b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_SUBTITLE.png.sha1
index 451a99f0..15a00e6 100644
--- a/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_SUBTITLE.png.sha1
+++ b/chrome/app/chromeos_strings_grdp/IDS_CELLULAR_SETUP_ESIM_PAGE_SCAN_QR_CODE_INPUT_SUBTITLE.png.sha1
@@ -1 +1 @@
-cc6ba64b78c992e9d5880984d6179cdd2fae033a
\ No newline at end of file
+b7b4038bca6c79df7ec3b145e12754eaa93bbb7a
\ No newline at end of file
diff --git a/chrome/app/chromium_strings.grd b/chrome/app/chromium_strings.grd
index b9c5fe1d..e548a4e 100644
--- a/chrome/app/chromium_strings.grd
+++ b/chrome/app/chromium_strings.grd
@@ -787,7 +787,7 @@
 
         <!-- Extension uninstall prompt -->
         <message name="IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX" desc="Checkbox text to ask the user whether they want to remove associated data at uninstall time. Only used when uninstalling an app associated with a particular website.">
-          Also clear data from Chromium (<ph name="URL">$1<ex>www.google.com</ex></ph>)
+          Also delete data from Chromium (<ph name="URL">$1<ex>www.google.com</ex></ph>)
         </message>
 
         <!-- Extension alerts. -->
diff --git a/chrome/app/chromium_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1 b/chrome/app/chromium_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1
new file mode 100644
index 0000000..c910b325
--- /dev/null
+++ b/chrome/app/chromium_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1
@@ -0,0 +1 @@
+91253664743adaff4cd899c3c1e08a3c83d519e6
\ No newline at end of file
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 1d04338..e4a0cd48 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -762,12 +762,6 @@
           <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION" desc="The context-menu item that asks whether to use the PDF OCR service to convert image to text.">
             Extract text from PDF
           </message>
-          <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS" desc="The context-menu sub-item that asks whether to use the PDF OCR service to convert image to text always.">
-            Always
-          </message>
-          <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE" desc="The context-menu sub-item that asks whether to use the PDF OCR service to convert image to text, for a single use.">
-            Just once
-          </message>
           <message name="IDS_CONTENT_CONTEXT_RUN_LAYOUT_EXTRACTION" desc="The context-menu item that runs a local machine intelligence model to extract visual layout semantics.">
            Recognize visual layout semantics
           </message>
@@ -1052,12 +1046,6 @@
           <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION" desc="The context-menu item that asks whether to use the PDF OCR service to convert image to text.">
             Extract Text From PDF
           </message>
-          <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS" desc="The context-menu sub-item that asks whether to use the PDF OCR service to convert image to text always.">
-            Always
-          </message>
-          <message name="IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE" desc="The context-menu sub-item that asks whether to use the PDF OCR service to convert image to text, for a single use.">
-            Just once
-          </message>
           <message name="IDS_CONTENT_CONTEXT_RUN_LAYOUT_EXTRACTION" desc="In Title Case: The context-menu item that runs a local machine intelligence model to extract visual layout semantics.">
            Recognize Visual Layout Semantics
           </message>
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS.png.sha1
deleted file mode 100644
index 7da0070..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-12a00036bde11df37a6c08ef9d9a4a3f60e5aa7d
\ No newline at end of file
diff --git a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE.png.sha1 b/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE.png.sha1
deleted file mode 100644
index b935a14..0000000
--- a/chrome/app/generated_resources_grd/IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-6101413baa79ef7b1d10990bb533dd704b27003b
\ No newline at end of file
diff --git a/chrome/app/google_chrome_strings.grd b/chrome/app/google_chrome_strings.grd
index 33882aa63..9239612 100644
--- a/chrome/app/google_chrome_strings.grd
+++ b/chrome/app/google_chrome_strings.grd
@@ -783,7 +783,7 @@
 
         <!-- Extension uninstall prompt -->
         <message name="IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX" desc="Checkbox text to ask the user whether they want to remove associated data at uninstall time. Only used when uninstalling an app associated with a particular website.">
-          Also clear data from Chrome (<ph name="URL">$1<ex>www.google.com</ex></ph>)
+          Also delete data from Chrome (<ph name="URL">$1<ex>www.google.com</ex></ph>)
         </message>
 
         <!-- Extension alerts. -->
diff --git a/chrome/app/google_chrome_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1 b/chrome/app/google_chrome_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1
index 34e4030..7422954 100644
--- a/chrome/app/google_chrome_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1
+++ b/chrome/app/google_chrome_strings_grd/IDS_EXTENSION_UNINSTALL_PROMPT_REMOVE_DATA_CHECKBOX.png.sha1
@@ -1 +1 @@
-75ff58c80b87ac92a2d1bfacf8342ea29089c2a3
\ No newline at end of file
+cb43e5018d7a88fb6977e0534b64d2a3f03ebe6f
\ No newline at end of file
diff --git a/chrome/app/settings_strings.grdp b/chrome/app/settings_strings.grdp
index e2dbdc66..7faebf9 100644
--- a/chrome/app/settings_strings.grdp
+++ b/chrome/app/settings_strings.grdp
@@ -488,7 +488,7 @@
   <message name="IDS_SETTINGS_MULTIPLE_MIGRATABLE_CARDS_INFO" desc="Display text under the save cards to Google Pay label. This text indicates user has multiple migratable cards on this device.">
     Right now, you have some cards that can only be used on this device
   </message>
-  <message name="IDS_SETTINGS_REMOTE_CREDIT_CARD_LINK_LABEL" desc="The title and ARIA (accessibility) label for a link in chrome://settings/payments that opens the credit card settings on https://pay.google.com. Note that the ARIA role and the link icon already convey that this is a link that will be opened when you click on it. Therefore, the message does not contain a verb.">
+  <message name="IDS_SETTINGS_REMOTE_PAYMENT_METHODS_LINK_LABEL" desc="The title and ARIA (accessibility) label for a link in chrome://settings/payments that opens the payment methods settings on https://pay.google.com. Note that the ARIA role and the link icon already convey that this is a link that will be opened when you click on it. Therefore, the message does not contain a verb.">
     Your payment methods in Google Pay
   </message>
   <message name="IDS_SETTINGS_NAME_ON_CREDIT_CARD" desc="The title for the input that lets users modify the name on the credit card.">
@@ -4401,4 +4401,9 @@
       =1 {Review 1 website with lots of notifications}
       other {Review {NUM_SITES} websites with lots of notifications}}
   </message>
+
+  <!-- Experimental enterprise plus_addresses settings strings -->
+  <message name="IDS_PLUS_ADDRESS_SETTINGS_LABEL" desc="A button in settings for management of experimental enterprise plus addresses" translateable="false">
+    Lorem Ipsum
+  </message>
 </grit-part>
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_CREDIT_CARD_LINK_LABEL.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_CREDIT_CARD_LINK_LABEL.png.sha1
deleted file mode 100644
index 33e643c8..0000000
--- a/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_CREDIT_CARD_LINK_LABEL.png.sha1
+++ /dev/null
@@ -1 +0,0 @@
-7203d112eab56cc5d2cf9520fc10c496f73f8971
\ No newline at end of file
diff --git a/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_PAYMENT_METHODS_LINK_LABEL.png.sha1 b/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_PAYMENT_METHODS_LINK_LABEL.png.sha1
new file mode 100644
index 0000000..b382486
--- /dev/null
+++ b/chrome/app/settings_strings_grdp/IDS_SETTINGS_REMOTE_PAYMENT_METHODS_LINK_LABEL.png.sha1
@@ -0,0 +1 @@
+e8503f7cdb4370f96c7c100337ea3d4bf706c36c
\ No newline at end of file
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index df5fbe8..46103db6 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -8359,6 +8359,16 @@
      FEATURE_VALUE_TYPE(
          privacy_sandbox::kTrackingProtectionOnboardingForceEligibility)},
 
+    {"tracking-protection-onboarding-reset-eligibility-for-testing",
+     flag_descriptions::
+         kTrackingProtectionOnboardingResetEligibilityForTestingName,
+     flag_descriptions::
+         kTrackingProtectionOnboardingResetEligibilityForTestingDescription,
+     kOsDesktop | kOsAndroid,
+     FEATURE_VALUE_TYPE(
+         privacy_sandbox::
+             kTrackingProtectionOnboardingResetEligibilityForTesting)},
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
     {kClipboardHistoryLongpressInternalName,
      flag_descriptions::kClipboardHistoryLongpressName,
@@ -11065,7 +11075,7 @@
 
 #if BUILDFLAG(ENABLE_HLS_DEMUXER)
     {"enable-builtin-hls", flag_descriptions::kEnableBuiltinHlsName,
-     flag_descriptions::kEnableBuiltinHlsDescription, kOsAndroid,
+     flag_descriptions::kEnableBuiltinHlsDescription, kOsAll,
      FEATURE_VALUE_TYPE(media::kBuiltInHlsPlayer)},
 #endif
 
diff --git a/chrome/browser/accessibility/image_annotation_browsertest.cc b/chrome/browser/accessibility/image_annotation_browsertest.cc
index 821e68a..99e4b791 100644
--- a/chrome/browser/accessibility/image_annotation_browsertest.cc
+++ b/chrome/browser/accessibility/image_annotation_browsertest.cc
@@ -538,8 +538,8 @@
       "Appears to say: red.png Annotation. Appears to be: red.png 'fr' Label");
 }
 
-// TODO(crbug.com/1476383): Fix flakiness on ChromeOS MSan
-#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
+// TODO(crbug.com/1476383): Fix flakiness on ChromeOS
+#if BUILDFLAG(IS_CHROMEOS)
 #define MAYBE_DoesntAnnotateInternalPages DISABLED_DoesntAnnotateInternalPages
 #else
 #define MAYBE_DoesntAnnotateInternalPages DoesntAnnotateInternalPages
diff --git a/chrome/browser/accessibility/pdf_ocr_controller.cc b/chrome/browser/accessibility/pdf_ocr_controller.cc
index 5b3a68b55..41d0cc4 100644
--- a/chrome/browser/accessibility/pdf_ocr_controller.cc
+++ b/chrome/browser/accessibility/pdf_ocr_controller.cc
@@ -138,29 +138,6 @@
   return GetPdfHtmlWebContentses(profile);
 }
 
-void PdfOcrController::RunPdfOcrOnlyOnce(content::WebContents* web_contents) {
-  if (!web_contents) {
-    CHECK_IS_TEST();
-    return;
-  }
-
-  if (MaybeScheduleRequest(web_contents)) {
-    // The request will be handled when the library is ready or discarded if it
-    // fails to load.
-    return;
-  }
-
-  // `web_contents` should be a PDF Viewer Mimehandler.
-  DCHECK_EQ(web_contents->GetContentsMimeType(), kHtmlMimeType);
-
-  ui::AXMode ax_mode = web_contents->GetAccessibilityMode();
-  ax_mode.set_mode(ui::AXMode::kPDFOcr, true);
-  web_contents->SetAccessibilityMode(ax_mode);
-
-  RecordAcceptLanguages(
-      profile_->GetPrefs()->GetString(language::prefs::kAcceptLanguages));
-}
-
 bool PdfOcrController::IsEnabled() const {
   return profile_->GetPrefs()->GetBoolean(
              prefs::kAccessibilityPdfOcrAlwaysActive) &&
@@ -191,7 +168,7 @@
   if (is_always_active) {
     RecordAcceptLanguages(
         profile_->GetPrefs()->GetString(language::prefs::kAcceptLanguages));
-    if (MaybeScheduleRequest(/*web_contents_for_only_once_request=*/nullptr)) {
+    if (MaybeScheduleRequest()) {
       // The request will be handled when the library is ready or discarded if
       // it fails to load.
       return;
@@ -220,8 +197,7 @@
   }
 }
 
-bool PdfOcrController::MaybeScheduleRequest(
-    content::WebContents* web_contents_for_only_once_request) {
+bool PdfOcrController::MaybeScheduleRequest() {
   ScreenAIInstallState::State current_install_state =
       ScreenAIInstallState::GetInstance()->get_state();
 
@@ -231,15 +207,7 @@
   }
 
   // Keep the request until the library is ready.
-  if (web_contents_for_only_once_request) {
-    // PDF OCR once request. Keep its weak pointer of the web contents
-    // requested for this. We only keep this request for one PDF (the last one).
-    last_webcontents_requested_for_run_once_ =
-        web_contents_for_only_once_request->GetWeakPtr();
-  } else {
-    // PDF OCR always request.
-    send_always_active_state_when_service_is_ready_ = true;
-  }
+  send_always_active_state_when_service_is_ready_ = true;
 
   // TODO(crbug.com/127829): Make sure requesting to repeat a failed download
   // will trigger a new one.
@@ -267,9 +235,6 @@
 
     case ScreenAIInstallState::State::kFailed:
       AnnounceToScreenReader(IDS_SETTINGS_PDF_OCR_DOWNLOAD_ERROR);
-      if (last_webcontents_requested_for_run_once_) {
-        last_webcontents_requested_for_run_once_.reset();
-      }
       if (send_always_active_state_when_service_is_ready_) {
         // Update the PDF OCR pref to be false to toggle off the button.
         profile_->GetPrefs()->SetBoolean(
@@ -285,10 +250,6 @@
       break;
 
     case ScreenAIInstallState::State::kReady:
-      if (last_webcontents_requested_for_run_once_) {
-        RunPdfOcrOnlyOnce(last_webcontents_requested_for_run_once_.get());
-        last_webcontents_requested_for_run_once_.reset();
-      }
       if (send_always_active_state_when_service_is_ready_) {
         send_always_active_state_when_service_is_ready_ = false;
         SendPdfOcrAlwaysActiveToAll(true);
diff --git a/chrome/browser/accessibility/pdf_ocr_controller.h b/chrome/browser/accessibility/pdf_ocr_controller.h
index 4b46caf..b0212ee 100644
--- a/chrome/browser/accessibility/pdf_ocr_controller.h
+++ b/chrome/browser/accessibility/pdf_ocr_controller.h
@@ -41,10 +41,6 @@
   // service to become ready.
   bool IsEnabled() const;
 
-  // Run PDF OCR only once regardless of the PDF OCR pref value. This function
-  // doesn't update the PDF OCR pref value.
-  void RunPdfOcrOnlyOnce(content::WebContents* web_contents);
-
   // ScreenAIInstallState::Observer:
   void StateChanged(ScreenAIInstallState::State state) override;
 
@@ -63,12 +59,7 @@
   //    not done before.
   //  - Asks for a retry on download if a previous download has failed.
   //  - Returns true.
-  //
-  // If `web_contents_for_only_once_request` is empty, a request to always run
-  // PDF OCR will be scheduled. Otherwise only the request for the last
-  // WebContents is scheduled.
-  bool MaybeScheduleRequest(
-      content::WebContents* web_contents_for_only_once_request);
+  bool MaybeScheduleRequest();
 
   // Observes changes in Screen AI component download and readiness state.
   base::ScopedObservation<ScreenAIInstallState, ScreenAIInstallState::Observer>
@@ -84,12 +75,6 @@
   // Screen AI service to be ready to send this bit.
   bool send_always_active_state_when_service_is_ready_{false};
 
-  // Store a weak pointer to `content::WebContents` the user requested last for
-  // the "Just once" option. Having a valid pointer indicates that user has
-  // requested running just once on WebContents, and we are waiting for the
-  // Screen AI service to be ready for the user request.
-  base::WeakPtr<content::WebContents> last_webcontents_requested_for_run_once_;
-
   base::WeakPtrFactory<PdfOcrController> weak_ptr_factory_{this};
 };
 
diff --git a/chrome/browser/android/chrome_backup_agent.cc b/chrome/browser/android/chrome_backup_agent.cc
index ed8c4fbd..514e314 100644
--- a/chrome/browser/android/chrome_backup_agent.cc
+++ b/chrome/browser/android/chrome_backup_agent.cc
@@ -17,7 +17,7 @@
 
 namespace {
 
-static_assert(49 == syncer::GetNumModelTypes(),
+static_assert(48 == syncer::GetNumModelTypes(),
               "If the new type has a corresponding pref, add it here");
 const char* backed_up_preferences_[] = {
     syncer::prefs::internal::kSyncKeepEverythingSynced,
diff --git a/chrome/browser/apps/app_service/media_access_browsertest.cc b/chrome/browser/apps/app_service/media_access_browsertest.cc
index bfbb83b4..b41b3de5 100644
--- a/chrome/browser/apps/app_service/media_access_browsertest.cc
+++ b/chrome/browser/apps/app_service/media_access_browsertest.cc
@@ -16,6 +16,7 @@
 #include "chrome/browser/ui/web_applications/web_app_controller_browsertest.h"
 #include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
 #include "chrome/browser/web_applications/test/web_app_test_observers.h"
+#include "chrome/common/chrome_features.h"
 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "components/app_constants/constants.h"
@@ -421,7 +422,7 @@
   web_app::LaunchWebAppBrowser(browser()->profile(), app_id);
   web_app::NavigateToURLAndWait(browser(), GetUrl1());
 
-  // Request accessing the camera for |app_id| in the new tab.
+  // Request accessing the microphone for |app_id| in the new tab.
   content::WebContents* web_content1 = GetWebContents();
   base::OnceClosure audio_closure1 = StartMediaCapture(
       web_content1, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
@@ -433,7 +434,7 @@
   ASSERT_TRUE(app_browser);
   ASSERT_NE(browser(), app_browser);
 
-  // Request accessing the camera for |app_id| in the new window.
+  // Request accessing the microphone for |app_id| in the new window.
   base::OnceClosure audio_closure2 = StartMediaCapture(
       web_content2, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
   EXPECT_TRUE(AccessingMicrophone(browser()->profile(), app_id));
@@ -442,11 +443,11 @@
   web_app::CloseAndWait(app_browser);
   EXPECT_TRUE(AccessingMicrophone(browser()->profile(), app_id));
 
-  // Stop accessing the camera for |app_id| in the tab.
+  // Stop accessing the microphone for |app_id| in the tab.
   std::move(audio_closure1).Run();
   EXPECT_FALSE(AccessingMicrophone(browser()->profile(), app_id));
 
-  // Stop accessing the camera for |app_id| in the window.
+  // Stop accessing the microphone for |app_id| in the window.
   std::move(audio_closure2).Run();
   EXPECT_FALSE(AccessingMicrophone(browser()->profile(), app_id));
 }
@@ -633,3 +634,69 @@
 
   web_app::CloseAndWait(browser());
 }
+
+class MediaAccessBrowserShortcutsTest : public MediaAccessWebAppsTest {
+ public:
+  std::string CreateShortcut(const GURL& url) const {
+    auto web_app_info = std::make_unique<web_app::WebAppInstallInfo>();
+    web_app_info->start_url = url;
+    return web_app::test::InstallWebApp(browser()->profile(),
+                                        std::move(web_app_info));
+  }
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_{
+      features::kCrosWebAppShortcutUiUpdate};
+};
+
+IN_PROC_BROWSER_TEST_F(MediaAccessBrowserShortcutsTest,
+                       RequestAccessingCamera) {
+  std::string shortcut_id = CreateShortcut(GetUrl1());
+
+  web_app::NavigateToURLAndWait(browser(), GetUrl1());
+
+  // Request accessing the camera for |shortcut_id| in the new tab.
+  content::WebContents* web_content = GetWebContents();
+  base::OnceClosure video_closure = StartMediaCapture(
+      web_content, blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
+
+  // Verify that the media access is published from the browser instead of the
+  // shortcut.
+  EXPECT_FALSE(AccessingCamera(browser()->profile(), shortcut_id));
+  EXPECT_TRUE(
+      AccessingCamera(browser()->profile(), app_constants::kChromeAppId));
+
+  // Stop accessing the camera for |shortcut_id| in the tab.
+  std::move(video_closure).Run();
+  EXPECT_FALSE(AccessingCamera(browser()->profile(), shortcut_id));
+  EXPECT_FALSE(
+      AccessingCamera(browser()->profile(), app_constants::kChromeAppId));
+
+  web_app::CloseAndWait(browser());
+}
+
+IN_PROC_BROWSER_TEST_F(MediaAccessBrowserShortcutsTest,
+                       RequestAccessingMicrophone) {
+  std::string shortcut_id = CreateShortcut(GetUrl1());
+
+  web_app::NavigateToURLAndWait(browser(), GetUrl1());
+
+  // Request accessing the microphone for |shortcut_id| in the new tab.
+  content::WebContents* web_content = GetWebContents();
+  base::OnceClosure video_closure = StartMediaCapture(
+      web_content, blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
+
+  // Verify that the media access is published from the browser instead of the
+  // shortcut.
+  EXPECT_FALSE(AccessingMicrophone(browser()->profile(), shortcut_id));
+  EXPECT_TRUE(
+      AccessingMicrophone(browser()->profile(), app_constants::kChromeAppId));
+
+  // Stop accessing the microphone for |shortcut_id| in the tab.
+  std::move(video_closure).Run();
+  EXPECT_FALSE(AccessingMicrophone(browser()->profile(), shortcut_id));
+  EXPECT_FALSE(
+      AccessingMicrophone(browser()->profile(), app_constants::kChromeAppId));
+
+  web_app::CloseAndWait(browser());
+}
diff --git a/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.cc b/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.cc
index 7fc08759..1c5de6a2 100644
--- a/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.cc
+++ b/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.cc
@@ -59,7 +59,9 @@
 #include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
 #include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
 #include "chrome/browser/ui/ash/session_controller_client_impl.h"
+#include "chrome/browser/web_applications/app_service/publisher_helper.h"
 #include "chrome/browser/web_applications/web_app_helpers.h"
+#include "chrome/browser/web_applications/web_app_provider.h"
 #include "chrome/browser/web_applications/web_app_tab_helper.h"
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/extensions/api/file_browser_handlers/file_browser_handler.h"
@@ -635,8 +637,12 @@
   const webapps::AppId* web_app_id =
       web_app::WebAppTabHelper::GetAppId(web_contents);
   if (web_app_id) {
-    // This media access is coming from a web app.
-    return;
+    if (web_app::WebAppProvider::GetForWebApps(profile()) &&
+        !web_app::IsAppServiceShortcut(
+            *web_app_id, *web_app::WebAppProvider::GetForWebApps(profile()))) {
+      // This media access is coming from a web app.
+      return;
+    }
   }
 
   std::string app_id = app_constants::kChromeAppId;
@@ -662,8 +668,12 @@
   const webapps::AppId* web_app_id =
       web_app::WebAppTabHelper::GetAppId(web_contents);
   if (web_app_id) {
-    // This media access is coming from a web app.
-    return;
+    if (web_app::WebAppProvider::GetForWebApps(profile()) &&
+        !web_app::IsAppServiceShortcut(
+            *web_app_id, *web_app::WebAppProvider::GetForWebApps(profile()))) {
+      // This media access is coming from a web app.
+      return;
+    }
   }
 
   std::string app_id = app_constants::kChromeAppId;
diff --git a/chrome/browser/ash/app_list/app_service/app_service_shortcut_icon_loader.cc b/chrome/browser/ash/app_list/app_service/app_service_shortcut_icon_loader.cc
index b20609a..850bb0e 100644
--- a/chrome/browser/ash/app_list/app_service/app_service_shortcut_icon_loader.cc
+++ b/chrome/browser/ash/app_list/app_service/app_service_shortcut_icon_loader.cc
@@ -119,6 +119,8 @@
       gfx::ImageSkiaOperations::CreateIconWithBadge(
           icon_value->uncompressed, badge_icon_value->uncompressed);
 
+  icon_map_[shortcut_id.value()] = icon_with_badge;
+
   delegate()->OnAppImageUpdated(shortcut_id.value(), icon_with_badge);
 
   // TODO(crbug.com/1412708): Add badge icon field in metadata and set the badge
diff --git a/chrome/browser/ash/arc/policy/managed_configuration_variables.cc b/chrome/browser/ash/arc/policy/managed_configuration_variables.cc
index 51496f1..a53b63f 100644
--- a/chrome/browser/ash/arc/policy/managed_configuration_variables.cc
+++ b/chrome/browser/ash/arc/policy/managed_configuration_variables.cc
@@ -13,6 +13,7 @@
 #include "base/functional/bind.h"
 #include "base/functional/callback_forward.h"
 #include "base/strings/string_piece_forward.h"
+#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/values.h"
diff --git a/chrome/browser/ash/base/locale_util.cc b/chrome/browser/ash/base/locale_util.cc
index e8744f9..ae8f39b0 100644
--- a/chrome/browser/ash/base/locale_util.cc
+++ b/chrome/browser/ash/base/locale_util.cc
@@ -10,6 +10,7 @@
 #include "base/containers/contains.h"
 #include "base/functional/bind.h"
 #include "base/memory/raw_ptr.h"
+#include "base/strings/string_split.h"
 #include "base/task/thread_pool.h"
 #include "chrome/browser/ash/login/session/user_session_manager.h"
 #include "chrome/browser/browser_process.h"
diff --git a/chrome/browser/ash/crosapi/browser_data_migrator_util.h b/chrome/browser/ash/crosapi/browser_data_migrator_util.h
index 6a2add9..d4bed48e 100644
--- a/chrome/browser/ash/crosapi/browser_data_migrator_util.h
+++ b/chrome/browser/ash/crosapi/browser_data_migrator_util.h
@@ -290,7 +290,7 @@
 };
 
 // List of data types in Sync Data that have to stay in Ash and Ash only.
-static_assert(49 == syncer::GetNumModelTypes(),
+static_assert(48 == syncer::GetNumModelTypes(),
               "If adding a new sync data type, update the lists below if"
               " you want to keep the new data type in Ash only.");
 constexpr syncer::ModelType kAshOnlySyncDataTypes[] = {
diff --git a/chrome/browser/ash/crostini/crostini_export_import.cc b/chrome/browser/ash/crostini/crostini_export_import.cc
index 0b2abc7..37fa0f03 100644
--- a/chrome/browser/ash/crostini/crostini_export_import.cc
+++ b/chrome/browser/ash/crostini/crostini_export_import.cc
@@ -7,6 +7,7 @@
 #include <algorithm>
 #include <utility>
 
+#include "base/containers/contains.h"
 #include "base/files/file_util.h"
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
@@ -254,8 +255,7 @@
     CrostiniManager::CrostiniResultCallback callback) {
   std::vector<guest_os::GuestId> existing_containers =
       guest_os::GetContainers(profile_, guest_os::VmType::TERMINA);
-  if (std::find(existing_containers.begin(), existing_containers.end(),
-                container_id) == existing_containers.end()) {
+  if (!base::Contains(existing_containers, container_id)) {
     LOG(ERROR) << "Attempting to import Crostini container backup into "
                   "non-existent container: "
                << container_id;
diff --git a/chrome/browser/ash/eol_notification_browsertest.cc b/chrome/browser/ash/eol_notification_browsertest.cc
index 2ea17c8..ad74e11 100644
--- a/chrome/browser/ash/eol_notification_browsertest.cc
+++ b/chrome/browser/ash/eol_notification_browsertest.cc
@@ -339,6 +339,14 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    EolNotificationTest,
+    ::testing::Values(TestCase::kIncentivesDisabled,
+                      TestCase::kIncentivesWithoutOffer,
+                      TestCase::kIncentivesWithOffer,
+                      TestCase::kIncentivesWithOfferAndAUEWarning));
+
 // Tests that verify EOL notifications are not shown on managed devices.
 class ManagedDeviceEolNotificationTest
     : public MixinBasedInProcessBrowserTest,
@@ -370,6 +378,14 @@
   base::test::ScopedFeatureList scoped_feature_list_;
 };
 
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    ManagedDeviceEolNotificationTest,
+    ::testing::Values(TestCase::kIncentivesDisabled,
+                      TestCase::kIncentivesWithoutOffer,
+                      TestCase::kIncentivesWithOffer,
+                      TestCase::kIncentivesWithOfferAndAUEWarning));
+
 // Tests that verify EOL notifications with incentives are not shown for child
 // users.
 class ChildUserEolNotificationTest
@@ -401,28 +417,35 @@
 
 INSTANTIATE_TEST_SUITE_P(
     All,
-    EolNotificationTest,
-    ::testing::Values(TestCase::kIncentivesDisabled,
-                      TestCase::kIncentivesWithoutOffer,
-                      TestCase::kIncentivesWithOffer,
-                      TestCase::kIncentivesWithOfferAndAUEWarning));
-
-INSTANTIATE_TEST_SUITE_P(
-    All,
-    ManagedDeviceEolNotificationTest,
-    ::testing::Values(TestCase::kIncentivesDisabled,
-                      TestCase::kIncentivesWithoutOffer,
-                      TestCase::kIncentivesWithOffer,
-                      TestCase::kIncentivesWithOfferAndAUEWarning));
-
-INSTANTIATE_TEST_SUITE_P(
-    All,
     ChildUserEolNotificationTest,
     ::testing::Values(TestCase::kIncentivesDisabled,
                       TestCase::kIncentivesWithoutOffer,
                       TestCase::kIncentivesWithOffer,
                       TestCase::kIncentivesWithOfferAndAUEWarning));
 
+class SuppressedNotificationTest : public MixinBasedInProcessBrowserTest,
+                                   public ::testing::WithParamInterface<bool> {
+ public:
+  SuppressedNotificationTest() {
+    scoped_feature_list_.InitWithFeatureState(
+        features::kSuppressFirstEolWarning, SuppressFirstWarningEnabled());
+  }
+
+  bool SuppressFirstWarningEnabled() { return GetParam(); }
+
+ protected:
+  EolStatusMixin eol_status_mixin_{&mixin_host_};
+  NotificationDisplayServiceMixin notifications_mixin_{&mixin_host_};
+  ash::LoggedInUserMixin logged_in_user_mixin_{
+      &mixin_host_, LoggedInUserMixin::LogInType::kRegular,
+      embedded_test_server(), this};
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All, SuppressedNotificationTest, testing::Bool());
+
 IN_PROC_BROWSER_TEST_P(EolNotificationTest, ShowNotificationForEolApproaching) {
   ASSERT_EQ(TimeSetupResult::kSuccess,
             eol_status_mixin_.SetUpTime(
@@ -926,4 +949,49 @@
       VIEW_ID_QS_EOL_NOTICE_BUTTON, /*open_tray=*/true));
 }
 
+// Test that the eol notification is not shown when just within 180 days, when
+// first eol warning is suppressed.
+IN_PROC_BROWSER_TEST_P(SuppressedNotificationTest,
+                       EolNotificationSupressed180DaysBefore) {
+  // Set eol date to be 173 days in the future.
+  ASSERT_EQ(TimeSetupResult::kSuccess,
+            eol_status_mixin_.SetUpTime(
+                /*now_string=*/"12 May 2023", /*eol_string=*/"1 November 2023",
+                /*profile_creation_string=*/"05 December 2021"));
+
+  logged_in_user_mixin_.LogInUser();
+
+  NotificationDisplayServiceTester* notification_display_service =
+      notifications_mixin_.WaitForDisplayService();
+  ASSERT_TRUE(notification_display_service);
+
+  base::RunLoop().RunUntilIdle();
+
+  absl::optional<message_center::Notification> notification =
+      notification_display_service->GetNotification(kEolNotificationId);
+  EXPECT_EQ(SuppressFirstWarningEnabled(), !notification);
+}
+
+// Test that the eol notification is shown when just within 90 days.
+IN_PROC_BROWSER_TEST_P(SuppressedNotificationTest,
+                       EolNotificationShown90DaysBefore) {
+  // Set eol date to be 83 days in the future.
+  ASSERT_EQ(TimeSetupResult::kSuccess,
+            eol_status_mixin_.SetUpTime(
+                /*now_string=*/"12 May 2023", /*eol_string=*/"3 August 2023",
+                /*profile_creation_string=*/"05 December 2021"));
+
+  logged_in_user_mixin_.LogInUser();
+
+  NotificationDisplayServiceTester* notification_display_service =
+      notifications_mixin_.WaitForDisplayService();
+  ASSERT_TRUE(notification_display_service);
+
+  base::RunLoop().RunUntilIdle();
+
+  absl::optional<message_center::Notification> notification =
+      notification_display_service->GetNotification(kEolNotificationId);
+  EXPECT_TRUE(notification);
+}
+
 }  // namespace ash
diff --git a/chrome/browser/ash/file_manager/file_tasks_browsertest.cc b/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
index 1a7d085..8a5e5313 100644
--- a/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
+++ b/chrome/browser/ash/file_manager/file_tasks_browsertest.cc
@@ -1527,6 +1527,7 @@
       provided_file_system_;  // Owned by Service.
   const blink::StorageKey kTestStorageKey =
       blink::StorageKey::CreateFromStringForTesting("chrome://abc");
+  base::HistogramTester histogram_;
 
  private:
   base::test::ScopedFeatureList feature_list_;
@@ -1809,6 +1810,16 @@
 
   auto launches = web_app_publisher_->GetLaunches();
   ASSERT_EQ(0u, launches.size());
+
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTransferRequiredMetric,
+      OfficeFilesTransferRequired::kNotRequired, 1);
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTaskResultMetricName,
+      ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
+  histogram_.ExpectUniqueSample(
+      kOneDriveErrorMetricName,
+      ash::cloud_upload::OfficeOneDriveOpenErrors::kEmailsDoNotMatch, 1);
 }
 
 // Test that OpenOrMoveFiles() will not open an Android OneDrive office file
@@ -1841,6 +1852,17 @@
 
   auto launches = web_app_publisher_->GetLaunches();
   ASSERT_EQ(0u, launches.size());
+
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTransferRequiredMetric,
+      OfficeFilesTransferRequired::kNotRequired, 1);
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTaskResultMetricName,
+      ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
+  // Expect get actions error for a non-existent path.
+  histogram_.ExpectUniqueSample(
+      kOneDriveErrorMetricName,
+      ash::cloud_upload::OfficeOneDriveOpenErrors::kGetActionsGenericError, 1);
 }
 
 // Test that OpenOrMoveFiles() will not open an Android OneDrive office file
@@ -1877,6 +1899,18 @@
 
   auto launches = web_app_publisher_->GetLaunches();
   ASSERT_EQ(0u, launches.size());
+
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTransferRequiredMetric,
+      OfficeFilesTransferRequired::kNotRequired, 1);
+  histogram_.ExpectUniqueSample(
+      ash::cloud_upload::kOneDriveTaskResultMetricName,
+      ash::cloud_upload::OfficeTaskResult::kFailedToOpen, 1);
+  // Expect the conversion to an ODFS equivalent URL to fail.
+  histogram_.ExpectUniqueSample(
+      kOneDriveErrorMetricName,
+      ash::cloud_upload::OfficeOneDriveOpenErrors::kConversionToODFSUrlError,
+      1);
 }
 
 // Test that the setup flow for office files, that has never been run before,
diff --git a/chrome/browser/ash/file_manager/office_file_tasks.cc b/chrome/browser/ash/file_manager/office_file_tasks.cc
index ac106ca..98c1dc6 100644
--- a/chrome/browser/ash/file_manager/office_file_tasks.cc
+++ b/chrome/browser/ash/file_manager/office_file_tasks.cc
@@ -77,8 +77,9 @@
     ash::cloud_upload::OfficeTaskResult task_result) {
   switch (fallback_reason) {
     case ash::office_fallback::FallbackReason::kOffline:
-      UMA_HISTOGRAM_ENUMERATION(kOneDriveErrorMetricName,
-                                OfficeOneDriveOpenErrors::kOffline);
+      UMA_HISTOGRAM_ENUMERATION(
+          kOneDriveErrorMetricName,
+          ash::cloud_upload::OfficeOneDriveOpenErrors::kOffline);
       break;
     case ash::office_fallback::FallbackReason::kDriveUnavailable:
       NOTREACHED();
diff --git a/chrome/browser/ash/file_manager/office_file_tasks.h b/chrome/browser/ash/file_manager/office_file_tasks.h
index fd1340c3c5..6eaba19 100644
--- a/chrome/browser/ash/file_manager/office_file_tasks.h
+++ b/chrome/browser/ash/file_manager/office_file_tasks.h
@@ -55,23 +55,6 @@
   kMaxValue = kSuccess,
 };
 
-// List of UMA enum values for opening Office files from OneDrive, with the
-// MS365 PWA. The enum values must be kept in sync with OfficeOneDriveOpenErrors
-// in tools/metrics/histograms/enums.xml.
-enum class OfficeOneDriveOpenErrors {
-  kSuccess = 0,
-  kOffline = 1,
-  kNoProfile = 2,
-  kNoFileSystemURL = 3,
-  kInvalidFileSystemURL = 4,
-  kGetActionsGenericError = 5,
-  kGetActionsReauthRequired = 6,
-  kGetActionsInvalidUrl = 7,
-  kGetActionsNoUrl = 8,
-  kGetActionsAccessDenied = 9,
-  kMaxValue = kGetActionsAccessDenied,
-};
-
 // UMA metric name that tracks the result of using a MS Office file outside
 // of Drive.
 constexpr char kUseOutsideDriveMetricName[] =
diff --git a/chrome/browser/ash/input_method/editor_text_actuator.cc b/chrome/browser/ash/input_method/editor_text_actuator.cc
index c5371bf..4fb4bd2 100644
--- a/chrome/browser/ash/input_method/editor_text_actuator.cc
+++ b/chrome/browser/ash/input_method/editor_text_actuator.cc
@@ -11,7 +11,8 @@
 namespace {
 
 bool IsUrlAllowed(const GURL& url) {
-  return url.SchemeIs(url::kHttpsScheme);
+  return url.SchemeIs(url::kHttpsScheme) ||
+         url.spec().starts_with("chrome://os-settings/osLanguages/input");
 }
 
 }  // namespace
diff --git a/chrome/browser/ash/input_method/suggestions_service_client.cc b/chrome/browser/ash/input_method/suggestions_service_client.cc
index 393db1f..6c554ab 100644
--- a/chrome/browser/ash/input_method/suggestions_service_client.cc
+++ b/chrome/browser/ash/input_method/suggestions_service_client.cc
@@ -44,7 +44,7 @@
     return MultiWordExperimentGroup::kGboardE;
   if (finch_trial == "gboard_f")
     return MultiWordExperimentGroup::kGboardF;
-  return MultiWordExperimentGroup::kDefault;
+  return MultiWordExperimentGroup::kGboardE;
 }
 
 chromeos::machine_learning::mojom::TextSuggestionMode ToTextSuggestionModeMojom(
diff --git a/chrome/browser/ash/input_method/ui/candidate_window_view.cc b/chrome/browser/ash/input_method/ui/candidate_window_view.cc
index a7fd4cb..b30ff16 100644
--- a/chrome/browser/ash/input_method/ui/candidate_window_view.cc
+++ b/chrome/browser/ash/input_method/ui/candidate_window_view.cc
@@ -9,10 +9,14 @@
 
 #include <string>
 
+#include "ash/constants/ash_features.h"
+#include "base/feature_list.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/ash/input_method/ui/candidate_view.h"
 #include "chrome/browser/ash/input_method/ui/candidate_window_constants.h"
 #include "ui/accessibility/ax_node_data.h"
+#include "ui/base/ime/ash/extension_ime_util.h"
+#include "ui/base/ime/ash/input_method_manager.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/color/color_id.h"
@@ -377,10 +381,47 @@
 void CandidateWindowView::SetCursorAndCompositionBounds(
     const gfx::Rect& cursor_bounds,
     const gfx::Rect& composition_bounds) {
-  if (candidate_window_.show_window_at_composition())
-    SetAnchorRect(composition_bounds);
-  else
-    SetAnchorRect(cursor_bounds);
+  if (base::FeatureList::IsEnabled(ash::features::kImeKoreanModeSwitchDebug)) {
+    auto* input_method_manager = ash::input_method::InputMethodManager::Get();
+
+    if (input_method_manager) {
+      const std::string& current_input_method_id =
+          input_method_manager->GetActiveIMEState()
+              ->GetCurrentInputMethod()
+              .id();
+
+      if (ash::extension_ime_util::IsCros1pKorean(current_input_method_id)) {
+        pending_anchor_rect_ = candidate_window_.show_window_at_composition()
+                                   ? composition_bounds
+                                   : cursor_bounds;
+        ash::input_method::GetTextFieldContextualInfo(base::BindOnce(
+            &CandidateWindowView::OnTextFieldContextualInfoAvailable,
+            base::Unretained(this)));
+        return;
+      }
+    }
+
+    if (candidate_window_.show_window_at_composition())
+      SetAnchorRect(composition_bounds);
+    else
+      SetAnchorRect(cursor_bounds);
+  }
+}
+
+void CandidateWindowView::OnTextFieldContextualInfoAvailable(
+    const ash::input_method::TextFieldContextualInfo& info) {
+  if (!base::FeatureList::IsEnabled(ash::features::kImeKoreanModeSwitchDebug)) {
+    return;
+  }
+
+  if (!info.tab_url.DomainIs("docs.google.com")) {
+    SetAnchorRect(pending_anchor_rect_);
+    return;
+  }
+
+  const gfx::Rect& display_bounds =
+      display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
+  SetAnchorRect(gfx::Rect(80, display_bounds.height() - 60, 0, 0));
 }
 
 void CandidateWindowView::MaybeInitializeCandidateViews(
diff --git a/chrome/browser/ash/input_method/ui/candidate_window_view.h b/chrome/browser/ash/input_method/ui/candidate_window_view.h
index ab4ce2a..a6b93e1 100644
--- a/chrome/browser/ash/input_method/ui/candidate_window_view.h
+++ b/chrome/browser/ash/input_method/ui/candidate_window_view.h
@@ -8,6 +8,7 @@
 #include <memory>
 
 #include "base/memory/raw_ptr.h"
+#include "chrome/browser/ash/input_method/text_field_contextual_info_fetcher.h"
 #include "ui/base/ime/candidate_window.h"
 #include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/chromeos/ui_chromeos_export.h"
@@ -90,6 +91,11 @@
 
   void CandidateViewPressed(int index);
 
+  // Only used when ash::features::kImeKoreanModeSwitchDebug flag is enabled.
+  // TODO(b/302460634): Remove when no longer needed.
+  void OnTextFieldContextualInfoAvailable(
+      const ash::input_method::TextFieldContextualInfo& info);
+
   // The candidate window data model.
   ui::CandidateWindow candidate_window_;
 
@@ -112,6 +118,10 @@
   gfx::Size previous_shortcut_column_size_;
   gfx::Size previous_candidate_column_size_;
   gfx::Size previous_annotation_column_size_;
+
+  // Only used when ash::features::kImeKoreanModeSwitchDebug flag is enabled.
+  // TODO(b/302460634): Remove when no longer needed.
+  gfx::Rect pending_anchor_rect_;
 };
 
 BEGIN_VIEW_BUILDER(UI_CHROMEOS_EXPORT,
diff --git a/chrome/browser/ash/login/oobe_metrics_helper.cc b/chrome/browser/ash/login/oobe_metrics_helper.cc
index 6441427..0547b567 100644
--- a/chrome/browser/ash/login/oobe_metrics_helper.cc
+++ b/chrome/browser/ash/login/oobe_metrics_helper.cc
@@ -13,9 +13,20 @@
 #include "chrome/browser/ash/login/login_pref_names.h"
 #include "chrome/browser/ash/login/oobe_screen.h"
 #include "chrome/browser/browser_process.h"
+#include "chrome/browser/ui/webui/ash/login/auto_enrollment_check_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/consumer_update_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/demo_preferences_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/demo_setup_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/enable_debugging_screen_handler.h"
 #include "chrome/browser/ui/webui/ash/login/enrollment_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/gaia_info_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/hid_detection_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/network_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/oobe_ui.h"
+#include "chrome/browser/ui/webui/ash/login/packaged_license_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/quick_start_screen_handler.h"
 #include "chrome/browser/ui/webui/ash/login/terms_of_service_screen_handler.h"
-#include "chrome/browser/ui/webui/ash/login/welcome_screen_handler.h"
+#include "chrome/browser/ui/webui/ash/login/update_screen_handler.h"
 #include "components/prefs/pref_service.h"
 #include "components/startup_metric_utils/common/startup_metric_utils.h"
 #include "components/version_info/version_info.h"
@@ -28,6 +39,11 @@
 constexpr char kUmaScreenShownStatusName[] = "OOBE.StepShownStatus.";
 // Legacy histogram, use legacy screen names.
 constexpr char kUmaScreenCompletionTimeName[] = "OOBE.StepCompletionTime.";
+
+// Updated histograms to replace legacy ones.
+constexpr char kUmaScreenShownStatusName2[] = "OOBE.StepShownStatus2.";
+constexpr char kUmaScreenCompletionTimeName2[] = "OOBE.StepCompletionTime2.";
+
 constexpr char kUmaStepCompletionTimeByExitReasonName[] =
     "OOBE.StepCompletionTimeByExitReason.";
 constexpr char kUmaBootToOobeCompleted[] = "OOBE.BootToOOBECompleted.";
@@ -54,6 +70,24 @@
     {WelcomeView::kScreenId, "network"},
     {TermsOfServiceScreenView::kScreenId, "tos"}};
 
+// This list must be kept in sync with `OOBEOnlyScreenName` variants in
+// src/tools/metrics/histograms/metadata/oobe/histograms.xml file.
+const StaticOobeScreenId kOobeOnlyScreenNames[] = {
+    AutoEnrollmentCheckScreenView::kScreenId,
+    WelcomeView::kScreenId,
+    ConsumerUpdateScreenView::kScreenId,
+    EnableDebuggingScreenView::kScreenId,
+    DemoPreferencesScreenView::kScreenId,
+    DemoPreferencesScreenView::kScreenId,
+    EnrollmentScreenView::kScreenId,
+    GaiaInfoScreenView::kScreenId,
+    HIDDetectionView::kScreenId,
+    NetworkScreenView::kScreenId,
+    PackagedLicenseView::kScreenId,
+    QuickStartView::kScreenId,
+    UpdateView::kScreenId,
+};
+
 std::string GetUmaLegacyScreenName(const OobeScreenId& screen_id) {
   // Make sure to use initial UMA name if the name has changed.
   std::string uma_name = screen_id.name;
@@ -67,6 +101,28 @@
   return uma_name;
 }
 
+std::string GetCaptializedScreenName(const OobeScreenId& screen_id) {
+  std::string id = screen_id.name;
+  id[0] = base::ToUpperASCII(id[0]);
+  return id;
+}
+
+bool IsOobeOnlyScreen(const OobeScreenId& screen_id) {
+  for (const auto& oobe_screen : kOobeOnlyScreenNames) {
+    if (screen_id == oobe_screen) {
+      return true;
+    }
+  }
+  return false;
+}
+
+std::string GetOnboardingTypeSuffix() {
+  base::Time oobe_time =
+      g_browser_process->local_state()->GetTime(prefs::kOobeStartTime);
+  return oobe_time.is_null() ? kUmaSubsequentOnboardingSuffix
+                             : kUmaFirstOnboardingSuffix;
+}
+
 }  // namespace
 
 OobeMetricsHelper::OobeMetricsHelper() = default;
@@ -84,6 +140,19 @@
   std::string screen_name = GetUmaLegacyScreenName(screen);
   std::string histogram_name = kUmaScreenShownStatusName + screen_name;
   base::UmaHistogramEnumeration(histogram_name, status);
+
+  RecordUpdatedStepShownStatus(screen, status);
+}
+
+void OobeMetricsHelper::RecordUpdatedStepShownStatus(OobeScreenId screen,
+                                                     ScreenShownStatus status) {
+  // New histogram, don't use legacy screen names.
+  std::string screen_name = GetCaptializedScreenName(screen);
+  std::string histogram_name = kUmaScreenShownStatusName2 + screen_name;
+  if (!IsOobeOnlyScreen(screen)) {
+    histogram_name += "." + GetOnboardingTypeSuffix();
+  }
+  base::UmaHistogramEnumeration(histogram_name, status);
 }
 
 void OobeMetricsHelper::OnScreenExited(OobeScreenId screen,
@@ -97,9 +166,10 @@
       base::TimeTicks::Now() - screen_show_times_[screen];
   base::UmaHistogramMediumTimes(histogram_name, step_time);
 
+  RecordUpdatedStepCompletionTime(screen, step_time);
+
   // Use for this histogram real screen names.
-  std::string screen_name = screen.name;
-  screen_name[0] = base::ToUpperASCII(screen_name[0]);
+  std::string screen_name = GetCaptializedScreenName(screen);
   std::string histogram_name_with_reason =
       kUmaStepCompletionTimeByExitReasonName + screen_name + "." + exit_reason;
 
@@ -107,6 +177,19 @@
                                 base::Milliseconds(10), base::Minutes(10), 100);
 }
 
+void OobeMetricsHelper::RecordUpdatedStepCompletionTime(
+    OobeScreenId screen,
+    base::TimeDelta step_time) {
+  // New histogram, don't use legacy screen names.
+  std::string screen_name = GetCaptializedScreenName(screen);
+  std::string histogram_name = kUmaScreenCompletionTimeName2 + screen_name;
+  if (!IsOobeOnlyScreen(screen)) {
+    histogram_name += "." + GetOnboardingTypeSuffix();
+  }
+  base::UmaHistogramCustomTimes(histogram_name, step_time,
+                                base::Milliseconds(10), base::Minutes(10), 100);
+}
+
 void OobeMetricsHelper::OnPreLoginOobeFirstStart() {
   // Record `False` to report the `Started` bucket.
   base::UmaHistogramBoolean(kUmaOobeFlowStatus, false);
@@ -140,17 +223,15 @@
 
 void OobeMetricsHelper::OnOnboardingFlowStarted(base::Time oobe_start_time) {
   std::string onboarding_type;
-  if (oobe_start_time.is_null()) {
-    onboarding_type = kUmaSubsequentOnboardingSuffix;
-  } else {
+  if (!oobe_start_time.is_null()) {
     base::UmaHistogramCustomTimes(
         kUmaOobeStartToOnboardingStart, base::Time::Now() - oobe_start_time,
         base::Milliseconds(10), base::Minutes(30), 100);
-    onboarding_type = kUmaFirstOnboardingSuffix;
   }
 
   // Record `False` to report the `Started` bucket.
-  base::UmaHistogramBoolean(kUmaOnboardingFlowStatus + onboarding_type, false);
+  base::UmaHistogramBoolean(
+      kUmaOnboardingFlowStatus + GetOnboardingTypeSuffix(), false);
 }
 
 void OobeMetricsHelper::OnOnboadingFlowCompleted(
@@ -164,9 +245,7 @@
   }
 
   if (!onboarding_start_time.is_null()) {
-    std::string type = oobe_start_time.is_null()
-                           ? kUmaSubsequentOnboardingSuffix
-                           : kUmaFirstOnboardingSuffix;
+    std::string type = GetOnboardingTypeSuffix();
 
     // Record `True` to report the `Completed` bucket.
     base::UmaHistogramBoolean(kUmaOnboardingFlowStatus + type, true);
diff --git a/chrome/browser/ash/login/oobe_metrics_helper.h b/chrome/browser/ash/login/oobe_metrics_helper.h
index d692a88..9e8917d 100644
--- a/chrome/browser/ash/login/oobe_metrics_helper.h
+++ b/chrome/browser/ash/login/oobe_metrics_helper.h
@@ -68,6 +68,11 @@
   void RecordChromeVersion();
 
  private:
+  void RecordUpdatedStepShownStatus(OobeScreenId screen,
+                                    ScreenShownStatus status);
+  void RecordUpdatedStepCompletionTime(OobeScreenId screen,
+                                       base::TimeDelta step_time);
+
   // Maps screen names to last time of their shows.
   std::map<OobeScreenId, base::TimeTicks> screen_show_times_;
 };
diff --git a/chrome/browser/ash/login/screens/consolidated_consent_screen_browsertest.cc b/chrome/browser/ash/login/screens/consolidated_consent_screen_browsertest.cc
index c6d4c57f..44756840 100644
--- a/chrome/browser/ash/login/screens/consolidated_consent_screen_browsertest.cc
+++ b/chrome/browser/ash/login/screens/consolidated_consent_screen_browsertest.cc
@@ -9,6 +9,7 @@
 #include "base/hash/sha1.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "chrome/browser/ash/arc/session/arc_service_launcher.h"
+#include "chrome/browser/ash/login/login_pref_names.h"
 #include "chrome/browser/ash/login/oobe_screen.h"
 #include "chrome/browser/ash/login/test/device_state_mixin.h"
 #include "chrome/browser/ash/login/test/fake_arc_tos_mixin.h"
@@ -321,9 +322,18 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ConsolidatedConsentScreenTest, Accept) {
+  // The preference `prefs::kOobeStartTime` is not set due to advancing to
+  // login.
+  PrefService* prefs = g_browser_process->local_state();
+  prefs->SetTime(prefs::kOobeStartTime, base::Time::Now());
+
   LoginAsRegularUser();
   OobeScreenWaiter(ConsolidatedConsentScreenView::kScreenId).Wait();
   test::OobeJS().CreateVisibilityWaiter(true, kLoadedDialog)->Wait();
+  histogram_tester_.ExpectTotalCount(
+      "OOBE.StepShownStatus.Consolidated-consent", 1);
+  histogram_tester_.ExpectTotalCount(
+      "OOBE.StepShownStatus2.Consolidated-consent.FirstOnboarding", 1);
 
   test::OobeJS().CreateVisibilityWaiter(true, kAcceptButton)->Wait();
   test::OobeJS().ClickOnPath(kAcceptButton);
@@ -334,7 +344,8 @@
   histogram_tester_.ExpectTotalCount(
       "OOBE.StepCompletionTime.Consolidated-consent", 1);
   histogram_tester_.ExpectTotalCount(
-      "OOBE.StepShownStatus.Consolidated-consent", 1);
+      "OOBE.StepCompletionTime2.Consolidated-consent.FirstOnboarding", 1);
+
   histogram_tester_.ExpectTotalCount(
       "OOBE.StepCompletionTimeByExitReason.Consolidated-consent."
       "AcceptedRegular",
diff --git a/chrome/browser/ash/login/screens/welcome_screen_browsertest.cc b/chrome/browser/ash/login/screens/welcome_screen_browsertest.cc
index a95a13c..c3c9b5b 100644
--- a/chrome/browser/ash/login/screens/welcome_screen_browsertest.cc
+++ b/chrome/browser/ash/login/screens/welcome_screen_browsertest.cc
@@ -219,8 +219,10 @@
 
 IN_PROC_BROWSER_TEST_F(WelcomeScreenBrowserTest, WelcomeScreenNext) {
   test::WaitForWelcomeScreen();
+  histogram_tester_.ExpectTotalCount("OOBE.StepShownStatus2.Connect", 1);
   test::OobeJS().TapOnPath({"connect", "welcomeScreen", "getStarted"});
   WaitForScreenExit();
+  histogram_tester_.ExpectTotalCount("OOBE.StepCompletionTime2.Connect", 1);
 }
 
 // Set of browser tests for Welcome Screen Language options.
diff --git a/chrome/browser/autofill/autofill_captured_sites_interactive_uitest.cc b/chrome/browser/autofill/autofill_captured_sites_interactive_uitest.cc
index 881f2df..2dc9296 100644
--- a/chrome/browser/autofill/autofill_captured_sites_interactive_uitest.cc
+++ b/chrome/browser/autofill/autofill_captured_sites_interactive_uitest.cc
@@ -48,6 +48,7 @@
 #include "components/autofill/core/common/autofill_util.h"
 #include "components/metrics/content/subprocess_metrics_provider.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
+#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "components/user_prefs/user_prefs.h"
 #include "components/variations/variations_switches.h"
 #include "content/public/test/browser_test.h"
@@ -294,6 +295,9 @@
                 test::ServerCacheReplayer::kOptionSplitRequestsByForm)));
 
     metrics_scraper_ = MetricsScraper::MaybeCreate(GetParam().site_name);
+
+    browser()->profile()->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled,
+                                                 false);
   }
 
   void TearDownOnMainThread() override {
diff --git a/chrome/browser/autofill/autofill_interactive_uitest.cc b/chrome/browser/autofill/autofill_interactive_uitest.cc
index 5fd5137d..da89925 100644
--- a/chrome/browser/autofill/autofill_interactive_uitest.cc
+++ b/chrome/browser/autofill/autofill_interactive_uitest.cc
@@ -3271,7 +3271,7 @@
                                    const FormStructure& form) {
     size_t num_found = 0u;
     for (const std::unique_ptr<AutofillField>& field : form.fields()) {
-      if (field->form_control_type == control_type) {
+      if (field->form_control_type == StringToFormControlType(control_type)) {
         ++num_found;
       }
     }
diff --git a/chrome/browser/cart/cart_service.cc b/chrome/browser/cart/cart_service.cc
index 524ef9ab..306e7b9 100644
--- a/chrome/browser/cart/cart_service.cc
+++ b/chrome/browser/cart/cart_service.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/cart/cart_service.h"
 
+#include "base/containers/contains.h"
 #include "base/json/json_reader.h"
 #include "base/metrics/field_trial_params.h"
 #include "base/no_destructor.h"
@@ -1110,8 +1111,7 @@
   if (!has_product_image && cached_image_url.has_value()) {
     std::string url_string = cached_image_url.value().spec();
     auto existing_images = existing_proto.product_image_urls();
-    if (std::find(existing_images.begin(), existing_images.end(), url_string) ==
-        existing_images.end()) {
+    if (!base::Contains(existing_images, url_string)) {
       existing_proto.add_product_image_urls(url_string);
     }
   }
diff --git a/chrome/browser/compose/OWNERS b/chrome/browser/compose/OWNERS
new file mode 100644
index 0000000..8503ca0
--- /dev/null
+++ b/chrome/browser/compose/OWNERS
@@ -0,0 +1 @@
+file://components/compose/OWNERS
\ No newline at end of file
diff --git a/chrome/browser/compose/chrome_compose_client.cc b/chrome/browser/compose/chrome_compose_client.cc
index 835c88bd..502690e 100644
--- a/chrome/browser/compose/chrome_compose_client.cc
+++ b/chrome/browser/compose/chrome_compose_client.cc
@@ -14,3 +14,5 @@
 void ChromeComposeClient::ShowComposeDialog(ComposeDialogCallback callback) {
   // TODO(b/301609035) Add the compose dialog call here.
 }
+
+WEB_CONTENTS_USER_DATA_KEY_IMPL(ChromeComposeClient);
diff --git a/chrome/browser/devtools/protocol/autofill_handler.cc b/chrome/browser/devtools/protocol/autofill_handler.cc
index e29530629..3f71ca7 100644
--- a/chrome/browser/devtools/protocol/autofill_handler.cc
+++ b/chrome/browser/devtools/protocol/autofill_handler.cc
@@ -257,7 +257,8 @@
             .SetId(base::UTF16ToASCII(field.second->id_attribute))
             .SetName(base::UTF16ToASCII(field.second->name_attribute))
             .SetValue(base::UTF16ToASCII(field.first->value))
-            .SetHtmlType(field.second->form_control_type)
+            .SetHtmlType(std::string(autofill::FormControlTypeToString(
+                field.second->form_control_type)))
             .SetAutofillType(
                 std::string(FieldTypeToDeveloperRepresentationString(
                     field.second->Type().GetStorableType())))
diff --git a/chrome/browser/devtools/protocol/devtools_autofill_browsertest.cc b/chrome/browser/devtools/protocol/devtools_autofill_browsertest.cc
index 1270440..72b942d3 100644
--- a/chrome/browser/devtools/protocol/devtools_autofill_browsertest.cc
+++ b/chrome/browser/devtools/protocol/devtools_autofill_browsertest.cc
@@ -461,8 +461,10 @@
     EXPECT_THAT(ff, FilledFieldHasAttributeWithValue16("value", ffd->value));
     EXPECT_THAT(ff,
                 Not(FilledFieldHasAttributeWithValue16("value", af->value)));
-    EXPECT_THAT(ff, FilledFieldHasAttributeWithValue("htmlType",
-                                                     af->form_control_type));
+    EXPECT_THAT(ff,
+                FilledFieldHasAttributeWithValue(
+                    "htmlType", std::string(autofill::FormControlTypeToString(
+                                    af->form_control_type))));
     EXPECT_THAT(ff,
                 FilledFieldHasAttributeWithValue16("name", af->name_attribute));
   }
diff --git a/chrome/browser/extensions/api/cookies/cookies_api.cc b/chrome/browser/extensions/api/cookies/cookies_api.cc
index 1fded3f..c4d24050 100644
--- a/chrome/browser/extensions/api/cookies/cookies_api.cc
+++ b/chrome/browser/extensions/api/cookies/cookies_api.cc
@@ -118,7 +118,12 @@
 void CookiesEventRouter::OnCookieChange(bool otr,
                                         const net::CookieChangeInfo& change) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-
+  // There is no way to represent non-serializable
+  // partition keys in JS so return to prevent a crash.
+  if (change.cookie.IsPartitioned() &&
+      !change.cookie.PartitionKey()->IsSerializeable()) {
+    return;
+  }
   base::Value::List args;
   base::Value::Dict dict;
   dict.Set(cookies_api_constants::kRemovedKey,
diff --git a/chrome/browser/extensions/api/cookies/cookies_helpers.cc b/chrome/browser/extensions/api/cookies/cookies_helpers.cc
index 409002e1..8e00fd2 100644
--- a/chrome/browser/extensions/api/cookies/cookies_helpers.cc
+++ b/chrome/browser/extensions/api/cookies/cookies_helpers.cc
@@ -136,6 +136,7 @@
   cookie.store_id = store_id;
 
   if (canonical_cookie.PartitionKey()) {
+    CHECK(canonical_cookie.PartitionKey()->IsSerializeable());
     std::string top_level_site;
     CHECK_EQ(base::FeatureList::IsEnabled(net::features::kPartitionedCookies),
              net::CookiePartitionKey::Serialize(canonical_cookie.PartitionKey(),
@@ -255,6 +256,10 @@
     return !cookie.IsPartitioned();
   }
 
+  if (cookie.IsPartitioned() && !cookie.PartitionKey()->IsSerializeable()) {
+    return false;
+  }
+
   std::string serialized_partition_key;
   return net::CookiePartitionKey::Serialize(cookie.PartitionKey(),
                                             serialized_partition_key) &&
diff --git a/chrome/browser/extensions/api/cookies/cookies_unittest.cc b/chrome/browser/extensions/api/cookies/cookies_unittest.cc
index 0998b47..d8cfc11 100644
--- a/chrome/browser/extensions/api/cookies/cookies_unittest.cc
+++ b/chrome/browser/extensions/api/cookies/cookies_unittest.cc
@@ -11,6 +11,7 @@
 #include <utility>
 #include <vector>
 
+#include "base/test/gtest_util.h"
 #include "base/values.h"
 #include "chrome/browser/extensions/api/cookies/cookies_api_constants.h"
 #include "chrome/browser/extensions/api/cookies/cookies_helpers.h"
@@ -208,4 +209,70 @@
   EXPECT_EQ(std::string(), cookie.path);
 }
 
+TEST_F(ExtensionCookiesTest, PartitionKeySerialization) {
+  std::string top_level_site = "https://toplevelsite.com";
+  absl::optional<extensions::api::cookies::CookiePartitionKey>
+      partition_key_for_nonce_and_regular =
+          extensions::api::cookies::CookiePartitionKey();
+  absl::optional<extensions::api::cookies::CookiePartitionKey>
+      partition_key_for_opaque = extensions::api::cookies::CookiePartitionKey();
+  partition_key_for_nonce_and_regular->top_level_site = top_level_site;
+  partition_key_for_opaque->top_level_site = "";
+
+  // Make a CanonicalCookie with a opaque top_level_site or nonce in partition
+  // key.
+  auto cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
+      "__Host-A", "B", "x.y", "/", base::Time(), base::Time(), base::Time(),
+      base::Time(), /*secure=*/true,
+      /*httponly=*/false, net::CookieSameSite::UNSPECIFIED,
+      net::COOKIE_PRIORITY_LOW, /*same_party=*/false,
+      net::CookiePartitionKey::FromURLForTesting(GURL(top_level_site)));
+  EXPECT_TRUE(cookie->IsPartitioned());
+  EXPECT_FALSE(net::CookiePartitionKey::HasNonce(cookie->PartitionKey()));
+  EXPECT_TRUE(cookie->PartitionKey()->IsSerializeable());
+
+  // Make a CanonicalCookie with a opaque partition key.
+  auto opaque_cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
+      "__Host-A", "B", "x.y", "/", base::Time(), base::Time(), base::Time(),
+      base::Time(), /*secure=*/true,
+      /*httponly=*/false, net::CookieSameSite::UNSPECIFIED,
+      net::COOKIE_PRIORITY_LOW, /*same_party=*/false,
+      net::CookiePartitionKey::FromURLForTesting(GURL()));
+
+  EXPECT_TRUE(opaque_cookie->IsPartitioned());
+  EXPECT_FALSE(opaque_cookie->PartitionKey()->IsSerializeable());
+
+  // Make a CanonicalCookie with an nonce partition key.
+  auto nonce_cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
+      "__Host-A", "B", "x.y", "/", base::Time(), base::Time(), base::Time(),
+      base::Time(), /*secure=*/true,
+      /*httponly=*/false, net::CookieSameSite::UNSPECIFIED,
+      net::COOKIE_PRIORITY_LOW, /*same_party=*/false,
+      net::CookiePartitionKey::FromURLForTesting(
+          GURL("https://toplevelsite.com"), base::UnguessableToken::Create()));
+
+  EXPECT_TRUE(nonce_cookie->IsPartitioned());
+  EXPECT_TRUE(net::CookiePartitionKey::HasNonce(nonce_cookie->PartitionKey()));
+  EXPECT_FALSE(nonce_cookie->PartitionKey()->IsSerializeable());
+
+  // Confirm that to be matchable, the partition key
+  // must be serializable.
+  EXPECT_TRUE(cookies_helpers::CookieMatchesPartitionKeyInDetails(
+      partition_key_for_nonce_and_regular, *cookie));
+  EXPECT_FALSE(cookies_helpers::CookieMatchesPartitionKeyInDetails(
+      partition_key_for_nonce_and_regular, *nonce_cookie));
+  EXPECT_FALSE(cookies_helpers::CookieMatchesPartitionKeyInDetails(
+      partition_key_for_opaque, *opaque_cookie));
+
+  // Confirm that a CanonicalCookie with serializable partition key
+  // can be used to create a cookie.
+  auto api_cookie = cookies_helpers::CreateCookie(*cookie, "0");
+  EXPECT_TRUE(api_cookie.partition_key);
+
+  // Confirm that a CanonicalCookie with a non-serializable partition key
+  // dies when a cookie is attempted to be created.
+  EXPECT_CHECK_DEATH(cookies_helpers::CreateCookie(*nonce_cookie, "0"));
+  EXPECT_CHECK_DEATH(cookies_helpers::CreateCookie(*opaque_cookie, "0"));
+}
+
 }  // namespace extensions
diff --git a/chrome/browser/extensions/api/language_settings_private/language_settings_private_api.cc b/chrome/browser/extensions/api/language_settings_private/language_settings_private_api.cc
index f2731f32d..85b2d35 100644
--- a/chrome/browser/extensions/api/language_settings_private/language_settings_private_api.cc
+++ b/chrome/browser/extensions/api/language_settings_private/language_settings_private_api.cc
@@ -148,13 +148,10 @@
   ime_state->GetInputMethodExtensions(&descriptors);
 
   // Filter out the IMEs not in |third_party_ime_set|.
-  descriptors.erase(
-      std::remove_if(
-          descriptors.begin(), descriptors.end(),
+      base::EraseIf(descriptors,
           [&third_party_ime_set](const InputMethodDescriptor& descriptor) {
             return !third_party_ime_set.contains(descriptor.id());
-          }),
-      descriptors.end());
+          });
 
   // A set of the elements of |ime_list|.
   std::set<std::string> ime_set;
diff --git a/chrome/browser/extensions/api/printing/print_job_controller.h b/chrome/browser/extensions/api/printing/print_job_controller.h
index a41b767b..d8852cc 100644
--- a/chrome/browser/extensions/api/printing/print_job_controller.h
+++ b/chrome/browser/extensions/api/printing/print_job_controller.h
@@ -8,12 +8,20 @@
 #include <memory>
 #include <string>
 
+#include "base/memory/raw_ref.h"
 #include "base/memory/scoped_refptr.h"
 
 namespace printing {
 class MetafileSkia;
+class PrintedDocument;
 class PrintJob;
 class PrintSettings;
+
+struct PrintJobCreatedInfo {
+  const int32_t job_id;
+  const raw_ref<PrintedDocument> document;
+};
+
 }  // namespace printing
 
 namespace extensions {
diff --git a/chrome/browser/extensions/api/printing/print_job_submitter.cc b/chrome/browser/extensions/api/printing/print_job_submitter.cc
index 9fcbaaa..0b0b0196 100644
--- a/chrome/browser/extensions/api/printing/print_job_submitter.cc
+++ b/chrome/browser/extensions/api/printing/print_job_submitter.cc
@@ -14,6 +14,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/task/sequenced_task_runner.h"
+#include "base/types/expected.h"
 #include "base/values.h"
 #include "chrome/browser/extensions/api/printing/print_job_controller.h"
 #include "chrome/browser/extensions/api/printing/printing_api_utils.h"
@@ -219,8 +220,8 @@
                         ->enabled_extensions()
                         .Contains(extension_->id())) {
     base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
-        FROM_HERE, base::BindOnce(std::move(callback_), absl::nullopt, nullptr,
-                                  nullptr, absl::nullopt));
+        FROM_HERE,
+        base::BindOnce(std::move(callback_), base::unexpected(absl::nullopt)));
     return;
   }
   StartPrintJob();
@@ -240,7 +241,9 @@
                                   printing::PrintedDocument* document) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   DCHECK(callback_);
-  std::move(callback_).Run(job_id, print_job_.get(), document, absl::nullopt);
+
+  auto document_ref = raw_ref<printing::PrintedDocument>::from_ptr(document);
+  std::move(callback_).Run(printing::PrintJobCreatedInfo{job_id, document_ref});
 }
 
 void PrintJobSubmitter::OnFailed() {
@@ -251,8 +254,7 @@
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   DCHECK(callback_);
   base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
-      FROM_HERE, base::BindOnce(std::move(callback_), absl::nullopt, nullptr,
-                                nullptr, error));
+      FROM_HERE, base::BindOnce(std::move(callback_), base::unexpected(error)));
 }
 
 // static
diff --git a/chrome/browser/extensions/api/printing/print_job_submitter.h b/chrome/browser/extensions/api/printing/print_job_submitter.h
index 65b4f53..52ce603 100644
--- a/chrome/browser/extensions/api/printing/print_job_submitter.h
+++ b/chrome/browser/extensions/api/printing/print_job_submitter.h
@@ -14,6 +14,7 @@
 #include "base/memory/raw_ref.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
+#include "base/types/expected.h"
 #include "chrome/browser/printing/print_job.h"
 #include "chrome/common/extensions/api/printing.h"
 #include "chromeos/crosapi/mojom/local_printer.mojom-forward.h"
@@ -37,6 +38,7 @@
 class PdfBlobDataFlattener;
 class PrintedDocument;
 class PrintSettings;
+struct PrintJobCreatedInfo;
 }  // namespace printing
 
 namespace extensions {
@@ -48,14 +50,13 @@
 // arguments, sending errors and submitting print job to the printer.
 class PrintJobSubmitter : public printing::PrintJob::Observer {
  public:
-  // At most one of `job_id` and `error` are set.
-  // Either `job_id`, `print_job`, and `document` are all set or none of them
-  // are set.
-  using SubmitJobCallback =
-      base::OnceCallback<void(absl::optional<int> job_id,
-                              printing::PrintJob* print_job,
-                              printing::PrintedDocument* document,
-                              absl::optional<std::string> error)>;
+  // The error field in `PrintJobCreationResult` is absl::nullopt when a print
+  // job is rejected by the user. In all other possible failure cases the error
+  // is well-formed.
+  using PrintJobCreationResult = base::expected<printing::PrintJobCreatedInfo,
+                                                absl::optional<std::string>>;
+
+  using SubmitJobCallback = base::OnceCallback<void(PrintJobCreationResult)>;
 
   PrintJobSubmitter(gfx::NativeWindow native_window,
                     content::BrowserContext* browser_context,
diff --git a/chrome/browser/extensions/api/printing/printing_api_handler.cc b/chrome/browser/extensions/api/printing/printing_api_handler.cc
index 31257e2..0828114 100644
--- a/chrome/browser/extensions/api/printing/printing_api_handler.cc
+++ b/chrome/browser/extensions/api/printing/printing_api_handler.cc
@@ -162,24 +162,25 @@
   // PrintingAPIHandler must outlive PrintJobSubmitter. Even if the WeakPtr
   // expires, PrintJobSubmitter will continue to access PrintingAPIHandler
   // member variables.
+
+  std::string extension_id = extension->id();
   PrintJobSubmitter::Run(std::make_unique<PrintJobSubmitter>(
       native_window, browser_context_, print_job_controller_.get(),
       pdf_blob_data_flattener_.get(), std::move(extension),
       std::move(params->request), local_printer_,
       base::BindOnce(&PrintingAPIHandler::OnPrintJobSubmitted,
-                     weak_ptr_factory_.GetWeakPtr(), std::move(callback))));
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
+                     std::move(extension_id))));
 }
 
 void PrintingAPIHandler::OnPrintJobSubmitted(
     SubmitJobCallback callback,
-    absl::optional<int> job_id,
-    printing::PrintJob* print_job,
-    printing::PrintedDocument* document,
-    absl::optional<std::string> error) {
+    const std::string& extension_id,
+    PrintJobSubmitter::PrintJobCreationResult result) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
-  DCHECK(!(job_id && error));
 
-  if (!job_id) {
+  if (!result.has_value()) {
+    absl::optional<std::string> error = std::move(result).error();
     absl::optional<api::printing::SubmitJobStatus> status;
     if (!error)
       status = api::printing::SUBMIT_JOB_STATUS_USER_REJECTED;
@@ -189,13 +190,12 @@
     return;
   }
 
-  DCHECK_EQ(print_job->source(), crosapi::mojom::PrintJob::Source::kExtension);
-
+  printing::PrintJobCreatedInfo info = std::move(result).value();
   std::string printer_id =
-      base::UTF16ToUTF8(document->settings().device_name());
+      base::UTF16ToUTF8(info.document->settings().device_name());
   DCHECK(!printer_id.empty());
 
-  std::string cups_id = CreateUniqueId(printer_id, *job_id);
+  std::string cups_id = CreateUniqueId(printer_id, info.job_id);
 
   base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
       FROM_HERE,
@@ -203,15 +203,15 @@
                      cups_id, absl::nullopt));
 
   DCHECK(!base::Contains(print_jobs_, cups_id));
-  print_jobs_[cups_id] =
-      PrintJobInfo{printer_id, *job_id, print_job->source_id()};
+  print_jobs_[cups_id] = PrintJobInfo{printer_id, info.job_id, extension_id};
 
 #if BUILDFLAG(IS_CHROMEOS_LACROS)
-  NotifyAshJobCreated(*print_job, *job_id, *document, local_printer_);
+  NotifyAshJobCreated(info.job_id, *info.document,
+                      crosapi::mojom::PrintJob::Source::kExtension,
+                      extension_id, local_printer_);
 #endif
 
-  if (!extension_registry_->enabled_extensions().Contains(
-          print_job->source_id())) {
+  if (!extension_registry_->enabled_extensions().Contains(extension_id)) {
     return;
   }
 
@@ -220,8 +220,7 @@
                               api::printing::OnJobStatusChanged::kEventName,
                               api::printing::OnJobStatusChanged::Create(
                                   cups_id, api::printing::JOB_STATUS_PENDING));
-  event_router_->DispatchEventToExtension(print_job->source_id(),
-                                          std::move(event));
+  event_router_->DispatchEventToExtension(extension_id, std::move(event));
 }
 
 absl::optional<std::string> PrintingAPIHandler::CancelJob(
diff --git a/chrome/browser/extensions/api/printing/printing_api_handler.h b/chrome/browser/extensions/api/printing/printing_api_handler.h
index f07c9fd..6882ae8 100644
--- a/chrome/browser/extensions/api/printing/printing_api_handler.h
+++ b/chrome/browser/extensions/api/printing/printing_api_handler.h
@@ -13,6 +13,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/memory/weak_ptr.h"
 #include "build/chromeos_buildflags.h"
+#include "chrome/browser/extensions/api/printing/print_job_submitter.h"
 #include "chrome/common/extensions/api/printing.h"
 #include "chromeos/crosapi/mojom/local_printer.mojom.h"
 #include "extensions/browser/browser_context_keyed_api_factory.h"
@@ -37,10 +38,9 @@
 }  // namespace content
 
 namespace printing {
-struct PrinterStatus;
 class PdfBlobDataFlattener;
-class PrintJob;
-class PrintedDocument;
+struct PrinterStatus;
+struct PrintJobCreatedInfo;
 }  // namespace printing
 
 namespace extensions {
@@ -135,10 +135,8 @@
   };
 
   void OnPrintJobSubmitted(SubmitJobCallback callback,
-                           absl::optional<int> job_id,
-                           printing::PrintJob* print_job,
-                           printing::PrintedDocument* document,
-                           absl::optional<std::string> error);
+                           const std::string& extension_id,
+                           PrintJobSubmitter::PrintJobCreationResult result);
 
   void OnPrintersRetrieved(
       GetPrintersCallback callback,
diff --git a/chrome/browser/extensions/chrome_content_verifier_delegate.cc b/chrome/browser/extensions/chrome_content_verifier_delegate.cc
index f982ce3..c76f389 100644
--- a/chrome/browser/extensions/chrome_content_verifier_delegate.cc
+++ b/chrome/browser/extensions/chrome_content_verifier_delegate.cc
@@ -212,13 +212,6 @@
 
   const VerifyInfo info = GetVerifyInfo(*extension);
 
-  SYSLOG(WARNING) << "Corruption detected in extension " << extension_id
-                  << " installed at: " << extension->path().value()
-                  << ", from webstore: " << info.is_from_webstore
-                  << ", corruption reason: " << reason
-                  << ", should be repaired: " << info.should_repair
-                  << ", extension location: " << extension->location();
-
   if (reason == ContentVerifyJob::MISSING_ALL_HASHES) {
     // If the failure was due to hashes missing, only "enforce_strict" would
     // disable the extension, but not "enforce".
@@ -240,6 +233,13 @@
     }
   }
 
+  SYSLOG(WARNING) << "Corruption detected in extension " << extension_id
+                  << " installed at: " << extension->path().value()
+                  << ", from webstore: " << info.is_from_webstore
+                  << ", corruption reason: " << reason
+                  << ", should be repaired: " << info.should_repair
+                  << ", extension location: " << extension->location();
+
   const bool should_disable = info.mode >= VerifyInfo::Mode::ENFORCE;
   // Configuration when we should repair extension, but not disable it, is
   // invalid.
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 5221a346..cb4ff55 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -8076,6 +8076,14 @@
     "expiry_milestone": 125
   },
   {
+    "name": "tracking-protection-onboarding-reset-eligibility-for-testing",
+    "owners": [
+      "boujane@google.com",
+      "koilos@google.com"
+    ],
+    "expiry_milestone": 125
+  },
+  {
     "name": "traffic-counters",
     "owners": ["khorimoto", "cros-connectivity@google.com"],
     "expiry_milestone": 120
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 78a96fa..c6b94ced 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -3425,6 +3425,12 @@
 const char kTrackingProtectionOnboardingForceEligibilityDescription[] =
     "Enables the onboarding flow for Tracking Protections";
 
+const char kTrackingProtectionOnboardingResetEligibilityForTestingName[] =
+    "Reset Tracking Protection Onboarding";
+const char
+    kTrackingProtectionOnboardingResetEligibilityForTestingDescription[] =
+        "Resets the tracking protection onboarding profile data on startup";
+
 const char kUserBypassUIName[] = "User Bypass UI";
 const char kUserBypassUIDescription[] = "Enables the User Bypass UI. ";
 
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 0da3108c..17e4a81 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1975,6 +1975,10 @@
 extern const char kTrackingProtectionOnboardingForceEligibilityName[];
 extern const char kTrackingProtectionOnboardingForceEligibilityDescription[];
 
+extern const char kTrackingProtectionOnboardingResetEligibilityForTestingName[];
+extern const char
+    kTrackingProtectionOnboardingResetEligibilityForTestingDescription[];
+
 extern const char kUnifiedPasswordManagerAndroidName[];
 extern const char kUnifiedPasswordManagerAndroidDescription[];
 
diff --git a/chrome/browser/media/cast_mirroring_service_host_browsertest.cc b/chrome/browser/media/cast_mirroring_service_host_browsertest.cc
index b338c4b0..c4a0019 100644
--- a/chrome/browser/media/cast_mirroring_service_host_browsertest.cc
+++ b/chrome/browser/media/cast_mirroring_service_host_browsertest.cc
@@ -96,8 +96,7 @@
   MOCK_METHOD1(OnBufferDestroyedCall, void(int buffer_id));
   MOCK_METHOD1(OnStateChangedCall, void(media::mojom::VideoCaptureState state));
   MOCK_METHOD1(OnVideoCaptureErrorCall, void(media::VideoCaptureError error));
-  MOCK_METHOD1(OnFrameDroppedEarly,
-               void(media::VideoCaptureFrameDropReason reason));
+  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason reason));
 
   // media::mojom::VideoCaptureObserver implementation.
   void OnNewBuffer(int32_t buffer_id,
diff --git a/chrome/browser/metrics/ukm_browsertest.cc b/chrome/browser/metrics/ukm_browsertest.cc
index 32481f67..30c4f1a 100644
--- a/chrome/browser/metrics/ukm_browsertest.cc
+++ b/chrome/browser/metrics/ukm_browsertest.cc
@@ -1229,7 +1229,6 @@
 
   // History Sync is not active.
   ASSERT_FALSE(sync_service->GetActiveDataTypes().Has(syncer::HISTORY));
-  ASSERT_FALSE(sync_service->GetActiveDataTypes().Has(syncer::TYPED_URLS));
   ASSERT_FALSE(sync_service->GetActiveDataTypes().Has(
       syncer::HISTORY_DELETE_DIRECTIVES));
 
diff --git a/chrome/browser/net/profile_network_context_service.cc b/chrome/browser/net/profile_network_context_service.cc
index 8825dc2..1d6a86f 100644
--- a/chrome/browser/net/profile_network_context_service.cc
+++ b/chrome/browser/net/profile_network_context_service.cc
@@ -333,10 +333,6 @@
   // When any of the following CT preferences change, we schedule an update
   // to aggregate the actual update using a |ct_policy_update_timer_|.
   pref_change_registrar_.Add(
-      certificate_transparency::prefs::kCTRequiredHosts,
-      base::BindRepeating(&ProfileNetworkContextService::ScheduleUpdateCTPolicy,
-                          base::Unretained(this)));
-  pref_change_registrar_.Add(
       certificate_transparency::prefs::kCTExcludedHosts,
       base::BindRepeating(&ProfileNetworkContextService::ScheduleUpdateCTPolicy,
                           base::Unretained(this)));
@@ -516,8 +512,6 @@
 
 network::mojom::CTPolicyPtr ProfileNetworkContextService::GetCTPolicy() {
   auto* prefs = profile_->GetPrefs();
-  const base::Value::List& ct_required =
-      prefs->GetList(certificate_transparency::prefs::kCTRequiredHosts);
   const base::Value::List& ct_excluded =
       prefs->GetList(certificate_transparency::prefs::kCTExcludedHosts);
   const base::Value::List& ct_excluded_spkis =
@@ -525,14 +519,13 @@
   const base::Value::List& ct_excluded_legacy_spkis =
       prefs->GetList(certificate_transparency::prefs::kCTExcludedLegacySPKIs);
 
-  std::vector<std::string> required(TranslateStringArray(ct_required));
   std::vector<std::string> excluded(TranslateStringArray(ct_excluded));
   std::vector<std::string> excluded_spkis(
       TranslateStringArray(ct_excluded_spkis));
   std::vector<std::string> excluded_legacy_spkis(
       TranslateStringArray(ct_excluded_legacy_spkis));
 
-  return network::mojom::CTPolicy::New(std::move(required), std::move(excluded),
+  return network::mojom::CTPolicy::New(std::move(excluded),
                                        std::move(excluded_spkis),
                                        std::move(excluded_legacy_spkis));
 }
diff --git a/chrome/browser/net/sandboxed_network_change_notifier_win_browsertest.cc b/chrome/browser/net/sandboxed_network_change_notifier_win_browsertest.cc
index 173ea4d..5add6e9 100644
--- a/chrome/browser/net/sandboxed_network_change_notifier_win_browsertest.cc
+++ b/chrome/browser/net/sandboxed_network_change_notifier_win_browsertest.cc
@@ -33,6 +33,7 @@
 #include "content/public/browser/network_service_instance.h"
 #include "content/public/common/content_features.h"
 #include "content/public/test/browser_test.h"
+#include "net/base/features.h"
 #include "sandbox/policy/features.h"
 #include "services/network/public/mojom/network_change_manager.mojom.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -137,7 +138,10 @@
   SandboxedNetworkChangeNotifierBrowserTest() {
     if (GetParam()) {
       scoped_feature_list_.InitWithFeatures(
-          {sandbox::policy::features::kNetworkServiceSandbox},
+          {sandbox::policy::features::kNetworkServiceSandbox,
+           // When running inside the sandbox, the GetNetworkConnectivityHint
+           // API must be used.
+           net::features::kEnableGetNetworkConnectivityHintAPI},
           {features::kNetworkServiceInProcess});
     } else {
       scoped_feature_list_.InitWithFeatures(
diff --git a/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc b/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
index b28f316..712701a 100644
--- a/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
+++ b/chrome/browser/page_load_metrics/integration_tests/largest_contentful_paint_browsertest.cc
@@ -1200,7 +1200,8 @@
   Validate();
 }
 
-#if BUILDFLAG(IS_CHROMEOS_LACROS)
+// TODO(https://crbug.com/1487837): This test is failing on Linux CFI.
+#if BUILDFLAG(IS_CHROMEOS_LACROS) || BUILDFLAG(IS_LINUX)
 #define MAYBE_ManualLazyLoadingImage DISABLED_ManualLazyLoadingImage
 #else
 #define MAYBE_ManualLazyLoadingImage ManualLazyLoadingImage
diff --git a/chrome/browser/password_check/android/password_check_manager.cc b/chrome/browser/password_check/android/password_check_manager.cc
index 48ccf679..75ff182 100644
--- a/chrome/browser/password_check/android/password_check_manager.cc
+++ b/chrome/browser/password_check/android/password_check_manager.cc
@@ -13,7 +13,7 @@
 #include "components/password_manager/core/browser/affiliation/affiliation_utils.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_client.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/browser/password_ui_utils.h"
 #include "components/password_manager/core/browser/ui/credential_ui_entry.h"
 #include "components/password_manager/core/browser/ui/insecure_credentials_manager.h"
@@ -314,7 +314,7 @@
 }
 
 bool PasswordCheckManager::CanUseAccountCheck() const {
-  SyncState sync_state = password_manager_util::GetPasswordSyncState(
+  SyncState sync_state = password_manager::sync_util::GetPasswordSyncState(
       SyncServiceFactory::GetForProfile(profile_));
   switch (sync_state) {
     case SyncState::kNotSyncing:
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 7a59202..62c8de4 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -77,8 +77,8 @@
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "components/password_manager/core/browser/password_manager_setting.h"
 #include "components/password_manager/core/browser/password_manager_settings_service.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
 #include "components/password_manager/core/browser/password_requirements_service.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "components/prefs/pref_service.h"
 #include "components/profile_metrics/browser_profile_type.h"
@@ -715,7 +715,7 @@
     const {
   const syncer::SyncService* sync_service =
       SyncServiceFactory::GetForProfile(profile_);
-  return password_manager_util::GetPasswordSyncState(sync_service);
+  return password_manager::sync_util::GetPasswordSyncState(sync_service);
 }
 
 bool ChromePasswordManagerClient::WasLastNavigationHTTPError() const {
diff --git a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
index be9ff0f..1730e30 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client_unittest.cc
@@ -140,7 +140,7 @@
   field.name = u"password-element";
   field.id_attribute = field.name;
   field.name_attribute = field.name;
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.unique_renderer_id = FieldRendererId(123);
   form_data.fields.push_back(field);
 
diff --git a/chrome/browser/password_manager/password_manager_browsertest.cc b/chrome/browser/password_manager/password_manager_browsertest.cc
index 4177c1c..0693896 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -1177,7 +1177,7 @@
   form_data.url = signin_form.url;
   // Username
   autofill::FormFieldData username_field;
-  username_field.form_control_type = "text";
+  username_field.form_control_type = autofill::StringToFormControlType("text");
   username_field.id_attribute = u"username_field";
   username_field.name = username_field.id_attribute;
   username_field.value = u"example@example.com";
@@ -1186,7 +1186,8 @@
   form_data.fields.push_back(username_field);
   // Password
   autofill::FormFieldData password_field;
-  password_field.form_control_type = "password";
+  password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   password_field.id_attribute = u"password_field";
   password_field.name = password_field.id_attribute;
   password_field.value = u"htmlPass";
diff --git a/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc b/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
index 06c6e18f..a9a74adc 100644
--- a/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
+++ b/chrome/browser/password_manager/password_manager_captured_sites_interactive_uitest.cc
@@ -24,6 +24,7 @@
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_test_utils.h"
 #include "components/password_manager/core/common/password_manager_features.h"
+#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "components/sync/test/test_sync_service.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/test_utils.h"
@@ -220,6 +221,9 @@
     ChromePasswordManagerClient* client =
         ChromePasswordManagerClient::FromWebContents(WebContents());
     client->SetTestObserver(&observer_);
+
+    browser()->profile()->GetPrefs()->SetBoolean(prefs::kSafeBrowsingEnabled,
+                                                 false);
   }
 
   void SetUpCommandLine(base::CommandLine* command_line) override {
diff --git a/chrome/browser/picture_in_picture/document_picture_in_picture_window_controller_browsertest.cc b/chrome/browser/picture_in_picture/document_picture_in_picture_window_controller_browsertest.cc
index 6dec3a3..4ffea254 100644
--- a/chrome/browser/picture_in_picture/document_picture_in_picture_window_controller_browsertest.cc
+++ b/chrome/browser/picture_in_picture/document_picture_in_picture_window_controller_browsertest.cc
@@ -155,6 +155,12 @@
          ",height:", base::NumberToString(window_size.height()), "})"});
     ASSERT_EQ(true, EvalJs(active_web_contents, script));
     ASSERT_TRUE(window_controller() != nullptr);
+    // Especially on Linux, this isn't synchronous.
+    ui_test_utils::CheckWaiter(
+        base::BindRepeating(&content::RenderWidgetHostView::IsShowing,
+                            base::Unretained(GetRenderWidgetHostView())),
+        true, base::Seconds(30))
+        .Wait();
     ASSERT_TRUE(GetRenderWidgetHostView()->IsShowing());
   }
 
@@ -514,6 +520,29 @@
   EXPECT_EQ(false, pip_frame_view->frame()->IsMenuRunnerRunningForTesting());
 }
 
+IN_PROC_BROWSER_TEST_F(DocumentPictureInPictureWindowControllerBrowserTest,
+                       WindowClosesEvenIfDisconnectedFromWindowManager) {
+  // Rarely, `PictureInPictureWindowManager` fails to close the pip browser
+  // window. It's unclear why this happens, but the pip browser frame should
+  // fall back and close itself.
+  LoadTabAndEnterPictureInPicture(browser());
+  auto* pip_web_contents = window_controller()->GetChildWebContents();
+  ASSERT_NE(nullptr, pip_web_contents);
+  WaitForPageLoad(pip_web_contents);
+  auto* browser_view = static_cast<BrowserView*>(
+      BrowserWindow::FindBrowserWindowWithWebContents(pip_web_contents));
+  auto* pip_frame_view = static_cast<PictureInPictureBrowserFrameView*>(
+      browser_view->frame()->GetFrameView());
+  // Make the window manager forget about the window controller, which will
+  // cause it to fail to close the window when asked.
+  PictureInPictureWindowManager::GetInstance()
+      ->set_window_controller_for_testing(nullptr);
+  ClickButton(
+      views::Button::AsButton(pip_frame_view->GetCloseButtonForTesting()));
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(window_controller()->GetChildWebContents());
+}
+
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 // Verify that it is possible to resize a document picture in picture window
 // using the resize outside bound in ChromeOS ASH.
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
index af52cf29..4775cbba 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.cc
@@ -142,6 +142,27 @@
   return content::PictureInPictureResult::kSuccess;
 }
 
+bool PictureInPictureWindowManager::ExitPictureInPictureViaWindowUi(
+    UiBehavior behavior) {
+  if (!pip_window_controller_) {
+    return false;
+  }
+
+  switch (behavior) {
+    case UiBehavior::kCloseWindowOnly:
+      pip_window_controller_->Close(/*should_pause_video=*/false);
+      break;
+    case UiBehavior::kCloseWindowAndPauseVideo:
+      pip_window_controller_->Close(/*should_pause_video=*/true);
+      break;
+    case UiBehavior::kCloseWindowAndFocusOpener:
+      pip_window_controller_->CloseAndFocusInitiator();
+      break;
+  }
+
+  return true;
+}
+
 bool PictureInPictureWindowManager::ExitPictureInPicture() {
   if (pip_window_controller_) {
     CloseWindowInternal();
diff --git a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h
index a3acf94..85ae9fb 100644
--- a/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h
+++ b/chrome/browser/picture_in_picture/picture_in_picture_window_manager.h
@@ -83,6 +83,25 @@
   void EnterPictureInPictureWithController(
       content::PictureInPictureWindowController* pip_window_controller);
 
+  // Expected behavior of the window UI-initiated close.
+  enum class UiBehavior {
+    // Close the window, but don't try to pause the video.  This is also the
+    // behavior of `ExitPictureInPicture()`.
+    kCloseWindowOnly,
+
+    // Close the window, and also pause the video.
+    kCloseWindowAndPauseVideo,
+
+    // Act like the back-to-tab button: focus the opener window, and don't pause
+    // the video.
+    kCloseWindowAndFocusOpener,
+  };
+
+  // The user has requested to close the pip window.  This is similar to
+  // `ExitPictureInPicture()`, except that it's strictly user-initiated via the
+  // window UI.
+  bool ExitPictureInPictureViaWindowUi(UiBehavior behavior);
+
   // Closes any existing picture-in-picture windows (video or document pip).
   // Returns true if a picture-in-picture window was closed, and false if there
   // were no picture-in-picture windows to close.
@@ -162,6 +181,11 @@
   // https://www.w3.org/TR/picture-in-picture/#defines
   std::vector<url::Origin> GetActiveSessionOrigins();
 
+  void set_window_controller_for_testing(
+      content::PictureInPictureWindowController* controller) {
+    pip_window_controller_ = controller;
+  }
+
  private:
   friend struct base::DefaultSingletonTraits<PictureInPictureWindowManager>;
   class VideoWebContentsObserver;
diff --git a/chrome/browser/printing/print_job_utils_lacros.cc b/chrome/browser/printing/print_job_utils_lacros.cc
index b455c54..2826b7d 100644
--- a/chrome/browser/printing/print_job_utils_lacros.cc
+++ b/chrome/browser/printing/print_job_utils_lacros.cc
@@ -45,17 +45,17 @@
 
 }  // namespace
 
-void NotifyAshJobCreated(const PrintJob& job,
-                         int job_id,
+void NotifyAshJobCreated(int job_id,
                          const PrintedDocument& document,
+                         const crosapi::mojom::PrintJob::Source& source,
+                         const std::string& source_id,
                          crosapi::mojom::LocalPrinter* local_printer) {
   if (!local_printer) {
     LOG(ERROR) << "Could not report print job queued";
     return;
   }
   local_printer->CreatePrintJob(
-      PrintJobToMojom(job_id, document, job.source(), job.source_id()),
-      base::DoNothing());
+      PrintJobToMojom(job_id, document, source, source_id), base::DoNothing());
 }
 
 void NotifyAshJobCreated(const PrintJob& job,
@@ -65,7 +65,8 @@
   chromeos::LacrosService* service = chromeos::LacrosService::Get();
   if (service->IsAvailable<crosapi::mojom::LocalPrinter>())
     local_printer = service->GetRemote<crosapi::mojom::LocalPrinter>().get();
-  NotifyAshJobCreated(job, job_id, document, local_printer);
+  NotifyAshJobCreated(job_id, document, job.source(), job.source_id(),
+                      local_printer);
 }
 
 }  // namespace printing
diff --git a/chrome/browser/printing/print_job_utils_lacros.h b/chrome/browser/printing/print_job_utils_lacros.h
index ff9c89c..3b193a4 100644
--- a/chrome/browser/printing/print_job_utils_lacros.h
+++ b/chrome/browser/printing/print_job_utils_lacros.h
@@ -5,11 +5,7 @@
 #ifndef CHROME_BROWSER_PRINTING_PRINT_JOB_UTILS_LACROS_H_
 #define CHROME_BROWSER_PRINTING_PRINT_JOB_UTILS_LACROS_H_
 
-namespace crosapi {
-namespace mojom {
-class LocalPrinter;
-}
-}  // namespace crosapi
+#include "chromeos/crosapi/mojom/local_printer.mojom.h"
 
 namespace printing {
 
@@ -18,9 +14,10 @@
 
 // Notify Ash Chrome of a new print job.
 // If `local_printer` is null, this method fails.
-void NotifyAshJobCreated(const PrintJob& job,
-                         int job_id,
+void NotifyAshJobCreated(int job_id,
                          const PrintedDocument& document,
+                         const crosapi::mojom::PrintJob::Source& source,
+                         const std::string& source_id,
                          crosapi::mojom::LocalPrinter* local_printer);
 
 // Same as above but gets the LocalPrinter from LacrosService.
diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.cc b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.cc
index ae8086a..7ade89d 100644
--- a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.cc
+++ b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.cc
@@ -10,6 +10,7 @@
 #include "base/check.h"
 #include "base/feature_list.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/metrics/histogram_functions.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
 #include "build/buildflag.h"
@@ -177,6 +178,18 @@
     return true;
   }
 
+  TpcdExperimentEligibility eligibility =
+      GetCookieDeprecationExperimentCurrentEligibility();
+  base::UmaHistogramEnumeration(
+      "PrivacySandbox.CookieDeprecationFacilitatedTesting.ProfileEligibility",
+      eligibility);
+
+  return eligibility == TpcdExperimentEligibility::kEligible;
+}
+
+PrivacySandboxSettingsDelegate::TpcdExperimentEligibility
+PrivacySandboxSettingsDelegate::
+    GetCookieDeprecationExperimentCurrentEligibility() const {
   // Whether third-party cookies are blocked.
   scoped_refptr<content_settings::CookieSettings> cookie_settings =
       CookieSettingsFactory::GetForProfile(profile_);
@@ -184,7 +197,7 @@
   if (cookie_settings->ShouldBlockThirdPartyCookies() ||
       cookie_settings->GetDefaultCookieSetting() ==
           ContentSetting::CONTENT_SETTING_BLOCK) {
-    return false;
+    return TpcdExperimentEligibility::k3pCookiesBlocked;
   }
 
   // Whether the privacy sandbox Ads APIs notice has been seen.
@@ -197,12 +210,12 @@
   const bool eaa_notice_acknowledged = profile_->GetPrefs()->GetBoolean(
       prefs::kPrivacySandboxM1EEANoticeAcknowledged);
   if (!row_notice_acknowledged && !eaa_notice_acknowledged) {
-    return false;
+    return TpcdExperimentEligibility::kHasNotSeenNotice;
   }
 
   // Whether it's a dasher account.
   if (IsSubjectToEnterprisePolicies()) {
-    return false;
+    return TpcdExperimentEligibility::kEnterpriseUser;
   }
 
   // TODO(linnan): Consider moving the following client-level filtering to
@@ -213,17 +226,17 @@
       g_browser_process->local_state()->GetInt64(metrics::prefs::kInstallDate));
   if (install_date.is_null() ||
       base::Time::Now() - install_date < base::Days(30)) {
-    return false;
+    return TpcdExperimentEligibility::kNewUser;
   }
 
 // Whether PWA or TWA has been installed on Android.
 #if BUILDFLAG(IS_ANDROID)
   if (!webapp_registry_->GetOriginsWithInstalledApp().empty()) {
-    return false;
+    return TpcdExperimentEligibility::kPwaOrTwaInstalled;
   }
 #endif
 
-  return true;
+  return TpcdExperimentEligibility::kEligible;
 }
 
 bool PrivacySandboxSettingsDelegate::IsSubjectToEnterprisePolicies() const {
diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.h b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.h
index 6fa1a8dc..356e10a 100644
--- a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.h
+++ b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.h
@@ -40,6 +40,21 @@
 #endif
 
  private:
+  // These values are persisted to logs. Entries should not be renumbered and
+  // numeric values should never be reused.
+  enum class TpcdExperimentEligibility {
+    kEligible = 0,
+    k3pCookiesBlocked = 1,
+    kHasNotSeenNotice = 2,
+    kNewUser = 3,
+    kEnterpriseUser = 4,
+    kPwaOrTwaInstalled = 5,  // Android only
+    kMaxValue = kPwaOrTwaInstalled,
+  };
+
+  TpcdExperimentEligibility GetCookieDeprecationExperimentCurrentEligibility()
+      const;
+
   bool PrivacySandboxRestrictedNoticeRequired() const;
   bool IsSubjectToEnterprisePolicies() const;
   raw_ptr<Profile> profile_;
diff --git a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate_unittest.cc b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate_unittest.cc
index 9b3c65a..410f24f 100644
--- a/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate_unittest.cc
+++ b/chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate_unittest.cc
@@ -4,10 +4,13 @@
 
 #include "chrome/browser/privacy_sandbox/privacy_sandbox_settings_delegate.h"
 
+#include <stddef.h>
+
 #include <memory>
 #include <string>
 #include <vector>
 
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -287,6 +290,7 @@
   absl::optional<bool> expected_eligible_before;
   bool expected_eligible;
   bool expected_currently_eligible;
+  absl::optional<size_t> expected_reported_histogram;
 };
 
 const CookieDeprecationExperimentEligibilityTestCase
@@ -294,21 +298,24 @@
         {
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 2  // kHasNotSeenNotice
         },
-        {
-            .force_eligible = true,
-            .expected_eligible = true,
-            .expected_currently_eligible = true,
-        },
+        {.force_eligible = true,
+         .expected_eligible = true,
+         .expected_currently_eligible = true,
+         // No histogram should be reported if the eligibility is forced.
+         .expected_reported_histogram = absl::nullopt},
         {
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .expected_eligible = true,
             .expected_currently_eligible = true,
+            .expected_reported_histogram = 0  // kEligible
         },
         {
             .privacy_sandbox_row_notice_acknowledged_pref = true,
             .expected_eligible = true,
             .expected_currently_eligible = true,
+            .expected_reported_histogram = 0  // kEligible
         },
         {
             .cookie_controls_mode_pref =
@@ -316,36 +323,42 @@
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 1  // k3pCookiesBlocked
         },
         {
             .cookie_content_setting = ContentSetting::CONTENT_SETTING_BLOCK,
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 1  // k3pCookiesBlocked
         },
         {
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .install_date = absl::nullopt,
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 3  // kNewUser
         },
         {
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .install_date = kCurrentTime - base::Days(29),
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 3  // kNewUser
         },
         {
             .is_subject_to_enterprise_policies = true,
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 4  // kEnterpriseUser
         },
         {
             .is_subject_to_enterprise_policies = false,
             .privacy_sandbox_eea_notice_acknowledged_pref = true,
             .expected_eligible = true,
             .expected_currently_eligible = true,
+            .expected_reported_histogram = 0  // kEligible
         },
 #if BUILDFLAG(IS_ANDROID)
         {
@@ -354,6 +367,7 @@
                 std::vector<std::string>({"https://a.test"}),
             .expected_eligible = false,
             .expected_currently_eligible = false,
+            .expected_reported_histogram = 5  // kPwaOrTwaInstalled
         },
 #endif
         {
@@ -361,6 +375,7 @@
             .expected_eligible_before = false,
             .expected_eligible = false,
             .expected_currently_eligible = true,
+            .expected_reported_histogram = 0  // kEligible
         },
 };
 
@@ -441,8 +456,19 @@
       .WillByDefault(testing::Return(test_case.origins_with_installed_app));
 #endif
 
+  base::HistogramTester histograms;
   EXPECT_EQ(delegate()->IsCookieDeprecationExperimentCurrentlyEligible(),
             test_case.expected_currently_eligible);
+  if (test_case.expected_reported_histogram.has_value()) {
+    histograms.ExpectUniqueSample(
+        "PrivacySandbox.CookieDeprecationFacilitatedTesting.ProfileEligibility",
+        *test_case.expected_reported_histogram, /*expected_bucket_count=*/1);
+  } else {
+    histograms.ExpectTotalCount(
+        "PrivacySandbox.CookieDeprecationFacilitatedTesting.ProfileEligibility",
+        0);
+  }
+
   EXPECT_EQ(delegate()->IsCookieDeprecationExperimentEligible(),
             test_case.expected_eligible);
 }
diff --git a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
index 3df851e..94b6515 100644
--- a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
+++ b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
@@ -185,6 +185,7 @@
 #include "chrome/browser/ui/prefs/prefs_tab_helper.h"
 #include "chrome/browser/ui/safety_hub/password_status_check_service_factory.h"
 #include "chrome/browser/ui/tabs/pinned_tab_service_factory.h"
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h"
 #include "chrome/browser/ui/toolbar/toolbar_actions_model_factory.h"
 #include "chrome/browser/ui/ui_features.h"
 #include "chrome/browser/ui/webui/ntp/ntp_resource_cache_factory.h"
@@ -916,6 +917,7 @@
 #if !BUILDFLAG(IS_ANDROID)
   PhotosServiceFactory::GetInstance();
   PinnedTabServiceFactory::GetInstance();
+  PinnedToolbarActionsModelFactory::GetInstance();
 #endif
   PlatformNotificationServiceFactory::GetInstance();
 #if BUILDFLAG(ENABLE_PLUGINS)
diff --git a/chrome/browser/renderer_context_menu/mock_render_view_context_menu.cc b/chrome/browser/renderer_context_menu/mock_render_view_context_menu.cc
index 185e063..363b573 100644
--- a/chrome/browser/renderer_context_menu/mock_render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/mock_render_view_context_menu.cc
@@ -237,27 +237,11 @@
   }
 }
 
-void MockRenderViewContextMenu::AddPdfOcrMenuItem(bool is_checked) {
+void MockRenderViewContextMenu::AddPdfOcrMenuItem() {
 #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
-  if (is_checked) {
-    AddCheckItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION));
-  } else {
-    ui::SimpleMenuModel pdf_ocr_submenu_model_(this);
-    pdf_ocr_submenu_model_.AddItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS,
-        l10n_util::GetStringUTF16(
-            IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS));
-    pdf_ocr_submenu_model_.AddItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR_ONCE,
-        l10n_util::GetStringUTF16(
-            IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE));
-    AddSubMenu(
-        IDC_CONTENT_CONTEXT_PDF_OCR,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION),
-        &pdf_ocr_submenu_model_);
-  }
+  AddCheckItem(
+      IDC_CONTENT_CONTEXT_PDF_OCR,
+      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION));
 #endif  // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
 }
 
diff --git a/chrome/browser/renderer_context_menu/mock_render_view_context_menu.h b/chrome/browser/renderer_context_menu/mock_render_view_context_menu.h
index cadf031..d951eed 100644
--- a/chrome/browser/renderer_context_menu/mock_render_view_context_menu.h
+++ b/chrome/browser/renderer_context_menu/mock_render_view_context_menu.h
@@ -79,7 +79,7 @@
   void RemoveSeparatorBeforeMenuItem(int command_id) override;
   void AddSpellCheckServiceItem(bool is_checked) override;
   void AddAccessibilityLabelsServiceItem(bool is_checked) override;
-  void AddPdfOcrMenuItem(bool is_checked) override;
+  void AddPdfOcrMenuItem() override;
   content::RenderViewHost* GetRenderViewHost() const override;
   content::BrowserContext* GetBrowserContext() const override;
   content::WebContents* GetWebContents() const override;
diff --git a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.cc b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.cc
index c7963ac..a7d880b 100644
--- a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.cc
+++ b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.cc
@@ -44,73 +44,50 @@
   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
   DCHECK(profile != nullptr);
   if (ShouldShowPdfOcrMenuItem()) {
-    proxy_->AddPdfOcrMenuItem(profile->GetPrefs()->GetBoolean(
-        prefs::kAccessibilityPdfOcrAlwaysActive));
+    proxy_->AddPdfOcrMenuItem();
   }
 }
 
 bool PdfOcrMenuObserver::IsCommandIdSupported(int command_id) {
-  return command_id == IDC_CONTENT_CONTEXT_PDF_OCR ||
-         command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS ||
-         command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ONCE;
+  return command_id == IDC_CONTENT_CONTEXT_PDF_OCR;
 }
 
 bool PdfOcrMenuObserver::IsCommandIdChecked(int command_id) {
   DCHECK(IsCommandIdSupported(command_id));
   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
   DCHECK(profile != nullptr);
-  if (command_id == IDC_CONTENT_CONTEXT_PDF_OCR ||
-      command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS ||
-      command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ONCE) {
-    return profile->GetPrefs()->GetBoolean(
-        prefs::kAccessibilityPdfOcrAlwaysActive);
-  }
-  return false;
+  return (command_id == IDC_CONTENT_CONTEXT_PDF_OCR
+              ? profile->GetPrefs()->GetBoolean(
+                    prefs::kAccessibilityPdfOcrAlwaysActive)
+              : false);
 }
 
 bool PdfOcrMenuObserver::IsCommandIdEnabled(int command_id) {
   DCHECK(IsCommandIdSupported(command_id));
-  if (command_id == IDC_CONTENT_CONTEXT_PDF_OCR ||
-      command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS ||
-      command_id == IDC_CONTENT_CONTEXT_PDF_OCR_ONCE) {
-    return ShouldShowPdfOcrMenuItem();
-  }
-  return false;
+  return (command_id == IDC_CONTENT_CONTEXT_PDF_OCR ? ShouldShowPdfOcrMenuItem()
+                                                    : false);
 }
 
 void PdfOcrMenuObserver::ExecuteCommand(int command_id) {
   DCHECK(IsCommandIdSupported(command_id));
   Profile* profile = Profile::FromBrowserContext(proxy_->GetBrowserContext());
   DCHECK(profile != nullptr);
-  bool is_always_active =
-      profile->GetPrefs()->GetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive);
   switch (command_id) {
     case IDC_CONTENT_CONTEXT_PDF_OCR:
-      // If the user has selected to make PDF OCR always active, we directly
-      // update the profile and change it to the original menu item when the
-      // user disables this item.
-      DCHECK(is_always_active);
-      VLOG(2) << "Turning off PDF OCR from the context menu";
-      profile->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
-                                      false);
-      RecordUserSelection(PdfOcrUserSelection::kTurnOffFromContextMenu);
-      break;
-    case IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS:
-      // When a user choose "Always" to run the PDF OCR, we save this
-      // preference and change this item to a check item in the context menu.
-      if (!is_always_active) {
-        VLOG(2) << "Setting PDF OCR to be always active from the context menu";
+      // If PDF OCR is already on, turn off PDF OCR. Otherwise, turn on PDF OCR.
+      if (profile->GetPrefs()->GetBoolean(
+              prefs::kAccessibilityPdfOcrAlwaysActive)) {
+        VLOG(2) << "Turning off PDF OCR from the context menu";
+        profile->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
+                                        false);
+        RecordUserSelection(PdfOcrUserSelection::kTurnOffFromContextMenu);
+      } else {
+        VLOG(2) << "Turning on PDF OCR from the context menu";
         profile->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
                                         true);
         RecordUserSelection(PdfOcrUserSelection::kTurnOnAlwaysFromContextMenu);
       }
       break;
-    case IDC_CONTENT_CONTEXT_PDF_OCR_ONCE:
-      VLOG(2) << "Running PDF OCR only once from the context menu";
-      screen_ai::PdfOcrControllerFactory::GetForProfile(profile)
-          ->RunPdfOcrOnlyOnce(proxy_->GetWebContents());
-      RecordUserSelection(PdfOcrUserSelection::kTurnOnOnceFromContextMenu);
-      break;
     default:
       NOTREACHED();
   }
diff --git a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.h b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.h
index d5d8b61a..e739630 100644
--- a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.h
+++ b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer.h
@@ -17,7 +17,7 @@
 // numeric values should never be reused. Must be kept in sync with
 // PdfOcrUserSelection enum in //tools/metrics/histograms/enums.xml.
 enum class PdfOcrUserSelection {
-  kTurnOnOnceFromContextMenu = 0,
+  kDeprecated_TurnOnOnceFromContextMenu = 0,
   kTurnOnAlwaysFromContextMenu = 1,
   kTurnOffFromContextMenu = 2,
   kTurnOnAlwaysFromMoreActions = 3,
diff --git a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer_browsertest.cc b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer_browsertest.cc
index 3202669..5423a9f 100644
--- a/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer_browsertest.cc
+++ b/chrome/browser/renderer_context_menu/pdf_ocr_menu_observer_browsertest.cc
@@ -101,7 +101,7 @@
   InitMenu();
 
   // Shows but is not checked.
-  ASSERT_EQ(3u, menu()->GetMenuSize());
+  EXPECT_EQ(1u, menu()->GetMenuSize());
   MockRenderViewContextMenu::MockMenuItem item;
   menu()->GetMenuItem(0, &item);
   EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR, item.command_id);
@@ -109,24 +109,12 @@
   EXPECT_FALSE(item.checked);
   EXPECT_FALSE(item.hidden);
 
-  // The submenu items exist.
-  menu()->GetMenuItem(1, &item);
-  EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS, item.command_id);
-  EXPECT_TRUE(item.enabled);
-  EXPECT_FALSE(item.checked);
-  EXPECT_FALSE(item.hidden);
-  menu()->GetMenuItem(2, &item);
-  EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR_ONCE, item.command_id);
-  EXPECT_TRUE(item.enabled);
-  EXPECT_FALSE(item.checked);
-  EXPECT_FALSE(item.hidden);
-
   Reset(false);
-  // Shows and is checked when a screen reader and the setting are both on.
+  // Shows and is checked when a screen reader and PDF OCR are both on.
   menu()->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive, true);
   InitMenu();
 
-  ASSERT_EQ(1u, menu()->GetMenuSize());
+  EXPECT_EQ(1u, menu()->GetMenuSize());
   menu()->GetMenuItem(0, &item);
   EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR, item.command_id);
   EXPECT_TRUE(item.enabled);
@@ -135,7 +123,7 @@
 }
 
 IN_PROC_BROWSER_TEST_F(PdfOcrMenuObserverTest,
-                       CheckUmaWhenTurnOnPdfOcrAlwaysFromContextMenu) {
+                       CheckUmaWhenTurnOnPdfOcrFromContextMenu) {
 #if BUILDFLAG(IS_CHROMEOS_ASH)
   // Enable Chromevox.
   ash::AccessibilityManager::Get()->EnableSpokenFeedback(true);
@@ -148,14 +136,15 @@
   menu()->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
                                  false);
   InitMenu();
-  ASSERT_EQ(menu()->GetMenuSize(), 3u);
+  EXPECT_EQ(1u, menu()->GetMenuSize());
 
-  // Get the PDF OCR always menu item.
+  // Get the PDF OCR menu item.
   MockRenderViewContextMenu::MockMenuItem item;
-  menu()->GetMenuItem(1, &item);
-  ASSERT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS, item.command_id);
+  menu()->GetMenuItem(0, &item);
+  EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR, item.command_id);
+  EXPECT_FALSE(item.checked);
 
-  // Turn on PDF OCR always.
+  // Turn on PDF OCR.
   base::HistogramTester histograms;
   menu()->ExecuteCommand(item.command_id, 0);
 
@@ -179,13 +168,13 @@
 
   menu()->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive, true);
   InitMenu();
-  ASSERT_EQ(menu()->GetMenuSize(), 1u);
+  EXPECT_EQ(1u, menu()->GetMenuSize());
 
   // Get the PDF OCR checked menu item.
   MockRenderViewContextMenu::MockMenuItem item;
   menu()->GetMenuItem(0, &item);
-  ASSERT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR, item.command_id);
-  ASSERT_TRUE(item.checked);
+  EXPECT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR, item.command_id);
+  EXPECT_TRUE(item.checked);
 
   // Turn off PDF OCR.
   base::HistogramTester histograms;
@@ -196,34 +185,3 @@
                                 PdfOcrUserSelection::kTurnOffFromContextMenu,
                                 /*expected_bucket_count=*/1);
 }
-
-IN_PROC_BROWSER_TEST_F(PdfOcrMenuObserverTest,
-                       CheckUmaWhenTurnOnPdfOcrOnceFromContextMenu) {
-#if BUILDFLAG(IS_CHROMEOS_ASH)
-  // Enable Chromevox.
-  ash::AccessibilityManager::Get()->EnableSpokenFeedback(true);
-#else
-  // Spoof a screen reader.
-  content::ScopedAccessibilityModeOverride scoped_accessibility_mode(
-      ui::AXMode::kScreenReader);
-#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
-
-  menu()->GetPrefs()->SetBoolean(prefs::kAccessibilityPdfOcrAlwaysActive,
-                                 false);
-  InitMenu();
-  ASSERT_EQ(menu()->GetMenuSize(), 3u);
-
-  // Get the PDF OCR once menu item.
-  MockRenderViewContextMenu::MockMenuItem item;
-  menu()->GetMenuItem(2, &item);
-  ASSERT_EQ(IDC_CONTENT_CONTEXT_PDF_OCR_ONCE, item.command_id);
-
-  // Run PDF OCR once.
-  base::HistogramTester histograms;
-  menu()->ExecuteCommand(item.command_id, 0);
-
-  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
-  histograms.ExpectUniqueSample("Accessibility.PdfOcr.UserSelection",
-                                PdfOcrUserSelection::kTurnOnOnceFromContextMenu,
-                                /*expected_bucket_count=*/1);
-}
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
index 641c0cb7..bbde9b1 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.cc
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.cc
@@ -487,8 +487,8 @@
        {IDC_CONTENT_CONTEXT_ADD_A_NOTE, 124},
        {IDC_LIVE_CAPTION, 125},
        {IDC_CONTENT_CONTEXT_PDF_OCR, 126},
-       {IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS, 127},
-       {IDC_CONTENT_CONTEXT_PDF_OCR_ONCE, 128},
+       // Removed: {IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS, 127},
+       // Removed: {IDC_CONTENT_CONTEXT_PDF_OCR_ONCE, 128},
        {IDC_CONTENT_CONTEXT_AUTOFILL_FEEDBACK, 129},
        {IDC_CONTENT_CONTEXT_TRANSLATEIMAGEWITHWEB, 130},
        {IDC_CONTENT_CONTEXT_TRANSLATEIMAGEWITHLENS, 131},
@@ -3316,30 +3316,13 @@
   }
 }
 
-void RenderViewContextMenu::AddPdfOcrMenuItem(bool is_always_active) {
+void RenderViewContextMenu::AddPdfOcrMenuItem() {
 #if BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
-  if (is_always_active) {
-    // Only a checked item needs to be added to the context menu when the user
-    // selects "Always" or toggles on PDF OCR to make it always active.
-    menu_model_.AddCheckItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION));
-  } else {
-    // Add the submenu when the user doesn't select "Always" nor toggle on the
-    // the PDF OCR.
-    pdf_ocr_submenu_model_->AddItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR_ALWAYS,
-        l10n_util::GetStringUTF16(
-            IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ALWAYS));
-    pdf_ocr_submenu_model_->AddItem(
-        IDC_CONTENT_CONTEXT_PDF_OCR_ONCE,
-        l10n_util::GetStringUTF16(
-            IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION_ONCE));
-    menu_model_.AddSubMenu(
-        IDC_CONTENT_CONTEXT_PDF_OCR,
-        l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION),
-        pdf_ocr_submenu_model_.get());
-  }
+  // Add an item to the context menu. Its check state will be determined by
+  // whether PDF OCR is on. If on, it will be checked; otherwise, unchecked.
+  menu_model_.AddCheckItem(
+      IDC_CONTENT_CONTEXT_PDF_OCR,
+      l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_PDF_OCR_MENU_OPTION));
 #endif  // BUILDFLAG(ENABLE_SCREEN_AI_SERVICE)
 }
 
diff --git a/chrome/browser/renderer_context_menu/render_view_context_menu.h b/chrome/browser/renderer_context_menu/render_view_context_menu.h
index b74da355..45f2a2a82 100644
--- a/chrome/browser/renderer_context_menu/render_view_context_menu.h
+++ b/chrome/browser/renderer_context_menu/render_view_context_menu.h
@@ -133,7 +133,7 @@
   void ExecuteCommand(int command_id, int event_flags) override;
   void AddSpellCheckServiceItem(bool is_checked) override;
   void AddAccessibilityLabelsServiceItem(bool is_checked) override;
-  void AddPdfOcrMenuItem(bool is_always_active) override;
+  void AddPdfOcrMenuItem() override;
 
   // Registers a one-time callback that will be called the next time a context
   // menu is shown.
diff --git a/chrome/browser/resources/PRESUBMIT.py b/chrome/browser/resources/PRESUBMIT.py
index c0a710d..3e2e038 100644
--- a/chrome/browser/resources/PRESUBMIT.py
+++ b/chrome/browser/resources/PRESUBMIT.py
@@ -133,6 +133,7 @@
   EXCLUDED_PATHS = [
     'chrome/browser/resources/.eslintrc',
     'chrome/browser/resources/about_sys/',
+    'chrome/browser/resources/ash/settings/.eslintrc',
     'chrome/browser/resources/bluetooth_internals/',
     'chrome/browser/resources/chromeos/',
     'chrome/browser/resources/device_log_ui/',
diff --git a/chrome/browser/resources/ash/settings/.eslintrc.js b/chrome/browser/resources/ash/settings/.eslintrc.js
new file mode 100644
index 0000000..07ba45f
--- /dev/null
+++ b/chrome/browser/resources/ash/settings/.eslintrc.js
@@ -0,0 +1,31 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * CrOS Settings SWA specific ESLint rules.
+ */
+
+module.exports = {
+  // Disable clang-format because it produces odd formatting for these rules.
+  // clang-format off
+  rules: {
+    /**
+     * https://google.github.io/styleguide/tsguide.html#return-types
+     * The Google TS style guide makes no formal rule on enforcing explicit
+     * return types. However, explicit return types have clear advantages in
+     * both readability and maintainability.
+     */
+    '@typescript-eslint/explicit-function-return-type': [
+      'error',
+      {
+        // Function expressions are exempt.
+        allowExpressions: true,
+        // Avoid checking Polymer static getter methods.
+        allowedNames: ['is', 'template', 'properties', 'observers'],
+      },
+    ],
+  },
+  // clang-format on
+};
diff --git a/chrome/browser/resources/ash/settings/date_time_page/timezone_browser_proxy.ts b/chrome/browser/resources/ash/settings/date_time_page/timezone_browser_proxy.ts
index 75a6732..d820bfe 100644
--- a/chrome/browser/resources/ash/settings/date_time_page/timezone_browser_proxy.ts
+++ b/chrome/browser/resources/ash/settings/date_time_page/timezone_browser_proxy.ts
@@ -26,23 +26,23 @@
     return instance || (instance = new TimeZoneBrowserProxyImpl());
   }
 
-  static setInstanceForTesting(obj: TimeZoneBrowserProxy) {
+  static setInstanceForTesting(obj: TimeZoneBrowserProxy): void {
     instance = obj;
   }
 
-  showParentAccessForTimeZone() {
+  showParentAccessForTimeZone(): void {
     chrome.send('handleShowParentAccessForTimeZone');
   }
 
-  dateTimePageReady() {
+  dateTimePageReady(): void {
     chrome.send('dateTimePageReady');
   }
 
-  showSetDateTimeUi() {
+  showSetDateTimeUi(): void {
     chrome.send('showSetDateTimeUI');
   }
 
-  getTimeZones() {
+  getTimeZones(): Promise<string[][]> {
     return sendWithPromise('getTimeZones');
   }
 }
diff --git a/chrome/browser/resources/ash/settings/date_time_page/timezone_selector.ts b/chrome/browser/resources/ash/settings/date_time_page/timezone_selector.ts
index f6b7279..121ff143 100644
--- a/chrome/browser/resources/ash/settings/date_time_page/timezone_selector.ts
+++ b/chrome/browser/resources/ash/settings/date_time_page/timezone_selector.ts
@@ -93,7 +93,7 @@
     this.getTimeZonesRequestSent_ = false;
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.maybeGetTimeZoneList_();
@@ -103,7 +103,7 @@
    * Fetches the list of time zones if necessary.
    * @param perUserTimeZoneMode Expected value of per-user time zone.
    */
-  private maybeGetTimeZoneList_(perUserTimeZoneMode?: boolean) {
+  private maybeGetTimeZoneList_(perUserTimeZoneMode?: boolean): void {
     if (typeof (perUserTimeZoneMode) !== 'undefined') {
       /* This method is called as observer. Skip if if current mode does not
        * match expected.
@@ -150,14 +150,14 @@
   /**
    * Prefs observer for Per-user time zone enabled mode.
    */
-  private maybeGetTimeZoneListPerUser_() {
+  private maybeGetTimeZoneListPerUser_(): void {
     this.maybeGetTimeZoneList_(true);
   }
 
   /**
    * Prefs observer for Per-user time zone disabled mode.
    */
-  private maybeGetTimeZoneListPerSystem_() {
+  private maybeGetTimeZoneListPerSystem_(): void {
     this.maybeGetTimeZoneList_(false);
   }
 
@@ -165,7 +165,7 @@
    * Converts the C++ response into an array of menu options.
    * @param timeZones C++ time zones response.
    */
-  private setTimeZoneList_(timeZones: string[][]) {
+  private setTimeZoneList_(timeZones: string[][]): void {
     this.timeZoneList_ = timeZones.map((timeZonePair) => {
       return {
         name: timeZonePair[1],
@@ -180,7 +180,7 @@
    * Updates active time zone display name when changed.
    * @param activeTimeZoneId value of cros.system.timezone preference.
    */
-  private updateActiveTimeZoneName_(activeTimeZoneId: string) {
+  private updateActiveTimeZoneName_(activeTimeZoneId: string): void {
     const activeTimeZone = this.timeZoneList_.find(
         (timeZone) => timeZone.value.toString() === activeTimeZoneId);
     if (activeTimeZone) {
diff --git a/chrome/browser/resources/ash/settings/date_time_page/timezone_subpage.ts b/chrome/browser/resources/ash/settings/date_time_page/timezone_subpage.ts
index 8138006..fa02adc 100644
--- a/chrome/browser/resources/ash/settings/date_time_page/timezone_subpage.ts
+++ b/chrome/browser/resources/ash/settings/date_time_page/timezone_subpage.ts
@@ -92,7 +92,7 @@
    * 'access-code-validation-complete' event is triggered which invokes
    * enableTimeZoneSetting_.
    */
-  override currentRouteChanged(newRoute: Route, _oldRoute?: Route) {
+  override currentRouteChanged(newRoute: Route, _oldRoute?: Route): void {
     if (newRoute !== routes.DATETIME_TIMEZONE_SUBPAGE) {
       return;
     }
@@ -112,7 +112,8 @@
   /**
    * Returns value list for timeZoneResolveMethodDropdown menu.
    */
-  private getTimeZoneResolveMethodsList_() {
+  private getTimeZoneResolveMethodsList_():
+      Array<{name: string, value: number}> {
     const result: Array<{name: string, value: number}> = [];
     const pref =
         this.getPref('generated.resolve_timezone_by_geolocation_method_short');
@@ -150,7 +151,7 @@
   /**
    * Enables all dropdowns and radio buttons.
    */
-  private enableTimeZoneSetting_() {
+  private enableTimeZoneSetting_(): void {
     const radios = this.shadowRoot!.querySelectorAll('controlled-radio-button');
     for (const radio of radios) {
       radio.disabled = false;
@@ -166,7 +167,7 @@
   /**
    * Disables all dropdowns and radio buttons.
    */
-  private disableTimeZoneSetting_() {
+  private disableTimeZoneSetting_(): void {
     this.$.timeZoneResolveMethodDropdown.disabled = true;
     this.$.timezoneSelector.shouldDisableTimeZoneGeoSelector = true;
     const radios = this.shadowRoot!.querySelectorAll('controlled-radio-button');
diff --git a/chrome/browser/resources/ash/settings/device_page/customize_button_row.ts b/chrome/browser/resources/ash/settings/device_page/customize_button_row.ts
index d7039d3..73e6edd 100644
--- a/chrome/browser/resources/ash/settings/device_page/customize_button_row.ts
+++ b/chrome/browser/resources/ash/settings/device_page/customize_button_row.ts
@@ -27,8 +27,8 @@
 const NO_REMAPPING_OPTION_LABEL = 'none';
 const KEY_COMBINATION_OPTION_LABEL = 'key combination';
 const OPEN_DIALOG_OPTION_LABEL = 'open key combination dialog';
-const ACTION_PREFIX = 'action';
-const HARDCODED_ACTION_PREFIX = 'hardcodedAction';
+const ACCELERATOR_ACTION_PREFIX = 'acceleratorAction';
+const STATICS_SHORTCUT_ACTION_PREFIX = 'staticShortcutAction';
 
 /**
  * Bit mask of modifiers.
@@ -277,19 +277,20 @@
     // pages, and pass it as a value instead of creating fake data here.
     for (const actionChoice of this.actionList) {
       const acceleratorAction = actionChoice.actionType.acceleratorAction;
-      const hardcodedAction = actionChoice.actionType.hardcodedAction;
+      const staticShortcutAction = actionChoice.actionType.staticShortcutAction;
       if (acceleratorAction !== undefined) {
-        // Prepend an action prefix to distinguish it from the hardcodedAction
-        // enum.
+        // Prepend an acceleratorAction prefix to distinguish it from the
+        // StaticShortcutAction enum.
         this.buttonMapTargets_.push({
-          value: ACTION_PREFIX + acceleratorAction.toString(),
+          value: ACCELERATOR_ACTION_PREFIX + acceleratorAction.toString(),
           name: actionChoice.name,
         });
-      } else if (hardcodedAction !== undefined) {
-        // Prepend a hardcodedAction prefix to distinguish it from the action
-        // enum.
+      } else if (staticShortcutAction !== undefined) {
+        // Prepend a staticShortcutAction prefix to distinguish it from the
+        // AcceleratorAction enum.
         this.buttonMapTargets_.push({
-          value: HARDCODED_ACTION_PREFIX + hardcodedAction.toString(),
+          value:
+              STATICS_SHORTCUT_ACTION_PREFIX + staticShortcutAction.toString(),
           name: actionChoice.name,
         });
       }
@@ -305,18 +306,21 @@
     // Set the dropdown option label to default 'Key combination'.
     this.keyCombinationLabel_ = this.i18n('keyCombinationOptionLabel');
 
-    // For accelerator actions, the remappingAction.action value is number.
-    const action = this.buttonRemapping_.remappingAction?.action;
-    const keyEvent = this.buttonRemapping_.remappingAction?.keyEvent;
-    // For hardcoded actions, the remappingAction.hardcodedAction value is
+    // For accelerator actions, the remappingAction.acceleratorAction value is
     // number.
-    const hardcodedAction =
-        this.buttonRemapping_.remappingAction?.hardcodedAction;
-    if (action !== undefined && !isNaN(action)) {
-      // Prepend an action prefix to distinguish it from the hardcodedAction
-      // enum.
-      const originalAction = ACTION_PREFIX + action.toString();
-      this.initializeDropdown_(originalAction, dropdown);
+    const acceleratorAction =
+        this.buttonRemapping_.remappingAction?.acceleratorAction;
+    const keyEvent = this.buttonRemapping_.remappingAction?.keyEvent;
+    // For static shortcut actions, the remappingAction.staticShortcutAction
+    // value is number.
+    const staticShortcutAction =
+        this.buttonRemapping_.remappingAction?.staticShortcutAction;
+    if (acceleratorAction !== undefined && !isNaN(acceleratorAction)) {
+      // Prepend an acceleratorAction prefix to distinguish it from the
+      // staticShortcutAction enum.
+      const originalAcceleratorAction =
+          ACCELERATOR_ACTION_PREFIX + acceleratorAction.toString();
+      this.initializeDropdown_(originalAcceleratorAction, dropdown);
     } else if (keyEvent) {
       this.set('fakePref_.value', KEY_COMBINATION_OPTION_LABEL);
       this.keyCombinationLabel_ = getKeyCombinationLabel(keyEvent) ??
@@ -326,12 +330,13 @@
         dropdown.value = KEY_COMBINATION_OPTION_LABEL;
         this.prevChoice_ = dropdown.value;
       });
-    } else if (hardcodedAction !== undefined && !isNaN(hardcodedAction)) {
-      // Prepend a hardcodedAction prefix to distinguish it from the action
-      // enum.
-      const originalHardcodedAction =
-          HARDCODED_ACTION_PREFIX + hardcodedAction.toString();
-      this.initializeDropdown_(originalHardcodedAction, dropdown);
+    } else if (
+        staticShortcutAction !== undefined && !isNaN(staticShortcutAction)) {
+      // Prepend a staticShortcutAction prefix to distinguish it from
+      // the acceleratorAction enum.
+      const originalStaticShortcutAction =
+          STATICS_SHORTCUT_ACTION_PREFIX + staticShortcutAction.toString();
+      this.initializeDropdown_(originalStaticShortcutAction, dropdown);
     } else {
       this.set('fakePref_.value', NO_REMAPPING_OPTION_LABEL);
       microTask.run(() => {
@@ -374,19 +379,20 @@
     }
     // Otherwise the button is remapped to a remappingAction.
     let remappingAction: RemappingAction|undefined = undefined;
-    if (this.fakePref_.value.startsWith(ACTION_PREFIX)) {
-      // Remove the action prefix to get the real enum value.
-      this.fakePref_.value = this.fakePref_.value.slice(ACTION_PREFIX.length);
+    if (this.fakePref_.value.startsWith(ACCELERATOR_ACTION_PREFIX)) {
+      // Remove the acceleratorAction prefix to get the real enum value.
+      this.fakePref_.value =
+          this.fakePref_.value.slice(ACCELERATOR_ACTION_PREFIX.length);
       remappingAction = {
-        action: Number(this.fakePref_.value),
+        acceleratorAction: Number(this.fakePref_.value),
       };
     }
-    if (this.fakePref_.value.startsWith(HARDCODED_ACTION_PREFIX)) {
-      // Remove the hardcodedAction prefix to get the real enum value.
+    if (this.fakePref_.value.startsWith(STATICS_SHORTCUT_ACTION_PREFIX)) {
+      // Remove the staticShortcutAction prefix to get the real enum value.
       this.fakePref_.value =
-          this.fakePref_.value.slice(HARDCODED_ACTION_PREFIX.length);
+          this.fakePref_.value.slice(STATICS_SHORTCUT_ACTION_PREFIX.length);
       remappingAction = {
-        hardcodedAction: Number(this.fakePref_.value),
+        staticShortcutAction: Number(this.fakePref_.value),
       };
     }
     const updatedRemapping: ButtonRemapping = {
diff --git a/chrome/browser/resources/ash/settings/device_page/fake_input_device_data.ts b/chrome/browser/resources/ash/settings/device_page/fake_input_device_data.ts
index 4044141..2914699 100644
--- a/chrome/browser/resources/ash/settings/device_page/fake_input_device_data.ts
+++ b/chrome/browser/resources/ash/settings/device_page/fake_input_device_data.ts
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {AcceleratorAction, ActionChoice, CustomizableButton, ExtendedFkeysModifier, GraphicsTablet, HardCodedAction, Keyboard, MetaKey, ModifierKey, Mouse, PointingStick, SimulateRightClickModifier, SixPackKeyInfo, SixPackShortcutModifier, Stylus, TopRowActionKey, Touchpad, Vkey} from './input_device_settings_types.js';
+import {AcceleratorAction, ActionChoice, CustomizableButton, ExtendedFkeysModifier, GraphicsTablet, Keyboard, MetaKey, ModifierKey, Mouse, PointingStick, SimulateRightClickModifier, SixPackKeyInfo, SixPackShortcutModifier, StaticShortcutAction, Stylus, TopRowActionKey, Touchpad, Vkey} from './input_device_settings_types.js';
 
 const defaultSixPackKeyRemappings: SixPackKeyInfo = {
   pageDown: SixPackShortcutModifier.kSearch,
@@ -322,7 +322,7 @@
             customizableButton: CustomizableButton.kBack,
           },
           remappingAction: {
-            hardcodedAction: HardCodedAction.kCopy,
+            staticShortcutAction: StaticShortcutAction.kCopy,
           },
         },
         {
@@ -331,7 +331,7 @@
             customizableButton: CustomizableButton.kForward,
           },
           remappingAction: {
-            action: AcceleratorAction.kCycleForwardMru,
+            acceleratorAction: AcceleratorAction.kCycleForwardMru,
           },
         },
         {
@@ -401,7 +401,7 @@
             customizableButton: CustomizableButton.kMiddle,
           },
           remappingAction: {
-            action: AcceleratorAction.kToggleClipboardHistory,
+            acceleratorAction: AcceleratorAction.kToggleClipboardHistory,
           },
         },
       ],
@@ -492,7 +492,7 @@
             vkey: Vkey.kNum0,
           },
           remappingAction: {
-            action: AcceleratorAction.kCycleBackwardMru,
+            acceleratorAction: AcceleratorAction.kCycleBackwardMru,
           },
         },
         {
@@ -501,7 +501,7 @@
             vkey: Vkey.kNum1,
           },
           remappingAction: {
-            action: AcceleratorAction.kCycleForwardMru,
+            acceleratorAction: AcceleratorAction.kCycleForwardMru,
           },
         },
       ],
@@ -551,7 +551,7 @@
             vkey: Vkey.kNum0,
           },
           remappingAction: {
-            action: AcceleratorAction.kBrightnessUp,
+            acceleratorAction: AcceleratorAction.kBrightnessUp,
           },
         },
         {
@@ -560,7 +560,7 @@
             vkey: Vkey.kNum1,
           },
           remappingAction: {
-            action: AcceleratorAction.kBrightnessDown,
+            acceleratorAction: AcceleratorAction.kBrightnessDown,
           },
         },
       ],
@@ -603,13 +603,13 @@
 export const fakeMouseButtonActions: ActionChoice[] = [
   {
     actionType: {
-      hardcodedAction: HardCodedAction.kCopy,
+      staticShortcutAction: StaticShortcutAction.kCopy,
     },
     name: 'Copy',
   },
   {
     actionType: {
-      hardcodedAction: HardCodedAction.kPaste,
+      staticShortcutAction: StaticShortcutAction.kPaste,
     },
     name: 'Paste',
   },
diff --git a/chrome/browser/resources/ash/settings/device_page/input_device_settings_types.ts b/chrome/browser/resources/ash/settings/device_page/input_device_settings_types.ts
index df7060b..431dd50 100644
--- a/chrome/browser/resources/ash/settings/device_page/input_device_settings_types.ts
+++ b/chrome/browser/resources/ash/settings/device_page/input_device_settings_types.ts
@@ -142,8 +142,10 @@
 export type CustomizableButton = InputDeviceSettingsTypes.CustomizableButton;
 export const CustomizableButton = InputDeviceSettingsTypes.CustomizableButton;
 
-export type HardCodedAction = InputDeviceSettingsTypes.HardCodedAction;
-export const HardCodedAction = InputDeviceSettingsTypes.HardCodedAction;
+export type StaticShortcutAction =
+    InputDeviceSettingsTypes.StaticShortcutAction;
+export const StaticShortcutAction =
+    InputDeviceSettingsTypes.StaticShortcutAction;
 
 export interface KeyboardObserverInterface {
   // Fired when the keyboard list is updated.
diff --git a/chrome/browser/resources/ash/settings/main_page_container/main_page_container.ts b/chrome/browser/resources/ash/settings/main_page_container/main_page_container.ts
index 4c8b5b6..4042c8b 100644
--- a/chrome/browser/resources/ash/settings/main_page_container/main_page_container.ts
+++ b/chrome/browser/resources/ash/settings/main_page_container/main_page_container.ts
@@ -217,14 +217,14 @@
     this.advancedTogglingInProgress_ = false;
   }
 
-  override ready() {
+  override ready(): void {
     super.ready();
 
     this.setAttribute('role', 'main');
     this.addEventListener('showing-subpage', this.onShowingSubpage);
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.currentRoute_ = Router.getInstance().currentRoute;
@@ -240,7 +240,7 @@
     });
   }
 
-  override currentRouteChanged(newRoute: Route, oldRoute?: Route) {
+  override currentRouteChanged(newRoute: Route, oldRoute?: Route): void {
     this.currentRoute_ = newRoute;
 
     if (isAdvancedRoute(newRoute)) {
@@ -261,7 +261,7 @@
     super.currentRouteChanged(newRoute, oldRoute);
   }
 
-  override containsRoute(_route: Route|undefined) {
+  override containsRoute(_route: Route|undefined): boolean {
     // All routes are contained under this element.
     return true;
   }
@@ -286,7 +286,7 @@
     return !this.isShowingSubpage_ && this.showEolIncentive_;
   }
 
-  private androidAppsInfoUpdate_(info: AndroidAppsInfo) {
+  private androidAppsInfoUpdate_(info: AndroidAppsInfo): void {
     this.androidAppsInfo = info;
   }
 
@@ -294,7 +294,7 @@
    * Hides the update required EOL banner. It is shown again when Settings is
    * re-opened.
    */
-  private onCloseEolBannerClicked_() {
+  private onCloseEolBannerClicked_(): void {
     this.showUpdateRequiredEolBanner_ = false;
   }
 
@@ -305,7 +305,7 @@
   /**
    * Render the advanced page now (don't wait for idle).
    */
-  private advancedToggleExpandedChanged_() {
+  private advancedToggleExpandedChanged_(): void {
     if (!this.advancedToggleExpanded) {
       return;
     }
@@ -317,7 +317,7 @@
     });
   }
 
-  private advancedToggleClicked_() {
+  private advancedToggleClicked_(): void {
     if (this.advancedTogglingInProgress_) {
       return;
     }
diff --git a/chrome/browser/resources/ash/settings/main_page_container/page_displayer.ts b/chrome/browser/resources/ash/settings/main_page_container/page_displayer.ts
index 19e226bd..daf1ae7 100644
--- a/chrome/browser/resources/ash/settings/main_page_container/page_displayer.ts
+++ b/chrome/browser/resources/ash/settings/main_page_container/page_displayer.ts
@@ -36,13 +36,13 @@
   active: boolean;
   section: Section;
 
-  override ready() {
+  override ready(): void {
     super.ready();
 
     assert(this.section in Section, `Invalid section: ${this.section}.`);
   }
 
-  override focus() {
+  override focus(): void {
     this.shadowRoot!.getElementById('focusHost')!.focus();
   }
 }
diff --git a/chrome/browser/resources/ash/settings/os_a11y_page/text_to_speech_subpage.ts b/chrome/browser/resources/ash/settings/os_a11y_page/text_to_speech_subpage.ts
index 3515f7e..d7d68335 100644
--- a/chrome/browser/resources/ash/settings/os_a11y_page/text_to_speech_subpage.ts
+++ b/chrome/browser/resources/ash/settings/os_a11y_page/text_to_speech_subpage.ts
@@ -36,7 +36,7 @@
  * and should always reflect it (do not change one without changing the other).
  */
 export enum PdfOcrUserSelection {
-  TURN_ON_ONCE_FROM_CONTEXT_MENU = 0,
+  DEPRECATED_TURN_ON_ONCE_FROM_CONTEXT_MENU = 0,
   TURN_ON_ALWAYS_FROM_CONTEXT_MENU = 1,
   TURN_OFF_FROM_CONTEXT_MENU = 2,
   TURN_ON_ALWAYS_FROM_MORE_ACTIONS = 3,
diff --git a/chrome/browser/resources/ash/settings/os_about_page/about_page_browser_proxy.ts b/chrome/browser/resources/ash/settings/os_about_page/about_page_browser_proxy.ts
index 062e2c1..2ffe7b80 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/about_page_browser_proxy.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/about_page_browser_proxy.ts
@@ -240,61 +240,61 @@
     return instance || (instance = new AboutPageBrowserProxyImpl());
   }
 
-  static setInstanceForTesting(obj: AboutPageBrowserProxy) {
+  static setInstanceForTesting(obj: AboutPageBrowserProxy): void {
     instance = obj;
   }
 
-  applyDeferredUpdate() {
+  applyDeferredUpdate(): void {
     chrome.send('applyDeferredUpdate');
   }
 
-  pageReady() {
+  pageReady(): void {
     chrome.send('aboutPageReady');
   }
 
-  refreshUpdateStatus() {
+  refreshUpdateStatus(): void {
     chrome.send('refreshUpdateStatus');
   }
 
-  launchReleaseNotes() {
+  launchReleaseNotes(): void {
     chrome.send('launchReleaseNotes');
   }
 
   // <if expr="_google_chrome">
-  openFeedbackDialog() {
+  openFeedbackDialog(): void {
     chrome.send('openFeedbackDialog');
   }
   // </if>
 
-  openDiagnostics() {
+  openDiagnostics(): void {
     chrome.send('openDiagnostics');
   }
 
-  openProductLicenseOther() {
+  openProductLicenseOther(): void {
     chrome.send('openProductLicenseOther');
   }
 
-  openOsHelpPage() {
+  openOsHelpPage(): void {
     chrome.send('openOsHelpPage');
   }
 
-  openFirmwareUpdatesPage() {
+  openFirmwareUpdatesPage(): void {
     chrome.send('openFirmwareUpdatesPage');
   }
 
-  getFirmwareUpdateCount() {
+  getFirmwareUpdateCount(): Promise<number> {
     return sendWithPromise('getFirmwareUpdateCount');
   }
 
-  requestUpdate() {
+  requestUpdate(): void {
     chrome.send('requestUpdate');
   }
 
-  requestUpdateOverCellular(targetVersion: string, targetSize: string) {
+  requestUpdateOverCellular(targetVersion: string, targetSize: string): void {
     chrome.send('requestUpdateOverCellular', [targetVersion, targetSize]);
   }
 
-  setChannel(channel: BrowserChannel, isPowerwashAllowed: boolean) {
+  setChannel(channel: BrowserChannel, isPowerwashAllowed: boolean): void {
     chrome.send('setChannel', [channel, isPowerwashAllowed]);
   }
 
diff --git a/chrome/browser/resources/ash/settings/os_about_page/channel_switcher_dialog.ts b/chrome/browser/resources/ash/settings/os_about_page/channel_switcher_dialog.ts
index 3aa83d96..037f2db 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/channel_switcher_dialog.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/channel_switcher_dialog.ts
@@ -88,7 +88,7 @@
     this.browserProxy_ = AboutPageBrowserProxyImpl.getInstance();
   }
 
-  override ready() {
+  override ready(): void {
     super.ready();
 
     this.browserProxy_.getChannelInfo().then(info => {
@@ -101,7 +101,7 @@
     });
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.$.dialog.showModal();
@@ -111,25 +111,25 @@
     return castExists(this.shadowRoot!.querySelector('cr-radio-group'));
   }
 
-  private onCancelClick_() {
+  private onCancelClick_(): void {
     this.$.dialog.close();
   }
 
-  private onChangeChannelClick_() {
+  private onChangeChannelClick_(): void {
     const selectedChannel = this.getRadioGroup_().selected as BrowserChannel;
     this.browserProxy_.setChannel(selectedChannel, false);
     this.$.dialog.close();
     this.fireTargetChannelChangedEvent_(selectedChannel);
   }
 
-  private onChangeChannelAndPowerwashClick_() {
+  private onChangeChannelAndPowerwashClick_(): void {
     const selectedChannel = this.getRadioGroup_().selected as BrowserChannel;
     this.browserProxy_.setChannel(selectedChannel, true);
     this.$.dialog.close();
     this.fireTargetChannelChangedEvent_(selectedChannel);
   }
 
-  private fireTargetChannelChangedEvent_(detail = {}) {
+  private fireTargetChannelChangedEvent_(detail = {}): void {
     const event = new CustomEvent(
         'target-channel-changed', {bubbles: true, composed: true, detail});
     this.dispatchEvent(event);
@@ -141,7 +141,7 @@
    *    button should be visible.
    */
   private updateButtons_(
-      changeChannel: boolean, changeChannelAndPowerwash: boolean) {
+      changeChannel: boolean, changeChannelAndPowerwash: boolean): void {
     if (changeChannel || changeChannelAndPowerwash) {
       // Ensure that at most one button is visible at any given time.
       assert(changeChannel !== changeChannelAndPowerwash);
@@ -153,7 +153,7 @@
     };
   }
 
-  private onChannelSelectionChanged_() {
+  private onChannelSelectionChanged_(): void {
     const selectedChannel = this.getRadioGroup_().selected as BrowserChannel;
 
     // Selected channel is the same as the target channel so only show 'cancel'.
diff --git a/chrome/browser/resources/ash/settings/os_about_page/consumer_auto_update_toggle_dialog.ts b/chrome/browser/resources/ash/settings/os_about_page/consumer_auto_update_toggle_dialog.ts
index 76c30ad..3bea84d 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/consumer_auto_update_toggle_dialog.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/consumer_auto_update_toggle_dialog.ts
@@ -24,13 +24,13 @@
     return getTemplate();
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.$.dialog.showModal();
   }
 
-  private onTurnOffClick_() {
+  private onTurnOffClick_(): void {
     this.dispatchEvent(new CustomEvent('set-consumer-auto-update', {
       bubbles: true,
       composed: true,
@@ -41,7 +41,7 @@
     this.$.dialog.close();
   }
 
-  private onKeepUpdatesClick_() {
+  private onKeepUpdatesClick_(): void {
     this.dispatchEvent(new CustomEvent('set-consumer-auto-update', {
       bubbles: true,
       composed: true,
diff --git a/chrome/browser/resources/ash/settings/os_about_page/detailed_build_info_subpage.ts b/chrome/browser/resources/ash/settings/os_about_page/detailed_build_info_subpage.ts
index 0ad320c..0d686efe 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/detailed_build_info_subpage.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/detailed_build_info_subpage.ts
@@ -182,7 +182,7 @@
     this.deviceNameBrowserProxy_ = DeviceNameBrowserProxyImpl.getInstance();
   }
 
-  override ready() {
+  override ready(): void {
     super.ready();
     this.aboutPageBrowserProxy_.pageReady();
 
@@ -215,7 +215,7 @@
     }
   }
 
-  override currentRouteChanged(route: Route, _oldRoute?: Route) {
+  override currentRouteChanged(route: Route, _oldRoute?: Route): void {
     // Does not apply to this page.
     if (route !== routes.ABOUT_DETAILED_BUILD_INFO) {
       return;
@@ -228,7 +228,7 @@
     return this.isManaged_ || !this.eolMessageWithMonthAndYear;
   }
 
-  private updateChannelInfo_() {
+  private updateChannelInfo_(): void {
     // canChangeChannel() call is expected to be low-latency, so fetch this
     // value by itself to ensure UI consistency (see https://crbug.com/848750).
     this.aboutPageBrowserProxy_.canChangeChannel().then(canChangeChannel => {
@@ -246,20 +246,20 @@
     });
   }
 
-  private syncManagedAutoUpdateToggle_() {
+  private syncManagedAutoUpdateToggle_(): void {
     this.aboutPageBrowserProxy_.isManagedAutoUpdateEnabled().then(
         isManagedAutoUpdateEnabled => {
           this.isManagedAutoUpdateEnabled_ = isManagedAutoUpdateEnabled;
         });
   }
 
-  private syncConsumerAutoUpdateToggle_() {
+  private syncConsumerAutoUpdateToggle_(): void {
     this.aboutPageBrowserProxy_.isConsumerAutoUpdateEnabled().then(enabled => {
       this.aboutPageBrowserProxy_.setConsumerAutoUpdate(enabled);
     });
   }
 
-  private updateDeviceNameMetadata_(data: DeviceNameMetadata) {
+  private updateDeviceNameMetadata_(data: DeviceNameMetadata): void {
     this.deviceNameMetadata_ = data;
   }
 
@@ -337,12 +337,12 @@
         CrPolicyIndicatorType.OWNER;
   }
 
-  private onChangeChannelClick_(e: Event) {
+  private onChangeChannelClick_(e: Event): void {
     e.preventDefault();
     this.showChannelSwitcherDialog_ = true;
   }
 
-  private onEditHostnameClick_(e: Event) {
+  private onEditHostnameClick_(e: Event): void {
     e.preventDefault();
     this.showEditHostnameDialog_ = true;
   }
@@ -351,7 +351,7 @@
     return !!this.versionInfo_ && !!this.channelInfo_;
   }
 
-  private onCopyBuildDetailsToClipBoardClick_() {
+  private onCopyBuildDetailsToClipBoardClick_(): void {
     const buildInfo: {[key: string]: string|boolean} = {
       'application_label': loadTimeData.getString('aboutBrowserVersion'),
       'platform': this.versionInfo_.osVersion,
@@ -373,14 +373,14 @@
     navigator.clipboard.writeText(entries.join('\n'));
   }
 
-  private onConsumerAutoUpdateToggled_(_event: Event) {
+  private onConsumerAutoUpdateToggled_(_event: Event): void {
     if (!this.isConsumerAutoUpdateTogglingAllowed_) {
       return;
     }
     this.showDialogOrFlushConsumerAutoUpdateToggle();
   }
 
-  private onConsumerAutoUpdateToggledSettingsBox_() {
+  private onConsumerAutoUpdateToggledSettingsBox_(): void {
     if (!this.isConsumerAutoUpdateTogglingAllowed_) {
       return;
     }
@@ -391,7 +391,7 @@
     this.showDialogOrFlushConsumerAutoUpdateToggle();
   }
 
-  private showDialogOrFlushConsumerAutoUpdateToggle() {
+  private showDialogOrFlushConsumerAutoUpdateToggle(): void {
     if (!this.getPref('settings.consumer_auto_update_toggle').value) {
       // Only show dialog when turning the toggle off.
       this.showConsumerAutoUpdateToggleDialog_ = true;
@@ -401,23 +401,23 @@
     this.aboutPageBrowserProxy_.setConsumerAutoUpdate(true);
   }
 
-  private onConsumerAutoUpdateToggleDialogClosed_() {
+  private onConsumerAutoUpdateToggleDialogClosed_(): void {
     this.showConsumerAutoUpdateToggleDialog_ = false;
   }
 
-  private onVisitBuildDetailsPageClick_(e: Event) {
+  private onVisitBuildDetailsPageClick_(e: Event): void {
     e.preventDefault();
     window.open('chrome://version');
   }
 
-  private onChannelSwitcherDialogClosed_() {
+  private onChannelSwitcherDialogClosed_(): void {
     this.showChannelSwitcherDialog_ = false;
     const button = castExists(this.shadowRoot!.querySelector('cr-button'));
     focusWithoutInk(button);
     this.updateChannelInfo_();
   }
 
-  private onEditHostnameDialogClosed_() {
+  private onEditHostnameDialogClosed_(): void {
     this.showEditHostnameDialog_ = false;
     const button = castExists(this.shadowRoot!.querySelector('cr-button'));
     focusWithoutInk(button);
diff --git a/chrome/browser/resources/ash/settings/os_about_page/edit_hostname_dialog.ts b/chrome/browser/resources/ash/settings/os_about_page/edit_hostname_dialog.ts
index 8654d1a..e075ac3 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/edit_hostname_dialog.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/edit_hostname_dialog.ts
@@ -92,7 +92,7 @@
         MAX_INPUT_LENGTH.toLocaleString());
   }
 
-  private onCancelClick_() {
+  private onCancelClick_(): void {
     this.$.dialog.close();
   }
 
@@ -101,7 +101,7 @@
    * Emojis and truncating it to MAX_INPUT_LENGTH. This method will be
    * recursively called until deviceName_ is fully sanitized.
    */
-  private onDeviceNameChanged_(_newValue: string, oldValue: string) {
+  private onDeviceNameChanged_(_newValue: string, oldValue: string): void {
     if (oldValue) {
       const sanitizedOldValue = oldValue.replace(EMOJI_REGEX_EXP, '');
       // If sanitizedOldValue.length > MAX_INPUT_LENGTH, the user attempted to
@@ -124,7 +124,7 @@
     }
   }
 
-  private onDoneClick_() {
+  private onDoneClick_(): void {
     this.deviceNameBrowserProxy_.attemptSetDeviceName(this.deviceName_)
         .then(result => {
           this.handleSetDeviceNameResponse_(result);
@@ -132,7 +132,7 @@
     this.$.dialog.close();
   }
 
-  private handleSetDeviceNameResponse_(result: SetDeviceNameResult) {
+  private handleSetDeviceNameResponse_(result: SetDeviceNameResult): void {
     if (result !== SetDeviceNameResult.UPDATE_SUCCESSFUL) {
       console.error('ERROR IN UPDATE', result);
     }
diff --git a/chrome/browser/resources/ash/settings/os_about_page/eol_offer_section.ts b/chrome/browser/resources/ash/settings/os_about_page/eol_offer_section.ts
index 84db61c..8befad84 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/eol_offer_section.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/eol_offer_section.ts
@@ -37,7 +37,7 @@
 
   shouldShowOfferText: boolean;
 
-  private onIncentiveButtonClick_() {
+  private onIncentiveButtonClick_(): void {
     AboutPageBrowserProxyImpl.getInstance().endOfLifeIncentiveButtonClicked();
   }
 
diff --git a/chrome/browser/resources/ash/settings/os_about_page/os_about_page.ts b/chrome/browser/resources/ash/settings/os_about_page/os_about_page.ts
index 65891cb1..366a159 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/os_about_page.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/os_about_page.ts
@@ -291,7 +291,7 @@
     this.aboutBrowserProxy_ = AboutPageBrowserProxyImpl.getInstance();
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.aboutBrowserProxy_.pageReady();
@@ -333,7 +333,7 @@
     }
   }
 
-  override ready() {
+  override ready(): void {
     super.ready();
 
     this.addFocusConfig(
@@ -358,7 +358,7 @@
     });
   }
 
-  private startListening_() {
+  private startListening_(): void {
     this.addWebUiListener(
         'update-status-changed', this.onUpdateStatusChanged_.bind(this));
     this.aboutBrowserProxy_.refreshUpdateStatus();
@@ -368,7 +368,7 @@
     this.aboutBrowserProxy_.refreshTpmFirmwareUpdateStatus();
   }
 
-  private onUpdateStatusChanged_(event: UpdateStatusChangedEvent) {
+  private onUpdateStatusChanged_(event: UpdateStatusChangedEvent): void {
     if (event.status === UpdateStatus.CHECKING) {
       this.hasCheckedForUpdates_ = true;
     } else if (event.status === UpdateStatus.NEED_PERMISSION_TO_UPDATE) {
@@ -379,13 +379,14 @@
     this.currentUpdateStatusEvent_ = event;
   }
 
-  private onLearnMoreClick_(event: Event) {
+  private onLearnMoreClick_(event: Event): void {
     // Stop the propagation of events, so that clicking on links inside
     // actionable items won't trigger action.
     event.stopPropagation();
   }
 
-  private onProductLicenseOtherClicked_(event: CustomEvent<{event: Event}>) {
+  private onProductLicenseOtherClicked_(event: CustomEvent<{event: Event}>):
+      void {
     // Prevent the default link click behavior
     event.detail.event.preventDefault();
 
@@ -393,30 +394,30 @@
     this.aboutBrowserProxy_.openProductLicenseOther();
   }
 
-  private onReleaseNotesClick_() {
+  private onReleaseNotesClick_(): void {
     this.aboutBrowserProxy_.launchReleaseNotes();
   }
 
-  private onHelpClick_() {
+  private onHelpClick_(): void {
     this.aboutBrowserProxy_.openOsHelpPage();
   }
 
-  private onDiagnosticsClick_() {
+  private onDiagnosticsClick_(): void {
     this.aboutBrowserProxy_.openDiagnostics();
     recordSettingChange(Setting.kDiagnostics);
   }
 
-  private onFirmwareUpdatesClick_() {
+  private onFirmwareUpdatesClick_(): void {
     this.aboutBrowserProxy_.openFirmwareUpdatesPage();
     recordSettingChange(Setting.kFirmwareUpdates);
   }
 
-  private onRelaunchClick_() {
+  private onRelaunchClick_(): void {
     recordSettingChange();
     LifetimeBrowserProxyImpl.getInstance().relaunch();
   }
 
-  private updateShowUpdateStatus_() {
+  private updateShowUpdateStatus_(): void {
     // Do not show the "updated" status or error states from a previous update
     // attempt if we haven't checked yet or the update warning dialog is shown
     // to user.
@@ -445,7 +446,7 @@
    * Hide the button container if all buttons are hidden, otherwise the
    * container displays an unwanted border (see separator class).
    */
-  private updateShowButtonContainer_() {
+  private updateShowButtonContainer_(): void {
     this.showButtonContainer_ = this.showRelaunch_ || this.showCheckUpdates_;
 
     // Check if we have yet to focus the check for update button.
@@ -460,7 +461,7 @@
     });
   }
 
-  private computeShowRelaunch_() {
+  private computeShowRelaunch_(): boolean {
     return this.checkStatus_(UpdateStatus.NEARLY_UPDATED);
   }
 
@@ -601,7 +602,7 @@
     return this.currentUpdateStatusEvent_.status === status;
   }
 
-  private onManagementPageClick_() {
+  private onManagementPageClick_(): void {
     window.open('chrome://management');
   }
 
@@ -609,7 +610,7 @@
     return !!this.currentUpdateStatusEvent_.powerwash;
   }
 
-  private onDetailedBuildInfoClick_() {
+  private onDetailedBuildInfoClick_(): void {
     Router.getInstance().navigateTo(routes.ABOUT_DETAILED_BUILD_INFO);
   }
 
@@ -621,18 +622,18 @@
     return '';
   }
 
-  private onCheckUpdatesClick_() {
+  private onCheckUpdatesClick_(): void {
     this.onUpdateStatusChanged_({status: UpdateStatus.CHECKING});
     this.aboutBrowserProxy_.requestUpdate();
     this.$.updateStatusMessageInner.focus();
   }
 
-  private onApplyDeferredUpdateClick_() {
+  private onApplyDeferredUpdateClick_(): void {
     this.aboutBrowserProxy_.applyDeferredUpdate();
     this.$.updateStatusMessageInner.focus();
   }
 
-  private onApplyAndSetAutoUpdateClick_() {
+  private onApplyAndSetAutoUpdateClick_(): void {
     this.aboutBrowserProxy_.setConsumerAutoUpdate(true);
     this.onApplyDeferredUpdateClick_();
   }
@@ -667,7 +668,7 @@
   /**
    * @param enabled True if Crostini is enabled.
    */
-  private handleCrostiniEnabledChanged_(enabled: boolean) {
+  private handleCrostiniEnabledChanged_(enabled: boolean): void {
     this.showCrostiniLicense_ = enabled && isCrostiniSupported();
   }
 
@@ -683,7 +684,7 @@
     return this.shouldShowSafetyInfo_() || this.shouldShowRegulatoryInfo_();
   }
 
-  private onUpdateWarningDialogClose_() {
+  private onUpdateWarningDialogClose_(): void {
     this.showUpdateWarningDialog_ = false;
     // Shows 'check for updates' button in case that the user cancels the
     // dialog and then intends to check for update again.
@@ -691,19 +692,19 @@
   }
 
   private onTpmFirmwareUpdateStatusChanged_(
-      event: TpmFirmwareUpdateStatusChangedEvent) {
+      event: TpmFirmwareUpdateStatusChangedEvent): void {
     this.showTPMFirmwareUpdateLineItem_ = event.updateAvailable;
   }
 
-  private onTpmFirmwareUpdateClick_() {
+  private onTpmFirmwareUpdateClick_(): void {
     this.showTPMFirmwareUpdateDialog_ = true;
   }
 
-  private onPowerwashDialogClose_() {
+  private onPowerwashDialogClose_(): void {
     this.showTPMFirmwareUpdateDialog_ = false;
   }
 
-  private onProductLogoClick_() {
+  private onProductLogoClick_(): void {
     this.$['product-logo'].animate(
         {
           transform: ['none', 'rotate(-10turn)'],
@@ -715,7 +716,7 @@
   }
 
   // <if expr="_google_chrome">
-  private onReportIssueClick_() {
+  private onReportIssueClick_(): void {
     this.aboutBrowserProxy_.openFeedbackDialog();
   }
 
diff --git a/chrome/browser/resources/ash/settings/os_about_page/update_warning_dialog.ts b/chrome/browser/resources/ash/settings/os_about_page/update_warning_dialog.ts
index 0d5a754..abb41fba 100644
--- a/chrome/browser/resources/ash/settings/os_about_page/update_warning_dialog.ts
+++ b/chrome/browser/resources/ash/settings/os_about_page/update_warning_dialog.ts
@@ -57,17 +57,17 @@
     this.browserProxy_ = AboutPageBrowserProxyImpl.getInstance();
   }
 
-  override connectedCallback() {
+  override connectedCallback(): void {
     super.connectedCallback();
 
     this.$.dialog.showModal();
   }
 
-  private onCancelClick_() {
+  private onCancelClick_(): void {
     this.$.dialog.close();
   }
 
-  private onContinueClick_() {
+  private onContinueClick_(): void {
     if (!this.updateInfo || !this.updateInfo.version || !this.updateInfo.size) {
       console.warn('ERROR: requestUpdateOverCellular arguments are undefined');
       return;
@@ -77,7 +77,7 @@
     this.$.dialog.close();
   }
 
-  private updateInfoChanged_() {
+  private updateInfoChanged_(): void {
     if (!this.updateInfo || this.updateInfo.size === undefined) {
       console.warn('ERROR: Update size is undefined');
       return;
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output.js
index 72ee792..c80d7846 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output.js
@@ -867,10 +867,8 @@
         continue;
       }
 
-      const parentRole = roleInfo.inherits || CustomRole.NO_ROLE;
       const rule = new AncestryOutputRule(
-          type, formatNode.role, parentRole, navigationType,
-          this.formatAsBraille);
+          type, formatNode.role, navigationType, this.formatAsBraille);
       if (!rule.defined) {
         continue;
       }
@@ -917,14 +915,11 @@
     }
 
     const rule = new OutputRule(type);
-    const parentRole =
-        (OutputRoleInfo[node.role] || {}).inherits || CustomRole.NO_ROLE;
     rule.output = outputTypes.OutputFormatType.SPEAK;
-    rule.populateRole(node.role, parentRole, rule.output);
+    rule.populateRole(node.role, rule.output);
     if (this.formatOptions_.braille) {
       // Overwrite rule by braille rule if exists.
-      if (rule.populateRole(
-              node.role, parentRole, outputTypes.OutputFormatType.BRAILLE)) {
+      if (rule.populateRole(node.role, outputTypes.OutputFormatType.BRAILLE)) {
         rule.output = outputTypes.OutputFormatType.BRAILLE;
       }
     }
diff --git a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_rules.js b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_rules.js
index e49f2f1d..a5c4320 100644
--- a/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_rules.js
+++ b/chrome/browser/resources/chromeos/accessibility/chromevox/background/output/output_rules.js
@@ -7,6 +7,7 @@
  */
 import {AbstractRole, ChromeVoxRole, CustomRole} from '../../common/role_type.js';
 
+import {OutputRoleInfo} from './output_role_info.js';
 import {OutputCustomEvent, OutputEventType, OutputFormatType, OutputNavigationType} from './output_types.js';
 
 const EventType = chrome.automation.EventType;
@@ -63,16 +64,17 @@
 
   /**
    * @param {ChromeVoxRole|undefined} role
-   * @param {ChromeVoxRole|undefined} parentRole
    * @param {!OutputFormatType|!OutputNavigationType|undefined} formatName
    * @return {boolean} true if the role was set, false otherwise.
    */
-  populateRole(role, parentRole, formatName) {
+  populateRole(role, formatName) {
     if (this.hasRule_(role, formatName) && role) {
       this.role_ = role;
       return true;
-    } else if (this.hasRule_(parentRole, formatName) && parentRole) {
-      this.role_ = parentRole;
+    } else if (
+        this.hasRule_(parent(role), formatName) &&
+        parent(role) !== CustomRole.NO_ROLE) {
+      this.role_ = parent(role);
       return true;
     }
     return false;
@@ -126,14 +128,14 @@
 export class AncestryOutputRule extends OutputRule {
   /**
    * @param {!OutputEventType} eventType
-   * @param {ChromeVoxRole|undefined} nodeRole
-   * @param {ChromeVoxRole|undefined} parentRole
+   * @param {ChromeVoxRole|undefined} role
    * @param {!OutputNavigationType|undefined} navigationType
    * @param {boolean} tryBraille
    */
-  constructor(eventType, nodeRole, parentRole, navigationType, tryBraille) {
+  constructor(eventType, role, navigationType, tryBraille) {
     super(eventType);
-    this.populateRole(nodeRole, parentRole, navigationType);
+
+    this.populateRole(role, navigationType);
     this.populateNavigation(navigationType);
     this.populateOutput(tryBraille);
   }
@@ -173,6 +175,14 @@
 }
 
 /**
+ * @param {ChromeVoxRole|undefined} role
+ * @return {!ChromeVoxRole}
+ */
+function parent(role) {
+  return OutputRoleInfo[role]?.inherits ?? CustomRole.NO_ROLE;
+}
+
+/**
  * An object that specifies the rules for outputting a certain role on a
  * specific event, based on the type of output.
  *
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_button.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_button.html
index 9796a098..539d5a6e 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_button.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_button.html
@@ -77,7 +77,11 @@
   }
 
   #tooltip::part(tooltip) {
-    box-shadow: var(--cr-elevation-1);
+    /*
+     * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+     * launched.
+     */
+    box-shadow: var(--cros-elevation-1-shadow, --cr-elevation-1);
     font: var(--cros-annotation-1-font);
     margin: 4px;
     padding: 4px 8px 4px 8px;
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_group.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_group.html
index 6c0e82d1..869df211 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_group.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_group.html
@@ -117,7 +117,11 @@
     top: calc(var(--emoji-group-heading-padding-top)
       + var(--emoji-group-heading-padding-bottom)
       + var(--emoji-group-heading-size));
-    box-shadow: var(--cr-elevation-1);
+    /*
+     * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+     * launched.
+     */
+    box-shadow: var(--cros-elevation-1-shadow, --cr-elevation-1);
     border-radius: 4px;
     overflow: hidden;
     /* Declare the z-index here to ensure this container is visible above
@@ -217,7 +221,11 @@
   }
 
   paper-tooltip::part(tooltip) {
-    box-shadow: var(--cr-elevation-1);
+    /*
+     * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+     * launched.
+     */
+    box-shadow: var(--cros-elevation-1-shadow, --cr-elevation-1);
     font-family: 'Roboto', sans-serif;
     font-size: 12px;
     margin: 4px;
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_image.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_image.html
index b1ac894..e7f2698 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_image.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_image.html
@@ -48,7 +48,11 @@
   background-color: var(--emoji-picker-container-color);
   border: 2px solid transparent;
   border-radius: 4px;
-  box-shadow: var(--cr-elevation-1);
+  /*
+   * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+   * launched.
+   */
+  box-shadow: var(--cros-elevation-1-shadow, --cr-elevation-1);
   color: var(--cros-text-color-secondary);
   cursor: pointer;
   font: var(--cros-button-2-font);
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
index 70bb31d..0550f80 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_picker.html
@@ -53,7 +53,7 @@
                                            var(--cros-nudge-background-color));
     --emoji-picker-nudge-icon-color: var(--cros-sys-on_primary,
                                      var(--cros-button-icon-color-primary));
-    --emoji-picker-search-field-clear-icon-color: var(--cros-sys-primary,
+    --emoji-picker-search-field-clear-icon-color: var(--cros-sys-secondary,
                                                   var(--cros-icon-color-primary));
     --emoji-picker-search-field-placeholder-color: var(--cros-sys-secondary,
                                                    var(--cros-text-color-disabled));
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_search.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_search.html
index e4cbe5e..c37359b 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_search.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_search.html
@@ -110,7 +110,11 @@
 
   #searchShadow {
     background-color: var(--emoji-picker-container-color);
-    box-shadow: var(--cr-elevation-2);
+    /*
+     * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+     * launched.
+     */
+    box-shadow: var(--cros-elevation-2-shadow, --cr-elevation-2);
     display: flex;
     margin-bottom: 7px;
     margin-inline-end: calc(0px - var(--emoji-picker-side-padding));
diff --git a/chrome/browser/resources/chromeos/emoji_picker/emoji_variants.html b/chrome/browser/resources/chromeos/emoji_picker/emoji_variants.html
index 44ff5c6..0952a7e 100644
--- a/chrome/browser/resources/chromeos/emoji_picker/emoji_variants.html
+++ b/chrome/browser/resources/chromeos/emoji_picker/emoji_variants.html
@@ -2,7 +2,11 @@
   :host {
     background: var(--emoji-picker-container-color);
     border-radius: 4px; /* TODO: might need to be 8 querying with UX */
-    box-shadow: var(--cr-elevation-2);
+    /*
+     * TODO(b/263055563): Remove --cr-elevation reference once jelly is fully
+     * launched.
+     */
+    box-shadow: var(--cros-elevation-2-shadow, --cr-elevation-2);
     display: grid;
     grid-template-columns: max-content max-content max-content;
     grid-template-rows: max-content max-content;
diff --git a/chrome/browser/resources/chromeos/login/debug/debug.js b/chrome/browser/resources/chromeos/login/debug/debug.js
index 7370dbd..0e2237aa 100644
--- a/chrome/browser/resources/chromeos/login/debug/debug.js
+++ b/chrome/browser/resources/chromeos/login/debug/debug.js
@@ -1608,52 +1608,40 @@
             screens: [
               {
                 screenID: 'screenID1',
-                icon: 'oobe-40:theme-choobe',
-                title: 'choobeThemeSelectionTitle',
-                subtitle: 'choobeThemeSelectionTitle',
-                synced: false,
+                icon: 'oobe-40:scroll-choobe',
+                title: 'choobeTouchpadScrollTitle',
+                subtitle: 'choobeTouchpadScrollSubtitleEnabled',
+                is_synced: true,
                 is_revisitable: true,
                 selected: false,
                 is_completed: false,
               },
               {
                 screenID: 'screenID2',
-                icon: 'oobe-40:scroll-choobe',
-                title: 'choobeThemeSelectionTitle',
-                subtitle: 'choobeThemeSelectionTitle',
-                synced: true,
+                icon: 'oobe-40:drive-pinning-choobe',
+                title: 'choobeDrivePinningTitle',
+                is_synced: false,
                 is_revisitable: false,
                 selected: false,
                 is_completed: false,
               },
               {
                 screenID: 'screenID3',
-                icon: 'oobe-40:scroll-choobe',
-                title: 'choobeThemeSelectionTitle',
-                synced: false,
+                icon: 'oobe-40:display-size-choobe',
+                title: 'choobeDisplaySizeTitle',
+                is_synced: false,
                 is_revisitable: false,
                 selected: false,
-                is_completed: true,
-              },
-              {
-                screenID: 'screenID4',
-                icon: 'oobe-40:scroll-choobe',
-                title: 'choobeThemeSelectionTitle',
-                subtitle: 'choobeThemeSelectionTitle',
-                synced: true,
-                is_revisitable: true,
-                selected: false,
                 is_completed: false,
               },
               {
-                screenID: 'screenID5',
-                icon: 'oobe-40:display-size-choobe',
+                screenID: 'screenID4',
+                icon: 'oobe-40:theme-choobe',
                 title: 'choobeThemeSelectionTitle',
-                subtitle: 'choobeThemeSelectionTitle',
-                synced: false,
-                is_revisitable: true,
+                is_synced: false,
+                is_revisitable: false,
                 selected: false,
-                is_completed: true,
+                is_completed: false,
               },
             ],
           },
diff --git a/chrome/browser/resources/pdf/constants.ts b/chrome/browser/resources/pdf/constants.ts
index c46356a..5538d016 100644
--- a/chrome/browser/resources/pdf/constants.ts
+++ b/chrome/browser/resources/pdf/constants.ts
@@ -80,7 +80,7 @@
  * and should always reflect it (do not change one without changing the other).
  */
 export enum PdfOcrUserSelection {
-  TURN_ON_ONCE_FROM_CONTEXT_MENU = 0,
+  DEPRECATED_TURN_ON_ONCE_FROM_CONTEXT_MENU = 0,
   TURN_ON_ALWAYS_FROM_CONTEXT_MENU = 1,
   TURN_OFF_FROM_CONTEXT_MENU = 2,
   TURN_ON_ALWAYS_FROM_MORE_ACTIONS = 3,
diff --git a/chrome/browser/resources/settings/autofill_page/autofill_page.html b/chrome/browser/resources/settings/autofill_page/autofill_page.html
index b454d11..10df5fc9e 100644
--- a/chrome/browser/resources/settings/autofill_page/autofill_page.html
+++ b/chrome/browser/resources/settings/autofill_page/autofill_page.html
@@ -15,6 +15,14 @@
             role-description="$i18n{subpageArrowRoleDescription}"
             external>
         </cr-link-row>
+        <template is="dom-if" if="[[isPlusAddressSettingEnabled_]]">
+          <cr-link-row id="plusAddressManagerButton"
+              label="$i18n{plusAddressSettings}"
+              on-click="onPlusAddressClick_"
+              role-description="$i18n{subpageArrowRoleDescription}"
+              external>
+          </cr-link-row>
+        </template>
         <cr-link-row id="paymentManagerButton"
             start-icon="settings20:credit-card" label="$i18n{creditCards}"
             on-click="onPaymentsClick_"
diff --git a/chrome/browser/resources/settings/autofill_page/autofill_page.ts b/chrome/browser/resources/settings/autofill_page/autofill_page.ts
index eba9426..67d8d508 100644
--- a/chrome/browser/resources/settings/autofill_page/autofill_page.ts
+++ b/chrome/browser/resources/settings/autofill_page/autofill_page.ts
@@ -16,9 +16,11 @@
 
 import {PrefsMixin} from 'chrome://resources/cr_components/settings_prefs/prefs_mixin.js';
 import {CrLinkRowElement} from 'chrome://resources/cr_elements/cr_link_row/cr_link_row.js';
+import {OpenWindowProxyImpl} from 'chrome://resources/js/open_window_proxy.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {BaseMixin} from '../base_mixin.js';
+import {loadTimeData} from '../i18n_setup.js';
 import {routes} from '../route.js';
 import {Router} from '../router.js';
 
@@ -61,6 +63,10 @@
           return map;
         },
       },
+      isPlusAddressSettingEnabled_: {
+        type: Boolean,
+        value: () => !!loadTimeData.getString('plusAddressManagementUrl'),
+      },
     };
   }
 
@@ -89,6 +95,11 @@
     PasswordManagerImpl.getInstance().showPasswordManager(
         PasswordManagerPage.PASSWORDS);
   }
+
+  private onPlusAddressClick_() {
+    OpenWindowProxyImpl.getInstance().openUrl(
+        loadTimeData.getString('plusAddressManagementUrl'));
+  }
 }
 
 declare global {
diff --git a/chrome/browser/resources/settings/autofill_page/credit_card_list_entry.html b/chrome/browser/resources/settings/autofill_page/credit_card_list_entry.html
index 5bdab0c5..f5e3e4e 100644
--- a/chrome/browser/resources/settings/autofill_page/credit_card_list_entry.html
+++ b/chrome/browser/resources/settings/autofill_page/credit_card_list_entry.html
@@ -81,7 +81,7 @@
           </template>
           <template is="dom-if" if="[[!showDots_(creditCard.metadata)]]">
             <cr-icon-button class="icon-external" id="remoteCreditCardLink"
-                title="$i18n{remoteCreditCardLinkLabel}" role="link"
+                title="$i18n{remotePaymentMethodsLinkLabel}" role="link"
                 on-click="onRemoteEditClick_"></cr-icon-button>
           </template>
         </div>
diff --git a/chrome/browser/resources/settings/autofill_page/iban_list_entry.html b/chrome/browser/resources/settings/autofill_page/iban_list_entry.html
index 0e89490..f84e880 100644
--- a/chrome/browser/resources/settings/autofill_page/iban_list_entry.html
+++ b/chrome/browser/resources/settings/autofill_page/iban_list_entry.html
@@ -28,9 +28,29 @@
     <div id="nickname" class="ellipses sub-label">[[iban.nickname]]</div>
   </div>
   <div role="cell" class="second-column">
-    <cr-icon-button class="icon-more-vert" id="ibanMenu"
-        title="[[getMoreActionsTitle_(iban)]]"
-        on-click="onDotsMenuClick_">
-    </cr-icon-button>
+    <div id="paymentsIndicator"
+        hidden$="[[!shouldShowGooglePaymentsIndicator_(iban.metadata)]]">
+<if expr="_google_chrome">
+        <picture id="paymentsIcon">
+          <source
+              srcset="chrome://theme/IDR_AUTOFILL_GOOGLE_PAY_DARK_SMALL"
+              media="(prefers-color-scheme: dark)">
+          <img alt="" src="chrome://theme/IDR_AUTOFILL_GOOGLE_PAY_SMALL">
+        </picture>
+</if>
+<if expr="not _google_chrome">
+        <span class="sub-label">$i18n{googlePayments}</span>
+</if>
+    </div>
+    <template is="dom-if" if="[[showDotsMenu_(iban.metadata)]]">
+      <cr-icon-button class="icon-more-vert" id="ibanMenu"
+          title="[[getMoreActionsTitle_(iban)]]" on-click="onDotsMenuClick_">
+      </cr-icon-button>
+    </template>
+    <template is="dom-if" if="[[!showDotsMenu_(iban.metadata)]]">
+      <cr-icon-button class="icon-external" id="remoteIbanLink"
+          title="$i18n{remotePaymentMethodsLinkLabel}" role="link"
+          on-click="onRemoteEditClick_"></cr-icon-button>
+    </template>
   </div>
 </div>
diff --git a/chrome/browser/resources/settings/autofill_page/iban_list_entry.ts b/chrome/browser/resources/settings/autofill_page/iban_list_entry.ts
index 8bc3558..238f765 100644
--- a/chrome/browser/resources/settings/autofill_page/iban_list_entry.ts
+++ b/chrome/browser/resources/settings/autofill_page/iban_list_entry.ts
@@ -7,13 +7,13 @@
  * page.
  */
 
+import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
 import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
 import '../i18n_setup.js';
 import '../settings_shared.css.js';
 import './passwords_shared.css.js';
 
 import {I18nMixin} from '//resources/cr_elements/i18n_mixin.js';
-import {CrButtonElement} from 'chrome://resources/cr_elements/cr_button/cr_button.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {getTemplate} from './iban_list_entry.html.js';
@@ -29,12 +29,6 @@
   }
 }
 
-export interface SettingsIbanListEntryElement {
-  $: {
-    ibanMenu: CrButtonElement,
-  };
-}
-
 const SettingsIbanListEntryElementBase = I18nMixin(PolymerElement);
 
 export class SettingsIbanListEntryElement extends
@@ -56,8 +50,22 @@
 
   iban: chrome.autofillPrivate.IbanEntry;
 
-  get dotsMenu(): HTMLElement {
-    return this.$.ibanMenu;
+  get dotsMenu(): HTMLElement|null {
+    return this.shadowRoot!.getElementById('ibanMenu');
+  }
+
+  /**
+   * The 3-dot menu should be shown if the IBAN is a local IBAN.
+   */
+  private showDotsMenu_(): boolean {
+    return !!this.iban.metadata!.isLocal;
+  }
+
+  /**
+   * The Google Payments icon should be shown if the IBAN is a server IBAN.
+   */
+  private shouldShowGooglePaymentsIndicator_(): boolean {
+    return !this.iban.metadata!.isLocal;
   }
 
   /**
@@ -69,7 +77,7 @@
       composed: true,
       detail: {
         iban: this.iban,
-        anchorElement: this.$.ibanMenu,
+        anchorElement: this.dotsMenu,
       },
     }));
   }
diff --git a/chrome/browser/resources/settings/autofill_page/payments_section.html b/chrome/browser/resources/settings/autofill_page/payments_section.html
index d48b9af..9bea43a 100644
--- a/chrome/browser/resources/settings/autofill_page/payments_section.html
+++ b/chrome/browser/resources/settings/autofill_page/payments_section.html
@@ -135,6 +135,7 @@
     class="list-frame payment-list-margin-start"
     credit-cards="[[creditCards]]" ibans="[[ibans]]"
     on-dots-iban-menu-click="onDotsIbanMenuClick_"
+    on-remote-iban-menu-click="onRemoteEditIbanMenuClick_"
     on-dots-card-menu-click="onCreditCardDotsMenuClick_"
     on-remote-card-menu-click="onRemoteEditCreditCardClick_"
     aria-label="$i18n{paymentsMethodsTableAriaLabel}">
diff --git a/chrome/browser/resources/settings/autofill_page/payments_section.ts b/chrome/browser/resources/settings/autofill_page/payments_section.ts
index f02503a4..c70ebbe 100644
--- a/chrome/browser/resources/settings/autofill_page/payments_section.ts
+++ b/chrome/browser/resources/settings/autofill_page/payments_section.ts
@@ -29,6 +29,7 @@
 import {I18nMixin} from 'chrome://resources/cr_elements/i18n_mixin.js';
 import {assert} from 'chrome://resources/js/assert_ts.js';
 import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
+import {OpenWindowProxyImpl} from 'chrome://resources/js/open_window_proxy.js';
 import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 
 import {loadTimeData} from '../i18n_setup.js';
@@ -376,7 +377,13 @@
 
   private onRemoteEditCreditCardClick_() {
     this.paymentsManager_.logServerCardLinkClicked();
-    window.open(loadTimeData.getString('manageCreditCardsUrl'));
+    OpenWindowProxyImpl.getInstance().openUrl(
+        loadTimeData.getString('managePaymentMethodsUrl'));
+  }
+
+  private onRemoteEditIbanMenuClick_() {
+    OpenWindowProxyImpl.getInstance().openUrl(
+        loadTimeData.getString('managePaymentMethodsUrl'));
   }
 
   private onLocalCreditCardRemoveConfirmationDialogClose_() {
diff --git a/chrome/browser/resources/side_panel/read_anything/app.html b/chrome/browser/resources/side_panel/read_anything/app.html
index 7e5526cb..5301d28 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.html
+++ b/chrome/browser/resources/side_panel/read_anything/app.html
@@ -72,13 +72,11 @@
       color: var(--google-grey-900);
     }
   }
-  /* TODO(crbug.com/1474951): These are placeholder colors. Use the UX colors
-     once they're finalized. Also, allow user selection for highlight color. */
   .current-read-highlight {
     background-color: var(--current-highlight-bg-color);
   }
   .previous-read-highlight {
-    color: var(--google-grey-600);
+    color: var(--previous-highlight-color);
     background-color: transparent;
   }
 </style>
diff --git a/chrome/browser/resources/side_panel/read_anything/app.ts b/chrome/browser/resources/side_panel/read_anything/app.ts
index 299c4e4..eedeb152 100644
--- a/chrome/browser/resources/side_panel/read_anything/app.ts
+++ b/chrome/browser/resources/side_panel/read_anything/app.ts
@@ -25,6 +25,13 @@
   visited: string;
 }
 
+interface UtteranceSettings {
+  lang: string;
+  volume: number;
+  pitch: number;
+  rate: number;
+}
+
 interface VoicesByLanguage {
   [lang: string]: SpeechSynthesisVoice[];
 }
@@ -170,6 +177,7 @@
   private utterancesToSpeak_: SpeechSynthesisUtterance[] = [];
   private currentUtteranceIndex_: number = 0;
   private previousHighlight_: HTMLElement|null;
+  private currentColorSuffix_: string;
 
   // If the WebUI toolbar should be shown. This happens when the WebUI feature
   // flag is enabled.
@@ -416,7 +424,7 @@
   }
 
   defaultVoice(): SpeechSynthesisVoice|undefined {
-    // TODO(crbug.com/1474951): Additional logic to find defaualt voice if there
+    // TODO(crbug.com/1474951): Additional logic to find default voice if there
     // isn't a voice marked as default
     return this.synth.getVoices().find(
         ({localService, default: isDefaultVoice}) =>
@@ -439,6 +447,23 @@
     this.voice = voice;
   }
 
+  previewSpeechSynthesisVoice(voice: SpeechSynthesisVoice) {
+    const defaultUtteranceSettings = this.defaultUtteranceSettings();
+
+    // TODO(crbug.com/1474951): Translate the utterance into the language of
+    // the voice being previewed, and remove the hard-coded string.
+    // TODO(crbug.com/1474951): Call this.synth.cancel() to interrupt reading
+    // and reset the play icon.
+    const utterance = new SpeechSynthesisUtterance('Hi. This is a preview');
+    utterance.voice = voice;
+    utterance.lang = defaultUtteranceSettings.lang;
+    utterance.volume = defaultUtteranceSettings.volume;
+    utterance.pitch = defaultUtteranceSettings.pitch;
+    utterance.rate = defaultUtteranceSettings.rate;
+
+    this.synth.speak(utterance);
+  }
+
   stopSpeech() {
     // TODO(crbug.com/1474951): When pausing, can we pause on the previous
     // word so that speech doesn't resume in the middle of the word?
@@ -596,9 +621,6 @@
   playCurrentMessage() {
     const message = this.utterancesToSpeak_[this.currentUtteranceIndex_];
 
-    // TODO(crbug.com/1474951): Use correct locale when speaking.
-    const languageCode = chrome.readingMode.speechSynthesisLanguageCode;
-    message.lang = languageCode;
     const voice = this.getSpeechSynthesisVoice();
     if (!voice) {
       // TODO(crbug.com/1474951): Handle when no voices are available.
@@ -607,20 +629,33 @@
 
     message.voice = voice;
 
-    // TODO(crbug.com/1474951): Ensure the correct default values are used.
-    message.volume = 1;
-    message.pitch = 1;
-
-    // TODO(crbug.com/1474951): Ensure rate change happens immediately, rather
-    // than on the next set of text.
-    // TODO(crbug.com/1474951): Ensure the rate is valid for the current speech
-    // engine.
-    message.rate = this.rate;
+    const utteranceSettings = this.defaultUtteranceSettings();
+    message.lang = utteranceSettings.lang;
+    message.volume = utteranceSettings.volume;
+    message.pitch = utteranceSettings.pitch;
+    message.rate = utteranceSettings.rate;
 
     this.speechStarted = true;
     this.synth.speak(message);
   }
 
+  private defaultUtteranceSettings(): UtteranceSettings {
+    // TODO(crbug.com/1474951): Use correct locale when speaking.
+    const lang = chrome.readingMode.speechSynthesisLanguageCode;
+
+    return {
+      lang,
+      // TODO(crbug.com/1474951): Ensure rate change happens immediately, rather
+      // than on the next set of text.
+      // TODO(crbug.com/1474951): Ensure the rate is valid for the current
+      // speech engine.
+      rate: this.rate,
+      // TODO(crbug.com/1474951): Ensure the correct default values are used.
+      volume: 1,
+      pitch: 1,
+    };
+  }
+
   // The following results in
   // <span>
   //   <span class="previous-read-highlight"> prefix text </span>
@@ -811,10 +846,10 @@
   }
 
   updateHighlight(show: boolean) {
-    // TODO(crbug.com/1474951): This is a placeholder color. Use the UX color
-    // once finalized.
+    const highlightBackground =
+        this.getCurrentHighlightColorVar(this.currentColorSuffix_);
     this.updateStyles({
-      '--current-highlight-bg-color': show ? 'var(--google-yellow-400)' :
+      '--current-highlight-bg-color': show ? highlightBackground :
                                              'transparent',
     });
   }
@@ -822,12 +857,17 @@
   // TODO(crbug.com/1465029): This method should be renamed to updateTheme()
   // and replace the one below once we've removed the Views toolbar.
   updateThemeFromWebUi(colorSuffix: string) {
+    this.currentColorSuffix_ = colorSuffix;
     const emptyStateBodyColor = colorSuffix ?
         this.getEmptyStateBodyColorFromWebUi_(colorSuffix) :
         'var(--color-side-panel-card-secondary-foreground)';
     this.updateStyles({
       '--background-color': this.getBackgroundColorVar(colorSuffix),
       '--foreground-color': this.getForegroundColorVar(colorSuffix),
+      '--current-highlight-bg-color':
+          this.getCurrentHighlightColorVar(colorSuffix),
+      '--previous-highlight-color':
+          this.getPreviousHighlightColorVar(colorSuffix),
       '--sp-empty-state-heading-color':
           `var(--color-read-anything-foreground${colorSuffix})`,
       '--sp-empty-state-body-color': emptyStateBodyColor,
@@ -839,6 +879,14 @@
     });
   }
 
+  getCurrentHighlightColorVar(colorSuffix: string) {
+    return `var(--color-current-read-aloud-highlight${colorSuffix})`;
+  }
+
+  getPreviousHighlightColorVar(colorSuffix: string) {
+    return `var(--color-previous-read-aloud-highlight${colorSuffix})`;
+  }
+
   getBackgroundColorVar(colorSuffix: string) {
     return `var(--color-read-anything-background${colorSuffix})`;
   }
diff --git a/chrome/browser/resources/side_panel/read_anything/icons.html b/chrome/browser/resources/side_panel/read_anything/icons.html
index 61fc269..83b2c64 100644
--- a/chrome/browser/resources/side_panel/read_anything/icons.html
+++ b/chrome/browser/resources/side_panel/read_anything/icons.html
@@ -106,6 +106,10 @@
       <g id="pause" viewBox="0 0 18 18">
         <path d="M6.41667 12.0417H8V5.95833H6.41667V12.0417ZM10 12.0417H11.5833V5.95833H10V12.0417ZM9 17.1667C7.875 17.1667 6.8125 16.9583 5.8125 16.5417C4.82639 16.1111 3.95833 15.5278 3.20833 14.7917C2.47222 14.0417 1.88889 13.1736 1.45833 12.1875C1.04167 11.1875 0.833333 10.125 0.833333 9C0.833333 7.86111 1.04167 6.79861 1.45833 5.8125C1.88889 4.82639 2.47222 3.96528 3.20833 3.22917C3.95833 2.47917 4.82639 1.89583 5.8125 1.47917C6.8125 1.04861 7.875 0.833333 9 0.833333C10.1389 0.833333 11.2014 1.04861 12.1875 1.47917C13.1736 1.89583 14.0347 2.47917 14.7708 3.22917C15.5208 3.96528 16.1042 4.83333 16.5208 5.83333C16.9514 6.81944 17.1667 7.875 17.1667 9C17.1667 10.125 16.9514 11.1875 16.5208 12.1875C16.1042 13.1736 15.5208 14.0417 14.7708 14.7917C14.0347 15.5278 13.1667 16.1111 12.1667 16.5417C11.1806 16.9583 10.125 17.1667 9 17.1667Z"></path>
       </g>
+      <g id="play-circle" viewBox="0 -960 960 960">
+        <path
+          d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"></path>
+      </g>
     </defs>
   </svg>
 </iron-iconset-svg>
diff --git a/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.html b/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.html
index 78bcd28..017ca68 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.html
+++ b/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.html
@@ -30,6 +30,18 @@
   .dropdown-item {
     display: block;
   }
+  .dropdown-voice-selection {
+    display: flex;
+    min-width: 312px;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .dropdown-voice-selection cr-icon-button {
+    margin: 0;
+  }
+  .dropdown-voice-selection p {
+    margin: 8px 0;
+  }
   /* TODO(b/1465029): Investigate why chrome refresh colors don't always
      work on first launch. */
   :host-context([chrome-refresh-2023]) cr-action-menu {
@@ -133,17 +145,20 @@
   </cr-action-menu>
   <cr-action-menu id="voiceSelectionMenu">
     <template is="dom-repeat" items="[[voiceSelectionOptions_]]">
-      <!--  TODO(crbug.com/1474951): add play icon for voice previews -->
       <!--  TODO(crbug.com/1474951): show a header for each locale group before
             listing the languages of the locale -->
       <!--  TODO(crbug.com/1474951): show currently selected language -->
       <!--  TODO(crbug.com/1474951): allow user to change language -->
-      <button class="dropdown-item" on-click="onVoiceSelectClick_">
-        [[item.title]]
+      <button class="dropdown-item dropdown-voice-selection"
+        on-click="onVoiceSelectClick_">
+        <p>[[item.title]]</p>
+        <!-- TODO(crbug.com/1474951): Show pause button when a preview is
+                      playing -->
+        <cr-icon-button on-click="onVoicePreviewClick_" class="button-image"
+          iron-icon="read-anything-20:play-circle"></cr-icon-button>
       </button>
     </template>
   </cr-action-menu>
-
   <cr-action-menu id="fontSizeMenu">
     <cr-icon-button
         class="font-size"
diff --git a/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.ts b/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.ts
index 5a6c4f1..53e32ea 100644
--- a/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.ts
+++ b/chrome/browser/resources/side_panel/read_anything/read_anything_toolbar.ts
@@ -33,10 +33,10 @@
   };
 }
 
-interface MenuStateItem {
+interface MenuStateItem<T> {
   title: string;
   icon: string;
-  data: any;
+  data: T;
   callback: () => void;
 }
 
@@ -149,7 +149,7 @@
   // If you change these fonts, please also update read_anything_constants.h
   private fontOptions_: string[] = [];
 
-  private letterSpacingOptions_: MenuStateItem[] = [
+  private letterSpacingOptions_: Array<MenuStateItem<number>> = [
     {
       title: loadTimeData.getString('letterSpacingStandardTitle'),
       icon: 'read-anything:letter-spacing-standard',
@@ -173,7 +173,7 @@
     },
   ];
 
-  private lineSpacingOptions_: MenuStateItem[] = [
+  private lineSpacingOptions_: Array<MenuStateItem<number>> = [
     {
       title: loadTimeData.getString('lineSpacingStandardTitle'),
       icon: 'read-anything:line-spacing-standard',
@@ -197,7 +197,7 @@
     },
   ];
 
-  private colorOptions_: MenuStateItem[] = [
+  private colorOptions_: Array<MenuStateItem<string>> = [
     {
       title: loadTimeData.getString('defaultColorTitle'),
       icon: 'read-anything:default-theme',
@@ -230,7 +230,8 @@
     },
   ];
 
-  private voiceSelectionOptions_: MenuStateItem[] = [];
+  private voiceSelectionOptions_: Array<MenuStateItem<SpeechSynthesisVoice>> =
+      [];
 
   private rateOptions_: number[] = [0.5, 0.8, 1, 1.2, 1.5, 2, 3, 4];
 
@@ -333,8 +334,8 @@
             parseFloat(chrome.readingMode.letterSpacing.toFixed(2))));
   }
 
-  private getIndexOfSetting_(menuArray: MenuStateItem[], dataToFind: any):
-      number {
+  private getIndexOfSetting_(
+      menuArray: Array<MenuStateItem<any>>, dataToFind: any): number {
     return menuArray.findIndex((item) => (item.data === dataToFind));
   }
 
@@ -415,23 +416,23 @@
     if (this.contentPage) {
       const voices = this.contentPage.getVoices();
       this.voiceSelectionOptions_ = Object.entries(voices).reduce(
-          (aggregateVoiceList: MenuStateItem[], [_, voiceListForLang]) => ([
-            ...aggregateVoiceList,
-            ...(voiceListForLang).map(speechSynthesisVoice => ({
-                                        title: speechSynthesisVoice.name,
-                                        icon: '',
-                                        data: speechSynthesisVoice,
-                                        callback: () => {},
-                                      })),
-          ]),
+          (aggregateVoiceList: Array<MenuStateItem<SpeechSynthesisVoice>>,
+           [_, voiceListForLang]) =>
+              ([
+                ...aggregateVoiceList,
+                ...(voiceListForLang).map(speechSynthesisVoice => ({
+                                            title: speechSynthesisVoice.name,
+                                            icon: '',
+                                            data: speechSynthesisVoice,
+                                            callback: () => {},
+                                          })),
+              ]),
           []);
 
       this.openMenu_(this.$.voiceSelectionMenu, event.target as HTMLElement);
     }
   }
 
-
-
   private onMoreOptionsClick_(event: MouseEvent) {
     this.openMenu_(this.$.moreOptionsMenu, event.target as HTMLElement);
   }
@@ -480,37 +481,50 @@
     }
   }
 
-  private onLetterSpacingClick_(event: DomRepeatEvent<MenuStateItem>) {
+  private onLetterSpacingClick_(event: DomRepeatEvent<MenuStateItem<number>>) {
     this.onTextStyleClick_(
         event, ReadAnythingSettingsChange.LETTER_SPACING_CHANGE,
         this.$.letterSpacingMenu,
         ReadAnythingElement.prototype.updateLetterSpacing);
   }
 
-  private onLineSpacingClick_(event: DomRepeatEvent<MenuStateItem>) {
+  private onLineSpacingClick_(event: DomRepeatEvent<MenuStateItem<number>>) {
     this.onTextStyleClick_(
         event, ReadAnythingSettingsChange.LINE_HEIGHT_CHANGE,
         this.$.lineSpacingMenu,
         ReadAnythingElement.prototype.updateLineSpacing);
   }
 
-  private onColorClick_(event: DomRepeatEvent<MenuStateItem>) {
+  private onColorClick_(event: DomRepeatEvent<MenuStateItem<string>>) {
     this.onTextStyleClick_(
         event, ReadAnythingSettingsChange.THEME_CHANGE, this.$.colorMenu,
         ReadAnythingElement.prototype.updateThemeFromWebUi);
   }
 
-  private onVoiceSelectClick_(event: DomRepeatEvent<MenuStateItem>) {
+  private onVoiceSelectClick_(
+      event: DomRepeatEvent<MenuStateItem<SpeechSynthesisVoice>>) {
     // TODO(crbug.com/1474951): Save voice to prefs.
     if (this.contentPage) {
-      this.contentPage.setSpeechSynthesisVoice(
+      this.contentPage.setSpeechSynthesisVoice(event.model.item.data);
+    }
+  }
+
+  private onVoicePreviewClick_(
+      event: DomRepeatEvent<MenuStateItem<SpeechSynthesisVoice>>) {
+    // Because the preview button is layered onto the voice-selection button,
+    // the onVoiceSelectClick_() listener is also subscribed to this event. This
+    // line is to make sure that the voice-selection callback is not triggered.
+    event.stopImmediatePropagation();
+
+    if (this.contentPage) {
+      this.contentPage.previewSpeechSynthesisVoice(
           event.model.item.data as SpeechSynthesisVoice);
     }
   }
 
   private onTextStyleClick_(
-      event: DomRepeatEvent<MenuStateItem>, logVal: ReadAnythingSettingsChange,
-      menuClicked: CrActionMenuElement,
+      event: DomRepeatEvent<MenuStateItem<any>>,
+      logVal: ReadAnythingSettingsChange, menuClicked: CrActionMenuElement,
       contentPageCallback: ((data: any) => void)) {
     event.model.item.callback();
     chrome.metricsPrivate.recordEnumerationValue(
diff --git a/chrome/browser/safe_browsing/download_protection/download_protection_service.cc b/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
index 7dd6bb6..904656ad 100644
--- a/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
+++ b/chrome/browser/safe_browsing/download_protection/download_protection_service.cc
@@ -10,6 +10,7 @@
 #include "base/command_line.h"
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_macros.h"
+#include "base/strings/string_split.h"
 #include "base/task/task_traits.h"
 #include "base/task/thread_pool.h"
 #include "chrome/browser/browser_process.h"
diff --git a/chrome/browser/search_engine_choice/search_engine_choice_browsertest.cc b/chrome/browser/search_engine_choice/search_engine_choice_browsertest.cc
index d2b4975..f5aa53a0 100644
--- a/chrome/browser/search_engine_choice/search_engine_choice_browsertest.cc
+++ b/chrome/browser/search_engine_choice/search_engine_choice_browsertest.cc
@@ -27,6 +27,7 @@
 #include "chrome/browser/web_applications/test/web_app_test_utils.h"
 #include "chrome/browser/web_applications/web_app_install_info.h"
 #include "chrome/browser/web_applications/web_app_provider.h"
+#include "chrome/common/webui_url_constants.h"
 #include "chrome/test/base/in_process_browser_test.h"
 #include "chrome/test/base/testing_browser_process.h"
 #include "chrome/test/base/ui_test_utils.h"
@@ -298,6 +299,13 @@
       ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
   EXPECT_FALSE(service->IsShowingDialog(browser()));
 
+  // Make sure that the dialog doesn't open on the devtools url
+  ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
+      browser(), GURL(chrome::kChromeUIDevToolsURL),
+      WindowOpenDisposition::CURRENT_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
+  EXPECT_FALSE(service->IsShowingDialog(browser()));
+
   // Dialog gets displayed when we navigate to chrome://new-tab-page.
   ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
       browser(), GURL(chrome::kChromeUINewTabPageURL),
diff --git a/chrome/browser/search_engine_choice/search_engine_choice_service.cc b/chrome/browser/search_engine_choice/search_engine_choice_service.cc
index 43daafd..bd53e50 100644
--- a/chrome/browser/search_engine_choice/search_engine_choice_service.cc
+++ b/chrome/browser/search_engine_choice/search_engine_choice_service.cc
@@ -208,6 +208,9 @@
   if (url == chrome::kChromeUINewTabPageURL || url == url::kAboutBlankURL) {
     return true;
   }
+  if (url.SchemeIs(content::kChromeDevToolsScheme)) {
+    return false;
+  }
   // Don't show the dialog over remaining urls that start with 'chrome://'.
   return !url.SchemeIs(content::kChromeUIScheme);
 }
diff --git a/chrome/browser/signin/bound_session_credentials/bound_session_refresh_cookie_fetcher_impl_unittest.cc b/chrome/browser/signin/bound_session_credentials/bound_session_refresh_cookie_fetcher_impl_unittest.cc
index 38527198..33c9ea9 100644
--- a/chrome/browser/signin/bound_session_credentials/bound_session_refresh_cookie_fetcher_impl_unittest.cc
+++ b/chrome/browser/signin/bound_session_credentials/bound_session_refresh_cookie_fetcher_impl_unittest.cc
@@ -10,6 +10,7 @@
 #include "base/base64url.h"
 #include "base/containers/span.h"
 #include "base/json/json_reader.h"
+#include "base/strings/string_split.h"
 #include "base/strings/stringprintf.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/task_environment.h"
diff --git a/chrome/browser/signin/signin_ui_util.cc b/chrome/browser/signin/signin_ui_util.cc
index 4aef5e1..fd3a181 100644
--- a/chrome/browser/signin/signin_ui_util.cc
+++ b/chrome/browser/signin/signin_ui_util.cc
@@ -13,6 +13,7 @@
 #include "base/metrics/user_metrics.h"
 #include "base/notreached.h"
 #include "base/strings/strcat.h"
+#include "base/strings/string_split.h"
 #include "base/strings/string_util.h"
 #include "base/strings/sys_string_conversions.h"
 #include "base/strings/utf_string_conversions.h"
diff --git a/chrome/browser/sync/chrome_sync_client.cc b/chrome/browser/sync/chrome_sync_client.cc
index f0a06ea..8c056c2 100644
--- a/chrome/browser/sync/chrome_sync_client.cc
+++ b/chrome/browser/sync/chrome_sync_client.cc
@@ -681,7 +681,6 @@
     case syncer::SECURITY_EVENTS:
     case syncer::SEND_TAB_TO_SELF:
     case syncer::SESSIONS:
-    case syncer::TYPED_URLS:
       NOTREACHED();
       return base::WeakPtr<syncer::ModelTypeControllerDelegate>();
 
diff --git a/chrome/browser/sync/sync_service_factory_unittest.cc b/chrome/browser/sync/sync_service_factory_unittest.cc
index 071277e3..21e36716 100644
--- a/chrome/browser/sync/sync_service_factory_unittest.cc
+++ b/chrome/browser/sync/sync_service_factory_unittest.cc
@@ -85,7 +85,7 @@
 
   // Returns the collection of default datatypes.
   syncer::ModelTypeSet DefaultDatatypes() {
-    static_assert(49 == syncer::GetNumModelTypes(),
+    static_assert(48 == syncer::GetNumModelTypes(),
                   "When adding a new type, you probably want to add it here as "
                   "well (assuming it is already enabled).");
 
diff --git a/chrome/browser/sync/test/integration/autofill_helper.cc b/chrome/browser/sync/test/integration/autofill_helper.cc
index 8deb1d5..6d4afaaa 100644
--- a/chrome/browser/sync/test/integration/autofill_helper.cc
+++ b/chrome/browser/sync/test/integration/autofill_helper.cc
@@ -9,6 +9,7 @@
 #include <map>
 #include <ostream>
 #include <sstream>
+#include <utility>
 
 #include "base/functional/bind.h"
 #include "base/run_loop.h"
@@ -24,6 +25,7 @@
 #include "components/autofill/core/browser/autofill_type.h"
 #include "components/autofill/core/browser/data_model/autofill_profile.h"
 #include "components/autofill/core/browser/personal_data_manager.h"
+#include "components/autofill/core/browser/personal_data_manager_test_utils.h"
 #include "components/autofill/core/browser/webdata/autofill_entry.h"
 #include "components/autofill/core/browser/webdata/autofill_table.h"
 #include "components/autofill/core/browser/webdata/autofill_webdata_service.h"
@@ -169,10 +171,6 @@
 
 namespace autofill_helper {
 
-ACTION_P(QuitMessageLoop, loop) {
-  loop->Quit();
-}
-
 AutofillProfile CreateAutofillProfile(ProfileType type) {
   AutofillProfile profile;
   switch (type) {
@@ -276,44 +274,22 @@
   return GetAllKeys(profile_a) == GetAllKeys(profile_b);
 }
 
-void SetProfiles(int profile, std::vector<AutofillProfile>* autofill_profiles) {
-  PersonalDataLoadedObserverMock personal_data_observer;
-  PersonalDataManager* pdm = GetPersonalDataManager(profile);
-  base::RunLoop run_loop;
-
-  pdm->AddObserver(&personal_data_observer);
-  EXPECT_CALL(personal_data_observer, OnPersonalDataFinishedProfileTasks())
-      .WillRepeatedly(QuitMessageLoop(&run_loop));
-  EXPECT_CALL(personal_data_observer, OnPersonalDataChanged())
-      .Times(testing::AnyNumber());
-
-  pdm->SetProfilesForAllSources(autofill_profiles);
-
-  run_loop.Run();
-  pdm->RemoveObserver(&personal_data_observer);
-}
-
 void SetCreditCards(int profile, std::vector<CreditCard>* credit_cards) {
   GetPersonalDataManager(profile)->SetCreditCards(credit_cards);
 }
 
 void AddProfile(int profile, const AutofillProfile& autofill_profile) {
-  std::vector<AutofillProfile> autofill_profiles;
-  for (AutofillProfile* p : GetAllAutoFillProfiles(profile)) {
-    autofill_profiles.push_back(*p);
-  }
-  autofill_profiles.push_back(autofill_profile);
-  autofill_helper::SetProfiles(profile, &autofill_profiles);
+  PersonalDataManager* pdm = GetPersonalDataManager(profile);
+  autofill::PersonalDataProfileTaskWaiter waiter(*pdm);
+  pdm->AddProfile(autofill_profile);
+  std::move(waiter).Wait();
 }
 
 void RemoveProfile(int profile, const std::string& guid) {
-  std::vector<AutofillProfile> autofill_profiles;
-  for (AutofillProfile* p : GetAllAutoFillProfiles(profile)) {
-    if (p->guid() != guid) {
-      autofill_profiles.push_back(*p);
-    }
-  }
-  autofill_helper::SetProfiles(profile, &autofill_profiles);
+  PersonalDataManager* pdm = GetPersonalDataManager(profile);
+  autofill::PersonalDataProfileTaskWaiter waiter(*pdm);
+  pdm->RemoveByGUID(guid);
+  std::move(waiter).Wait();
 }
 
 void UpdateProfile(int profile,
@@ -321,29 +297,24 @@
                    const AutofillType& type,
                    const std::u16string& value,
                    autofill::VerificationStatus status) {
-  std::vector<AutofillProfile> profiles;
-  for (AutofillProfile* p : GetAllAutoFillProfiles(profile)) {
-    profiles.push_back(*p);
-    if (p->guid() == guid) {
-      profiles.back().SetRawInfoWithVerificationStatus(type.GetStorableType(),
-                                                       value, status);
-    }
-  }
-  autofill_helper::SetProfiles(profile, &profiles);
+  PersonalDataManager* pdm = GetPersonalDataManager(profile);
+  AutofillProfile* pdm_profile = pdm->GetProfileByGUID(guid);
+  ASSERT_TRUE(pdm_profile);
+  // `pdm_profile` points to the PDM's internal copy of the data. It shouldn't
+  // be modified directly.
+  AutofillProfile updated_profile = *pdm_profile;
+  updated_profile.SetRawInfoWithVerificationStatus(type.GetStorableType(),
+                                                   value, status);
+  autofill::PersonalDataProfileTaskWaiter waiter(*pdm);
+  pdm->UpdateProfile(updated_profile);
+  std::move(waiter).Wait();
 }
 
 std::vector<AutofillProfile*> GetAllAutoFillProfiles(int profile) {
-  PersonalDataLoadedObserverMock personal_data_observer;
-  base::RunLoop run_loop;
-
   PersonalDataManager* pdm = GetPersonalDataManager(profile);
-  pdm->AddObserver(&personal_data_observer);
-
+  autofill::PersonalDataProfileTaskWaiter waiter(*pdm);
+  EXPECT_CALL(waiter.mock_observer(), OnPersonalDataChanged);
   pdm->Refresh();
-  EXPECT_CALL(personal_data_observer, OnPersonalDataFinishedProfileTasks())
-      .WillOnce(QuitMessageLoop(&run_loop));
-  EXPECT_CALL(personal_data_observer, OnPersonalDataChanged()).Times(1);
-
   // PersonalDataManager::GetProfiles() simply returns the current values that
   // have been last reported to the UI sequence. PersonalDataManager::Refresh()
   // will post a task to the DB sequence to read back the latest values, and we
@@ -359,9 +330,7 @@
   // cancel outstanding queries, this is only instigated on the UI sequence,
   // which we are about to block, which means we are safe.
   WaitForCurrentTasksToComplete(GetWebDataService(profile)->GetDBTaskRunner());
-  run_loop.Run();
-  pdm->RemoveObserver(&personal_data_observer);
-
+  std::move(waiter).Wait();
   return pdm->GetProfiles();
 }
 
@@ -419,38 +388,26 @@
 
 bool AutofillProfileChecker::Wait() {
   DLOG(WARNING) << "AutofillProfileChecker::Wait() started";
-  PersonalDataLoadedObserverMock personal_data_observer;
-  base::RunLoop run_loop_a;
-  base::RunLoop run_loop_b;
   PersonalDataManager* pdm_a =
       autofill_helper::GetPersonalDataManager(profile_a_);
   PersonalDataManager* pdm_b =
       autofill_helper::GetPersonalDataManager(profile_b_);
-  pdm_a->AddObserver(&personal_data_observer);
-  pdm_b->AddObserver(&personal_data_observer);
 
-  EXPECT_CALL(personal_data_observer, OnPersonalDataChanged())
-      .Times(testing::AnyNumber());
-
-  EXPECT_CALL(personal_data_observer, OnPersonalDataFinishedProfileTasks())
-      .WillRepeatedly(autofill_helper::QuitMessageLoop(&run_loop_a));
+  autofill::PersonalDataProfileTaskWaiter waiter_a(*pdm_a);
   pdm_a->Refresh();
   // Similar to GetAllAutoFillProfiles() we need to make sure we are not reading
   // before any locally instigated async writes. This is run exactly one time
   // before the first IsExitConditionSatisfied() is called.
   WaitForCurrentTasksToComplete(
       GetWebDataService(profile_a_)->GetDBTaskRunner());
-  run_loop_a.Run();
+  std::move(waiter_a).Wait();
 
-  EXPECT_CALL(personal_data_observer, OnPersonalDataFinishedProfileTasks())
-      .WillRepeatedly(autofill_helper::QuitMessageLoop(&run_loop_b));
+  autofill::PersonalDataProfileTaskWaiter waiter_b(*pdm_b);
   pdm_b->Refresh();
   WaitForCurrentTasksToComplete(
       GetWebDataService(profile_b_)->GetDBTaskRunner());
-  run_loop_b.Run();
+  std::move(waiter_b).Wait();
 
-  pdm_a->RemoveObserver(&personal_data_observer);
-  pdm_b->RemoveObserver(&personal_data_observer);
   DLOG(WARNING) << "AutofillProfileChecker::Wait() completed";
   return StatusChangeChecker::Wait();
 }
@@ -468,6 +425,3 @@
 void AutofillProfileChecker::OnPersonalDataChanged() {
   CheckExitCondition();
 }
-
-PersonalDataLoadedObserverMock::PersonalDataLoadedObserverMock() = default;
-PersonalDataLoadedObserverMock::~PersonalDataLoadedObserverMock() = default;
diff --git a/chrome/browser/sync/test/integration/autofill_helper.h b/chrome/browser/sync/test/integration/autofill_helper.h
index e0a76b3..507abfe 100644
--- a/chrome/browser/sync/test/integration/autofill_helper.h
+++ b/chrome/browser/sync/test/integration/autofill_helper.h
@@ -54,11 +54,6 @@
 // |profile_a| and |profile_b|. Returns true if they match.
 [[nodiscard]] bool KeysMatch(int profile_a, int profile_b);
 
-// Replaces the Autofill profiles in sync profile |profile| with
-// |autofill_profiles|.
-void SetProfiles(int profile,
-                 std::vector<autofill::AutofillProfile>* autofill_profiles);
-
 // Replaces the CreditCard profiles in sync profile |profile| with
 // |credit_cards|.
 void SetCreditCards(int profile,
@@ -139,14 +134,4 @@
   const absl::optional<unsigned int> expected_count_;
 };
 
-class PersonalDataLoadedObserverMock
-    : public autofill::PersonalDataManagerObserver {
- public:
-  PersonalDataLoadedObserverMock();
-  ~PersonalDataLoadedObserverMock() override;
-
-  MOCK_METHOD(void, OnPersonalDataChanged, (), (override));
-  MOCK_METHOD(void, OnPersonalDataFinishedProfileTasks, (), (override));
-};
-
 #endif  // CHROME_BROWSER_SYNC_TEST_INTEGRATION_AUTOFILL_HELPER_H_
diff --git a/chrome/browser/sync/test/integration/password_manager_sync_test.cc b/chrome/browser/sync/test/integration/password_manager_sync_test.cc
index a45dd17..49dc23e7 100644
--- a/chrome/browser/sync/test/integration/password_manager_sync_test.cc
+++ b/chrome/browser/sync/test/integration/password_manager_sync_test.cc
@@ -41,7 +41,6 @@
 #include "components/password_manager/core/browser/features/password_manager_features_util.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_test_utils.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
 #include "components/password_manager/core/browser/password_store_interface.h"
 #include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
@@ -1000,8 +999,9 @@
                 GetSyncService(0),
                 IdentityManagerFactory::GetForProfile(GetProfile(0))),
             kExpectedUsername);
-  EXPECT_EQ(password_manager_util::GetPasswordSyncState(GetSyncService(0)),
-            password_manager::SyncState::kSyncingNormalEncryption);
+  EXPECT_EQ(
+      password_manager::sync_util::GetPasswordSyncState(GetSyncService(0)),
+      password_manager::SyncState::kSyncingNormalEncryption);
 
   // Enter a persistent auth error state.
   GetClient(0)->EnterSyncPausedStateForPrimaryAccount();
@@ -1010,8 +1010,9 @@
   // auth error).
   EXPECT_FALSE(
       password_manager::sync_util::IsPasswordSyncActive(GetSyncService(0)));
-  EXPECT_EQ(password_manager_util::GetPasswordSyncState(GetSyncService(0)),
-            password_manager::SyncState::kNotSyncing);
+  EXPECT_EQ(
+      password_manager::sync_util::GetPasswordSyncState(GetSyncService(0)),
+      password_manager::SyncState::kNotSyncing);
 
   // In the current implementation, the APIs below treat sync as enabled/active
   // even while paused.
diff --git a/chrome/browser/sync/test/integration/performance/autofill_sync_perf_test.cc b/chrome/browser/sync/test/integration/performance/autofill_sync_perf_test.cc
index e51c4c9..37cc9fa 100644
--- a/chrome/browser/sync/test/integration/performance/autofill_sync_perf_test.cc
+++ b/chrome/browser/sync/test/integration/performance/autofill_sync_perf_test.cc
@@ -22,11 +22,13 @@
 
 using autofill::AutofillKey;
 using autofill::AutofillProfile;
+using autofill_helper::AddProfile;
 using autofill_helper::GetAllAutoFillProfiles;
 using autofill_helper::GetKeyCount;
 using autofill_helper::GetProfileCount;
 using autofill_helper::RemoveKeys;
-using autofill_helper::SetProfiles;
+using autofill_helper::RemoveProfile;
+using autofill_helper::UpdateProfile;
 using sync_timing_helper::TimeMutualSyncCycle;
 
 // These numbers should be as far away from a multiple of
@@ -116,33 +118,36 @@
 };
 
 void AutofillProfileSyncPerfTest::AddProfiles(int profile, int num_profiles) {
-  const std::vector<AutofillProfile*>& all_profiles =
-      GetAllAutoFillProfiles(profile);
-  std::vector<AutofillProfile> autofill_profiles;
-  for (AutofillProfile* autofill_profile : all_profiles) {
-    autofill_profiles.push_back(*autofill_profile);
-  }
   for (int i = 0; i < num_profiles; ++i) {
-    autofill_profiles.push_back(NextAutofillProfile());
+    AddProfile(profile, NextAutofillProfile());
   }
-  SetProfiles(profile, &autofill_profiles);
 }
 
 void AutofillProfileSyncPerfTest::UpdateProfiles(int profile) {
-  const std::vector<AutofillProfile*>& all_profiles =
-      GetAllAutoFillProfiles(profile);
-  std::vector<AutofillProfile> autofill_profiles;
-  for (AutofillProfile* autofill_profile : all_profiles) {
-    autofill_profiles.push_back(*autofill_profile);
-    autofill_profiles.back().SetRawInfo(autofill::NAME_FIRST,
-                                        base::UTF8ToUTF16(NextName()));
+  // Since `UpdateProfile()` invalidates the pointers returned by
+  // `GetAllAutoFillProfiles()`, collect the `guids` first.
+  std::vector<std::string> guids;
+  for (const AutofillProfile* autofill_profile :
+       GetAllAutoFillProfiles(profile)) {
+    guids.push_back(autofill_profile->guid());
   }
-  SetProfiles(profile, &autofill_profiles);
+  for (const std::string& guid : guids) {
+    UpdateProfile(profile, guid, autofill::AutofillType(autofill::NAME_FIRST),
+                  base::UTF8ToUTF16(NextName()));
+  }
 }
 
 void AutofillProfileSyncPerfTest::RemoveProfiles(int profile) {
-  std::vector<AutofillProfile> empty;
-  SetProfiles(profile, &empty);
+  // Since `RemoveProfile()` invalidates the pointers returned by
+  // `GetAllAutoFillProfiles()`, collect the `guids` first.
+  std::vector<std::string> guids;
+  for (const AutofillProfile* autofill_profile :
+       GetAllAutoFillProfiles(profile)) {
+    guids.push_back(autofill_profile->guid());
+  }
+  for (const std::string& guid : guids) {
+    RemoveProfile(profile, guid);
+  }
 }
 
 const AutofillProfile AutofillProfileSyncPerfTest::NextAutofillProfile() {
diff --git a/chrome/browser/sync/test/integration/single_client_offer_sync_test.cc b/chrome/browser/sync/test/integration/single_client_offer_sync_test.cc
index f3a45a1..35b86ba6 100644
--- a/chrome/browser/sync/test/integration/single_client_offer_sync_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_offer_sync_test.cc
@@ -13,6 +13,7 @@
 #include "components/autofill/core/browser/data_model/autofill_offer_data.h"
 #include "components/autofill/core/browser/personal_data_manager.h"
 #include "components/autofill/core/browser/personal_data_manager_observer.h"
+#include "components/autofill/core/browser/personal_data_manager_test_utils.h"
 #include "components/sync/base/model_type.h"
 #include "components/sync/protocol/model_type_state.pb.h"
 #include "components/sync/service/sync_service.h"
@@ -48,7 +49,8 @@
 
  protected:
   void WaitForOnPersonalDataChanged(autofill::PersonalDataManager* pdm) {
-    testing::NiceMock<PersonalDataLoadedObserverMock> personal_data_observer;
+    testing::NiceMock<autofill::PersonalDataLoadedObserverMock>
+        personal_data_observer;
     pdm->AddObserver(&personal_data_observer);
     base::RunLoop run_loop;
     EXPECT_CALL(personal_data_observer, OnPersonalDataChanged())
diff --git a/chrome/browser/sync/test/integration/single_client_wallet_sync_test.cc b/chrome/browser/sync/test/integration/single_client_wallet_sync_test.cc
index 3ecb1d6..783e7cd 100644
--- a/chrome/browser/sync/test/integration/single_client_wallet_sync_test.cc
+++ b/chrome/browser/sync/test/integration/single_client_wallet_sync_test.cc
@@ -29,6 +29,7 @@
 #include "components/autofill/core/browser/payments/payments_customer_data.h"
 #include "components/autofill/core/browser/personal_data_manager.h"
 #include "components/autofill/core/browser/personal_data_manager_observer.h"
+#include "components/autofill/core/browser/personal_data_manager_test_utils.h"
 #include "components/autofill/core/browser/test_autofill_clock.h"
 #include "components/autofill/core/browser/webdata/autofill_sync_bridge_util.h"
 #include "components/autofill/core/common/autofill_switches.h"
@@ -241,7 +242,8 @@
 
   void AdvanceAutofillClockByOneDay() { test_clock_.Advance(base::Days(1)); }
 
-  testing::NiceMock<PersonalDataLoadedObserverMock> personal_data_observer_;
+  testing::NiceMock<autofill::PersonalDataLoadedObserverMock>
+      personal_data_observer_;
   base::HistogramTester histogram_tester_;
   autofill::TestAutofillClock test_clock_;
 };
diff --git a/chrome/browser/sync/test/integration/sync_test.cc b/chrome/browser/sync/test/integration/sync_test.cc
index 60ef75fc..b04c768 100644
--- a/chrome/browser/sync/test/integration/sync_test.cc
+++ b/chrome/browser/sync/test/integration/sync_test.cc
@@ -1119,7 +1119,7 @@
 }
 
 syncer::ModelTypeSet AllowedTypesInStandaloneTransportMode() {
-  static_assert(49 == syncer::GetNumModelTypes(),
+  static_assert(48 == syncer::GetNumModelTypes(),
                 "Add new types below if they can run in transport mode");
   // Only some types will run by default in transport mode (i.e. without their
   // own separate opt-in).
diff --git a/chrome/browser/tpcd/experiment/eligibility_service.cc b/chrome/browser/tpcd/experiment/eligibility_service.cc
index 10f1b10..eadcfd4 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service.cc
+++ b/chrome/browser/tpcd/experiment/eligibility_service.cc
@@ -54,11 +54,11 @@
 
   experiment_manager_->SetClientEligibility(
       IsProfileEligible(),
-      base::BindOnce(&EligibilityService::OnClientEligibilityChanged,
+      base::BindOnce(&EligibilityService::MarkProfileEligibility,
                      weak_factory_.GetWeakPtr()));
 }
 
-void EligibilityService::OnClientEligibilityChanged(bool is_eligible) {
+void EligibilityService::MarkProfileEligibility(bool is_eligible) {
   // For each storage partition, update the cookie deprecation label to the
   // updated value from the CookieDeprecationLabelManager.
   profile_->ForEachLoadedStoragePartition(
@@ -70,10 +70,6 @@
         }
       }));
 
-  MarkProfileEligibility(is_eligible);
-}
-
-void EligibilityService::MarkProfileEligibility(bool is_eligible) {
   // Update the eligibility for the onboarding UX flow. Check that the user is
   // in Mode B (kDisable3PCookies is true).
   if (onboarding_service_ && kDisable3PCookies.Get()) {
diff --git a/chrome/browser/tpcd/experiment/eligibility_service.h b/chrome/browser/tpcd/experiment/eligibility_service.h
index fa049da..6847d16 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service.h
+++ b/chrome/browser/tpcd/experiment/eligibility_service.h
@@ -35,9 +35,6 @@
   friend class EligibilityServiceFactory;
   friend class EligibilityServiceBrowserTest;
 
-  // OnClientEligibilityChanged should only be called for currently loaded
-  // profiles when the ExperimentManager computes a client eligibility.
-  void OnClientEligibilityChanged(bool is_eligible);
   // MarkProfileEligibility should be called for all profiles to set their
   // eligibility, whether currently loaded or created later.
   void MarkProfileEligibility(bool is_eligible);
diff --git a/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc b/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
index 9494946..ef52281 100644
--- a/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
+++ b/chrome/browser/tpcd/experiment/eligibility_service_browsertest.cc
@@ -76,11 +76,11 @@
         ->FlushNetworkInterfaceForTesting();
   }
 
-  void FireOnClientEligibilityChanged(bool is_eligible) {
+  void MarkProfileEligibility(bool is_eligible) {
     auto* eligibility_service =
         tpcd::experiment::EligibilityServiceFactory::GetForProfile(
             browser()->profile());
-    eligibility_service->OnClientEligibilityChanged(is_eligible);
+    eligibility_service->MarkProfileEligibility(is_eligible);
   }
 
   net::EmbeddedTestServer https_server_{
@@ -122,7 +122,7 @@
 
   ASSERT_FALSE(privacy_sandbox_settings->IsCookieDeprecationLabelAllowed());
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/false);
+  MarkProfileEligibility(/*is_eligible=*/false);
 
   // Ensures the cookie deprecation label is updated in the network context.
   FlushNetworkInterface();
@@ -157,7 +157,7 @@
 
   ASSERT_TRUE(privacy_sandbox_settings->IsCookieDeprecationLabelAllowed());
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/true);
+  MarkProfileEligibility(/*is_eligible=*/true);
 
   // Ensures the cookie deprecation label is updated in the network context.
   FlushNetworkInterface();
@@ -185,13 +185,13 @@
                                : privacy_sandbox::TrackingProtectionOnboarding::
                                      OnboardingStatus::kIneligible);
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/false);
+  MarkProfileEligibility(/*is_eligible=*/false);
 
   EXPECT_EQ(onboarding_service->GetOnboardingStatus(),
             privacy_sandbox::TrackingProtectionOnboarding::OnboardingStatus::
                 kIneligible);
 
-  FireOnClientEligibilityChanged(/*is_eligible=*/true);
+  MarkProfileEligibility(/*is_eligible=*/true);
 
   // Onboarding should be marked eligible for the cohort where 3PCD is enabled,
   // but not when 3PCD is disabled.
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 0c6546c..328c818 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -1520,6 +1520,10 @@
       "toolbar/media_router_action_controller.h",
       "toolbar/media_router_contextual_menu.cc",
       "toolbar/media_router_contextual_menu.h",
+      "toolbar/pinned_toolbar_actions_model.cc",
+      "toolbar/pinned_toolbar_actions_model.h",
+      "toolbar/pinned_toolbar_actions_model_factory.cc",
+      "toolbar/pinned_toolbar_actions_model_factory.h",
       "toolbar/reading_list_sub_menu_model.cc",
       "toolbar/reading_list_sub_menu_model.h",
       "toolbar/recent_tabs_sub_menu_model.cc",
@@ -1996,6 +2000,7 @@
       "//third_party/blink/public/common:headers",
       "//third_party/libaddressinput",
       "//third_party/libaddressinput:strings",
+      "//ui/actions:actions_headers",
       "//ui/base/dragdrop:types",
       "//ui/base/dragdrop/mojom",
       "//ui/color:color_provider_key",
diff --git a/chrome/browser/ui/android/device_lock/OWNERS b/chrome/browser/ui/android/device_lock/OWNERS
index 0fa757c..129c4d9 100644
--- a/chrome/browser/ui/android/device_lock/OWNERS
+++ b/chrome/browser/ui/android/device_lock/OWNERS
@@ -1 +1,2 @@
+clhager@google.com
 twellington@chromium.org
diff --git a/chrome/browser/ui/android/toolbar/BUILD.gn b/chrome/browser/ui/android/toolbar/BUILD.gn
index a679768..c0f14b85 100644
--- a/chrome/browser/ui/android/toolbar/BUILD.gn
+++ b/chrome/browser/ui/android/toolbar/BUILD.gn
@@ -49,7 +49,7 @@
     "java/src/org/chromium/chrome/browser/toolbar/adaptive/OptionalNewTabButtonController.java",
     "java/src/org/chromium/chrome/browser/toolbar/adaptive/TranslateToolbarButtonController.java",
     "java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarHeaderPreference.java",
-    "java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragment.java",
+    "java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarSettingsFragment.java",
     "java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/RadioButtonGroupAdaptiveToolbarPreference.java",
     "java/src/org/chromium/chrome/browser/toolbar/bottom/BottomControlsContentDelegate.java",
     "java/src/org/chromium/chrome/browser/toolbar/bottom/BottomControlsCoordinator.java",
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonController.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonController.java
index 820047a..d2681b49 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonController.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonController.java
@@ -34,7 +34,7 @@
 import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
 import org.chromium.chrome.browser.toolbar.ButtonDataProvider.ButtonDataObserver;
 import org.chromium.chrome.browser.toolbar.R;
-import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarPreferenceFragment;
+import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarSettingsFragment;
 import org.chromium.components.browser_ui.settings.SettingsLauncher;
 import org.chromium.content_public.browser.NavigationHandle;
 import org.chromium.ui.permissions.AndroidPermissionDelegate;
@@ -102,7 +102,7 @@
             if (id == R.id.customize_adaptive_button_menu_id) {
                 RecordUserAction.record("MobileAdaptiveMenuCustomize");
                 settingsLauncher.launchSettingsActivity(
-                        context, AdaptiveToolbarPreferenceFragment.class);
+                        context, AdaptiveToolbarSettingsFragment.class);
                 return;
             }
             assert false : "unknown adaptive button menu id: " + id;
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonControllerTest.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonControllerTest.java
index 45541a5..0814213 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonControllerTest.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/AdaptiveToolbarButtonControllerTest.java
@@ -51,7 +51,7 @@
 import org.chromium.chrome.browser.toolbar.ButtonDataProvider;
 import org.chromium.chrome.browser.toolbar.ButtonDataProvider.ButtonDataObserver;
 import org.chromium.chrome.browser.toolbar.R;
-import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarPreferenceFragment;
+import org.chromium.chrome.browser.toolbar.adaptive.settings.AdaptiveToolbarSettingsFragment;
 import org.chromium.chrome.test.util.browser.Features;
 import org.chromium.chrome.test.util.browser.Features.DisableFeatures;
 import org.chromium.chrome.test.util.browser.Features.EnableFeatures;
@@ -243,7 +243,7 @@
         adaptiveToolbarButtonController.destroy();
 
         verify(settingsLauncher)
-                .launchSettingsActivity(activity, AdaptiveToolbarPreferenceFragment.class);
+                .launchSettingsActivity(activity, AdaptiveToolbarSettingsFragment.class);
     }
 
     @Test
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragmentTest.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragmentTest.java
index 705abc3..01672b0 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragmentTest.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragmentTest.java
@@ -42,7 +42,7 @@
 import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
 import org.chromium.components.browser_ui.widget.RadioButtonWithDescription;
 /**
- * Tests for {@link AdaptiveToolbarPreferenceFragment}.
+ * Tests for {@link AdaptiveToolbarSettingsFragment}.
  */
 @RunWith(BaseRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
@@ -83,14 +83,14 @@
     @Test
     @SmallTest
     public void testSelectShortcuts() {
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mSwitchPreference = (ChromeSwitchPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_TOOLBAR_SHORTCUT_SWITCH);
+                    AdaptiveToolbarSettingsFragment.PREF_TOOLBAR_SHORTCUT_SWITCH);
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             Assert.assertFalse(SharedPreferencesManager.getInstance().contains(
                     ADAPTIVE_TOOLBAR_CUSTOMIZATION_ENABLED));
@@ -162,12 +162,12 @@
     @SmallTest
     @EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_TRANSLATE)
     public void testTranslateOption_Enabled() {
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Select Translate.
             Assert.assertEquals(R.id.adaptive_option_translate,
@@ -189,12 +189,12 @@
         // Set initial preference to translate.
         SharedPreferencesManager.getInstance().writeInt(
                 ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS, AdaptiveToolbarButtonVariant.TRANSLATE);
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Translate option should be hidden, and we should have reverted back to "Auto".
             Assert.assertEquals(R.id.adaptive_option_translate,
@@ -213,12 +213,12 @@
     @SmallTest
     @EnableFeatures(ChromeFeatureList.ADAPTIVE_BUTTON_IN_TOP_TOOLBAR_ADD_TO_BOOKMARKS)
     public void testAddToBookmarksOption_Enabled() {
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Select Add to bookmarks.
             Assert.assertEquals(R.id.adaptive_option_add_to_bookmarks,
@@ -241,12 +241,12 @@
         // Set initial preference to add to bookmarks.
         SharedPreferencesManager.getInstance().writeInt(ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS,
                 AdaptiveToolbarButtonVariant.ADD_TO_BOOKMARKS);
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Add to bookmarks option should be hidden, and we should have reverted back to "Auto".
             Assert.assertEquals(R.id.adaptive_option_add_to_bookmarks,
@@ -267,12 +267,12 @@
     public void testReadAloudOption_Enabled() {
         AdaptiveToolbarFeatures.setProfile(mProfile);
         UnifiedConsentServiceBridge.setUrlKeyedAnonymizedDataCollectionEnabled(true);
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Select Read Aloud.
             Assert.assertEquals(R.id.adaptive_option_read_aloud,
@@ -294,12 +294,12 @@
         // Set initial preference to Read Aloud.
         SharedPreferencesManager.getInstance().writeInt(
                 ADAPTIVE_TOOLBAR_CUSTOMIZATION_SETTINGS, AdaptiveToolbarButtonVariant.READ_ALOUD);
-        FragmentScenario<AdaptiveToolbarPreferenceFragment> scenario =
-                FragmentScenario.launchInContainer(AdaptiveToolbarPreferenceFragment.class,
+        FragmentScenario<AdaptiveToolbarSettingsFragment> scenario =
+                FragmentScenario.launchInContainer(AdaptiveToolbarSettingsFragment.class,
                         Bundle.EMPTY, R.style.Theme_Chromium_Settings);
         scenario.onFragment(fragment -> {
             mRadioPreference = (RadioButtonGroupAdaptiveToolbarPreference) fragment.findPreference(
-                    AdaptiveToolbarPreferenceFragment.PREF_ADAPTIVE_RADIO_GROUP);
+                    AdaptiveToolbarSettingsFragment.PREF_ADAPTIVE_RADIO_GROUP);
 
             // Read Aloud option should be hidden, and we should have reverted back to
             // "Auto".
diff --git a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragment.java b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarSettingsFragment.java
similarity index 97%
rename from chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragment.java
rename to chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarSettingsFragment.java
index 7211f7d..2da296e1 100644
--- a/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarPreferenceFragment.java
+++ b/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/adaptive/settings/AdaptiveToolbarSettingsFragment.java
@@ -27,7 +27,7 @@
 /**
  * Fragment that allows the user to configure toolbar shorcut preferences.
  */
-public class AdaptiveToolbarPreferenceFragment extends PreferenceFragmentCompat {
+public class AdaptiveToolbarSettingsFragment extends PreferenceFragmentCompat {
     /** The key for the switch taggle on the setting page. */
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     public static final String PREF_TOOLBAR_SHORTCUT_SWITCH = "toolbar_shortcut_switch";
diff --git a/chrome/browser/ui/ash/desks/chrome_saved_desk_delegate.cc b/chrome/browser/ui/ash/desks/chrome_saved_desk_delegate.cc
index 2408f02..354bb75 100644
--- a/chrome/browser/ui/ash/desks/chrome_saved_desk_delegate.cc
+++ b/chrome/browser/ui/ash/desks/chrome_saved_desk_delegate.cc
@@ -258,7 +258,12 @@
   DCHECK(full_restore_data);
 
   const std::string app_id = full_restore::GetAppId(window);
-  DCHECK(!app_id.empty());
+  // TODO: b/296445956 - Implement a long term fix for saving the arc ghost
+  // window.
+  if (app_id.empty()) {
+    std::move(callback).Run({});
+    return;
+  }
 
   auto& app_registry_cache =
       apps::AppServiceProxyFactory::GetForProfile(user_profile)
diff --git a/chrome/browser/ui/ash/network/network_state_notifier.cc b/chrome/browser/ui/ash/network/network_state_notifier.cc
index 305bb430..a25842f 100644
--- a/chrome/browser/ui/ash/network/network_state_notifier.cc
+++ b/chrome/browser/ui/ash/network/network_state_notifier.cc
@@ -6,6 +6,7 @@
 
 #include <string>
 
+#include "ash/constants/ash_features.h"
 #include "ash/constants/notifier_catalogs.h"
 #include "ash/public/cpp/notification_utils.h"
 #include "ash/public/cpp/system_tray_client.h"
@@ -575,6 +576,10 @@
   if (IsSimLockConnectionFailure(error_name, network)) {
     on_click = base::BindRepeating(&NetworkStateNotifier::ShowSimUnlockSettings,
                                    weak_ptr_factory_.GetWeakPtr());
+  } else if (features::IsApnRevampEnabled() && network &&
+             network->GetError() == shill::kErrorInvalidAPN) {
+    on_click = base::BindRepeating(&NetworkStateNotifier::ShowApnSettings,
+                                   weak_ptr_factory_.GetWeakPtr(), guid);
   } else {
     on_click = base::BindRepeating(&NetworkStateNotifier::ShowNetworkSettings,
                                    weak_ptr_factory_.GetWeakPtr(), guid);
@@ -629,6 +634,16 @@
   system_tray_client_->ShowSettingsSimUnlock();
 }
 
+void NetworkStateNotifier::ShowApnSettings(const std::string& network_id) {
+  CHECK(features::IsApnRevampEnabled());
+  if (!system_tray_client_) {
+    return;
+  }
+
+  NET_LOG(USER) << "Opening APN subpage for network: " << network_id;
+  system_tray_client_->ShowApnSubpage(network_id);
+}
+
 void NetworkStateNotifier::ShowCarrierAccountDetail(
     const std::string& network_id) {
   NetworkConnect::Get()->ShowCarrierAccountDetail(network_id);
diff --git a/chrome/browser/ui/ash/network/network_state_notifier.h b/chrome/browser/ui/ash/network/network_state_notifier.h
index 0ed5377..4cc59fbb 100644
--- a/chrome/browser/ui/ash/network/network_state_notifier.h
+++ b/chrome/browser/ui/ash/network/network_state_notifier.h
@@ -116,6 +116,7 @@
   // Shows the network settings for |network_id|.
   void ShowNetworkSettings(const std::string& network_id);
   void ShowSimUnlockSettings();
+  void ShowApnSettings(const std::string& network_id);
 
   // Shows the carrier account detail page for |network_id|.
   void ShowCarrierAccountDetail(const std::string& network_id);
diff --git a/chrome/browser/ui/ash/network/network_state_notifier_unittest.cc b/chrome/browser/ui/ash/network/network_state_notifier_unittest.cc
index f667782d..3c801cf0 100644
--- a/chrome/browser/ui/ash/network/network_state_notifier_unittest.cc
+++ b/chrome/browser/ui/ash/network/network_state_notifier_unittest.cc
@@ -6,11 +6,13 @@
 
 #include <memory>
 
+#include "ash/constants/ash_features.h"
 #include "ash/public/cpp/test/test_system_tray_client.h"
 #include "ash/strings/grit/ash_strings.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
+#include "base/test/scoped_feature_list.h"
 #include "chrome/browser/notifications/notification_display_service_tester.h"
 #include "chrome/browser/notifications/system_notification_helper.h"
 #include "chrome/grit/generated_resources.h"
@@ -37,6 +39,7 @@
 namespace {
 
 const char kWiFi1Guid[] = "wifi1_guid";
+const char kCellular1ServicePath[] = "/service/cellular1";
 const char kCellular1Guid[] = "cellular1_guid";
 const char kCellular1NetworkName[] = "cellular1";
 const char16_t kCellular1NetworkName16[] = u"cellular1";
@@ -96,7 +99,16 @@
                                                 local_state_.registry());
 
     network_handler_test_helper_->InitializePrefs(&user_prefs_, &local_state_);
+  }
 
+  void TearDown() override {
+    NetworkConnect::Shutdown();
+    network_connect_delegate_.reset();
+    network_handler_test_helper_.reset();
+    BrowserWithTestWindowTest::TearDown();
+  }
+
+  void Init() {
     SetupDefaultShillState();
     base::RunLoop().RunUntilIdle();
 
@@ -109,13 +121,6 @@
     NetworkConnect::Initialize(network_connect_delegate_.get());
   }
 
-  void TearDown() override {
-    NetworkConnect::Shutdown();
-    network_connect_delegate_.reset();
-    network_handler_test_helper_.reset();
-    BrowserWithTestWindowTest::TearDown();
-  }
-
  protected:
   void SetupESimNetwork() {
     const char kTestEuiccPath[] = "euicc_path";
@@ -186,7 +191,6 @@
         kWiFi1ServicePath, shill::kPassphraseProperty, base::Value("failure"));
 
     // Set up Cellular device, and add a single locked network.
-    const char kCellular1ServicePath[] = "/service/cellular1";
     const char kCellular1Iccid[] = "iccid";
     device_test->AddDevice(kCellularDevicePath, shill::kTypeCellular,
                            "stub_cellular_device1");
@@ -199,11 +203,7 @@
     service_test->SetServiceProperty(
         kCellular1ServicePath, shill::kActivationStateProperty,
         base::Value(shill::kActivationStateActivated));
-    base::Value::Dict sim_lock_status;
-    sim_lock_status.Set(shill::kSIMLockTypeProperty, shill::kSIMLockPin);
-    device_test->SetDeviceProperty(
-        kCellularDevicePath, shill::kSIMLockStatusProperty,
-        base::Value(std::move(sim_lock_status)), /*notify_changed=*/true);
+
     base::Value::List sim_slot_infos;
     base::Value::Dict slot_info_item;
     slot_info_item.Set(shill::kSIMSlotInfoICCID, kCellular1Iccid);
@@ -224,11 +224,13 @@
   TestSystemTrayClient test_system_tray_client_;
   std::unique_ptr<NetworkHandlerTestHelper> network_handler_test_helper_;
   std::unique_ptr<NetworkConnectTestDelegate> network_connect_delegate_;
+  base::test::ScopedFeatureList scoped_feature_list_;
   TestingPrefServiceSimple user_prefs_;
   TestingPrefServiceSimple local_state_;
 };
 
 TEST_F(NetworkStateNotifierTest, WiFiConnectionFailure) {
+  Init();
   TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
       std::make_unique<SystemNotificationHelper>());
   NotificationDisplayServiceTester tester(nullptr /* profile */);
@@ -240,6 +242,8 @@
 }
 
 TEST_F(NetworkStateNotifierTest, CellularLockedSimConnectionFailure) {
+  Init();
+  SetCellularDeviceLocked(/*is_locked=*/true);
   TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
       std::make_unique<SystemNotificationHelper>());
   NotificationDisplayServiceTester tester(nullptr /* profile */);
@@ -264,6 +268,8 @@
 }
 
 TEST_F(NetworkStateNotifierTest, CellularEsimConnectionFailure) {
+  Init();
+  SetCellularDeviceLocked(/*is_locked=*/true);
   SetupESimNetwork();
   TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
       std::make_unique<SystemNotificationHelper>());
@@ -299,4 +305,66 @@
   EXPECT_FALSE(notification);
 }
 
+TEST_F(NetworkStateNotifierTest,
+       CellularInvalidApnConnectionFailureApnRevampEnabled) {
+  scoped_feature_list_.InitAndEnableFeature(ash::features::kApnRevamp);
+  Init();
+  TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
+      std::make_unique<SystemNotificationHelper>());
+  NotificationDisplayServiceTester tester(nullptr /* profile */);
+  network_handler_test_helper_->service_test()
+      ->SetErrorForNextConnectionAttempt(shill::kErrorInvalidAPN);
+  network_handler_test_helper_->service_test()->SetServiceProperty(
+      kCellular1ServicePath, shill::kConnectableProperty, base::Value(true));
+  network_handler_test_helper_->service_test()->SetServiceProperty(
+      kCellular1ServicePath, shill::kErrorProperty,
+      base::Value(shill::kErrorInvalidAPN));
+  NetworkConnect::Get()->ConnectToNetworkId(kCellular1Guid);
+  base::RunLoop().RunUntilIdle();
+
+  // Failure should spawn a notification.
+  absl::optional<message_center::Notification> notification =
+      tester.GetNotification(
+          NetworkStateNotifier::kNetworkConnectNotificationId);
+  EXPECT_TRUE(notification);
+
+  // Clicking the notification should open the APN subpage.
+  notification->delegate()->Click(/*button_index=*/absl::nullopt,
+                                  /*reply=*/absl::nullopt);
+  EXPECT_EQ(1, test_system_tray_client_.show_apn_subpage_count());
+  EXPECT_EQ(kCellular1Guid,
+            test_system_tray_client_.last_apn_subpage_network_id());
+}
+
+TEST_F(NetworkStateNotifierTest,
+       CellularInvalidApnConnectionFailureApnRevampDisabled) {
+  Init();
+  TestingBrowserProcess::GetGlobal()->SetSystemNotificationHelper(
+      std::make_unique<SystemNotificationHelper>());
+  NotificationDisplayServiceTester tester(nullptr /* profile */);
+  network_handler_test_helper_->service_test()
+      ->SetErrorForNextConnectionAttempt(shill::kErrorInvalidAPN);
+  network_handler_test_helper_->service_test()->SetServiceProperty(
+      kCellular1ServicePath, shill::kConnectableProperty, base::Value(true));
+  network_handler_test_helper_->service_test()->SetServiceProperty(
+      kCellular1ServicePath, shill::kErrorProperty,
+      base::Value(shill::kErrorInvalidAPN));
+  NetworkConnect::Get()->ConnectToNetworkId(kCellular1Guid);
+  base::RunLoop().RunUntilIdle();
+
+  // Failure should spawn a notification.
+  absl::optional<message_center::Notification> notification =
+      tester.GetNotification(
+          NetworkStateNotifier::kNetworkConnectNotificationId);
+  EXPECT_TRUE(notification);
+
+  // Clicking the notification should open the network settings page.
+  notification->delegate()->Click(/*button_index=*/absl::nullopt,
+                                  /*reply=*/absl::nullopt);
+  EXPECT_EQ(0, test_system_tray_client_.show_apn_subpage_count());
+  EXPECT_EQ(1, test_system_tray_client_.show_network_settings_count());
+  EXPECT_EQ(kCellular1Guid,
+            test_system_tray_client_.last_network_settings_network_id());
+}
+
 }  // namespace ash
diff --git a/chrome/browser/ui/ash/system_tray_client_impl.cc b/chrome/browser/ui/ash/system_tray_client_impl.cc
index fbfee2f..1e83e74 100644
--- a/chrome/browser/ui/ash/system_tray_client_impl.cc
+++ b/chrome/browser/ui/ash/system_tray_client_impl.cc
@@ -648,6 +648,14 @@
   ShowSettingsSubPageForActiveUser(page);
 }
 
+void SystemTrayClientImpl::ShowApnSubpage(const std::string& network_id) {
+  CHECK(ash::features::IsApnRevampEnabled());
+  std::string page = chromeos::settings::mojom::kApnSubpagePath +
+                     std::string("?guid=") +
+                     base::EscapeUrlEncodedData(network_id, /*use_plus=*/true);
+  ShowSettingsSubPageForActiveUser(page);
+}
+
 void SystemTrayClientImpl::ShowNetworkSettings(const std::string& network_id) {
   ShowNetworkSettingsHelper(network_id, false /* show_configure */);
 }
diff --git a/chrome/browser/ui/ash/system_tray_client_impl.h b/chrome/browser/ui/ash/system_tray_client_impl.h
index 2d1c42d..b1992128 100644
--- a/chrome/browser/ui/ash/system_tray_client_impl.h
+++ b/chrome/browser/ui/ash/system_tray_client_impl.h
@@ -93,6 +93,7 @@
   void ShowNetworkCreate(const std::string& type) override;
   void ShowSettingsCellularSetup(bool show_psim_flow) override;
   void ShowSettingsSimUnlock() override;
+  void ShowApnSubpage(const std::string& network_id) override;
   void ShowThirdPartyVpnCreate(const std::string& extension_id) override;
   void ShowArcVpnCreate(const std::string& app_id) override;
   void ShowNetworkSettings(const std::string& network_id) override;
diff --git a/chrome/browser/ui/ash/system_tray_client_impl_unittest.cc b/chrome/browser/ui/ash/system_tray_client_impl_unittest.cc
index da7623a58..b45e695 100644
--- a/chrome/browser/ui/ash/system_tray_client_impl_unittest.cc
+++ b/chrome/browser/ui/ash/system_tray_client_impl_unittest.cc
@@ -91,4 +91,15 @@
             user_action_tester.GetActionCount(kShowRemapKeysSettingsSubpage));
 }
 
+TEST_F(SystemTrayClientImplTest, ShowApnSubpage) {
+  base::test::ScopedFeatureList feature_list;
+  feature_list.InitAndEnableFeature(ash::features::kApnRevamp);
+  client_impl_->ShowApnSubpage(/*network_id=*/"guid");
+  EXPECT_EQ(settings_window_manager_->last_url(),
+            base::StrCat({chrome::GetOSSettingsUrl(
+                              chromeos::settings::mojom::kApnSubpagePath)
+                              .spec(),
+                          "?guid=guid"}));
+}
+
 }  // namespace
diff --git a/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc b/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
index 4939a4fc0..23f32e24 100644
--- a/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
+++ b/chrome/browser/ui/autofill/autofill_popup_controller_impl_unittest.cc
@@ -10,7 +10,9 @@
 
 #include "base/memory/weak_ptr.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/gmock_callback_support.h"
 #include "base/test/metrics/histogram_tester.h"
+#include "base/test/mock_callback.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
 #include "build/build_config.h"
@@ -31,6 +33,7 @@
 #include "components/autofill/core/browser/autofill_external_delegate.h"
 #include "components/autofill/core/browser/autofill_manager.h"
 #include "components/autofill/core/browser/autofill_test_utils.h"
+#include "components/autofill/core/browser/browser_autofill_manager_test_api.h"
 #include "components/autofill/core/browser/ui/popup_item_ids.h"
 #include "components/autofill/core/browser/ui/suggestion.h"
 #include "components/autofill/core/common/aliases.h"
@@ -38,6 +41,7 @@
 #include "components/prefs/pref_service.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/common/input/native_web_keyboard_event.h"
+#include "content/public/test/navigation_simulator.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
@@ -59,8 +63,6 @@
 #endif
 
 #if BUILDFLAG(IS_ANDROID)
-#include "base/test/gmock_callback_support.h"
-#include "base/test/mock_callback.h"
 #include "chrome/browser/autofill/manual_filling_controller_impl.h"
 #include "chrome/browser/autofill/mock_address_accessory_controller.h"
 #include "chrome/browser/autofill/mock_credit_card_accessory_controller.h"
@@ -73,6 +75,7 @@
 using ::testing::_;
 using ::testing::AtLeast;
 using ::testing::Eq;
+using ::testing::Invoke;
 using ::testing::Mock;
 using ::testing::NiceMock;
 using ::testing::Return;
@@ -94,16 +97,6 @@
   MOCK_METHOD(ui::AXTreeID, GetAxTreeId, (), (const override));
 };
 
-class MockBrowserAutofillManager : public BrowserAutofillManager {
- public:
-  MockBrowserAutofillManager(AutofillDriver* driver,
-                             ContentAutofillClient* client)
-      : BrowserAutofillManager(driver, client, "en-US") {}
-  MockBrowserAutofillManager(MockBrowserAutofillManager&) = delete;
-  MockBrowserAutofillManager& operator=(MockBrowserAutofillManager&) = delete;
-  ~MockBrowserAutofillManager() override = default;
-};
-
 class MockAutofillExternalDelegate : public AutofillExternalDelegate {
  public:
   explicit MockAutofillExternalDelegate(
@@ -208,6 +201,150 @@
   }
 };
 
+class BrowserAutofillManagerWithMockDelegate : public BrowserAutofillManager {
+ public:
+  BrowserAutofillManagerWithMockDelegate(AutofillDriver* driver,
+                                         ContentAutofillClient* client)
+      : BrowserAutofillManager(driver, client, "en-US") {
+    test_api(*this).SetExternalDelegate(
+        std::make_unique<NiceMock<MockAutofillExternalDelegate>>(this));
+  }
+
+  BrowserAutofillManagerWithMockDelegate(
+      BrowserAutofillManagerWithMockDelegate&) = delete;
+  BrowserAutofillManagerWithMockDelegate& operator=(
+      BrowserAutofillManagerWithMockDelegate&) = delete;
+
+  ~BrowserAutofillManagerWithMockDelegate() override = default;
+
+  NiceMock<MockAutofillExternalDelegate>& external_delegate() {
+    return static_cast<NiceMock<MockAutofillExternalDelegate>&>(
+        *test_api(*this).external_delegate());
+  }
+};
+
+class TestContentAutofillClientWithMockController
+    : public TestContentAutofillClient {
+ public:
+  explicit TestContentAutofillClientWithMockController(
+      content::WebContents* web_contents)
+      : TestContentAutofillClient(web_contents) {
+    ON_CALL(popup_view(), CreateSubPopupView)
+        .WillByDefault(Return(sub_popup_view().GetWeakPtr()));
+  }
+
+  ~TestContentAutofillClientWithMockController() override { DoHide(); }
+
+  // Returns the current controller. Controllers are specific to the `manager`'s
+  // AutofillExternalDelegate. Therefore, when there are two consecutive
+  // `popup_controller(x)` and `popup_controller(y)`, the second call hides the
+  // old and creates new controller iff `x` and `y` are distinct.
+  NiceMock<TestAutofillPopupController>& popup_controller(
+      BrowserAutofillManagerWithMockDelegate& manager) {
+    if (manager_of_last_controller_.get() != &manager) {
+      DoHide();
+      CHECK(!popup_controller_);
+    }
+    if (!popup_controller_) {
+      popup_controller_ = (new NiceMock<TestAutofillPopupController>(
+                               manager.external_delegate().GetWeakPtrForTest(),
+                               &GetWebContents(), gfx::RectF(),
+                               show_pwd_migration_warning_callback_.Get()))
+                              ->GetWeakPtr();
+      popup_controller_->SetViewForTesting(popup_view_->GetWeakPtr());
+      manager_of_last_controller_ = manager.GetWeakPtr();
+      ON_CALL(cast_popup_controller(), Hide)
+          .WillByDefault([this](PopupHidingReason reason) { DoHide(reason); });
+    }
+    return cast_popup_controller();
+  }
+
+  MockAutofillPopupView& popup_view() { return *popup_view_; }
+
+  MockAutofillPopupView& sub_popup_view() { return *sub_popup_view_; }
+
+#if BUILDFLAG(IS_ANDROID)
+  base::MockCallback<base::RepeatingCallback<
+      void(gfx::NativeWindow,
+           Profile*,
+           password_manager::metrics_util::PasswordMigrationWarningTriggers)>>&
+  show_pwd_migration_warning_callback() {
+    return show_pwd_migration_warning_callback_;
+  }
+#endif
+
+ private:
+  void DoHide(PopupHidingReason reason) {
+    if (popup_controller_) {
+      cast_popup_controller().DoHide(reason);
+    }
+  }
+
+  void DoHide() {
+    if (popup_controller_) {
+      cast_popup_controller().DoHide();
+    }
+  }
+
+  NiceMock<TestAutofillPopupController>& cast_popup_controller() {
+    return static_cast<NiceMock<TestAutofillPopupController>&>(
+        *popup_controller_);
+  }
+
+  base::WeakPtr<AutofillPopupControllerImpl> popup_controller_;
+  base::WeakPtr<AutofillManager> manager_of_last_controller_;
+
+  std::unique_ptr<NiceMock<MockAutofillPopupView>> popup_view_ =
+      std::make_unique<NiceMock<MockAutofillPopupView>>();
+  std::unique_ptr<NiceMock<MockAutofillPopupView>> sub_popup_view_ =
+      std::make_unique<NiceMock<MockAutofillPopupView>>();
+  base::MockCallback<base::RepeatingCallback<void(
+      gfx::NativeWindow,
+      Profile*,
+      password_manager::metrics_util::PasswordMigrationWarningTriggers)>>
+      show_pwd_migration_warning_callback_;
+};
+
+content::RenderFrameHost* CreateAndNavigateChildFrame(
+    content::RenderFrameHost* parent,
+    const GURL& url,
+    std::string_view name) {
+  content::RenderFrameHost* rfh =
+      content::RenderFrameHostTester::For(parent)->AppendChild(
+          std::string(name));
+  // ContentAutofillDriverFactory::DidFinishNavigation() creates a driver for
+  // subframes only if
+  // `NavigationHandle::HasSubframeNavigationEntryCommitted()` is true. This
+  // is not the case for the first navigation. (In non-unit-tests, the first
+  // navigation creates a driver in
+  // ContentAutofillDriverFactory::BindAutofillDriver().) Therefore,
+  // we simulate *two* navigations here, and explicitly set the transition
+  // type for the second navigation.
+  std::unique_ptr<content::NavigationSimulator> simulator;
+  // First navigation: `HasSubframeNavigationEntryCommitted() == false`.
+  // Must be a different URL from the second navigation.
+  GURL about_blank("about:blank");
+  CHECK_NE(about_blank, url);
+  simulator =
+      content::NavigationSimulator::CreateRendererInitiated(about_blank, rfh);
+  simulator->Commit();
+  rfh = simulator->GetFinalRenderFrameHost();
+  // Second navigation: `HasSubframeNavigationEntryCommitted() == true`.
+  // Must set the transition type to ui::PAGE_TRANSITION_MANUAL_SUBFRAME.
+  simulator = content::NavigationSimulator::CreateRendererInitiated(url, rfh);
+  simulator->SetTransition(ui::PAGE_TRANSITION_MANUAL_SUBFRAME);
+  simulator->Commit();
+  return simulator->GetFinalRenderFrameHost();
+}
+
+content::RenderFrameHost* NavigateAndCommitFrame(content::RenderFrameHost* rfh,
+                                                 const GURL& url) {
+  std::unique_ptr<content::NavigationSimulator> simulator =
+      content::NavigationSimulator::CreateRendererInitiated(url, rfh);
+  simulator->Commit();
+  return simulator->GetFinalRenderFrameHost();
+}
+
 }  // namespace
 
 class AutofillPopupControllerImplTest : public ChromeRenderViewHostTestHarness {
@@ -219,59 +356,41 @@
 
   void SetUp() override {
     ChromeRenderViewHostTestHarness::SetUp();
-    // Make sure RenderFrame is created.
-    NavigateAndCommit(GURL("about:blank"));
+    NavigateAndCommit(GURL("https://foo.com/"));
     FocusWebContentsOnMainFrame();
     ASSERT_TRUE(web_contents()->GetFocusedFrame());
-    external_delegate_ = CreateExternalDelegate();
-    autofill_popup_view_ = std::make_unique<NiceMock<MockAutofillPopupView>>();
-    autofill_sub_popup_view_ =
-        std::make_unique<NiceMock<MockAutofillPopupView>>();
 
 #if BUILDFLAG(IS_ANDROID)
-    autofill_popup_controller_ =
-        (new NiceMock<TestAutofillPopupController>(
-             external_delegate_->GetWeakPtrForTest(), web_contents(),
-             gfx::RectF(), show_pwd_migration_warning_callback_.Get()))
-            ->GetWeakPtr();
     ManualFillingControllerImpl::CreateForWebContentsForTesting(
         web_contents(), mock_pwd_controller_.AsWeakPtr(),
         mock_address_controller_.AsWeakPtr(), mock_cc_controller_.AsWeakPtr(),
         std::make_unique<NiceMock<MockManualFillingView>>());
-#else
-    autofill_popup_controller_ =
-        (new NiceMock<TestAutofillPopupController>(
-             external_delegate_->GetWeakPtrForTest(), web_contents(),
-             gfx::RectF(), base::DoNothing()))
-            ->GetWeakPtr();
 #endif
-    autofill_popup_controller_->SetViewForTesting(
-        autofill_popup_view()->GetWeakPtr());
-
-    ON_CALL(*autofill_popup_view(), CreateSubPopupView)
-        .WillByDefault(Return(autofill_sub_popup_view()->GetWeakPtr()));
   }
 
-  void TearDown() override {
-    // This will make sure the controller and the view (if any) are both
-    // cleaned up.
-    if (autofill_popup_controller_) {
-      popup_controller().DoHide();
-    }
-
-    external_delegate_.reset();
-    ChromeRenderViewHostTestHarness::TearDown();
+  content::RenderFrameHost* main_frame() {
+    return web_contents()->GetPrimaryMainFrame();
   }
 
-  virtual std::unique_ptr<NiceMock<MockAutofillExternalDelegate>>
-  CreateExternalDelegate() {
-    return std::make_unique<NiceMock<MockAutofillExternalDelegate>>(
-        &autofill_manager());
+  TestContentAutofillClientWithMockController& client() {
+    return *autofill_client_injector_[web_contents()];
+  }
+
+  NiceMock<MockAutofillDriver>& driver(
+      content::RenderFrameHost* rfh = nullptr) {
+    return *autofill_driver_injector_
+        [rfh ? rfh : web_contents()->GetPrimaryMainFrame()];
+  }
+
+  BrowserAutofillManagerWithMockDelegate& manager(
+      content::RenderFrameHost* rfh = nullptr) {
+    return *autofill_manager_injector_[rfh ? rfh : main_frame()];
   }
 
   // Shows empty suggestions with the popup_item_id ids passed as
   // `popup_item_ids`.
   void ShowSuggestions(
+      BrowserAutofillManagerWithMockDelegate& manager,
       const std::vector<PopupItemId>& popup_item_ids,
       AutofillSuggestionTriggerSource trigger_source =
           AutofillSuggestionTriggerSource::kFormControlElementClicked) {
@@ -280,33 +399,17 @@
     for (PopupItemId popup_item_id : popup_item_ids) {
       suggestions.emplace_back(u"", popup_item_id);
     }
-    popup_controller().Show(std::move(suggestions), trigger_source,
-                            AutoselectFirstSuggestion(false));
+    ShowSuggestions(manager, std::move(suggestions), trigger_source);
   }
 
   void ShowSuggestions(
+      BrowserAutofillManagerWithMockDelegate& manager,
       std::vector<Suggestion> suggestions,
       AutofillSuggestionTriggerSource trigger_source =
           AutofillSuggestionTriggerSource::kFormControlElementClicked) {
-    popup_controller().Show(std::move(suggestions), trigger_source,
-                            AutoselectFirstSuggestion(false));
-  }
-
-  TestAutofillPopupController& popup_controller() {
-    return static_cast<TestAutofillPopupController&>(
-        *autofill_popup_controller_);
-  }
-
-  NiceMock<MockAutofillExternalDelegate>* delegate() {
-    return external_delegate_.get();
-  }
-
-  MockAutofillPopupView* autofill_popup_view() {
-    return autofill_popup_view_.get();
-  }
-
-  MockAutofillPopupView* autofill_sub_popup_view() {
-    return autofill_sub_popup_view_.get();
+    client().popup_controller(manager).Show(std::move(suggestions),
+                                            trigger_source,
+                                            AutoselectFirstSuggestion(false));
   }
 
   content::NativeWebKeyboardEvent CreateKeyPressEvent(int windows_key_code) {
@@ -318,111 +421,97 @@
     return event;
   }
 
- protected:
-  TestContentAutofillClient* autofill_client() {
-    return autofill_client_injector_[web_contents()];
-  }
+ private:
+  test::AutofillUnitTestEnvironment autofill_test_environment_;
 
-  NiceMock<MockAutofillDriver>* autofill_driver() {
-    return autofill_driver_injector_[web_contents()];
-  }
-
-  BrowserAutofillManager& autofill_manager() {
-    return static_cast<BrowserAutofillManager&>(
-        autofill_driver()->GetAutofillManager());
-  }
-
-  TestAutofillClientInjector<TestContentAutofillClient>
+  TestAutofillClientInjector<TestContentAutofillClientWithMockController>
       autofill_client_injector_;
   TestAutofillDriverInjector<NiceMock<MockAutofillDriver>>
       autofill_driver_injector_;
+  TestAutofillManagerInjector<BrowserAutofillManagerWithMockDelegate>
+      autofill_manager_injector_;
 
-  test::AutofillUnitTestEnvironment autofill_test_environment_;
-  std::unique_ptr<NiceMock<MockAutofillExternalDelegate>> external_delegate_;
-  std::unique_ptr<NiceMock<MockAutofillPopupView>> autofill_popup_view_;
-  std::unique_ptr<NiceMock<MockAutofillPopupView>> autofill_sub_popup_view_;
 #if BUILDFLAG(IS_ANDROID)
   NiceMock<MockPasswordAccessoryController> mock_pwd_controller_;
   NiceMock<MockAddressAccessoryController> mock_address_controller_;
   NiceMock<MockCreditCardAccessoryController> mock_cc_controller_;
-  base::MockCallback<base::RepeatingCallback<void(
-      gfx::NativeWindow,
-      Profile*,
-      password_manager::metrics_util::PasswordMigrationWarningTriggers)>>
-      show_pwd_migration_warning_callback_;
 #endif
-  base::WeakPtr<AutofillPopupControllerImpl> autofill_popup_controller_;
 };
 
 TEST_F(AutofillPopupControllerImplTest, RemoveSuggestion) {
-  ShowSuggestions({PopupItemId::kAddressEntry, PopupItemId::kAddressEntry,
+  ShowSuggestions(manager(),
+                  {PopupItemId::kAddressEntry, PopupItemId::kAddressEntry,
                    PopupItemId::kAutofillOptions});
 
   // Generate a popup, so it can be hidden later. It doesn't matter what the
   // external_delegate thinks is being shown in the process, since we are just
   // testing the popup here.
-  test::GenerateTestAutofillPopup(external_delegate_.get());
+  test::GenerateTestAutofillPopup(&manager().external_delegate());
 
   // Remove the first entry. The popup should be redrawn since its size has
   // changed.
-  EXPECT_CALL(popup_controller(), OnSuggestionsChanged());
-  EXPECT_TRUE(popup_controller().RemoveSuggestion(0));
-  Mock::VerifyAndClearExpectations(autofill_popup_view());
+  EXPECT_CALL(client().popup_controller(manager()), OnSuggestionsChanged());
+  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(0));
+  Mock::VerifyAndClearExpectations(&client().popup_view());
 
   // Remove the next entry. The popup should then be hidden since there are
   // no Autofill entries left.
-  EXPECT_CALL(popup_controller(), Hide(PopupHidingReason::kNoSuggestions));
-  EXPECT_TRUE(popup_controller().RemoveSuggestion(0));
+  EXPECT_CALL(client().popup_controller(manager()),
+              Hide(PopupHidingReason::kNoSuggestions));
+  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(0));
 }
 
 TEST_F(AutofillPopupControllerImplTest,
        RemoveAutocompleteSuggestion_AnnounceText) {
   base::HistogramTester histogram_tester;
-  ShowSuggestions({Suggestion(u"main text", PopupItemId::kAutocompleteEntry)});
-  test::GenerateTestAutofillPopup(external_delegate_.get());
+  ShowSuggestions(manager(),
+                  {Suggestion(u"main text", PopupItemId::kAutocompleteEntry)});
+  test::GenerateTestAutofillPopup(&manager().external_delegate());
 
-  EXPECT_CALL(*autofill_popup_view(),
+  EXPECT_CALL(*&client().popup_view(),
               AxAnnounce(Eq(u"Entry main text has been deleted")));
-  EXPECT_TRUE(popup_controller().RemoveSuggestion(0));
+  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(0));
 }
 
 TEST_F(AutofillPopupControllerImplTest,
        RemoveAutocompleteSuggestion_IgnoresClickOutsideCheck) {
-  ShowSuggestions(
-      {PopupItemId::kAutocompleteEntry, PopupItemId::kAutocompleteEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAutocompleteEntry,
+                              PopupItemId::kAutocompleteEntry});
 
   // Generate a popup, so it can be hidden later. It doesn't matter what the
   // external_delegate thinks is being shown in the process, since we are just
   // testing the popup here.
-  test::GenerateTestAutofillPopup(external_delegate_.get());
+  test::GenerateTestAutofillPopup(&manager().external_delegate());
 
   // Remove the first entry. The popup should be redrawn since its size has
   // changed.
-  EXPECT_CALL(popup_controller(), OnSuggestionsChanged());
-  EXPECT_TRUE(popup_controller().RemoveSuggestion(0));
-  Mock::VerifyAndClearExpectations(autofill_popup_view());
+  EXPECT_CALL(client().popup_controller(manager()), OnSuggestionsChanged());
+  EXPECT_TRUE(client().popup_controller(manager()).RemoveSuggestion(0));
+  Mock::VerifyAndClearExpectations(&client().popup_view());
 
-  EXPECT_TRUE(
-      popup_controller().ShouldIgnoreMouseObservedOutsideItemBoundsCheck());
+  EXPECT_TRUE(client()
+                  .popup_controller(manager())
+                  .ShouldIgnoreMouseObservedOutsideItemBoundsCheck());
 }
 
 TEST_F(AutofillPopupControllerImplTest,
        ManualFallBackTriggerSource_IgnoresClickOutsideCheck) {
-  ShowSuggestions({PopupItemId::kAddressEntry},
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry},
                   AutofillSuggestionTriggerSource::
                       kManualFallbackForAutocompleteUnrecognized);
 
   // Generate a popup, so it can be hidden later. It doesn't matter what the
   // external_delegate thinks is being shown in the process, since we are just
   // testing the popup here.
-  test::GenerateTestAutofillPopup(external_delegate_.get());
+  test::GenerateTestAutofillPopup(&manager().external_delegate());
 
-  EXPECT_TRUE(
-      popup_controller().ShouldIgnoreMouseObservedOutsideItemBoundsCheck());
+  EXPECT_TRUE(client()
+                  .popup_controller(manager())
+                  .ShouldIgnoreMouseObservedOutsideItemBoundsCheck());
 }
 
 TEST_F(AutofillPopupControllerImplTest, UpdateDataListValues) {
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
   // Add one data list entry.
   std::u16string value1 = u"data list value 1";
@@ -430,27 +519,32 @@
   std::u16string label1 = u"data list label 1";
   std::vector<std::u16string> data_list_labels{label1};
 
-  popup_controller().UpdateDataListValues(data_list_values, data_list_labels);
+  client().popup_controller(manager()).UpdateDataListValues(data_list_values,
+                                                            data_list_labels);
 
-  ASSERT_EQ(3, popup_controller().GetLineCount());
+  ASSERT_EQ(3, client().popup_controller(manager()).GetLineCount());
 
-  Suggestion result0 = popup_controller().GetSuggestionAt(0);
+  Suggestion result0 = client().popup_controller(manager()).GetSuggestionAt(0);
   EXPECT_EQ(value1, result0.main_text.value);
-  EXPECT_EQ(value1, popup_controller().GetSuggestionMainTextAt(0));
+  EXPECT_EQ(value1,
+            client().popup_controller(manager()).GetSuggestionMainTextAt(0));
   ASSERT_EQ(1u, result0.labels.size());
   ASSERT_EQ(1u, result0.labels[0].size());
   EXPECT_EQ(label1, result0.labels[0][0].value);
   EXPECT_EQ(std::u16string(), result0.additional_label);
-  EXPECT_EQ(label1, popup_controller().GetSuggestionLabelsAt(0)[0][0].value);
+  EXPECT_EQ(label1, client()
+                        .popup_controller(manager())
+                        .GetSuggestionLabelsAt(0)[0][0]
+                        .value);
   EXPECT_EQ(PopupItemId::kDatalistEntry, result0.popup_item_id);
 
-  Suggestion result1 = popup_controller().GetSuggestionAt(1);
+  Suggestion result1 = client().popup_controller(manager()).GetSuggestionAt(1);
   EXPECT_EQ(std::u16string(), result1.main_text.value);
   EXPECT_TRUE(result1.labels.empty());
   EXPECT_EQ(std::u16string(), result1.additional_label);
   EXPECT_EQ(PopupItemId::kSeparator, result1.popup_item_id);
 
-  Suggestion result2 = popup_controller().GetSuggestionAt(2);
+  Suggestion result2 = client().popup_controller(manager()).GetSuggestionAt(2);
   EXPECT_EQ(std::u16string(), result2.main_text.value);
   EXPECT_TRUE(result2.labels.empty());
   EXPECT_EQ(std::u16string(), result2.additional_label);
@@ -462,39 +556,67 @@
   std::u16string label2 = u"data list label 2";
   data_list_labels.push_back(label2);
 
-  popup_controller().UpdateDataListValues(data_list_values, data_list_labels);
-  ASSERT_EQ(4, popup_controller().GetLineCount());
+  client().popup_controller(manager()).UpdateDataListValues(data_list_values,
+                                                            data_list_labels);
+  ASSERT_EQ(4, client().popup_controller(manager()).GetLineCount());
 
   // Original one first, followed by new one, then separator.
-  EXPECT_EQ(value1, popup_controller().GetSuggestionAt(0).main_text.value);
-  EXPECT_EQ(value1, popup_controller().GetSuggestionMainTextAt(0));
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(0).labels.size());
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(0).labels[0].size());
-  EXPECT_EQ(label1, popup_controller().GetSuggestionAt(0).labels[0][0].value);
-  EXPECT_EQ(std::u16string(),
-            popup_controller().GetSuggestionAt(0).additional_label);
-  EXPECT_EQ(value2, popup_controller().GetSuggestionAt(1).main_text.value);
-  EXPECT_EQ(value2, popup_controller().GetSuggestionMainTextAt(1));
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(1).labels.size());
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(1).labels[0].size());
-  EXPECT_EQ(label2, popup_controller().GetSuggestionAt(1).labels[0][0].value);
-  EXPECT_EQ(std::u16string(),
-            popup_controller().GetSuggestionAt(1).additional_label);
-  EXPECT_EQ(PopupItemId::kSeparator,
-            popup_controller().GetSuggestionAt(2).popup_item_id);
+  EXPECT_EQ(
+      value1,
+      client().popup_controller(manager()).GetSuggestionAt(0).main_text.value);
+  EXPECT_EQ(value1,
+            client().popup_controller(manager()).GetSuggestionMainTextAt(0));
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(0).labels.size());
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(0).labels[0].size());
+  EXPECT_EQ(label1, client()
+                        .popup_controller(manager())
+                        .GetSuggestionAt(0)
+                        .labels[0][0]
+                        .value);
+  EXPECT_EQ(
+      std::u16string(),
+      client().popup_controller(manager()).GetSuggestionAt(0).additional_label);
+  EXPECT_EQ(
+      value2,
+      client().popup_controller(manager()).GetSuggestionAt(1).main_text.value);
+  EXPECT_EQ(value2,
+            client().popup_controller(manager()).GetSuggestionMainTextAt(1));
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(1).labels.size());
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(1).labels[0].size());
+  EXPECT_EQ(label2, client()
+                        .popup_controller(manager())
+                        .GetSuggestionAt(1)
+                        .labels[0][0]
+                        .value);
+  EXPECT_EQ(
+      std::u16string(),
+      client().popup_controller(manager()).GetSuggestionAt(1).additional_label);
+  EXPECT_EQ(
+      PopupItemId::kSeparator,
+      client().popup_controller(manager()).GetSuggestionAt(2).popup_item_id);
 
   // Clear all data list values.
   data_list_values.clear();
-  popup_controller().UpdateDataListValues(data_list_values, data_list_labels);
+  client().popup_controller(manager()).UpdateDataListValues(data_list_values,
+                                                            data_list_labels);
 
-  ASSERT_EQ(1, popup_controller().GetLineCount());
-  EXPECT_EQ(PopupItemId::kAddressEntry,
-            popup_controller().GetSuggestionAt(0).popup_item_id);
+  ASSERT_EQ(1, client().popup_controller(manager()).GetLineCount());
+  EXPECT_EQ(
+      PopupItemId::kAddressEntry,
+      client().popup_controller(manager()).GetSuggestionAt(0).popup_item_id);
 }
 
 TEST_F(AutofillPopupControllerImplTest, PopupsWithOnlyDataLists) {
   // Create the popup with a single datalist element.
-  ShowSuggestions({PopupItemId::kDatalistEntry});
+  ShowSuggestions(manager(), {PopupItemId::kDatalistEntry});
 
   // Replace the datalist element with a new one.
   std::u16string value1 = u"data list value 1";
@@ -502,130 +624,129 @@
   std::u16string label1 = u"data list label 1";
   std::vector<std::u16string> data_list_labels{label1};
 
-  popup_controller().UpdateDataListValues(data_list_values, data_list_labels);
+  client().popup_controller(manager()).UpdateDataListValues(data_list_values,
+                                                            data_list_labels);
 
-  ASSERT_EQ(1, popup_controller().GetLineCount());
-  EXPECT_EQ(value1, popup_controller().GetSuggestionAt(0).main_text.value);
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(0).labels.size());
-  ASSERT_EQ(1u, popup_controller().GetSuggestionAt(0).labels[0].size());
-  EXPECT_EQ(label1, popup_controller().GetSuggestionAt(0).labels[0][0].value);
-  EXPECT_EQ(std::u16string(),
-            popup_controller().GetSuggestionAt(0).additional_label);
-  EXPECT_EQ(PopupItemId::kDatalistEntry,
-            popup_controller().GetSuggestionAt(0).popup_item_id);
+  ASSERT_EQ(1, client().popup_controller(manager()).GetLineCount());
+  EXPECT_EQ(
+      value1,
+      client().popup_controller(manager()).GetSuggestionAt(0).main_text.value);
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(0).labels.size());
+  ASSERT_EQ(
+      1u,
+      client().popup_controller(manager()).GetSuggestionAt(0).labels[0].size());
+  EXPECT_EQ(label1, client()
+                        .popup_controller(manager())
+                        .GetSuggestionAt(0)
+                        .labels[0][0]
+                        .value);
+  EXPECT_EQ(
+      std::u16string(),
+      client().popup_controller(manager()).GetSuggestionAt(0).additional_label);
+  EXPECT_EQ(
+      PopupItemId::kDatalistEntry,
+      client().popup_controller(manager()).GetSuggestionAt(0).popup_item_id);
 
   // Clear datalist values and check that the popup becomes hidden.
-  EXPECT_CALL(popup_controller(), Hide(PopupHidingReason::kNoSuggestions));
+  EXPECT_CALL(client().popup_controller(manager()),
+              Hide(PopupHidingReason::kNoSuggestions));
   data_list_values.clear();
-  popup_controller().UpdateDataListValues(data_list_values, data_list_values);
+  client().popup_controller(manager()).UpdateDataListValues(data_list_values,
+                                                            data_list_values);
 }
 
 TEST_F(AutofillPopupControllerImplTest, GetOrCreateAndroid) {
-  NiceMock<MockAutofillExternalDelegate> delegate(&autofill_manager());
-
   WeakPtr<AutofillPopupControllerImpl> controller =
       AutofillPopupControllerImpl::GetOrCreate(
-          WeakPtr<AutofillPopupControllerImpl>(), delegate.GetWeakPtrForTest(),
-          web_contents(), nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
-  EXPECT_TRUE(controller.get());
+          WeakPtr<AutofillPopupControllerImpl>(),
+          manager().external_delegate().GetWeakPtrForTest(), web_contents(),
+          nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
+  EXPECT_TRUE(controller);
 
   controller->Hide(PopupHidingReason::kViewDestroyed);
+  EXPECT_FALSE(controller);
 
   controller = AutofillPopupControllerImpl::GetOrCreate(
-      WeakPtr<AutofillPopupControllerImpl>(), delegate.GetWeakPtrForTest(),
-      web_contents(), nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
-  EXPECT_TRUE(controller.get());
+      WeakPtr<AutofillPopupControllerImpl>(),
+      manager().external_delegate().GetWeakPtrForTest(), web_contents(),
+      nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
+  EXPECT_TRUE(controller);
 
   WeakPtr<AutofillPopupControllerImpl> controller2 =
       AutofillPopupControllerImpl::GetOrCreate(
-          controller, delegate.GetWeakPtrForTest(), web_contents(), nullptr,
-          gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
+          controller, manager().external_delegate().GetWeakPtrForTest(),
+          web_contents(), nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
   EXPECT_EQ(controller.get(), controller2.get());
-  controller->Hide(PopupHidingReason::kViewDestroyed);
-  NiceMock<TestAutofillPopupController>* test_controller =
-      new NiceMock<TestAutofillPopupController>(delegate.GetWeakPtrForTest(),
-                                                web_contents(), gfx::RectF(),
-                                                base::DoNothing());
-  EXPECT_CALL(*test_controller, Hide(PopupHidingReason::kViewDestroyed));
 
+  controller->Hide(PopupHidingReason::kViewDestroyed);
+  EXPECT_FALSE(controller);
+  EXPECT_FALSE(controller2);
+
+  EXPECT_CALL(client().popup_controller(manager()),
+              Hide(PopupHidingReason::kViewDestroyed));
   gfx::RectF bounds(0.f, 0.f, 1.f, 2.f);
   base::WeakPtr<AutofillPopupControllerImpl> controller3 =
       AutofillPopupControllerImpl::GetOrCreate(
-          test_controller->GetWeakPtr(), delegate.GetWeakPtrForTest(),
-          web_contents(), nullptr, bounds, base::i18n::UNKNOWN_DIRECTION);
+          client().popup_controller(manager()).GetWeakPtr(),
+          manager().external_delegate().GetWeakPtrForTest(), web_contents(),
+          nullptr, bounds, base::i18n::UNKNOWN_DIRECTION);
+  EXPECT_EQ(&client().popup_controller(manager()), controller3.get());
   EXPECT_EQ(bounds, static_cast<AutofillPopupController*>(controller3.get())
                         ->element_bounds());
   controller3->Hide(PopupHidingReason::kViewDestroyed);
 
-  // Hide the test_controller to delete it.
-  test_controller->DoHide();
-
-  test_controller = new NiceMock<TestAutofillPopupController>(
-      delegate.GetWeakPtrForTest(), web_contents(), gfx::RectF(),
-      base::DoNothing());
-  EXPECT_CALL(*test_controller, Hide).Times(0);
+  client().popup_controller(manager()).DoHide();
 
   const base::WeakPtr<AutofillPopupControllerImpl> controller4 =
       AutofillPopupControllerImpl::GetOrCreate(
-          test_controller->GetWeakPtr(), delegate.GetWeakPtrForTest(),
-          web_contents(), nullptr, bounds, base::i18n::UNKNOWN_DIRECTION);
+          client().popup_controller(manager()).GetWeakPtr(),
+          manager().external_delegate().GetWeakPtrForTest(), web_contents(),
+          nullptr, bounds, base::i18n::UNKNOWN_DIRECTION);
+  EXPECT_EQ(&client().popup_controller(manager()), controller4.get());
   EXPECT_EQ(bounds,
             static_cast<const AutofillPopupController*>(controller4.get())
                 ->element_bounds());
-  delete test_controller;
+
+  client().popup_controller(manager()).DoHide();
 }
 
 TEST_F(AutofillPopupControllerImplTest, ProperlyResetController) {
-  ShowSuggestions(
-      {PopupItemId::kAutocompleteEntry, PopupItemId::kAutocompleteEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAutocompleteEntry,
+                              PopupItemId::kAutocompleteEntry});
 
   // Now show a new popup with the same controller, but with fewer items.
   WeakPtr<AutofillPopupControllerImpl> controller =
       AutofillPopupControllerImpl::GetOrCreate(
-          popup_controller().GetWeakPtr(), delegate()->GetWeakPtrForTest(),
-          nullptr, nullptr, gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
+          client().popup_controller(manager()).GetWeakPtr(),
+          manager().external_delegate().GetWeakPtrForTest(), nullptr, nullptr,
+          gfx::RectF(), base::i18n::UNKNOWN_DIRECTION);
   EXPECT_EQ(0, controller->GetLineCountForTesting());
 }
 
 TEST_F(AutofillPopupControllerImplTest, HidingClearsPreview) {
-  // Create a new controller, because hiding destroys it and we can't destroy it
-  // twice.
-  StrictMock<MockAutofillExternalDelegate> delegate(&autofill_manager());
-  StrictMock<TestAutofillPopupController>* test_controller =
-      new StrictMock<TestAutofillPopupController>(delegate.GetWeakPtrForTest(),
-                                                  web_contents(), gfx::RectF(),
-                                                  base::DoNothing());
-  EXPECT_CALL(delegate, ClearPreviewedForm());
-  EXPECT_CALL(delegate, OnPopupHidden());
-  // Hide() also deletes the object itself.
-  test_controller->DoHide();
+  EXPECT_CALL(manager().external_delegate(), ClearPreviewedForm());
+  EXPECT_CALL(manager().external_delegate(), OnPopupHidden());
+  client().popup_controller(manager()).DoHide();
 }
 
 TEST_F(AutofillPopupControllerImplTest, DontHideWhenWaitingForData) {
-  EXPECT_CALL(*autofill_popup_view(), Hide).Times(0);
-  popup_controller().PinView();
+  EXPECT_CALL(client().popup_view(), Hide).Times(0);
+  client().popup_controller(manager()).PinView();
 
   // Hide() will not work for stale data or when focusing native UI.
-  popup_controller().DoHide(PopupHidingReason::kStaleData);
-  popup_controller().DoHide(PopupHidingReason::kEndEditing);
+  client().popup_controller(manager()).DoHide(PopupHidingReason::kStaleData);
+  client().popup_controller(manager()).DoHide(PopupHidingReason::kEndEditing);
 
   // Check the expectations now since TearDown will perform a successful hide.
-  Mock::VerifyAndClearExpectations(delegate());
-  Mock::VerifyAndClearExpectations(autofill_popup_view());
+  Mock::VerifyAndClearExpectations(&manager().external_delegate());
+  Mock::VerifyAndClearExpectations(&client().popup_view());
 }
 
 TEST_F(AutofillPopupControllerImplTest, ShouldReportHidingPopupReason) {
-  // Create a new controller, because hiding destroys it and we can't destroy it
-  // twice (since we already hide it in the destructor).
-  NiceMock<MockAutofillExternalDelegate> delegate(&autofill_manager());
-  NiceMock<TestAutofillPopupController>* test_controller =
-      new NiceMock<TestAutofillPopupController>(delegate.GetWeakPtrForTest(),
-                                                web_contents(), gfx::RectF(),
-                                                base::DoNothing());
   base::HistogramTester histogram_tester;
-  // DoHide() invokes Hide() that also deletes the object itself.
-  test_controller->DoHide(PopupHidingReason::kTabGone);
-
+  client().popup_controller(manager()).DoHide(PopupHidingReason::kTabGone);
   histogram_tester.ExpectTotalCount("Autofill.PopupHidingReason", 1);
   histogram_tester.ExpectBucketCount("Autofill.PopupHidingReason",
                                      /*kTabGone=*/8, 1);
@@ -634,28 +755,31 @@
 // This is a regression test for crbug.com/521133 to ensure that we don't crash
 // when suggestions updates race with user selections.
 TEST_F(AutofillPopupControllerImplTest, SelectInvalidSuggestion) {
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(0);
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(0);
 
   // The following should not crash:
-  popup_controller().AcceptSuggestion(
+  client().popup_controller(manager()).AcceptSuggestion(
       /*index=*/1, base::TimeTicks::Now());  // Out of bounds!
 }
 
 TEST_F(AutofillPopupControllerImplTest, AcceptSuggestionRespectsTimeout) {
   base::HistogramTester histogram_tester;
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
   // Calls before the threshold are ignored.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(0);
-  popup_controller().AcceptSuggestion(0, base::TimeTicks::Now());
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(0);
+  client().popup_controller(manager()).AcceptSuggestion(0,
+                                                        base::TimeTicks::Now());
   task_environment()->FastForwardBy(base::Milliseconds(100));
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
 
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion);
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion);
   task_environment()->FastForwardBy(base::Milliseconds(400));
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
 
   histogram_tester.ExpectTotalCount(
       "Autofill.Popup.AcceptanceDelayThresholdNotMet", 2);
@@ -664,30 +788,34 @@
 TEST_F(AutofillPopupControllerImplTest,
        AcceptSuggestionTimeoutIsUpdatedOnPopupMove) {
   base::HistogramTester histogram_tester;
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
   // Calls before the threshold are ignored.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(0);
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(0);
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
   task_environment()->FastForwardBy(base::Milliseconds(100));
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
 
   histogram_tester.ExpectTotalCount(
       "Autofill.Popup.AcceptanceDelayThresholdNotMet", 2);
   task_environment()->FastForwardBy(base::Milliseconds(400));
   // Show the suggestions again (simulating, e.g., a click somewhere slightly
   // different).
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(0);
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(0);
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
   histogram_tester.ExpectTotalCount(
       "Autofill.Popup.AcceptanceDelayThresholdNotMet", 3);
 
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion);
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion);
   // After waiting, suggestions are accepted again.
   task_environment()->FastForwardBy(base::Milliseconds(500));
-  popup_controller().AcceptSuggestion(/*index=*/0, base::TimeTicks::Now());
+  client().popup_controller(manager()).AcceptSuggestion(/*index=*/0,
+                                                        base::TimeTicks::Now());
   histogram_tester.ExpectTotalCount(
       "Autofill.Popup.AcceptanceDelayThresholdNotMet", 3);
 }
@@ -697,7 +825,8 @@
 // picture-in-picture window.
 TEST_F(AutofillPopupControllerImplTest,
        CheckBoundsOverlapWithPictureInPicture) {
-  EXPECT_CALL(*autofill_popup_view(), OverlapsWithPictureInPictureWindow)
+  client().popup_controller(manager());  // Creates the controller.
+  EXPECT_CALL(client().popup_view(), OverlapsWithPictureInPictureWindow)
       .Times(1);
   PictureInPictureWindowManager* picture_in_picture_window_manager =
       PictureInPictureWindowManager::GetInstance();
@@ -710,15 +839,15 @@
   base::test::ScopedFeatureList scoped_feature_list(
       password_manager::features::
           kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
-  ShowSuggestions({PopupItemId::kPasswordEntry});
+  ShowSuggestions(manager(), {PopupItemId::kPasswordEntry});
 
   // Calls are accepted immediately.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(1);
-  EXPECT_CALL(show_pwd_migration_warning_callback_,
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
+  EXPECT_CALL(client().show_pwd_migration_warning_callback(),
               Run(_, _,
                   password_manager::metrics_util::
                       PasswordMigrationWarningTriggers::kKeyboardAcessoryBar));
-  popup_controller().AcceptSuggestion(
+  client().popup_controller(manager()).AcceptSuggestion(
       0, base::TimeTicks::Now() + base::Milliseconds(500));
 }
 
@@ -727,12 +856,12 @@
   base::test::ScopedFeatureList scoped_feature_list(
       password_manager::features::
           kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
-  ShowSuggestions({PopupItemId::kUsernameEntry});
+  ShowSuggestions(manager(), {PopupItemId::kUsernameEntry});
 
   // Calls are accepted immediately.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(1);
-  EXPECT_CALL(show_pwd_migration_warning_callback_, Run);
-  popup_controller().AcceptSuggestion(
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
+  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run);
+  client().popup_controller(manager()).AcceptSuggestion(
       0, base::TimeTicks::Now() + base::Milliseconds(500));
 }
 
@@ -742,12 +871,12 @@
   scoped_feature_list.InitAndDisableFeature(
       password_manager::features::
           kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
-  ShowSuggestions({PopupItemId::kPasswordEntry});
+  ShowSuggestions(manager(), {PopupItemId::kPasswordEntry});
 
   // Calls are accepted immediately.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(1);
-  EXPECT_CALL(show_pwd_migration_warning_callback_, Run).Times(0);
-  popup_controller().AcceptSuggestion(
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
+  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run).Times(0);
+  client().popup_controller(manager()).AcceptSuggestion(
       0, base::TimeTicks::Now() + base::Milliseconds(500));
 }
 
@@ -755,12 +884,12 @@
   base::test::ScopedFeatureList scoped_feature_list(
       password_manager::features::
           kUnifiedPasswordManagerLocalPasswordsMigrationWarning);
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
 
   // Calls are accepted immediately.
-  EXPECT_CALL(*delegate(), DidAcceptSuggestion).Times(1);
-  EXPECT_CALL(show_pwd_migration_warning_callback_, Run).Times(0);
-  popup_controller().AcceptSuggestion(
+  EXPECT_CALL(manager().external_delegate(), DidAcceptSuggestion).Times(1);
+  EXPECT_CALL(client().show_pwd_migration_warning_callback(), Run).Times(0);
+  client().popup_controller(manager()).AcceptSuggestion(
       0, base::TimeTicks::Now() + base::Milliseconds(500));
 }
 #endif
@@ -768,41 +897,41 @@
 #if !BUILDFLAG(IS_ANDROID)
 TEST_F(AutofillPopupControllerImplTest, SubPopupIsCreatedWithViewFromParent) {
   base::WeakPtr<AutofillPopupController> sub_controller =
-      popup_controller().OpenSubPopup({0, 0, 10, 10}, {},
-                                      AutoselectFirstSuggestion(false));
+      client().popup_controller(manager()).OpenSubPopup(
+          {0, 0, 10, 10}, {}, AutoselectFirstSuggestion(false));
   EXPECT_TRUE(sub_controller);
 }
 
 TEST_F(AutofillPopupControllerImplTest,
        DelegateMethodsAreCalledOnlyByRootPopup) {
-  EXPECT_CALL(*external_delegate_, OnPopupShown()).Times(0);
+  EXPECT_CALL(manager().external_delegate(), OnPopupShown()).Times(0);
   base::WeakPtr<AutofillPopupController> sub_controller =
-      popup_controller().OpenSubPopup({0, 0, 10, 10}, {},
-                                      AutoselectFirstSuggestion(false));
+      client().popup_controller(manager()).OpenSubPopup(
+          {0, 0, 10, 10}, {}, AutoselectFirstSuggestion(false));
 
-  EXPECT_CALL(*external_delegate_, OnPopupHidden()).Times(0);
+  EXPECT_CALL(manager().external_delegate(), OnPopupHidden()).Times(0);
   sub_controller->Hide(PopupHidingReason::kUserAborted);
 
-  EXPECT_CALL(*external_delegate_, OnPopupHidden());
-  popup_controller().Hide(PopupHidingReason::kUserAborted);
+  EXPECT_CALL(manager().external_delegate(), OnPopupHidden());
+  client().popup_controller(manager()).Hide(PopupHidingReason::kUserAborted);
 }
 
 TEST_F(AutofillPopupControllerImplTest, EventsAreDelegatedToChildrenAndView) {
-  EXPECT_CALL(*external_delegate_, OnPopupShown()).Times(0);
+  EXPECT_CALL(manager().external_delegate(), OnPopupShown()).Times(0);
   base::WeakPtr<AutofillPopupController> sub_controller =
-      popup_controller().OpenSubPopup({0, 0, 10, 10}, {},
-                                      AutoselectFirstSuggestion(false));
+      client().popup_controller(manager()).OpenSubPopup(
+          {0, 0, 10, 10}, {}, AutoselectFirstSuggestion(false));
 
   content::NativeWebKeyboardEvent event = CreateKeyPressEvent(ui::VKEY_LEFT);
-  EXPECT_CALL(*autofill_sub_popup_view(), HandleKeyPressEvent)
+  EXPECT_CALL(client().sub_popup_view(), HandleKeyPressEvent)
       .WillOnce(Return(true));
-  EXPECT_CALL(*autofill_popup_view(), HandleKeyPressEvent).Times(0);
-  EXPECT_TRUE(popup_controller().HandleKeyPressEvent(event));
+  EXPECT_CALL(client().popup_view(), HandleKeyPressEvent).Times(0);
+  EXPECT_TRUE(client().popup_controller(manager()).HandleKeyPressEvent(event));
 
-  EXPECT_CALL(*autofill_sub_popup_view(), HandleKeyPressEvent)
+  EXPECT_CALL(client().sub_popup_view(), HandleKeyPressEvent)
       .WillOnce(Return(false));
-  EXPECT_CALL(*autofill_popup_view(), HandleKeyPressEvent).Times(1);
-  EXPECT_FALSE(popup_controller().HandleKeyPressEvent(event));
+  EXPECT_CALL(client().popup_view(), HandleKeyPressEvent).Times(1);
+  EXPECT_FALSE(client().popup_controller(manager()).HandleKeyPressEvent(event));
 }
 #endif
 
@@ -872,13 +1001,13 @@
   void SetUp() override {
     AutofillPopupControllerImplTest::SetUp();
 
-    ON_CALL(*autofill_driver(), GetAxTreeId())
-        .WillByDefault(Return(test_tree_id_));
-    ON_CALL(popup_controller(), GetRootAXPlatformNodeForWebContents)
+    ON_CALL(driver(), GetAxTreeId()).WillByDefault(Return(test_tree_id_));
+    ON_CALL(client().popup_controller(manager()),
+            GetRootAXPlatformNodeForWebContents)
         .WillByDefault(Return(&mock_ax_platform_node_));
     ON_CALL(mock_ax_platform_node_, GetDelegate)
         .WillByDefault(Return(&mock_ax_platform_node_delegate_));
-    ON_CALL(*autofill_popup_view_, GetAxUniqueId)
+    ON_CALL(client().popup_view(), GetAxUniqueId)
         .WillByDefault(Return(absl::optional<int32_t>(kAxUniqueId)));
     ON_CALL(mock_ax_platform_node_delegate_, GetFromTreeIDAndNodeID)
         .WillByDefault(Return(&mock_ax_platform_node_));
@@ -902,13 +1031,13 @@
 // Test for successfully firing controls changed event for popup show/hide.
 TEST_F(AutofillPopupControllerImplTestAccessibility,
        FireControlsChangedEventDuringShowAndHide) {
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
   // Manually fire the event for popup show since setting the test view results
   // in the fire controls changed event not being sent.
-  popup_controller().FireControlsChangedEvent(true);
+  client().popup_controller(manager()).FireControlsChangedEvent(true);
   EXPECT_EQ(kAxUniqueId, ui::GetActivePopupAxUniqueId());
 
-  popup_controller().DoHide();
+  client().popup_controller(manager()).DoHide();
   EXPECT_EQ(absl::nullopt, ui::GetActivePopupAxUniqueId());
 }
 
@@ -920,10 +1049,10 @@
   EXPECT_CALL(mock_ax_platform_node_delegate_, GetFromTreeIDAndNodeID)
       .WillOnce(Return(nullptr));
 
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
   // Manually fire the event for popup show since setting the test view results
   // in the fire controls changed event not being sent.
-  popup_controller().FireControlsChangedEvent(true);
+  client().popup_controller(manager()).FireControlsChangedEvent(true);
   EXPECT_EQ(absl::nullopt, ui::GetActivePopupAxUniqueId());
 }
 
@@ -932,13 +1061,13 @@
 // popup ax unique id is not set.
 TEST_F(AutofillPopupControllerImplTestAccessibility,
        FireControlsChangedEventNoPopupAxUniqueId) {
-  EXPECT_CALL(*autofill_popup_view_, GetAxUniqueId)
+  EXPECT_CALL(client().popup_view(), GetAxUniqueId)
       .WillOnce(testing::Return(absl::nullopt));
 
-  ShowSuggestions({PopupItemId::kAddressEntry});
+  ShowSuggestions(manager(), {PopupItemId::kAddressEntry});
   // Manually fire the event for popup show since setting the test view results
   // in the fire controls changed event not being sent.
-  popup_controller().FireControlsChangedEvent(true);
+  client().popup_controller(manager()).FireControlsChangedEvent(true);
   EXPECT_EQ(absl::nullopt, ui::GetActivePopupAxUniqueId());
 }
 #endif
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.cc b/chrome/browser/ui/autofill/chrome_autofill_client.cc
index 12b354ba..007a96f4 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.cc
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.cc
@@ -158,6 +158,10 @@
 #include "components/zoom/zoom_controller.h"
 #endif  // BUILDFLAG(IS_ANDROID)
 
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
+#include "chrome/browser/compose/chrome_compose_client.h"
+#endif
+
 #if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
 #include "chrome/browser/autofill/autofill_ml_prediction_model_service_factory.h"
 #include "components/autofill/core/browser/ml_model/autofill_ml_prediction_model_handler.h"
@@ -256,6 +260,15 @@
   return IbanManagerFactory::GetForProfile(profile);
 }
 
+compose::ComposeManager* ChromeAutofillClient::GetComposeManager() {
+#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_CHROMEOS)
+  auto* client = ChromeComposeClient::FromWebContents(web_contents());
+  return client ? &client->manager() : nullptr;
+#else
+  return nullptr;
+#endif
+}
+
 plus_addresses::PlusAddressService*
 ChromeAutofillClient::GetPlusAddressService() {
   // The `PlusAddressServiceFactory` should also ensure the service is not
@@ -621,7 +634,7 @@
 #endif
 }
 
-#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+#if !BUILDFLAG(IS_ANDROID)
 void ChromeAutofillClient::HideVirtualCardEnrollBubbleAndIconIfVisible() {
   VirtualCardEnrollBubbleControllerImpl::CreateForWebContents(web_contents());
   VirtualCardEnrollBubbleControllerImpl* controller =
diff --git a/chrome/browser/ui/autofill/chrome_autofill_client.h b/chrome/browser/ui/autofill/chrome_autofill_client.h
index b6ed389..7009dab 100644
--- a/chrome/browser/ui/autofill/chrome_autofill_client.h
+++ b/chrome/browser/ui/autofill/chrome_autofill_client.h
@@ -112,6 +112,7 @@
   AutocompleteHistoryManager* GetAutocompleteHistoryManager() override;
   IbanManager* GetIbanManager() override;
   plus_addresses::PlusAddressService* GetPlusAddressService() override;
+  compose::ComposeManager* GetComposeManager() override;
   void OfferPlusAddressCreation(
       const url::Origin& main_frame_origin,
       plus_addresses::PlusAddressCallback callback) override;
diff --git a/chrome/browser/ui/browser_window.h b/chrome/browser/ui/browser_window.h
index 2d119e3..f6ab8dd 100644
--- a/chrome/browser/ui/browser_window.h
+++ b/chrome/browser/ui/browser_window.h
@@ -22,7 +22,6 @@
 #include "chrome/browser/translate/chrome_translate_client.h"
 #include "chrome/browser/ui/bookmarks/bookmark_bar.h"
 #include "chrome/browser/ui/browser.h"
-#include "chrome/browser/ui/browser_dialogs.h"
 #include "chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.h"
 #include "chrome/browser/ui/hats/hats_service.h"
 #include "chrome/browser/ui/page_action/page_action_icon_type.h"
@@ -46,16 +45,16 @@
 #endif
 
 class Browser;
-class SharingDialog;
-struct SharingDialogData;
+class DownloadBubbleUIController;
 class DownloadShelf;
 class ExclusiveAccessContext;
 class ExtensionsContainer;
 class FindBar;
 class GURL;
 class LocationBar;
+class SharingDialog;
 class StatusBubble;
-class DownloadBubbleUIController;
+struct SharingDialogData;
 
 namespace autofill {
 class AutofillBubbleHandler;
@@ -75,10 +74,6 @@
 class QRCodeGeneratorBubbleView;
 }  // namespace qrcode_generator
 
-namespace signin_metrics {
-enum class AccessPoint;
-}
-
 namespace send_tab_to_self {
 class SendTabToSelfBubbleView;
 }  // namespace send_tab_to_self
@@ -88,6 +83,10 @@
 class SharingHubBubbleView;
 }  // namespace sharing_hub
 
+namespace signin_metrics {
+enum class AccessPoint;
+}
+
 namespace ui {
 class ColorProvider;
 class NativeTheme;
diff --git a/chrome/browser/ui/color/chrome_color_id.h b/chrome/browser/ui/color/chrome_color_id.h
index 0ae73eaf..18d6d9c 100644
--- a/chrome/browser/ui/color/chrome_color_id.h
+++ b/chrome/browser/ui/color/chrome_color_id.h
@@ -696,6 +696,16 @@
   E_CPONLY(kColorReadAnythingFocusRingBackgroundDark) \
   E_CPONLY(kColorReadAnythingFocusRingBackgroundLight) \
   E_CPONLY(kColorReadAnythingFocusRingBackgroundYellow) \
+  E_CPONLY(kColorCurrentReadAloudHighlight) \
+  E_CPONLY(kColorCurrentReadAloudHighlightBlue) \
+  E_CPONLY(kColorCurrentReadAloudHighlightDark) \
+  E_CPONLY(kColorCurrentReadAloudHighlightLight) \
+  E_CPONLY(kColorCurrentReadAloudHighlightYellow) \
+  E_CPONLY(kColorPreviousReadAloudHighlight) \
+  E_CPONLY(kColorPreviousReadAloudHighlightBlue) \
+  E_CPONLY(kColorPreviousReadAloudHighlightDark) \
+  E_CPONLY(kColorPreviousReadAloudHighlightLight) \
+  E_CPONLY(kColorPreviousReadAloudHighlightYellow) \
 
 #if BUILDFLAG(IS_CHROMEOS)
 #define CHROME_PLATFORM_SPECIFIC_COLOR_IDS \
diff --git a/chrome/browser/ui/color/chrome_color_mixer.cc b/chrome/browser/ui/color/chrome_color_mixer.cc
index d4506ce..865582f0 100644
--- a/chrome/browser/ui/color/chrome_color_mixer.cc
+++ b/chrome/browser/ui/color/chrome_color_mixer.cc
@@ -764,26 +764,33 @@
   mixer[kColorReadAnythingBackgroundDark] = {gfx::kGoogleGrey900};
   mixer[kColorReadAnythingBackgroundLight] = {gfx::kGoogleGrey100};
   mixer[kColorReadAnythingBackgroundYellow] = {gfx::kGoogleYellow100};
+  // The Read Anything themes need to be hard coded because they do not
+  // change with the chrome theme, which is the purpose of the Read Anything
+  // feature.
   mixer[kColorReadAnythingForeground] = {
       dark_mode ? kColorReadAnythingForegroundDark
                 : kColorReadAnythingForegroundLight};
-  mixer[kColorReadAnythingForegroundBlue] = ui::PickGoogleColorTwoBackgrounds(
-      kColorReadAnythingForegroundLight,
-      kColorReadAnythingDropdownBackgroundBlue,
-      kColorReadAnythingDropdownSelectedBlue,
-      color_utils::kMinimumReadableContrastRatio);
-  mixer[kColorReadAnythingForegroundDark] = ui::PickGoogleColorTwoBackgrounds(
-      gfx::kGoogleGrey200, kColorReadAnythingDropdownBackgroundDark,
-      kColorReadAnythingDropdownSelectedDark,
-      color_utils::kMinimumReadableContrastRatio);
-  mixer[kColorReadAnythingForegroundLight] = ui::PickGoogleColorTwoBackgrounds(
-      gfx::kGoogleGrey800, kColorReadAnythingBackgroundLight,
-      kColorReadAnythingDropdownSelectedLight,
-      color_utils::kMinimumReadableContrastRatio);
-  mixer[kColorReadAnythingForegroundYellow] = ui::PickGoogleColorTwoBackgrounds(
-      kColorReadAnythingForegroundLight, kColorReadAnythingBackgroundYellow,
-      kColorReadAnythingDropdownSelectedYellow,
-      color_utils::kMinimumReadableContrastRatio);
+  mixer[kColorReadAnythingForegroundBlue] = {SkColorSetRGB(31, 31, 31)};
+  mixer[kColorReadAnythingForegroundDark] = {SkColorSetRGB(227, 227, 227)};
+  mixer[kColorReadAnythingForegroundLight] = {SkColorSetRGB(31, 31, 31)};
+  mixer[kColorReadAnythingForegroundYellow] = {SkColorSetRGB(31, 31, 31)};
+  mixer[kColorCurrentReadAloudHighlight] = {
+      dark_mode ? kColorCurrentReadAloudHighlightDark
+                : kColorCurrentReadAloudHighlightLight};
+  mixer[kColorCurrentReadAloudHighlightDark] = {
+      SkColorSetARGB(25, 253, 252, 251)};
+  mixer[kColorCurrentReadAloudHighlightLight] = {
+      SkColorSetARGB(15, 31, 31, 31)};
+  mixer[kColorCurrentReadAloudHighlightBlue] = {SkColorSetARGB(15, 31, 31, 31)};
+  mixer[kColorCurrentReadAloudHighlightYellow] = {
+      SkColorSetARGB(15, 31, 31, 31)};
+  mixer[kColorPreviousReadAloudHighlight] = {
+      dark_mode ? kColorPreviousReadAloudHighlightDark
+                : kColorPreviousReadAloudHighlightLight};
+  mixer[kColorPreviousReadAloudHighlightDark] = {SkColorSetRGB(199, 199, 199)};
+  mixer[kColorPreviousReadAloudHighlightLight] = {SkColorSetRGB(71, 71, 71)};
+  mixer[kColorPreviousReadAloudHighlightBlue] = {SkColorSetRGB(71, 71, 71)};
+  mixer[kColorPreviousReadAloudHighlightYellow] = {SkColorSetRGB(71, 71, 71)};
   mixer[kColorReadAnythingSeparator] = {dark_mode
                                             ? kColorReadAnythingSeparatorDark
                                             : kColorReadAnythingSeparatorLight};
diff --git a/chrome/browser/ui/color/material_chrome_color_mixer.cc b/chrome/browser/ui/color/material_chrome_color_mixer.cc
index a6adccc..8a68280 100644
--- a/chrome/browser/ui/color/material_chrome_color_mixer.cc
+++ b/chrome/browser/ui/color/material_chrome_color_mixer.cc
@@ -94,6 +94,11 @@
   // Side Panel colors.
   mixer[kColorSidePanelBackground] = {ui::kColorSysBaseContainer};
 
+  // Read Anything (in the side panel) colors.
+  mixer[kColorReadAnythingForeground] = {ui::kColorSysOnSurface};
+  mixer[kColorCurrentReadAloudHighlight] = {ui::kColorSysStateHoverOnSubtle};
+  mixer[kColorPreviousReadAloudHighlight] = {ui::kColorSysOnSurfaceSecondary};
+
   // Tab Group Dialog colors.
   mixer[kColorTabGroupDialogIconEnabled] = {ui::kColorSysOnSurfaceSubtle};
 
diff --git a/chrome/browser/ui/passwords/bubble_controllers/manage_passwords_bubble_controller.cc b/chrome/browser/ui/passwords/bubble_controllers/manage_passwords_bubble_controller.cc
index 98aee8f..25f2dd056 100644
--- a/chrome/browser/ui/passwords/bubble_controllers/manage_passwords_bubble_controller.cc
+++ b/chrome/browser/ui/passwords/bubble_controllers/manage_passwords_bubble_controller.cc
@@ -18,8 +18,8 @@
 #include "components/favicon/core/favicon_util.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_form_metrics_recorder.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
 #include "components/password_manager/core/browser/password_store_interface.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/browser/reauth_purpose.h"
 #include "components/signin/public/base/consent_level.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
@@ -104,7 +104,7 @@
 ManagePasswordsBubbleController::GetPasswordSyncState() {
   const syncer::SyncService* sync_service =
       SyncServiceFactory::GetForProfile(GetProfile());
-  return password_manager_util::GetPasswordSyncState(sync_service);
+  return password_manager::sync_util::GetPasswordSyncState(sync_service);
 }
 
 std::u16string ManagePasswordsBubbleController::GetPrimaryAccountEmail() {
diff --git a/chrome/browser/ui/passwords/bubble_controllers/save_update_bubble_controller.cc b/chrome/browser/ui/passwords/bubble_controllers/save_update_bubble_controller.cc
index 73f3e777..65a3bf5 100644
--- a/chrome/browser/ui/passwords/bubble_controllers/save_update_bubble_controller.cc
+++ b/chrome/browser/ui/passwords/bubble_controllers/save_update_bubble_controller.cc
@@ -21,8 +21,8 @@
 #include "components/password_manager/core/browser/manage_passwords_referrer.h"
 #include "components/password_manager/core/browser/password_feature_manager.h"
 #include "components/password_manager/core/browser/password_form_metrics_recorder.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
 #include "components/password_manager/core/browser/password_store_interface.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/browser/reauth_purpose.h"
 #include "components/password_manager/core/browser/smart_bubble_stats_store.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
@@ -94,7 +94,7 @@
   const syncer::SyncService* sync_service =
       SyncServiceFactory::GetForProfile(profile);
   password_manager::SyncState sync_state =
-      password_manager_util::GetPasswordSyncState(sync_service);
+      password_manager::sync_util::GetPasswordSyncState(sync_service);
   return sync_state == password_manager::SyncState::kSyncingNormalEncryption ||
          sync_state ==
              password_manager::SyncState::kSyncingWithCustomPassphrase;
diff --git a/chrome/browser/ui/passwords/manage_passwords_test.cc b/chrome/browser/ui/passwords/manage_passwords_test.cc
index a15e62d4..caf6bf31 100644
--- a/chrome/browser/ui/passwords/manage_passwords_test.cc
+++ b/chrome/browser/ui/passwords/manage_passwords_test.cc
@@ -239,9 +239,9 @@
   autofill::FormData observed_form;
   observed_form.url = password_form_.url;
   autofill::FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   observed_form.fields.push_back(field);
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   observed_form.fields.push_back(field);
 
   auto form_manager = std::make_unique<PasswordFormManager>(
diff --git a/chrome/browser/ui/side_panel/read_anything/read_anything_side_panel_controller_utils.h b/chrome/browser/ui/side_panel/read_anything/read_anything_side_panel_controller_utils.h
index d5210a04..a25b197 100644
--- a/chrome/browser/ui/side_panel/read_anything/read_anything_side_panel_controller_utils.h
+++ b/chrome/browser/ui/side_panel/read_anything/read_anything_side_panel_controller_utils.h
@@ -7,15 +7,8 @@
 
 class Browser;
 
-namespace content {
-class WebContents;
-}  // namespace content
-
 // Used for reading mode option in context menu.
 void ShowReadAnythingSidePanel(Browser* browser);
 bool IsReadAnythingEntryShowing(Browser* browser);
-// Create and register a read anything side panel entry for the given web
-// contents.
-void CreateAndRegisterReadAnythingEntry(content::WebContents* web_contents);
 
 #endif  // CHROME_BROWSER_UI_SIDE_PANEL_READ_ANYTHING_READ_ANYTHING_SIDE_PANEL_CONTROLLER_UTILS_H_
diff --git a/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.cc b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.cc
new file mode 100644
index 0000000..a58dbc4b
--- /dev/null
+++ b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.cc
@@ -0,0 +1,220 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h"
+
+#include <iterator>
+#include <string>
+
+#include "base/functional/bind.h"
+#include "base/notreached.h"
+#include "base/observer_list.h"
+#include "base/values.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/browser.h"
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h"
+#include "chrome/browser/ui/toolbar/toolbar_pref_names.h"
+#include "components/prefs/pref_change_registrar.h"
+#include "components/prefs/pref_service.h"
+#include "ui/actions/action_id.h"
+#include "ui/actions/actions.h"
+
+PinnedToolbarActionsModel::PinnedToolbarActionsModel(Profile* profile)
+    : profile_(profile), pref_service_(profile_->GetPrefs()) {
+  pref_change_registrar_.Init(pref_service_);
+  pref_change_registrar_.Add(
+      prefs::kPinnedActions,
+      base::BindRepeating(&PinnedToolbarActionsModel::UpdatePinnedActionIds,
+                          base::Unretained(this)));
+
+  // Initialize the model with the current state of the kPinnedActions pref.
+  UpdatePinnedActionIds();
+}
+
+PinnedToolbarActionsModel::~PinnedToolbarActionsModel() = default;
+
+// static
+PinnedToolbarActionsModel* PinnedToolbarActionsModel::Get(Profile* profile) {
+  return PinnedToolbarActionsModelFactory::GetForProfile(profile);
+}
+
+void PinnedToolbarActionsModel::AddObserver(Observer* observer) {
+  observers_.AddObserver(observer);
+}
+
+void PinnedToolbarActionsModel::RemoveObserver(Observer* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+bool PinnedToolbarActionsModel::CanUpdate() {
+  // TODO(dljames/corising): Update this function as needed with other guards /
+  // requirements as they come up.
+
+  // At a minimum, incognito should be read-only. Guest mode should not be
+  // able to modify the prefs either.
+  return profile_->IsRegularProfile();
+}
+
+bool PinnedToolbarActionsModel::Contains(
+    const actions::ActionId& action_id) const {
+  auto iter = base::ranges::find(pinned_action_ids_, action_id);
+  return iter != pinned_action_ids_.end();
+}
+
+void PinnedToolbarActionsModel::UpdatePinnedState(
+    const actions::ActionId& action_id,
+    const bool should_pin) {
+  if (!CanUpdate()) {
+    // At a minimum, incognito should be read-only. Guest mode should not be
+    // able to modify the prefs either.
+    return;
+  }
+
+  const bool is_pinned = Contains(action_id);
+  if (!is_pinned && should_pin) {
+    PinAction(action_id);
+  } else if (is_pinned && !should_pin) {
+    UnpinAction(action_id);
+  }
+}
+
+void PinnedToolbarActionsModel::MovePinnedAction(
+    const actions::ActionId& action_id,
+    int target_index) {
+  if (!CanUpdate()) {
+    // At a minimum, incognito should be read-only. Guest mode should not be
+    // able to modify the prefs either.
+    return;
+  }
+
+  if (target_index < 0 || target_index >= int(pinned_action_ids_.size())) {
+    // Do nothing if the index is out of bounds.
+    return;
+  }
+
+  auto iter = base::ranges::find(pinned_action_ids_, action_id);
+  if (iter == pinned_action_ids_.end()) {
+    // Do nothing if this action is not pinned.
+    return;
+  }
+
+  int start_index = iter - pinned_action_ids_.begin();
+  if (start_index == target_index) {
+    return;
+  }
+
+  const absl::optional<std::string>& action_id_to_move =
+      actions::ActionManager::ActionIdToString(action_id);
+  const absl::optional<std::string>& action_id_of_target =
+      actions::ActionManager::ActionIdToString(
+          pinned_action_ids_[target_index]);
+
+  // Both ActionIds should have a string equivalent.
+  CHECK(action_id_to_move.has_value());
+  CHECK(action_id_of_target.has_value());
+
+  base::Value::List updated_pinned_action_ids =
+      pref_service_->GetList(prefs::kPinnedActions).Clone();
+  updated_pinned_action_ids.EraseValue(base::Value(action_id_to_move.value()));
+
+  auto prefs_iter = base::ranges::find(updated_pinned_action_ids,
+                                       action_id_of_target.value());
+  CHECK(prefs_iter != updated_pinned_action_ids.end());
+
+  if (target_index == 0) {
+    updated_pinned_action_ids.Insert(prefs_iter,
+                                     base::Value(action_id_to_move.value()));
+  } else {
+    updated_pinned_action_ids.Insert(++prefs_iter,
+                                     base::Value(action_id_to_move.value()));
+  }
+
+  // Updating the pref causes `UpdatePinnedActionIds()` to be called.
+  pref_service_->SetList(prefs::kPinnedActions,
+                         std::move(updated_pinned_action_ids));
+
+  // Notify observers the action was moved.
+  for (Observer& observer : observers_) {
+    observer.OnActionMoved(action_id, start_index, target_index);
+  }
+}
+
+void PinnedToolbarActionsModel::PinAction(const actions::ActionId& action_id) {
+  base::Value::List updated_pinned_action_ids =
+      pref_service_->GetList(prefs::kPinnedActions).Clone();
+  const absl::optional<std::string>& id =
+      actions::ActionManager::ActionIdToString(action_id);
+  // The ActionId should have a string equivalent.
+  CHECK(id.has_value());
+
+  updated_pinned_action_ids.Append(base::Value(id.value()));
+
+  // Updating the pref causes `UpdatePinnedActionIds()` to be called.
+  pref_service_->SetList(prefs::kPinnedActions,
+                         std::move(updated_pinned_action_ids));
+
+  // Notify observers the action was added.
+  for (Observer& observer : observers_) {
+    observer.OnActionAdded(action_id);
+  }
+}
+
+void PinnedToolbarActionsModel::UnpinAction(
+    const actions::ActionId& action_id) {
+  base::Value::List updated_pinned_action_ids =
+      pref_service_->GetList(prefs::kPinnedActions).Clone();
+  const absl::optional<std::string>& id =
+      actions::ActionManager::ActionIdToString(action_id);
+  // The ActionId should have a string equivalent.
+  CHECK(id.has_value());
+
+  auto prefs_iter =
+      base::ranges::find(updated_pinned_action_ids, base::Value(id.value()));
+  CHECK(prefs_iter != updated_pinned_action_ids.end());
+  updated_pinned_action_ids.erase(prefs_iter);
+
+  // Updating the pref causes `UpdatePinnedActionIds()` to be called.
+  pref_service_->SetList(prefs::kPinnedActions,
+                         std::move(updated_pinned_action_ids));
+
+  // Notify observers the action was removed.
+  for (Observer& observer : observers_) {
+    observer.OnActionRemoved(action_id);
+  }
+}
+
+void PinnedToolbarActionsModel::UpdatePinnedActionIds() {
+  const base::Value::List& updated_pinned_action_ids =
+      pref_service_->GetList(prefs::kPinnedActions);
+
+  // TODO(dljames): Investigate if there is a more optimal way to do this kind
+  // of conflict resolution. Ideally, we should only react to changes that did
+  // not occur directly on the model (Ex: Updates from sync). This would
+  // eliminate double processing.
+  pinned_action_ids_.clear();
+  pinned_action_ids_.reserve(updated_pinned_action_ids.size());
+
+  for (const base::Value& action_id : updated_pinned_action_ids) {
+    if (action_id.is_string()) {
+      const absl::optional<actions::ActionId>& id =
+          actions::ActionManager::StringToActionId(action_id.GetString());
+      // It could be possible that an ActionId is not mapped to the string if it
+      // comes from the prefs object. Example: One version could have an id that
+      // another one doesn't.
+      if (!id.has_value()) {
+        LOG(WARNING)
+            << "The following action id does not have a string equivalent: "
+            << action_id
+            << ". This can happen when different versions of the ActionId are "
+               "added to the prefs object.";
+        continue;
+      }
+      pinned_action_ids_.push_back(id.value());
+    }
+  }
+
+  for (Observer& observer : observers_) {
+    observer.OnActionsChanged();
+  }
+}
diff --git a/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h
new file mode 100644
index 0000000..776925d1
--- /dev/null
+++ b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h
@@ -0,0 +1,114 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_H_
+#define CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_H_
+
+#include <string>
+#include "base/observer_list.h"
+#include "chrome/browser/ui/browser.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/prefs/pref_change_registrar.h"
+#include "components/prefs/pref_service.h"
+#include "ui/actions/action_id.h"
+
+class Profile;
+
+// Used to keep track of the pinned elements/actions in the toolbar of the
+// browser. This is a per-profile instance, and manages the user's pinned action
+// preferences.
+class PinnedToolbarActionsModel : public KeyedService {
+ public:
+  explicit PinnedToolbarActionsModel(Profile* profile);
+
+  PinnedToolbarActionsModel(const PinnedToolbarActionsModel&) = delete;
+  PinnedToolbarActionsModel& operator=(const PinnedToolbarActionsModel&) =
+      delete;
+
+  ~PinnedToolbarActionsModel() override;
+
+  // Used to notify objects that extend this class that a change has occurred in
+  // the model.
+  class Observer {
+   public:
+    // Signals that `id` has been added to the model. This will
+    // *only* be called after the model has been initialized.
+    virtual void OnActionAdded(const actions::ActionId& id) = 0;
+
+    // Signals that the given action with `id` has been removed from the
+    // model.
+    virtual void OnActionRemoved(const actions::ActionId& id) = 0;
+
+    // Signals that the given action with `id` has been moved in the model.
+    virtual void OnActionMoved(const actions::ActionId& id,
+                               int from_index,
+                               int to_index) = 0;
+
+    // Called when the pinned actions change. Specifically, when an action is
+    // added, removed, or moved.
+    virtual void OnActionsChanged() = 0;
+
+   protected:
+    virtual ~Observer() = default;
+  };
+
+  // Convenience function to get the PinnedToolbarActionsModel for a Profile.
+  static PinnedToolbarActionsModel* Get(Profile* profile);
+
+  // Adds or removes an observer.
+  void AddObserver(Observer* observer);
+  void RemoveObserver(Observer* observer);
+
+  // Verify if we can update the model with the current profile.
+  bool CanUpdate();
+
+  // Returns true if `action_id` is in the toolbar model.
+  bool Contains(const actions::ActionId& action_id) const;
+
+  // Move the pinned action for |action_id| to |target_index|.
+  void MovePinnedAction(const actions::ActionId& action_id, int target_index);
+
+  // Updates the Action state of `action_id`.
+  // 1) Adds `action_id` to the model if `should_pin` is true and the id does
+  // not exist in the model.
+  // 2) Removes `action_id` from the model if
+  // `should_pin` is false and the id exists in the model.
+  void UpdatePinnedState(const actions::ActionId& action_id,
+                         const bool should_pin);
+
+  // Returns the ordered list of pinned ActionIds.
+  const std::vector<actions::ActionId>& pinned_action_ids() const {
+    return pinned_action_ids_;
+  }
+
+ private:
+  // Adds the `action_id` to the kPinnedActions pref.
+  void PinAction(const actions::ActionId& action_id);
+
+  // Removes the `action_id` from the kPinnedActions pref.
+  void UnpinAction(const actions::ActionId& action_id);
+
+  // Called when the kPinnedActions pref is changed. |pinned_action_ids_| is
+  // replaced with the entries in the kPinnedActions pref object. Should
+  // maintain insertion order. Notify observers the model has been updated with
+  // the latest data from the pref.
+  void UpdatePinnedActionIds();
+
+  // Our observers.
+  base::ObserverList<Observer>::Unchecked observers_;
+
+  raw_ptr<Profile> profile_;
+
+  // Used to retrieve and update the prefs object storing the currently pinned
+  // actions.
+  raw_ptr<PrefService> pref_service_;
+
+  // For observing changes to the pinned actions.
+  PrefChangeRegistrar pref_change_registrar_;
+
+  // Ordered list of pinned action IDs which will be displayed in the toolbar.
+  std::vector<actions::ActionId> pinned_action_ids_;
+};
+
+#endif  // CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_H_
diff --git a/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_browsertest.cc b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_browsertest.cc
new file mode 100644
index 0000000..19cf81a
--- /dev/null
+++ b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_browsertest.cc
@@ -0,0 +1,333 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h"
+
+#include <memory>
+
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h"
+#include "chrome/browser/ui/toolbar/toolbar_pref_names.h"
+#include "chrome/browser/ui/ui_features.h"
+#include "chrome/test/base/in_process_browser_test.h"
+#include "chrome/test/base/testing_profile.h"
+#include "content/public/test/browser_test.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/actions/action_id.h"
+#include "ui/actions/actions.h"
+
+namespace {
+const std::vector<absl::optional<std::string>> kTestActionIdStrings =
+    actions::ActionManager::ActionIdsToStrings(
+        {actions::kActionCut, actions::kActionCopy, actions::kActionPaste});
+
+// A simple observer that tracks the number of times certain events occur.
+class PinnedToolbarActionsModelTestObserver
+    : public PinnedToolbarActionsModel::Observer {
+ public:
+  explicit PinnedToolbarActionsModelTestObserver(
+      PinnedToolbarActionsModel* model)
+      : model_(model) {
+    model_->AddObserver(this);
+  }
+
+  PinnedToolbarActionsModelTestObserver(
+      const PinnedToolbarActionsModelTestObserver&) = delete;
+  PinnedToolbarActionsModelTestObserver& operator=(
+      const PinnedToolbarActionsModelTestObserver&) = delete;
+
+  ~PinnedToolbarActionsModelTestObserver() override {
+    model_->RemoveObserver(this);
+  }
+
+  int inserted_count() const { return inserted_count_; }
+  int removed_count() const { return removed_count_; }
+  int moved_to_index() const { return moved_to_index_; }
+
+  actions::ActionId last_changed_action() const { return last_changed_action_; }
+
+  const std::vector<actions::ActionId>& last_changed_ids() const {
+    return last_changed_ids_;
+  }
+
+ private:
+  // PinnedToolbarActionsModel::Observer:
+  void OnActionAdded(const actions::ActionId& action_id) override {
+    ++inserted_count_;
+    last_changed_action_ = action_id;
+  }
+
+  void OnActionRemoved(const actions::ActionId& action_id) override {
+    ++removed_count_;
+    last_changed_action_ = action_id;
+  }
+
+  void OnActionsChanged() override {
+    last_changed_ids_ = model_->pinned_action_ids();
+  }
+
+  // Signals that the given action with `id` has been moved in the model.
+  void OnActionMoved(const actions::ActionId& id,
+                     int from_index,
+                     int to_index) override {
+    moved_to_index_ = to_index;
+    last_changed_action_ = id;
+  }
+
+  const raw_ptr<PinnedToolbarActionsModel> model_;
+
+  int inserted_count_ = 0;
+  int removed_count_ = 0;
+  int moved_to_index_ = -1;
+
+  actions::ActionId last_changed_action_ = -1;
+  std::vector<actions::ActionId> last_changed_ids_;
+};
+}  // namespace
+
+class PinnedToolbarActionsModelBrowserTest : public InProcessBrowserTest {
+ public:
+  PinnedToolbarActionsModelBrowserTest() = default;
+
+  PinnedToolbarActionsModelBrowserTest(
+      const PinnedToolbarActionsModelBrowserTest&) = delete;
+  PinnedToolbarActionsModelBrowserTest& operator=(
+      const PinnedToolbarActionsModelBrowserTest&) = delete;
+
+  ~PinnedToolbarActionsModelBrowserTest() override = default;
+
+ protected:
+  void SetUpOnMainThread() override {
+    InProcessBrowserTest::SetUpOnMainThread();
+    model_ =
+        PinnedToolbarActionsModelFactory::GetForProfile(browser()->profile());
+    model_observer_ =
+        std::make_unique<PinnedToolbarActionsModelTestObserver>(model_);
+  }
+
+  void TearDownOnMainThread() override {
+    model_observer_.reset();
+    model_ = nullptr;
+    InProcessBrowserTest::TearDownOnMainThread();
+  }
+
+  PinnedToolbarActionsModel* model() { return model_; }
+
+  const PinnedToolbarActionsModelTestObserver* observer() const {
+    return model_observer_.get();
+  }
+
+ private:
+  raw_ptr<PinnedToolbarActionsModel> model_;
+  std::unique_ptr<PinnedToolbarActionsModelTestObserver> model_observer_;
+};
+
+// Verify that we are able to add new pinned actions to the model and that
+// it updates the prefs object accordingly.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest, PinActions) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  EXPECT_EQ(actions::kActionCut, observer()->last_changed_action());
+
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+  EXPECT_EQ(actions::kActionCopy, observer()->last_changed_action());
+
+  model()->UpdatePinnedState(actions::kActionPaste,
+                             /*should_pin=*/true);
+  EXPECT_EQ(actions::kActionPaste, observer()->last_changed_action());
+
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+
+  // Verify all actions ids were added to the model and that the prefs object
+  // maintains insertion order.
+  const base::Value::List& list =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+
+  ASSERT_EQ(3u, list.size());
+
+  EXPECT_EQ(kTestActionIdStrings[0], list[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list[2].GetString());
+}
+
+// Verify that we are able to remove pinned actions from the model and that
+// it updates the prefs object accordingly.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest, UnpinActions) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionPaste,
+                             /*should_pin=*/true);
+
+  // Expect unpinning the second ActionId will remove it from the model and the
+  // prefs object.
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/false);
+  EXPECT_EQ(1, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+
+  // Verify only the second ActionId was removed.
+  const base::Value::List& list =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(2u, list.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list[1].GetString());
+}
+
+// Verify that we are able to move pinned actions in the model and that
+// it updates the prefs object accordingly.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest,
+                       MovePinnedActions) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionPaste,
+                             /*should_pin=*/true);
+
+  // Expect moving the second action will put it at the end of the list.
+  model()->MovePinnedAction(actions::kActionCopy, 2);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(2, observer()->moved_to_index());
+
+  // Verify kActionCopy was moved to the end of the list which should be
+  // index 2.
+  const base::Value::List& list_1 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_1.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list_1[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list_1[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_1[2].GetString());
+
+  // Expect that we can move the first action after the second action correctly.
+  model()->MovePinnedAction(actions::kActionCut, 1);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(1, observer()->moved_to_index());
+
+  // Verify kActionCut was move to the end.
+  const base::Value::List& list_2 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_2.size());
+  EXPECT_EQ(kTestActionIdStrings[2], list_2[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[0], list_2[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_2[2].GetString());
+
+  // Expect that moving the kActionCopy action to the beginning of the list will
+  // place it in front of the current first element.
+  model()->MovePinnedAction(actions::kActionCopy, 0);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(0, observer()->moved_to_index());
+
+  // Verify kActionCopy was moved to index 0.
+  const base::Value::List& list_3 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_3.size());
+  EXPECT_EQ(kTestActionIdStrings[1], list_3[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list_3[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[0], list_3[2].GetString());
+}
+
+// Verify that trying to move a pinned action out of bounds will do nothing.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest,
+                       MovePinnedActionsOutOfBoundsDoesNothing) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionPaste,
+                             /*should_pin=*/true);
+
+  // Expect that moving an Action out of bounds at the end of the list does
+  // nothing.
+  model()->MovePinnedAction(actions::kActionCopy, 3);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(-1, observer()->moved_to_index());
+
+  // Verify the action did not move.
+  const base::Value::List& list_1 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_1.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list_1[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_1[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list_1[2].GetString());
+
+  // Expect that moving an action out of bounds before the list does nothing.
+  model()->MovePinnedAction(actions::kActionCut, -1);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(-1, observer()->moved_to_index());
+
+  // Verify the action did not move.
+  const base::Value::List& list_2 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_2.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list_2[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_2[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list_2[2].GetString());
+}
+
+// Verify that trying to move a pinned action out of bounds will do nothing.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest,
+                       MoveUnpinnedActionDoesNothing) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+
+  // Expect that moving an action which is not added to the model does nothing.
+  model()->MovePinnedAction(actions::kActionPaste, 2);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(2, observer()->inserted_count());
+  EXPECT_EQ(-1, observer()->moved_to_index());
+
+  // Verify nothing changed.
+  const base::Value::List& list_1 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(2u, list_1.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list_1[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_1[1].GetString());
+}
+
+// Verify that trying to move a pinned action to its current index does nothing.
+IN_PROC_BROWSER_TEST_F(PinnedToolbarActionsModelBrowserTest,
+                       MovePinnedActionToSameIndexDoesNothing) {
+  // Pin 3 ActionIds.
+  model()->UpdatePinnedState(actions::kActionCut,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionCopy,
+                             /*should_pin=*/true);
+  model()->UpdatePinnedState(actions::kActionPaste,
+                             /*should_pin=*/true);
+
+  // Expect that moving an action to the same index does nothing.
+  model()->MovePinnedAction(actions::kActionPaste, 2);
+  EXPECT_EQ(0, observer()->removed_count());
+  EXPECT_EQ(3, observer()->inserted_count());
+  EXPECT_EQ(-1, observer()->moved_to_index());
+
+  // Verify no action moved.
+  const base::Value::List& list_1 =
+      browser()->profile()->GetPrefs()->GetList(prefs::kPinnedActions);
+  ASSERT_EQ(3u, list_1.size());
+  EXPECT_EQ(kTestActionIdStrings[0], list_1[0].GetString());
+  EXPECT_EQ(kTestActionIdStrings[1], list_1[1].GetString());
+  EXPECT_EQ(kTestActionIdStrings[2], list_1[2].GetString());
+}
+
+// TODO(dljames): Write tests for guest and incognito mode profile that check
+// that we cannot modify the model at all.
+
+// TODO(dljames): Write tests for ids that are directly added to the prefs
+// object.
diff --git a/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.cc b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.cc
new file mode 100644
index 0000000..7e55100
--- /dev/null
+++ b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.cc
@@ -0,0 +1,47 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/toolbar/pinned_toolbar_actions_model.h"
+
+// static
+PinnedToolbarActionsModel* PinnedToolbarActionsModelFactory::GetForProfile(
+    Profile* profile) {
+  return static_cast<PinnedToolbarActionsModel*>(
+      GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+PinnedToolbarActionsModelFactory*
+PinnedToolbarActionsModelFactory::GetInstance() {
+  static base::NoDestructor<PinnedToolbarActionsModelFactory> instance;
+  return instance.get();
+}
+
+PinnedToolbarActionsModelFactory::PinnedToolbarActionsModelFactory()
+    : ProfileKeyedServiceFactory(
+          "PinnedToolbarActionsModel",
+          ProfileSelections::Builder()
+              .WithRegular(ProfileSelection::kOwnInstance)
+              .Build()) {}
+
+PinnedToolbarActionsModelFactory::~PinnedToolbarActionsModelFactory() = default;
+
+std::unique_ptr<KeyedService>
+PinnedToolbarActionsModelFactory::BuildServiceInstanceForBrowserContext(
+    content::BrowserContext* context) const {
+  return std::make_unique<PinnedToolbarActionsModel>(
+      Profile::FromBrowserContext(context));
+}
+
+bool PinnedToolbarActionsModelFactory::ServiceIsCreatedWithBrowserContext()
+    const {
+  return true;
+}
+
+bool PinnedToolbarActionsModelFactory::ServiceIsNULLWhileTesting() const {
+  return true;
+}
diff --git a/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h
new file mode 100644
index 0000000..cad7e86
--- /dev/null
+++ b/chrome/browser/ui/toolbar/pinned_toolbar_actions_model_factory.h
@@ -0,0 +1,33 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_FACTORY_H_
+#define CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_FACTORY_H_
+
+#include "base/no_destructor.h"
+#include "chrome/browser/profiles/profile_keyed_service_factory.h"
+
+class Profile;
+
+class PinnedToolbarActionsModel;
+
+class PinnedToolbarActionsModelFactory : public ProfileKeyedServiceFactory {
+ public:
+  static PinnedToolbarActionsModel* GetForProfile(Profile* profile);
+
+  static PinnedToolbarActionsModelFactory* GetInstance();
+
+ private:
+  friend base::NoDestructor<PinnedToolbarActionsModelFactory>;
+
+  PinnedToolbarActionsModelFactory();
+  ~PinnedToolbarActionsModelFactory() override;
+
+  std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
+      content::BrowserContext* profile) const override;
+  bool ServiceIsCreatedWithBrowserContext() const override;
+  bool ServiceIsNULLWhileTesting() const override;
+};
+
+#endif  // CHROME_BROWSER_UI_TOOLBAR_PINNED_TOOLBAR_ACTIONS_MODEL_FACTORY_H_
diff --git a/chrome/browser/ui/views/autofill/payments/local_card_migration_uitest.cc b/chrome/browser/ui/views/autofill/payments/local_card_migration_uitest.cc
index 7bdb22b..f8dc53d 100644
--- a/chrome/browser/ui/views/autofill/payments/local_card_migration_uitest.cc
+++ b/chrome/browser/ui/views/autofill/payments/local_card_migration_uitest.cc
@@ -61,6 +61,7 @@
 #include "components/autofill/core/browser/payments/payments_util.h"
 #include "components/autofill/core/browser/personal_data_manager.h"
 #include "components/autofill/core/browser/personal_data_manager_observer.h"
+#include "components/autofill/core/browser/personal_data_manager_test_utils.h"
 #include "components/autofill/core/browser/test_autofill_manager_waiter.h"
 #include "components/autofill/core/browser/test_event_waiter.h"
 #include "components/autofill/core/browser/webdata/autofill_table.h"
@@ -147,14 +148,6 @@
 
 }  // namespace
 
-class PersonalDataLoadedObserverMock : public PersonalDataManagerObserver {
- public:
-  PersonalDataLoadedObserverMock() = default;
-  ~PersonalDataLoadedObserverMock() override = default;
-
-  MOCK_METHOD(void, OnPersonalDataChanged, (), (override));
-};
-
 class LocalCardMigrationBrowserTest
     : public SyncTest,
       public LocalCardMigrationManager::ObserverForTest {
diff --git a/chrome/browser/ui/views/autofill/payments/offer_notification_icon_view.cc b/chrome/browser/ui/views/autofill/payments/offer_notification_icon_view.cc
index e1017e9..117fe243 100644
--- a/chrome/browser/ui/views/autofill/payments/offer_notification_icon_view.cc
+++ b/chrome/browser/ui/views/autofill/payments/offer_notification_icon_view.cc
@@ -77,6 +77,7 @@
     return;
   }
   should_extend_label_shown_duration_ = true;
+  SetPaintLabelOverSolidBackground(true);
   AnimateIn(IDS_DISCOUNT_ICON_EXPANDED_TEXT);
   controller->OnIconExpanded();
   SetAccessibilityProperties(
diff --git a/chrome/browser/ui/views/editor_menu/BUILD.gn b/chrome/browser/ui/views/editor_menu/BUILD.gn
index 3213ec6..c490e4a3 100644
--- a/chrome/browser/ui/views/editor_menu/BUILD.gn
+++ b/chrome/browser/ui/views/editor_menu/BUILD.gn
@@ -59,6 +59,8 @@
   sources = [
     "editor_menu_chip_view.cc",
     "editor_menu_chip_view.h",
+    "editor_menu_gradient_badge.cc",
+    "editor_menu_gradient_badge.h",
     "editor_menu_promo_card_view.cc",
     "editor_menu_promo_card_view.h",
     "editor_menu_textfield_view.cc",
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_browsertest.cc b/chrome/browser/ui/views/editor_menu/editor_menu_browsertest.cc
index 2afe6ae..b96faec 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_browsertest.cc
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_browsertest.cc
@@ -4,6 +4,9 @@
 
 #include "chrome/browser/ui/views/editor_menu/editor_menu_controller_impl.h"
 
+#include <string_view>
+
+#include "base/check.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/quick_answers/read_write_cards_manager_impl.h"
@@ -13,16 +16,32 @@
 #include "chromeos/constants/chromeos_features.h"
 #include "chromeos/crosapi/mojom/editor_panel.mojom.h"
 #include "content/public/test/browser_test.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "ui/display/screen.h"
 #include "ui/events/event_constants.h"
+#include "ui/views/controls/label.h"
 #include "ui/views/view.h"
 #include "ui/views/view_utils.h"
 
 namespace {
 
+using ::testing::ElementsAre;
 using ::testing::IsNull;
 using ::testing::Not;
+using ::testing::Property;
+using ::testing::SizeIs;
+
+crosapi::mojom::EditorPanelPresetTextQueryPtr CreateTestPresetTextQuery(
+    const std::string& text_query_id,
+    const std::string& name,
+    crosapi::mojom::EditorPanelPresetQueryCategory category) {
+  auto query = crosapi::mojom::EditorPanelPresetTextQuery::New();
+  query->text_query_id = text_query_id;
+  query->name = name;
+  query->category = category;
+  return query;
+}
 
 crosapi::mojom::EditorPanelContextPtr CreateTestEditorPanelContext(
     crosapi::mojom::EditorPanelMode editor_panel_mode) {
@@ -32,11 +51,35 @@
   return context;
 }
 
+crosapi::mojom::EditorPanelContextPtr
+CreateTestEditorPanelContextWithQueries() {
+  auto context =
+      CreateTestEditorPanelContext(crosapi::mojom::EditorPanelMode::kRewrite);
+  context->preset_text_queries.push_back(CreateTestPresetTextQuery(
+      "ID1", "Rephrase",
+      crosapi::mojom::EditorPanelPresetQueryCategory::kRephrase));
+  context->preset_text_queries.push_back(CreateTestPresetTextQuery(
+      "ID2", "Emojify",
+      crosapi::mojom::EditorPanelPresetQueryCategory::kEmojify));
+  context->preset_text_queries.push_back(CreateTestPresetTextQuery(
+      "ID3", "Shorten",
+      crosapi::mojom::EditorPanelPresetQueryCategory::kShorten));
+  context->preset_text_queries.push_back(CreateTestPresetTextQuery(
+      "ID4", "Elaborate",
+      crosapi::mojom::EditorPanelPresetQueryCategory::kElaborate));
+  context->preset_text_queries.push_back(CreateTestPresetTextQuery(
+      "ID5", "Formalize",
+      crosapi::mojom::EditorPanelPresetQueryCategory::kFormalize));
+  return context;
+}
+
+auto ChildrenSizeIs(int n) {
+  return Property(&views::View::children, SizeIs(n));
+}
+
 constexpr int kMarginDip = 8;
-constexpr gfx::Rect kAnchorBounds =
-    gfx::Rect(gfx::Point(500, 250), gfx::Size(80, 160));
-constexpr gfx::Rect kAnchorBoundsTop =
-    gfx::Rect(gfx::Point(500, 0), gfx::Size(80, 160));
+constexpr gfx::Rect kAnchorBounds(500, 300, 80, 160);
+constexpr gfx::Rect kAnchorBoundsTop(500, 10, 80, 160);
 
 }  // namespace
 
@@ -112,6 +155,38 @@
   GetEditorMenuView()->GetWidget()->Close();
 }
 
+IN_PROC_BROWSER_TEST_F(EditorMenuBrowserFeatureEnabledTest,
+                       ShowsRewriteUIWithChips) {
+  ASSERT_THAT(GetControllerImpl(), Not(IsNull()));
+
+  GetControllerImpl()->OnGetEditorPanelContextResultForTesting(
+      gfx::Rect(200, 300, 400, 200), CreateTestEditorPanelContextWithQueries());
+
+  // Editor menu should be showing with two rows of chips.
+  ASSERT_TRUE(views::IsViewClass<EditorMenuView>(GetEditorMenuView()));
+  const auto* chips_container =
+      views::AsViewClass<EditorMenuView>(GetEditorMenuView())
+          ->chips_container_for_testing();
+  EXPECT_THAT(chips_container->children(),
+              ElementsAre(ChildrenSizeIs(3), ChildrenSizeIs(2)));
+}
+
+IN_PROC_BROWSER_TEST_F(EditorMenuBrowserFeatureEnabledTest,
+                       ShowsWideRewriteUIWithChips) {
+  ASSERT_THAT(GetControllerImpl(), Not(IsNull()));
+
+  // Show editor menu with a wide anchor.
+  GetControllerImpl()->OnGetEditorPanelContextResultForTesting(
+      gfx::Rect(200, 300, 600, 200), CreateTestEditorPanelContextWithQueries());
+
+  // Editor menu should be wide enough to fit all chips in one row.
+  ASSERT_TRUE(views::IsViewClass<EditorMenuView>(GetEditorMenuView()));
+  const auto* chips_container =
+      views::AsViewClass<EditorMenuView>(GetEditorMenuView())
+          ->chips_container_for_testing();
+  EXPECT_THAT(chips_container->children(), ElementsAre(ChildrenSizeIs(5)));
+}
+
 IN_PROC_BROWSER_TEST_F(EditorMenuBrowserFeatureEnabledTest, CanShowPromoCard) {
   ASSERT_THAT(GetControllerImpl(), Not(IsNull()));
 
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.cc b/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.cc
new file mode 100644
index 0000000..ddb6e0f6
--- /dev/null
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.cc
@@ -0,0 +1,88 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.h"
+
+#include "ui/base/metadata/metadata_impl_macros.h"
+#include "ui/color/color_id.h"
+#include "ui/color/color_provider.h"
+#include "ui/gfx/canvas.h"
+#include "ui/gfx/font_list.h"
+#include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/geometry/skia_conversions.h"
+#include "ui/gfx/skia_paint_util.h"
+#include "ui/gfx/text_constants.h"
+#include "ui/gfx/text_utils.h"
+#include "ui/views/badge_painter.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/layout/layout_provider.h"
+#include "ui/views/view.h"
+
+namespace chromeos::editor_menu {
+
+namespace {
+
+// TODO(b/301537126): Move this to chromeos_strings.grd after the text is
+// finalised.
+constexpr char16_t kBadgeText[] = u"Experiment";
+
+// TODO(b/302209940): Replace these with color tokens to support dark mode.
+constexpr SkColor kBadgeBackgroundColorStart = SkColorSetRGB(0xB5, 0xC4, 0xFF);
+constexpr SkColor kBadgeBackgroundColorEnd = SkColorSetRGB(0xB3, 0xEF, 0xD4);
+
+}  // namespace
+
+EditorMenuGradientBadge::EditorMenuGradientBadge() = default;
+
+EditorMenuGradientBadge::~EditorMenuGradientBadge() = default;
+
+gfx::Size EditorMenuGradientBadge::CalculatePreferredSize() const {
+  return views::BadgePainter::GetBadgeSize(kBadgeText,
+                                           views::Label::GetDefaultFontList());
+}
+
+void EditorMenuGradientBadge::OnPaint(gfx::Canvas* canvas) {
+  const gfx::FontList& primary_font = views::Label::GetDefaultFontList();
+  gfx::FontList badge_font = views::BadgePainter::GetBadgeFont(primary_font);
+
+  // Calculate the bounding box for badge text.
+  const gfx::Rect badge_text_bounds(
+      gfx::Point(views::BadgePainter::kBadgeInternalPadding,
+                 gfx::GetFontCapHeightCenterOffset(primary_font, badge_font)),
+      gfx::GetStringSize(kBadgeText, badge_font));
+
+  // Outset the bounding box for padding.
+  gfx::Rect badge_outset_around_text(badge_text_bounds);
+  badge_outset_around_text.Inset(-gfx::AdjustVisualBorderForFont(
+      badge_font, gfx::Insets(views::BadgePainter::kBadgeInternalPadding)));
+
+  // Compute the rounded rect which will contain the gradient background.
+  SkPath path;
+  const int radius = views::LayoutProvider::Get()->GetCornerRadiusMetric(
+      views::ShapeContextTokens::kBadgeRadius);
+  path.addRoundRect(gfx::RectToSkRect(badge_outset_around_text), radius,
+                    radius);
+
+  // Draw the gradient background.
+  cc::PaintFlags flags;
+  flags.setBlendMode(SkBlendMode::kSrcOver);
+  flags.setShader(gfx::CreateGradientShader(
+      badge_outset_around_text.left_center(),
+      badge_outset_around_text.right_center(), kBadgeBackgroundColorStart,
+      kBadgeBackgroundColorEnd));
+  flags.setAntiAlias(true);
+  flags.setStyle(cc::PaintFlags::kFill_Style);
+  canvas->DrawPath(path, flags);
+
+  // Draw the badge text.
+  const SkColor foreground_color =
+      GetColorProvider()->GetColor(ui::kColorBadgeForeground);
+  canvas->DrawStringRect(kBadgeText, badge_font, foreground_color,
+                         badge_text_bounds);
+}
+
+BEGIN_METADATA(EditorMenuGradientBadge, views::View)
+END_METADATA
+
+}  // namespace chromeos::editor_menu
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.h b/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.h
new file mode 100644
index 0000000..1c80fd6e
--- /dev/null
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_EDITOR_MENU_EDITOR_MENU_GRADIENT_BADGE_H_
+#define CHROME_BROWSER_UI_VIEWS_EDITOR_MENU_EDITOR_MENU_GRADIENT_BADGE_H_
+
+#include "ui/base/metadata/metadata_header_macros.h"
+#include "ui/views/view.h"
+
+namespace chromeos::editor_menu {
+
+// A badge with a gradient background which is shown in the editor menu to
+// indicate it is an experimental feature.
+class EditorMenuGradientBadge : public views::View {
+ public:
+  METADATA_HEADER(EditorMenuGradientBadge);
+
+  EditorMenuGradientBadge();
+  EditorMenuGradientBadge(const EditorMenuGradientBadge&) = delete;
+  EditorMenuGradientBadge& operator=(const EditorMenuGradientBadge&) = delete;
+  ~EditorMenuGradientBadge() override;
+
+  // View:
+  gfx::Size CalculatePreferredSize() const override;
+  void OnPaint(gfx::Canvas* canvas) override;
+};
+
+}  // namespace chromeos::editor_menu
+
+#endif  // CHROME_BROWSER_UI_VIEWS_EDITOR_MENU_EDITOR_MENU_GRADIENT_BADGE_H_
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.cc b/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.cc
index d9d2df75..a8efe782 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.cc
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.cc
@@ -11,22 +11,20 @@
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ui/views/editor_menu/editor_menu_view_delegate.h"
 #include "components/vector_icons/vector_icons.h"
-#include "ui/base/metadata/metadata_header_macros.h"
 #include "ui/base/metadata/metadata_impl_macros.h"
 #include "ui/base/models/image_model.h"
-#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
 #include "ui/color/color_id.h"
 #include "ui/gfx/geometry/insets.h"
+#include "ui/gfx/geometry/size.h"
 #include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/animation/ink_drop.h"
 #include "ui/views/background.h"
 #include "ui/views/border.h"
 #include "ui/views/controls/button/image_button.h"
-#include "ui/views/controls/focus_ring.h"
-#include "ui/views/controls/highlight_path_generator.h"
+#include "ui/views/controls/focusable_border.h"
 #include "ui/views/controls/textfield/textfield.h"
-#include "ui/views/layout/box_layout.h"
-#include "ui/views/layout/flex_layout_view.h"
+#include "ui/views/layout/fill_layout.h"
+#include "ui/views/layout/layout_provider.h"
 #include "ui/views/view.h"
 #include "ui/views/view_class_properties.h"
 #include "ui/views/view_utils.h"
@@ -38,47 +36,7 @@
 
 constexpr char16_t kContainerTitle[] = u"Editor Menu Textfield";
 
-constexpr int kConatinerHeightDip = 30;
-constexpr int kBackgroundRadiusDip = 8;
-constexpr gfx::Insets kContainerInsets = gfx::Insets::TLBR(0, 16, 0, 6);
-constexpr int kTextIconSpacingDip = 8;
-constexpr int kButtonSizeDip = 32;
-constexpr int kBorderThicknessDip = 1;
-
-class EditorMenuTextfield : public views::Textfield {
- public:
-  METADATA_HEADER(EditorMenuTextfield);
-  EditorMenuTextfield() : views::Textfield() {
-    // TODO(b/300857651): Add a custom hover effect which covers the whole
-    // textfield container view. For now, just disable the default hover effect
-    // since it looks strange to only partially cover the textfield container.
-    RemoveHoverEffect();
-  }
-  EditorMenuTextfield(const EditorMenuTextfield&) = delete;
-  EditorMenuTextfield& operator=(const EditorMenuTextfield&) = delete;
-  ~EditorMenuTextfield() override = default;
-
-  void OnFocus() override {
-    views::Textfield::OnFocus();
-    NotifyTextfieldFocusChanged();
-  }
-
-  void OnBlur() override {
-    views::Textfield::OnBlur();
-    NotifyTextfieldFocusChanged();
-  }
-
- private:
-  void NotifyTextfieldFocusChanged() {
-    auto* textfield_container =
-        views::AsViewClass<EditorMenuTextfieldView>(parent());
-    CHECK(textfield_container);
-    textfield_container->OnTextfieldFocusChanged();
-  }
-};
-
-BEGIN_METADATA(EditorMenuTextfield, views::Textfield)
-END_METADATA
+constexpr gfx::Size kArrowButtonSize(32, 32);
 
 }  // namespace
 
@@ -86,20 +44,6 @@
     EditorMenuViewDelegate* delegate)
     : delegate_(delegate) {
   CHECK(delegate_);
-
-  // Install a focus ring to show when `textfield_` is focused. This focus ring
-  // is installed on the EditorMenuTextfieldView so that it surrounds the
-  // overall textfield container.
-  views::FocusRing::Install(this);
-  views::FocusRing::Get(this)->SetHasFocusPredicate(
-      base::BindRepeating([](const View* view) {
-        const auto* v = views::AsViewClass<EditorMenuTextfieldView>(view);
-        CHECK(v);
-        return v->textfield_ && v->textfield_->HasFocus();
-      }));
-  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);
-  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
-                                                kBackgroundRadiusDip);
 }
 
 EditorMenuTextfieldView::~EditorMenuTextfieldView() = default;
@@ -109,8 +53,12 @@
   InitLayout();
 }
 
-int EditorMenuTextfieldView::GetHeightForWidth(int width) const {
-  return kConatinerHeightDip;
+void EditorMenuTextfieldView::Layout() {
+  View::Layout();
+
+  // Place the arrow button at the right end of the textfield.
+  arrow_button_->SetBounds(width() - kArrowButtonSize.width(), 0,
+                           kArrowButtonSize.width(), height());
 }
 
 void EditorMenuTextfieldView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
@@ -135,35 +83,18 @@
   return true;
 }
 
-void EditorMenuTextfieldView::OnTextfieldFocusChanged() {
-  // The focus ring should be shown when the underlying `textfield_` is focused.
-  // Schedule a repaint to update its visibility if needed.
-  if (views::FocusRing::Get(this)) {
-    views::FocusRing::Get(this)->SchedulePaint();
-  }
-}
-
 void EditorMenuTextfieldView::InitLayout() {
-  SetBackground(views::CreateThemedRoundedRectBackground(
-      static_cast<ui::ColorId>(cros_tokens::kCrosSysSystemBaseElevated),
-      kBackgroundRadiusDip));
-  SetBorder(views::CreateThemedRoundedRectBorder(
-      kBorderThicknessDip, kBackgroundRadiusDip, ui::kColorSysNeutralOutline));
+  SetLayoutManager(std::make_unique<views::FillLayout>());
 
-  auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
-      views::BoxLayout::Orientation::kHorizontal, kContainerInsets,
-      kTextIconSpacingDip));
-  layout->set_cross_axis_alignment(
-      views::BoxLayout::CrossAxisAlignment::kCenter);
-
-  textfield_ = AddChildView(std::make_unique<EditorMenuTextfield>());
-  textfield_->SetAccessibleName(kContainerTitle);
+  textfield_ = AddChildView(std::make_unique<views::Textfield>());
   textfield_->set_controller(this);
-  textfield_->SetBorder(views::NullBorder());
-  textfield_->SetBackgroundColor(SK_ColorTRANSPARENT);
   textfield_->SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT);
+  textfield_->SetAccessibleName(kContainerTitle);
   textfield_->SetPlaceholderText(kContainerTitle);
-  layout->SetFlexForView(textfield_, 1, /*use_min_size=*/true);
+  textfield_->SetBackgroundColor(SK_ColorTRANSPARENT);
+  textfield_->RemoveHoverEffect();
+  textfield_->SetExtraInsets(
+      gfx::Insets::TLBR(0, 0, 0, kArrowButtonSize.width()));
 
   arrow_button_ =
       AddChildView(std::make_unique<views::ImageButton>(base::BindRepeating(
@@ -178,7 +109,7 @@
       views::ImageButton::HorizontalAlignment::ALIGN_CENTER);
   arrow_button_->SetImageVerticalAlignment(
       views::ImageButton::VerticalAlignment::ALIGN_MIDDLE);
-  arrow_button_->SetPreferredSize(gfx::Size(kButtonSizeDip, kButtonSizeDip));
+  arrow_button_->SetPreferredSize(kArrowButtonSize);
   arrow_button_->SetVisible(false);
   views::InkDrop::Get(arrow_button_)
       ->SetMode(views::InkDropHost::InkDropMode::ON);
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h b/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h
index 654df69..3aabd62 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h
@@ -36,15 +36,12 @@
   EditorMenuTextfieldView& operator=(const EditorMenuTextfieldView&) = delete;
   ~EditorMenuTextfieldView() override;
 
-  views::ImageButton* CreateArrowButton(
-      const base::RepeatingClosure& button_callback);
-
   views::ImageButton* arrow_button() { return arrow_button_; }
   views::Textfield* textfield() { return textfield_; }
 
   // views::View:
+  void Layout() override;
   void AddedToWidget() override;
-  int GetHeightForWidth(int width) const override;
   void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
 
   // views::TextfieldController:
@@ -53,8 +50,6 @@
   bool HandleKeyEvent(views::Textfield* sender,
                       const ui::KeyEvent& key_event) override;
 
-  void OnTextfieldFocusChanged();
-
  private:
   void InitLayout();
   void OnTextfieldArrowButtonPressed();
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_view.cc b/chrome/browser/ui/views/editor_menu/editor_menu_view.cc
index 96543577..4317e50 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_view.cc
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_view.cc
@@ -6,6 +6,7 @@
 
 #include <array>
 #include <string_view>
+#include <utility>
 #include <vector>
 
 #include "base/functional/bind.h"
@@ -13,6 +14,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/task/sequenced_task_runner.h"
 #include "chrome/browser/ui/views/editor_menu/editor_menu_chip_view.h"
+#include "chrome/browser/ui/views/editor_menu/editor_menu_gradient_badge.h"
 #include "chrome/browser/ui/views/editor_menu/editor_menu_textfield_view.h"
 #include "chrome/browser/ui/views/editor_menu/editor_menu_view_delegate.h"
 #include "chrome/browser/ui/views/editor_menu/utils/pre_target_handler.h"
@@ -26,14 +28,12 @@
 #include "ui/color/color_provider.h"
 #include "ui/compositor/layer.h"
 #include "ui/compositor/layer_owner.h"
-#include "ui/gfx/canvas.h"
 #include "ui/gfx/geometry/insets.h"
 #include "ui/gfx/geometry/rect.h"
-#include "ui/gfx/geometry/rounded_corners_f.h"
-#include "ui/gfx/skia_paint_util.h"
 #include "ui/views/accessibility/view_accessibility.h"
 #include "ui/views/animation/ink_drop.h"
 #include "ui/views/background.h"
+#include "ui/views/badge_painter.h"
 #include "ui/views/controls/button/image_button.h"
 #include "ui/views/controls/label.h"
 #include "ui/views/controls/textfield/textfield.h"
@@ -44,6 +44,7 @@
 #include "ui/views/layout/layout_manager.h"
 #include "ui/views/style/typography.h"
 #include "ui/views/view.h"
+#include "ui/views/view_utils.h"
 #include "ui/views/widget/widget.h"
 
 namespace chromeos::editor_menu {
@@ -53,20 +54,10 @@
 constexpr char kWidgetName[] = "EditorMenuViewWidget";
 constexpr char16_t kContainerTitle[] = u"Editor Menu";
 
-// TODO(b/302043981): Dynamically size the editor menu view according to the
-// anchor view bounds, instead of always using this min width.
-constexpr int kContainerMinWidthDip = 320;
 constexpr int kRadiusDip = 4;
 
 constexpr gfx::Insets kTitleContainerInsets = gfx::Insets::TLBR(10, 16, 10, 10);
 
-constexpr char16_t kBadgeText[] = u"Experiment";
-constexpr gfx::Insets kBadgeInsets = gfx::Insets::VH(0, 8);
-constexpr int kBadgeHorizontalPaddingDip = 8;
-constexpr int kBadgeVerticalPaddingDip = 8;
-constexpr SkColor kBadgeBackgroundColorStart = SkColorSetRGB(0xB5, 0xC4, 0xFF);
-constexpr SkColor kBadgeBackgroundColorEnd = SkColorSetRGB(0xB3, 0xEF, 0xD4);
-
 constexpr char16_t kSettingsTooltipString[] = u"Settings";
 constexpr int kSettingsIconSizeDip = 20;
 constexpr int kSettingsButtonBorderDip = 4;
@@ -74,53 +65,11 @@
 constexpr int kChipsContainerVerticalSpacingDip = 16;
 constexpr gfx::Insets kChipsMargin =
     gfx::Insets::TLBR(0, 8, kChipsContainerVerticalSpacingDip, 0);
-constexpr gfx::Insets kChipsContainerInsets = gfx::Insets::TLBR(0, 8, 0, 8);
+constexpr gfx::Insets kChipsContainerInsets = gfx::Insets::VH(0, 16);
 
 constexpr gfx::Insets kTextfieldContainerInsets =
     gfx::Insets::TLBR(0, 16, 10, 16);
 
-// A background that fills the canvas with rounded corners and gradient colors.
-class GradientRoundedRectBackground : public views::Background {
- public:
-  GradientRoundedRectBackground(float radius,
-                                SkColor start_color,
-                                SkColor end_color)
-      : radii_(gfx::RoundedCornersF{radius}),
-        start_color_(start_color),
-        end_color_(end_color) {}
-
-  GradientRoundedRectBackground(const GradientRoundedRectBackground&) = delete;
-  GradientRoundedRectBackground& operator=(
-      const GradientRoundedRectBackground&) = delete;
-
-  ~GradientRoundedRectBackground() override = default;
-
-  // views::Background:
-  void Paint(gfx::Canvas* canvas, views::View* view) const override {
-    const auto& bounds = view->GetContentsBounds();
-    gfx::Rect rect(bounds);
-    SkPath path;
-    SkScalar radii[8] = {radii_.upper_left(),  radii_.upper_left(),
-                         radii_.upper_right(), radii_.upper_right(),
-                         radii_.lower_right(), radii_.lower_right(),
-                         radii_.lower_left(),  radii_.lower_left()};
-    path.addRoundRect(gfx::RectToSkRect(rect), radii);
-
-    cc::PaintFlags flags;
-    flags.setBlendMode(SkBlendMode::kSrcOver);
-    flags.setShader(gfx::CreateGradientShader(
-        bounds.left_center(), bounds.right_center(), start_color_, end_color_));
-    flags.setAntiAlias(true);
-    flags.setStyle(cc::PaintFlags::kFill_Style);
-    canvas->DrawPath(path, flags);
-  }
-
- private:
-  const gfx::RoundedCornersF radii_;
-  const SkColor start_color_;
-  const SkColor end_color_;
-};
-
 }  // namespace
 
 EditorMenuView::EditorMenuView(const PresetTextQueries& preset_text_queries,
@@ -207,8 +156,7 @@
 
 void EditorMenuView::UpdateBounds(const gfx::Rect& anchor_view_bounds) {
   const int editor_menu_width = GetEditorMenuWidth(anchor_view_bounds.width());
-  // TODO(b/302241013): Adjust chips container layout according to the updated
-  // editor menu width.
+  UpdateChipsContainer(editor_menu_width);
 
   GetWidget()->SetBounds(GetEditorMenuBounds(
       anchor_view_bounds,
@@ -244,20 +192,11 @@
       views::style::STYLE_HEADLINE_5));
   title->SetEnabledColorId(ui::kColorSysOnSurface);
 
-  auto* badge =
-      title_container_->AddChildView(std::make_unique<views::FlexLayoutView>());
-  badge->SetMainAxisAlignment(views::LayoutAlignment::kCenter);
-  badge->SetCrossAxisAlignment(views::LayoutAlignment::kCenter);
-  badge->SetProperty(views::kMarginsKey, kBadgeInsets);
-  auto* text = badge->AddChildView(std::make_unique<views::Label>(
-      kBadgeText, views::style::CONTEXT_LABEL, views::style::STYLE_BODY_2));
-  text->SetEnabledColorId(ui::kColorSysOnSurface);
-  badge->SetPreferredSize(gfx::Size(
-      text->GetPreferredSize().width() + 2 * kBadgeHorizontalPaddingDip,
-      text->GetPreferredSize().height() + 2 * kBadgeVerticalPaddingDip));
-  float radius = badge->GetPreferredSize().height() / 2.0f;
-  badge->SetBackground(std::make_unique<GradientRoundedRectBackground>(
-      radius, kBadgeBackgroundColorStart, kBadgeBackgroundColorEnd));
+  auto* badge = title_container_->AddChildView(
+      std::make_unique<EditorMenuGradientBadge>());
+  badge->SetProperty(
+      views::kMarginsKey,
+      gfx::Insets::VH(0, views::BadgePainter::kBadgeHorizontalMargin));
 
   auto* spacer =
       title_container_->AddChildView(std::make_unique<views::View>());
@@ -292,42 +231,18 @@
     const PresetTextQueries& preset_text_queries) {
   chips_container_ = AddChildView(std::make_unique<views::FlexLayoutView>());
   chips_container_->SetOrientation(views::LayoutOrientation::kVertical);
+  chips_container_->SetProperty(views::kMarginsKey, kChipsContainerInsets);
 
-  // Add a new row if the chip cannot fit the rest space in the current row.
-  // A simple calculation of the running width, considering the margins and
-  // paddings.
-  int running_width = 0;
-  views::View* row = nullptr;
+  // Put all the chips in a single row while we are initially creating the
+  // editor menu. This layout will be adjusted once the editor menu bounds are
+  // set.
+  auto* row = AddChipsRow();
   for (const auto& preset_text_query : preset_text_queries) {
-    auto chip = std::make_unique<EditorMenuChipView>(
+    row->AddChildView(std::make_unique<EditorMenuChipView>(
         base::BindRepeating(&EditorMenuView::OnChipButtonPressed,
                             weak_factory_.GetWeakPtr(),
                             preset_text_query.text_query_id),
-        preset_text_query);
-
-    int chip_width = chip->GetPreferredSize().width();
-    if (running_width == 0) {
-      // Add the container's left insets.
-      running_width += kChipsContainerInsets.left();
-      running_width += chip_width;
-    } else {
-      // Add the chip's left margin.
-      running_width += kChipsMargin.left();
-      running_width += chip_width;
-    }
-    // Add the containers's right insets when decide if wrap the row.
-    const bool should_wrap_row =
-        running_width + kChipsContainerInsets.right() > kContainerMinWidthDip;
-    if (row == nullptr || should_wrap_row) {
-      running_width = should_wrap_row ? 0 : running_width;
-      row = chips_container_->AddChildView(std::make_unique<views::View>());
-      auto* layout =
-          row->SetLayoutManager(std::make_unique<views::FlexLayout>());
-      layout->SetOrientation(views::LayoutOrientation::kHorizontal)
-          .SetInteriorMargin(kChipsContainerInsets)
-          .SetDefault(views::kMarginsKey, kChipsMargin);
-    }
-    chips_.emplace_back(row->AddChildView(std::move(chip)));
+        preset_text_query));
   }
 }
 
@@ -337,6 +252,53 @@
   textfield_->SetProperty(views::kMarginsKey, kTextfieldContainerInsets);
 }
 
+void EditorMenuView::UpdateChipsContainer(int editor_menu_width) {
+  // Remove chips from their current rows and transfer ownership since we want
+  // to add the chips to new rows.
+  std::vector<std::unique_ptr<EditorMenuChipView>> chips;
+  for (auto* row : chips_container_->children()) {
+    while (!row->children().empty()) {
+      chips.push_back(row->RemoveChildViewT(
+          views::AsViewClass<EditorMenuChipView>(row->children()[0])));
+    }
+  }
+
+  // Remove existing rows from the chips container and delete them.
+  chips_container_->RemoveAllChildViews();
+
+  // Re-add the chips into new rows in the chips container, adjusting the layout
+  // according to the updated editor menu width. We keep track of the running
+  // width of the current chip row and start a new row of chips whenever the
+  // chip being added can't fit into the current row.
+  const int chip_container_width =
+      editor_menu_width - kChipsContainerInsets.width();
+  int running_width = 0;
+  views::View* row = nullptr;
+  for (auto& chip : chips) {
+    const int chip_width = chip->GetPreferredSize().width();
+    if (row != nullptr && running_width + chip_width + kChipsMargin.left() <=
+                              chip_container_width) {
+      // Add the chip to the current row if it can fit (including space for
+      // padding between chips).
+      running_width += chip_width + kChipsMargin.left();
+    } else {
+      // Otherwise, create a new row for the chip.
+      row = AddChipsRow();
+      running_width = chip_width;
+    }
+    row->AddChildView(std::move(chip));
+  }
+}
+
+views::View* EditorMenuView::AddChipsRow() {
+  auto* row =
+      chips_container_->AddChildView(std::make_unique<views::FlexLayoutView>());
+  row->SetCollapseMargins(true);
+  row->SetIgnoreDefaultMainAxisMargins(true);
+  row->SetDefault(views::kMarginsKey, kChipsMargin);
+  return row;
+}
+
 void EditorMenuView::OnSettingsButtonPressed() {
   CHECK(delegate_);
   delegate_->OnSettingsButtonPressed();
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_view.h b/chrome/browser/ui/views/editor_menu/editor_menu_view.h
index 9492ac7..901e797 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_view.h
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_view.h
@@ -19,11 +19,11 @@
 namespace views {
 class ImageButton;
 class FlexLayoutView;
+class View;
 }  // namespace views
 
 namespace chromeos::editor_menu {
 
-class EditorMenuChipView;
 class EditorMenuTextfieldView;
 class EditorMenuViewDelegate;
 class PreTargetHandler;
@@ -60,8 +60,8 @@
 
   void UpdateBounds(const gfx::Rect& anchor_view_bounds);
 
-  const std::vector<raw_ptr<EditorMenuChipView>>& chips_for_testing() const {
-    return chips_;
+  const views::View* chips_container_for_testing() const {
+    return chips_container_;
   }
 
  private:
@@ -70,6 +70,10 @@
   void AddChipsContainer(const PresetTextQueries& preset_text_queries);
   void AddTextfield();
 
+  void UpdateChipsContainer(int editor_menu_width);
+
+  views::View* AddChipsRow();
+
   void OnSettingsButtonPressed();
   void OnChipButtonPressed(const std::string& text_query_id);
 
@@ -86,7 +90,6 @@
 
   // Containing chips.
   raw_ptr<views::FlexLayoutView> chips_container_ = nullptr;
-  std::vector<raw_ptr<EditorMenuChipView>> chips_;
 
   raw_ptr<EditorMenuTextfieldView> textfield_ = nullptr;
 
diff --git a/chrome/browser/ui/views/editor_menu/editor_menu_view_unittest.cc b/chrome/browser/ui/views/editor_menu/editor_menu_view_unittest.cc
index 3f8ef8a..43a032f 100644
--- a/chrome/browser/ui/views/editor_menu/editor_menu_view_unittest.cc
+++ b/chrome/browser/ui/views/editor_menu/editor_menu_view_unittest.cc
@@ -20,6 +20,8 @@
 
 namespace {
 
+using ::testing::SizeIs;
+
 class MockEditorMenuViewDelegate : public EditorMenuViewDelegate {
  public:
   MockEditorMenuViewDelegate() = default;
@@ -38,21 +40,30 @@
   void OnEditorMenuVisibilityChanged(bool visible) override {}
 };
 
+std::u16string_view GetChipLabel(const views::View* chip) {
+  CHECK(views::IsViewClass<EditorMenuChipView>(chip));
+  return views::AsViewClass<EditorMenuChipView>(chip)->GetText();
+}
+
 using EditorMenuViewTest = views::ViewsTestBase;
 
 TEST_F(EditorMenuViewTest, CreatesChips) {
   MockEditorMenuViewDelegate delegate;
   const PresetTextQueries queries = {
-      PresetTextQuery("Query ID 1", u"Label 1", PresetQueryCategory::kUnknown),
-      PresetTextQuery("Query ID 2", u"Label 2", PresetQueryCategory::kUnknown)};
+      PresetTextQuery("ID1", u"Shorten", PresetQueryCategory::kShorten),
+      PresetTextQuery("ID2", u"Elaborate", PresetQueryCategory::kElaborate)};
 
   EditorMenuView editor_menu_view =
-      EditorMenuView(queries, gfx::Rect(200, 300, 80, 200), &delegate);
+      EditorMenuView(queries, gfx::Rect(200, 300, 400, 200), &delegate);
 
-  const auto& editor_menu_chips = editor_menu_view.chips_for_testing();
-  ASSERT_THAT(editor_menu_chips, testing::SizeIs(queries.size()));
-  EXPECT_EQ(editor_menu_chips[0]->GetText(), queries[0].name);
-  EXPECT_EQ(editor_menu_chips[1]->GetText(), queries[1].name);
+  // Chips should be in a single row.
+  const auto* chips_container = editor_menu_view.chips_container_for_testing();
+  ASSERT_THAT(chips_container->children(), SizeIs(1));
+  const auto* chip_row = chips_container->children()[0];
+  ASSERT_THAT(chip_row->children(), SizeIs(queries.size()));
+  // Chips should have correct text labels.
+  EXPECT_EQ(GetChipLabel(chip_row->children()[0]), queries[0].name);
+  EXPECT_EQ(GetChipLabel(chip_row->children()[1]), queries[1].name);
 }
 
 }  // namespace
diff --git a/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.cc b/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.cc
index cfddfedf..1fd1ddd 100644
--- a/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.cc
+++ b/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.cc
@@ -204,8 +204,21 @@
 };
 
 void DefinitelyExitPictureInPicture(
-    PictureInPictureBrowserFrameView& frame_view) {
-  if (!PictureInPictureWindowManager::GetInstance()->ExitPictureInPicture()) {
+    PictureInPictureBrowserFrameView& frame_view,
+    PictureInPictureWindowManager::UiBehavior behavior) {
+  switch (behavior) {
+    case PictureInPictureWindowManager::UiBehavior::kCloseWindowOnly:
+    case PictureInPictureWindowManager::UiBehavior::kCloseWindowAndPauseVideo:
+      frame_view.set_close_reason(
+          PictureInPictureBrowserFrameView::CloseReason::kCloseButton);
+      break;
+    case PictureInPictureWindowManager::UiBehavior::kCloseWindowAndFocusOpener:
+      frame_view.set_close_reason(
+          PictureInPictureBrowserFrameView::CloseReason::kBackToTabButton);
+      break;
+  }
+  if (!PictureInPictureWindowManager::GetInstance()
+           ->ExitPictureInPictureViaWindowUi(behavior)) {
     // If the picture-in-picture controller has been disconnected for
     // some reason, then just manually close the window to prevent
     // getting into a state where the back to tab button no longer
@@ -497,9 +510,9 @@
   back_to_tab_button_ = button_container_view_->AddChildView(
       std::make_unique<BackToTabButton>(base::BindRepeating(
           [](PictureInPictureBrowserFrameView* frame_view) {
-            frame_view->close_reason_ = CloseReason::kBackToTabButton;
-            PictureInPictureWindowManager::GetInstance()->FocusInitiator();
-            DefinitelyExitPictureInPicture(*frame_view);
+            DefinitelyExitPictureInPicture(
+                *frame_view, PictureInPictureWindowManager::UiBehavior::
+                                 kCloseWindowAndFocusOpener);
           },
           base::Unretained(this))));
 
@@ -507,8 +520,9 @@
   close_image_button_ = button_container_view_->AddChildView(
       std::make_unique<CloseImageButton>(base::BindRepeating(
           [](PictureInPictureBrowserFrameView* frame_view) {
-            frame_view->close_reason_ = CloseReason::kCloseButton;
-            DefinitelyExitPictureInPicture(*frame_view);
+            DefinitelyExitPictureInPicture(
+                *frame_view,
+                PictureInPictureWindowManager::UiBehavior::kCloseWindowOnly);
           },
           base::Unretained(this))));
 
diff --git a/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.h b/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.h
index 15393837..061bd69 100644
--- a/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.h
+++ b/chrome/browser/ui/views/frame/picture_in_picture_browser_frame_view.h
@@ -209,7 +209,6 @@
   views::View* GetCloseButtonForTesting();
   views::Label* GetWindowTitleForTesting();
 
- private:
   // These values are persisted to logs. Entries should not be renumbered and
   // numeric values should never be reused.
   enum class CloseReason {
@@ -219,6 +218,11 @@
     kMaxValue = kCloseButton
   };
 
+  void set_close_reason(CloseReason close_reason) {
+    close_reason_ = close_reason;
+  }
+
+ private:
   CloseReason close_reason_ = CloseReason::kOther;
 
 #if RESIZE_DOCUMENT_PICTURE_IN_PICTURE_TO_DIALOG
diff --git a/chrome/browser/ui/views/media_router/cast_dialog_sink_view.cc b/chrome/browser/ui/views/media_router/cast_dialog_sink_view.cc
index 5c457dc..86855015 100644
--- a/chrome/browser/ui/views/media_router/cast_dialog_sink_view.cc
+++ b/chrome/browser/ui/views/media_router/cast_dialog_sink_view.cc
@@ -49,6 +49,8 @@
 
 std::unique_ptr<views::Label> CreateSubtitle(
     const media_router::UIMediaSink& sink) {
+  // TODO(crbug.com/1486989): Error messages in Harmony dialog are not
+  // dismissible.
   auto subtitle = std::make_unique<views::Label>(sink.GetStatusTextForDisplay(),
                                                  views::style::CONTEXT_BUTTON,
                                                  views::style::STYLE_SECONDARY);
@@ -133,7 +135,7 @@
                                views::MaximumFlexSizeRule::kUnbounded));
   label_wrapper->SetProperty(views::kMarginsKey,
                              gfx::Insets::VH(vertical_spacing, 0));
-  label_wrapper->SetCanProcessEventsWithinSubtree(false);
+
   label_container->AddChildView(std::move(label_wrapper));
 
   return label_container;
diff --git a/chrome/browser/ui/views/overlay/video_overlay_window_views.cc b/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
index 692ce6c..50c222d23 100644
--- a/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
+++ b/chrome/browser/ui/views/overlay/video_overlay_window_views.cc
@@ -484,7 +484,10 @@
 #if BUILDFLAG(IS_WIN)
   if (event->type() == ui::ET_KEY_PRESSED && event->IsAltDown() &&
       event->key_code() == ui::VKEY_F4) {
-    GetController()->Close(true /* should_pause_video */);
+    PictureInPictureWindowManager::GetInstance()
+        ->ExitPictureInPictureViaWindowUi(
+            PictureInPictureWindowManager::UiBehavior::
+                kCloseWindowAndPauseVideo);
     event->SetHandled();
   }
 #endif  // BUILDFLAG(IS_WIN)
@@ -734,14 +737,23 @@
           [](VideoOverlayWindowViews* overlay) {
             // Only pause the video if play/pause is available.
             const bool should_pause_video = overlay->show_play_pause_button_;
-            overlay->controller_->Close(should_pause_video);
+            PictureInPictureWindowManager::GetInstance()
+                ->ExitPictureInPictureViaWindowUi(
+                    should_pause_video
+                        ? PictureInPictureWindowManager::UiBehavior::
+                              kCloseWindowAndPauseVideo
+                        : PictureInPictureWindowManager::UiBehavior::
+                              kCloseWindowOnly);
             overlay->RecordButtonPressed(OverlayWindowControl::kClose);
           },
           base::Unretained(this)));
   auto back_to_tab_label_button =
       std::make_unique<BackToTabLabelButton>(base::BindRepeating(
           [](VideoOverlayWindowViews* overlay) {
-            overlay->controller_->CloseAndFocusInitiator();
+            PictureInPictureWindowManager::GetInstance()
+                ->ExitPictureInPictureViaWindowUi(
+                    PictureInPictureWindowManager::UiBehavior::
+                        kCloseWindowAndFocusOpener);
             overlay->RecordButtonPressed(OverlayWindowControl::kBackToTab);
           },
           base::Unretained(this)));
@@ -1422,7 +1434,10 @@
     RecordTapGesture(OverlayWindowControl::kSkipAd);
     event->SetHandled();
   } else if (GetCloseControlsBounds().Contains(event->location())) {
-    controller_->Close(true /* should_pause_video */);
+    PictureInPictureWindowManager::GetInstance()
+        ->ExitPictureInPictureViaWindowUi(
+            PictureInPictureWindowManager::UiBehavior::
+                kCloseWindowAndPauseVideo);
     RecordTapGesture(OverlayWindowControl::kClose);
     event->SetHandled();
   } else if (GetPlayPauseControlsBounds().Contains(event->location())) {
diff --git a/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc b/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
index 0a8ad52..a6caf9ea 100644
--- a/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
+++ b/chrome/browser/ui/views/overlay/video_overlay_window_views_unittest.cc
@@ -12,6 +12,7 @@
 #include "build/build_config.h"
 #include "build/chromeos_buildflags.h"
 #include "chrome/browser/picture_in_picture/auto_pip_setting_overlay_view.h"
+#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
 #include "chrome/browser/ui/views/overlay/close_image_button.h"
 #include "chrome/browser/ui/views/overlay/simple_overlay_window_image_button.h"
 #include "chrome/test/base/testing_profile.h"
@@ -468,6 +469,8 @@
   // When the play/pause controls are visible, closing via the close button
   // should pause the video.
   overlay_window().SetPlayPauseButtonVisibility(true);
+  PictureInPictureWindowManager::GetInstance()
+      ->set_window_controller_for_testing(&pip_window_controller());
   EXPECT_CALL(pip_window_controller(), Close(true));
   close_button_clicker.NotifyClick(dummy_event);
   testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
@@ -478,6 +481,8 @@
   EXPECT_CALL(pip_window_controller(), Close(false));
   close_button_clicker.NotifyClick(dummy_event);
   testing::Mock::VerifyAndClearExpectations(&pip_window_controller());
+  PictureInPictureWindowManager::GetInstance()
+      ->set_window_controller_for_testing(nullptr);
 }
 
 TEST_F(VideoOverlayWindowViewsTest, PauseOnWidgetCloseWhenPauseAvailable) {
diff --git a/chrome/browser/ui/views/page_action/page_action_icon_controller.cc b/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
index 723a284..ca602db 100644
--- a/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
+++ b/chrome/browser/ui/views/page_action/page_action_icon_controller.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/views/page_action/page_action_icon_controller.h"
 
+#include "base/containers/contains.h"
 #include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/immediate_crash.h"
@@ -371,9 +372,7 @@
   }
   std::vector<PageActionIconView*> excluded_actions_on_page =
       page_actions_excluded_from_logging_[url];
-  if (!view->ephemeral() || std::find(excluded_actions_on_page.begin(),
-                                      excluded_actions_on_page.end(),
-                                      view) != excluded_actions_on_page.end()) {
+  if (!view->ephemeral() || base::Contains(excluded_actions_on_page, view)) {
     return;
   }
   RecordOverallMetrics();
@@ -445,9 +444,7 @@
   RecordOverallMetrics();
   for (auto icon_item : page_action_icon_views_) {
     if (!icon_item.second->ephemeral() || !icon_item.second->GetVisible() ||
-        std::find(excluded_actions_on_page.begin(),
-                  excluded_actions_on_page.end(),
-                  icon_item.second) != excluded_actions_on_page.end()) {
+        base::Contains(excluded_actions_on_page, icon_item.second)) {
       continue;
     }
     RecordIndividualMetrics(icon_item.first, icon_item.second);
diff --git a/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc b/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
index 5a4fbaf1..8f91d793 100644
--- a/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
+++ b/chrome/browser/ui/views/profiles/profile_picker_view_browsertest.cc
@@ -1808,17 +1808,11 @@
   EXPECT_EQ(new_browser->profile()->GetPath(), other_path);
   WaitForPickerClosed();
 
-// TODO(crbug.com/1447955): This check fails in version skew test. Instead of
-// filtering out tests on version skew bots, just disable the part of test for
-// now. Re-enable this check for Lacros once crrev.com/c/4542121 is in Ash
-// stable.
-#if !BUILDFLAG(IS_CHROMEOS_LACROS)
   histogram_tester.ExpectTotalCount(
       "ProfilePicker.FirstProfileTime.FirstWebContentsNonEmptyPaint", 1);
   histogram_tester.ExpectUniqueSample(
       "ProfilePicker.FirstProfileTime.FirstWebContentsFinishReason",
       metrics::StartupProfilingFinishReason::kDone, 1);
-#endif
 }
 
 // TODO(crbug.com/1289326) Test is flaky on Linux CFI, Linux dbg, Mac ASan
diff --git a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.cc b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.cc
index d0e4b798..2d554731 100644
--- a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.cc
+++ b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.cc
@@ -78,9 +78,8 @@
 
 void ReadAnythingCoordinator::InitModelWithUserPrefs() {
   Browser* browser = &GetBrowser();
-  if (!browser->profile() || !browser->profile()->GetPrefs()) {
+  if (!browser->profile() || !browser->profile()->GetPrefs())
     return;
-  }
 
   // Get user's default language to check for compatible fonts.
   language::LanguageModel* language_model =
@@ -135,9 +134,9 @@
   // Read Anything as a side panel entry observer.
   Browser* browser = &GetBrowser();
   BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
-  if (!browser_view) {
+  if (!browser_view)
     return;
-  }
+
   SidePanelRegistry* global_registry =
       SidePanelCoordinator::GetGlobalSidePanelRegistry(browser);
   global_registry->Deregister(
@@ -147,8 +146,8 @@
   Observe(nullptr);
 }
 
-void ReadAnythingCoordinator::CreateAndRegisterSidePanelEntry(
-    SidePanelRegistry* registry) {
+void ReadAnythingCoordinator::CreateAndRegisterEntry(
+    SidePanelRegistry* global_registry) {
   auto side_panel_entry = std::make_unique<SidePanelEntry>(
       SidePanelEntry::Id::kReadAnything,
       l10n_util::GetStringUTF16(IDS_READING_MODE_TITLE),
@@ -157,7 +156,7 @@
       base::BindRepeating(&ReadAnythingCoordinator::CreateContainerView,
                           base::Unretained(this)));
   side_panel_entry->AddObserver(this);
-  registry->Register(std::move(side_panel_entry));
+  global_registry->Register(std::move(side_panel_entry));
 }
 
 ReadAnythingController* ReadAnythingCoordinator::GetController() {
@@ -264,17 +263,6 @@
     TabStripModel* tab_strip_model,
     const TabStripModelChange& change,
     const TabStripSelectionChange& selection) {
-  if (change.type() == TabStripModelChange::Type::kInserted) {
-    for (const auto& inserted_tab : change.GetInsert()->contents) {
-      CreateAndRegisterReadAnythingEntry(inserted_tab.contents);
-    }
-  }
-  if (change.type() == TabStripModelChange::Type::kReplaced) {
-    raw_ptr<content::WebContents> new_contents =
-        change.GetReplace()->new_contents;
-    CHECK(new_contents);
-    CreateAndRegisterReadAnythingEntry(new_contents);
-  }
   if (!selection.active_tab_changed()) {
     return;
   }
diff --git a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.h b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.h
index 05fccc8b..4ec724b9 100644
--- a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.h
+++ b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.h
@@ -52,7 +52,7 @@
   explicit ReadAnythingCoordinator(Browser* browser);
   ~ReadAnythingCoordinator() override;
 
-  void CreateAndRegisterSidePanelEntry(SidePanelRegistry* registry);
+  void CreateAndRegisterEntry(SidePanelRegistry* global_registry);
   ReadAnythingController* GetController();
   ReadAnythingModel* GetModel();
 
diff --git a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator_unittest.cc b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator_unittest.cc
index a5b95c2..2352982 100644
--- a/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator_unittest.cc
+++ b/chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator_unittest.cc
@@ -35,41 +35,12 @@
     scoped_feature_list_.InitWithFeatures({features::kReadAnything}, {});
     TestWithBrowserView::SetUp();
 
-    // Ensure a kReadAnything entry is added to the contextual registry for the
-    // first tab.
-    AddTab(browser_view()->browser(), GURL("http://foo1.com"));
-    content::WebContents* active_contents =
-        browser_view()->GetActiveWebContents();
-    auto* tab_one_registry = SidePanelRegistry::Get(active_contents);
     side_panel_coordinator_ =
         SidePanelUtil::GetSidePanelCoordinatorForBrowser(browser());
+    side_panel_registry_ =
+        SidePanelCoordinator::GetGlobalSidePanelRegistry(browser());
     read_anything_coordinator_ =
         ReadAnythingCoordinator::GetOrCreateForBrowser(browser());
-    contextual_registries_.push_back(tab_one_registry);
-
-    // Ensure a kReadAnything entry is added to the contextual registry for the
-    // second tab.
-    AddTab(browser_view()->browser(), GURL("http://foo2.com"));
-    active_contents = browser_view()->GetActiveWebContents();
-    auto* tab_two_registry = SidePanelRegistry::Get(active_contents);
-    contextual_registries_.push_back(tab_two_registry);
-
-    // Verify the first tab has one entry, kReadAnything.
-    browser_view()->browser()->tab_strip_model()->ActivateTabAt(0);
-    active_contents = browser_view()->GetActiveWebContents();
-    SidePanelRegistry* contextual_registry =
-        SidePanelRegistry::Get(active_contents);
-    ASSERT_EQ(contextual_registry->entries().size(), 1u);
-    EXPECT_EQ(contextual_registry->entries()[0]->key().id(),
-              SidePanelEntry::Id::kReadAnything);
-
-    // Verify the second tab has one entry, kReadAnything.
-    browser_view()->browser()->tab_strip_model()->ActivateTabAt(1);
-    active_contents = browser_view()->GetActiveWebContents();
-    contextual_registry = SidePanelRegistry::Get(active_contents);
-    ASSERT_EQ(contextual_registry->entries().size(), 1u);
-    EXPECT_EQ(contextual_registry->entries()[0]->key().id(),
-              SidePanelEntry::Id::kReadAnything);
   }
 
   // Wrapper methods around the ReadAnythingCoordinator. These do nothing more
@@ -98,8 +69,7 @@
  protected:
   raw_ptr<SidePanelCoordinator, DanglingUntriaged> side_panel_coordinator_ =
       nullptr;
-  std::vector<raw_ptr<SidePanelRegistry, DanglingUntriaged>>
-      contextual_registries_;
+  raw_ptr<SidePanelRegistry, DanglingUntriaged> side_panel_registry_ = nullptr;
   raw_ptr<ReadAnythingCoordinator, DanglingUntriaged>
       read_anything_coordinator_ = nullptr;
 
@@ -151,7 +121,7 @@
 TEST_F(ReadAnythingCoordinatorTest,
        ActivateCalled_ShowAndHideReadAnythingEntry) {
   AddObserver(&coordinator_observer_);
-  SidePanelEntry* entry = contextual_registries_[0]->GetEntryForKey(
+  SidePanelEntry* entry = side_panel_registry_->GetEntryForKey(
       SidePanelEntry::Key(SidePanelEntry::Id::kReadAnything));
 
   EXPECT_CALL(coordinator_observer_, Activate(true)).Times(1);
diff --git a/chrome/browser/ui/views/side_panel/read_anything/read_anything_side_panel_controller_utils.cc b/chrome/browser/ui/views/side_panel/read_anything/read_anything_side_panel_controller_utils.cc
index 1c5b4ae..7f5e3798e 100644
--- a/chrome/browser/ui/views/side_panel/read_anything/read_anything_side_panel_controller_utils.cc
+++ b/chrome/browser/ui/views/side_panel/read_anything/read_anything_side_panel_controller_utils.cc
@@ -4,11 +4,7 @@
 
 #include "chrome/browser/ui/side_panel/read_anything/read_anything_side_panel_controller_utils.h"
 
-#include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/side_panel/side_panel_ui.h"
-#include "chrome/browser/ui/views/side_panel/read_anything/read_anything_coordinator.h"
-#include "chrome/browser/ui/views/side_panel/side_panel_registry.h"
-#include "content/public/browser/web_contents_observer.h"
 
 void ShowReadAnythingSidePanel(Browser* browser) {
   SidePanelUI* side_panel_ui = SidePanelUI::GetSidePanelUIForBrowser(browser);
@@ -28,22 +24,3 @@
          (side_panel_ui->GetCurrentEntryId() ==
           SidePanelEntryId::kReadAnything);
 }
-
-void CreateAndRegisterReadAnythingEntry(content::WebContents* web_contents) {
-  if (!web_contents) {
-    return;
-  }
-  auto* registry = SidePanelRegistry::Get(web_contents);
-  Browser* browser = chrome::FindBrowserWithWebContents(web_contents);
-  if (!registry || !browser) {
-    return;
-  }
-  // If the web contents is already registered to read anything, do nothing.
-  if (registry->GetEntryForKey(
-          SidePanelEntry::Key(SidePanelEntry::Id::kReadAnything))) {
-    return;
-  }
-
-  auto* coordinator = ReadAnythingCoordinator::GetOrCreateForBrowser(browser);
-  coordinator->CreateAndRegisterSidePanelEntry(registry);
-}
diff --git a/chrome/browser/ui/views/side_panel/read_anything/read_anything_toolbar_view_browsertest.cc b/chrome/browser/ui/views/side_panel/read_anything/read_anything_toolbar_view_browsertest.cc
index eb79d132..98a6e9c 100644
--- a/chrome/browser/ui/views/side_panel/read_anything/read_anything_toolbar_view_browsertest.cc
+++ b/chrome/browser/ui/views/side_panel/read_anything/read_anything_toolbar_view_browsertest.cc
@@ -43,8 +43,8 @@
       : ReadAnythingCoordinator(browser) {}
 
   MOCK_METHOD(void,
-              CreateAndRegisterSidePanelEntry,
-              (SidePanelRegistry * registry));
+              CreateAndRegisterEntry,
+              (SidePanelRegistry * global_registry));
   MOCK_METHOD(ReadAnythingController*, GetController, ());
   MOCK_METHOD(ReadAnythingModel*, GetModel, ());
   MOCK_METHOD(void,
diff --git a/chrome/browser/ui/views/side_panel/side_panel_util.cc b/chrome/browser/ui/views/side_panel/side_panel_util.cc
index 1194758..b4b6880 100644
--- a/chrome/browser/ui/views/side_panel/side_panel_util.cc
+++ b/chrome/browser/ui/views/side_panel/side_panel_util.cc
@@ -98,7 +98,8 @@
 
   // Add read anything.
   if (features::IsReadAnythingEnabled()) {
-    ReadAnythingCoordinator::GetOrCreateForBrowser(browser);
+    ReadAnythingCoordinator::GetOrCreateForBrowser(browser)
+        ->CreateAndRegisterEntry(global_registry);
   }
 
   // Create Search Companion coordinator.
diff --git a/chrome/browser/ui/views/tabs/tab_organization_button.cc b/chrome/browser/ui/views/tabs/tab_organization_button.cc
index 21266e2..1bdf047 100644
--- a/chrome/browser/ui/views/tabs/tab_organization_button.cc
+++ b/chrome/browser/ui/views/tabs/tab_organization_button.cc
@@ -39,7 +39,7 @@
   UpdateBackgroundFrameInactiveColorId(
       kColorNewTabButtonCRBackgroundFrameInactive);
 
-  SetPaintTransparentForCustomImageTheme(false);
+  set_paint_transparent_for_custom_image_theme(false);
 
   UpdateColors();
 }
diff --git a/chrome/browser/ui/views/tabs/tab_organization_button.h b/chrome/browser/ui/views/tabs/tab_organization_button.h
index 70451d4..ff906c1 100644
--- a/chrome/browser/ui/views/tabs/tab_organization_button.h
+++ b/chrome/browser/ui/views/tabs/tab_organization_button.h
@@ -27,12 +27,13 @@
   void SetSession(TabOrganizationSession* session) { session_ = session; }
   TabOrganizationSession* session_for_testing() { return session_; }
 
-  // views::View
+  // TabStripControlButton:
   gfx::Size CalculatePreferredSize() const override;
 
   void ButtonPressed(const ui::Event& event);
 
  protected:
+  // TabStripControlButton:
   int GetCornerRadius() const override;
 
  private:
diff --git a/chrome/browser/ui/views/tabs/tab_search_button.cc b/chrome/browser/ui/views/tabs/tab_search_button.cc
index 94e674ce..d55b69a 100644
--- a/chrome/browser/ui/views/tabs/tab_search_button.cc
+++ b/chrome/browser/ui/views/tabs/tab_search_button.cc
@@ -50,11 +50,6 @@
         kColorNewTabButtonCRBackgroundFrameInactive);
   }
 
-  const bool paint_transparent_for_custom_image_theme =
-      features::IsChromeRefresh2023() ? false : true;
-  SetPaintTransparentForCustomImageTheme(
-      paint_transparent_for_custom_image_theme);
-
   UpdateColors();
 }
 
diff --git a/chrome/browser/ui/views/tabs/tab_strip_control_button.cc b/chrome/browser/ui/views/tabs/tab_strip_control_button.cc
index ec176df..4d9cf419 100644
--- a/chrome/browser/ui/views/tabs/tab_strip_control_button.cc
+++ b/chrome/browser/ui/views/tabs/tab_strip_control_button.cc
@@ -84,7 +84,10 @@
   SetImageCentered(true);
   SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
 
-  paint_transparent_for_custom_image_theme_ = true;
+  // By default control buttons in the tab strip should be non-transparent for
+  // the updated Chrome refresh UX.
+  paint_transparent_for_custom_image_theme_ = !features::IsChromeRefresh2023();
+
   foreground_frame_active_color_id_ = kColorTabForegroundInactiveFrameActive;
   foreground_frame_inactive_color_id_ =
       kColorNewTabButtonCRForegroundFrameInactive;
diff --git a/chrome/browser/ui/views/tabs/tab_strip_control_button.h b/chrome/browser/ui/views/tabs/tab_strip_control_button.h
index 1f95994..3346e14 100644
--- a/chrome/browser/ui/views/tabs/tab_strip_control_button.h
+++ b/chrome/browser/ui/views/tabs/tab_strip_control_button.h
@@ -106,11 +106,7 @@
     UpdateColors();
   }
 
-  bool GetPaintTransparentForCustomImageTheme() {
-    return paint_transparent_for_custom_image_theme_;
-  }
-
-  void SetPaintTransparentForCustomImageTheme(
+  void set_paint_transparent_for_custom_image_theme(
       bool paint_transparent_for_custom_image_theme) {
     paint_transparent_for_custom_image_theme_ =
         paint_transparent_for_custom_image_theme;
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
index 93c4c405..d124ad1 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.cc
@@ -74,16 +74,6 @@
     "com.microsoft.skydrive.content.StorageAccessProvider";
 constexpr char kNotificationId[] = "cloud_upload_open_failure";
 
-constexpr char kDriveOpenSourceVolumeMetric[] =
-    "FileBrowser.OfficeFiles.Open.SourceVolume.GoogleDrive";
-constexpr char kOneDriveOpenSourceVolumeMetric[] =
-    "FileBrowser.OfficeFiles.Open.SourceVolume.MicrosoftOneDrive";
-
-constexpr char kDriveTransferRequiredMetric[] =
-    "FileBrowser.OfficeFiles.Open.TransferRequired.GoogleDrive";
-constexpr char kOneDriveTransferRequiredMetric[] =
-    "FileBrowser.OfficeFiles.Open.TransferRequired.OneDrive";
-
 constexpr char kFileHandlerSelectionMetricName[] =
     "FileBrowser.OfficeFiles.Setup.FileHandlerSelection";
 
@@ -104,17 +94,6 @@
   kMaxValue = kQuickOffice,
 };
 
-// Records the source volume that an office file is opened from. These values
-// represent the source volume types that are only relevant to office file
-// handling code - the rest are obtained from file_manager::VolumeType.
-//
-// These values are persisted to logs. Entries should not be renumbered and
-// numeric values should never be reused.
-enum class OfficeFilesSourceVolume {
-  kUnknown = 100,
-  kMicrosoftOneDrive = 101,
-};
-
 // Represents (as a bitmask) whether or not Microsoft 365 PWA and ODFS are set
 // up. Used to record this state when setup is launched.
 //
@@ -190,13 +169,12 @@
 
 // Logs UMA when the OneDrive task ends with an attempt to open a file.
 void LogOneDriveOpenResultUMA(OfficeTaskResult success_task_result,
-                              fm_tasks::OfficeOneDriveOpenErrors open_result) {
+                              OfficeOneDriveOpenErrors open_result) {
   UMA_HISTOGRAM_ENUMERATION(fm_tasks::kOneDriveErrorMetricName, open_result);
-  UMA_HISTOGRAM_ENUMERATION(
-      kOneDriveTaskResultMetricName,
-      open_result == fm_tasks::OfficeOneDriveOpenErrors::kSuccess
-          ? success_task_result
-          : OfficeTaskResult::kFailedToOpen);
+  UMA_HISTOGRAM_ENUMERATION(kOneDriveTaskResultMetricName,
+                            open_result == OfficeOneDriveOpenErrors::kSuccess
+                                ? success_task_result
+                                : OfficeTaskResult::kFailedToOpen);
 }
 
 // Handle system error notification "Sign in" click.
@@ -267,7 +245,7 @@
 // show the generic access error notification.
 void OnGetReauthenticationRequired(
     Profile* profile,
-    base::OnceCallback<void(fm_tasks::OfficeOneDriveOpenErrors)> callback,
+    base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
     base::expected<ODFSMetadata, base::File::Error> metadata) {
   bool reauthentication_required = false;
   if (metadata.has_value()) {
@@ -279,8 +257,8 @@
   ShowUnableToOpenNotification(profile, reauthentication_required);
   std::move(callback).Run(
       reauthentication_required
-          ? fm_tasks::OfficeOneDriveOpenErrors::kGetActionsReauthRequired
-          : fm_tasks::OfficeOneDriveOpenErrors::kGetActionsAccessDenied);
+          ? OfficeOneDriveOpenErrors::kGetActionsReauthRequired
+          : OfficeOneDriveOpenErrors::kGetActionsAccessDenied);
 }
 
 // Open file with |file_path| from ODFS |file_system|. Open in the OneDrive PWA
@@ -289,14 +267,13 @@
     Profile* profile,
     file_system_provider::ProvidedFileSystemInterface* file_system,
     const base::FilePath& file_path,
-    base::OnceCallback<void(fm_tasks::OfficeOneDriveOpenErrors)> callback) {
+    base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
   GetODFSEntryMetadata(
       file_system, file_path,
       base::BindOnce(
           [](Profile* profile,
              file_system_provider::ProvidedFileSystemInterface* file_system,
-             base::OnceCallback<void(fm_tasks::OfficeOneDriveOpenErrors)>
-                 callback,
+             base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
              base::expected<ODFSEntryMetadata, base::File::Error> metadata) {
             if (!metadata.has_value()) {
               switch (metadata.error()) {
@@ -309,8 +286,8 @@
                   break;
                 default:
                   ShowUnableToOpenNotification(profile);
-                  std::move(callback).Run(fm_tasks::OfficeOneDriveOpenErrors::
-                                              kGetActionsGenericError);
+                  std::move(callback).Run(
+                      OfficeOneDriveOpenErrors::kGetActionsGenericError);
                   break;
               }
               return;
@@ -318,14 +295,14 @@
             if (!metadata->url) {
               ShowUnableToOpenNotification(profile);
               std::move(callback).Run(
-                  fm_tasks::OfficeOneDriveOpenErrors::kGetActionsNoUrl);
+                  OfficeOneDriveOpenErrors::kGetActionsNoUrl);
               return;
             }
             GURL url(*metadata->url);
             if (!url.is_valid()) {
               ShowUnableToOpenNotification(profile);
               std::move(callback).Run(
-                  fm_tasks::OfficeOneDriveOpenErrors::kGetActionsInvalidUrl);
+                  OfficeOneDriveOpenErrors::kGetActionsInvalidUrl);
               return;
             }
             auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
@@ -333,28 +310,24 @@
                                     /*event_flags=*/ui::EF_NONE, url,
                                     apps::LaunchSource::kFromFileManager,
                                     /*window_info=*/nullptr);
-            std::move(callback).Run(
-                fm_tasks::OfficeOneDriveOpenErrors::kSuccess);
+            std::move(callback).Run(OfficeOneDriveOpenErrors::kSuccess);
           },
           profile, file_system, std::move(callback)));
 }
 
 // Open office file using the ODFS |url|.
-void OpenODFSUrl(
-    Profile* profile,
-    const storage::FileSystemURL& url,
-    base::OnceCallback<void(fm_tasks::OfficeOneDriveOpenErrors)> callback) {
+void OpenODFSUrl(Profile* profile,
+                 const storage::FileSystemURL& url,
+                 base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
   if (!url.is_valid()) {
     LOG(ERROR) << "Invalid uploaded file URL";
-    std::move(callback).Run(
-        fm_tasks::OfficeOneDriveOpenErrors::kNoFileSystemURL);
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kNoFileSystemURL);
     return;
   }
   ash::file_system_provider::util::FileSystemURLParser parser(url);
   if (!parser.Parse()) {
     LOG(ERROR) << "Path not in FSP";
-    std::move(callback).Run(
-        fm_tasks::OfficeOneDriveOpenErrors::kInvalidFileSystemURL);
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kInvalidFileSystemURL);
     return;
   }
   OpenFileFromODFS(profile, parser.file_system(), parser.file_path(),
@@ -366,18 +339,20 @@
 // open them from ODFS in the MS 365 PWA.
 void OpenAndroidOneDriveUrls(
     Profile* profile,
-    const std::vector<storage::FileSystemURL>& android_onedrive_urls) {
+    const std::vector<storage::FileSystemURL>& android_onedrive_urls,
+    base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
   for (const auto& android_onedrive_url : android_onedrive_urls) {
     absl::optional<ODFSFileSystemAndPath> fs_and_path =
         AndroidOneDriveUrlToODFS(profile, android_onedrive_url);
     if (!fs_and_path.has_value()) {
       // TODO(b/269364287): Handle when Android OneDrive file can't be opened.
       LOG(ERROR) << "Android OneDrive Url cannot be converted to ODFS";
+      std::move(callback).Run(
+          OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
       return;
     }
-    OpenFileFromODFS(
-        profile, fs_and_path->file_system, fs_and_path->file_path_within_odfs,
-        base::BindOnce(&LogOneDriveOpenResultUMA, OfficeTaskResult::kOpened));
+    OpenFileFromODFS(profile, fs_and_path->file_system,
+                     fs_and_path->file_path_within_odfs, std::move(callback));
   }
 }
 
@@ -548,7 +523,8 @@
     transfer_required_ = OfficeFilesTransferRequired::kNotRequired;
     UMA_HISTOGRAM_ENUMERATION(kOneDriveTransferRequiredMetric,
                               OfficeFilesTransferRequired::kNotRequired);
-    OpenAndroidOneDriveUrlsIfAccountMatchedODFS();
+    OpenAndroidOneDriveUrlsIfAccountMatchedODFS(
+        base::BindOnce(&LogOneDriveOpenResultUMA, OfficeTaskResult::kOpened));
   } else {
     // The files need to be moved.
     auto operation =
@@ -707,19 +683,23 @@
   return components[1];
 }
 
-void CloudOpenTask::OpenAndroidOneDriveUrlsIfAccountMatchedODFS() {
+void CloudOpenTask::OpenAndroidOneDriveUrlsIfAccountMatchedODFS(
+    base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback) {
   // Get email account associated with Android OneDrive.
   std::string authority;
   std::string root_document_id;
   base::FilePath path;
   if (!arc::ParseDocumentsProviderUrl(file_urls_.front(), &authority,
                                       &root_document_id, &path)) {
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kInvalidFileSystemURL);
     return;
   }
 
   absl::optional<std::string> android_onedrive_email =
       GetEmailFromAndroidOneDriveRootDoc(root_document_id);
   if (!android_onedrive_email.has_value()) {
+    std::move(callback).Run(
+        OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
     return;
   }
 
@@ -729,11 +709,14 @@
   if (!fs_and_path.has_value()) {
     // TODO(b/269364287): Handle when Android OneDrive file can't be opened.
     LOG(ERROR) << "Android OneDrive Url cannot be converted to ODFS";
+    std::move(callback).Run(
+        OfficeOneDriveOpenErrors::kConversionToODFSUrlError);
     return;
   }
-  GetODFSMetadata(fs_and_path->file_system,
-                  base::BindOnce(&CloudOpenTask::CheckEmailAndOpenURLs, this,
-                                 android_onedrive_email.value()));
+  GetODFSMetadata(
+      fs_and_path->file_system,
+      base::BindOnce(&CloudOpenTask::CheckEmailAndOpenURLs, this,
+                     android_onedrive_email.value(), std::move(callback)));
 }
 
 absl::optional<ODFSFileSystemAndPath> AndroidOneDriveUrlToODFS(
@@ -789,22 +772,26 @@
 
 void CloudOpenTask::CheckEmailAndOpenURLs(
     const std::string& android_onedrive_email,
+    base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
     base::expected<ODFSMetadata, base::File::Error> metadata_or_error) {
   if (!metadata_or_error.has_value()) {
     LOG(ERROR) << "Failed to get user email: " << metadata_or_error.error();
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kGetActionsGenericError);
     return;
   }
   if (metadata_or_error->user_email.empty()) {
     LOG(ERROR) << "User email is empty";
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kGetActionsNoEmail);
     return;
   }
   // Query whether the account logged into Android OneDrive is the
   // same as ODFS.
   if (android_onedrive_email == metadata_or_error->user_email) {
-    OpenAndroidOneDriveUrls(profile_, file_urls_);
+    OpenAndroidOneDriveUrls(profile_, file_urls_, std::move(callback));
   } else {
     LOG(ERROR) << "Email accounts associated with ODFS and "
                   "Android OneDrive don't match.";
+    std::move(callback).Run(OfficeOneDriveOpenErrors::kEmailsDoNotMatch);
   }
 }
 
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
index be20ce3..1574dd02 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_dialog.h
@@ -166,9 +166,11 @@
   void OpenOrMoveFiles();
   void OpenAlreadyHostedDriveUrls();
   void OpenODFSUrls(const OfficeTaskResult task_result_uma);
-  void OpenAndroidOneDriveUrlsIfAccountMatchedODFS();
+  void OpenAndroidOneDriveUrlsIfAccountMatchedODFS(
+      base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback);
   void CheckEmailAndOpenURLs(
       const std::string& android_onedrive_email,
+      base::OnceCallback<void(OfficeOneDriveOpenErrors)> callback,
       base::expected<cloud_upload::ODFSMetadata, base::File::Error>
           metadata_or_error);
 
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
index e9dc8ab5..5798e1e1 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
+++ b/chrome/browser/ui/webui/ash/cloud_upload/cloud_upload_util.h
@@ -74,6 +74,47 @@
 constexpr char kOneDriveCopyErrorMetricName[] =
     "FileBrowser.OfficeFiles.Open.IOTaskError.OneDrive.Copy";
 
+constexpr char kDriveOpenSourceVolumeMetric[] =
+    "FileBrowser.OfficeFiles.Open.SourceVolume.GoogleDrive";
+constexpr char kOneDriveOpenSourceVolumeMetric[] =
+    "FileBrowser.OfficeFiles.Open.SourceVolume.MicrosoftOneDrive";
+
+constexpr char kDriveTransferRequiredMetric[] =
+    "FileBrowser.OfficeFiles.Open.TransferRequired.GoogleDrive";
+constexpr char kOneDriveTransferRequiredMetric[] =
+    "FileBrowser.OfficeFiles.Open.TransferRequired.OneDrive";
+
+// List of UMA enum values for opening Office files from OneDrive, with the
+// MS365 PWA. The enum values must be kept in sync with OfficeOneDriveOpenErrors
+// in tools/metrics/histograms/enums.xml.
+enum class OfficeOneDriveOpenErrors {
+  kSuccess = 0,
+  kOffline = 1,
+  kNoProfile = 2,
+  kNoFileSystemURL = 3,
+  kInvalidFileSystemURL = 4,
+  kGetActionsGenericError = 5,
+  kGetActionsReauthRequired = 6,
+  kGetActionsInvalidUrl = 7,
+  kGetActionsNoUrl = 8,
+  kGetActionsAccessDenied = 9,
+  kGetActionsNoEmail = 10,
+  kConversionToODFSUrlError = 11,
+  kEmailsDoNotMatch = 12,
+  kMaxValue = kEmailsDoNotMatch,
+};
+
+// Records the source volume that an office file is opened from. These values
+// represent the source volume types that are only relevant to office file
+// handling code - the rest are obtained from file_manager::VolumeType.
+//
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class OfficeFilesSourceVolume {
+  kUnknown = 100,
+  kMicrosoftOneDrive = 101,
+};
+
 // List of UMA enum value for Web Drive Office task results. The enum values
 // must be kept in sync with OfficeTaskResult in
 // tools/metrics/histograms/enums.xml.
diff --git a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
index 6ab6e7b..eaf5056 100644
--- a/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
+++ b/chrome/browser/ui/webui/ash/cloud_upload/one_drive_upload_handler.cc
@@ -35,9 +35,6 @@
 namespace ash::cloud_upload {
 namespace {
 
-constexpr char kUploadResultMetricName[] =
-    "FileBrowser.OfficeFiles.Open.UploadResult.OneDrive";
-
 // Runs the callback provided to `OneDriveUploadHandler::Upload`.
 void OnUploadDone(scoped_refptr<OneDriveUploadHandler> one_drive_upload_handler,
                   OneDriveUploadHandler::UploadCallback callback,
@@ -154,7 +151,7 @@
 void OneDriveUploadHandler::OnEndUpload(
     base::expected<storage::FileSystemURL, std::string> url,
     OfficeFilesUploadResult result_metric) {
-  UMA_HISTOGRAM_ENUMERATION(kUploadResultMetricName, result_metric);
+  UMA_HISTOGRAM_ENUMERATION(kOneDriveUploadResultMetricName, result_metric);
   if (url.has_value()) {
     // Resolve notifications.
     if (notification_manager_) {
diff --git a/chrome/browser/ui/webui/ash/emoji/emoji_ui.cc b/chrome/browser/ui/webui/ash/emoji/emoji_ui.cc
index d58fb8a4..5bb5fe1 100644
--- a/chrome/browser/ui/webui/ash/emoji/emoji_ui.cc
+++ b/chrome/browser/ui/webui/ash/emoji/emoji_ui.cc
@@ -42,6 +42,14 @@
       : WebUIBubbleDialogView(nullptr, contents_wrapper.get()),
         contents_wrapper_(std::move(contents_wrapper)) {
     set_has_parent(false);
+
+    // With jelly support on, update border radius of bubble view.
+    // TODO(b/263055563): Remove this check once Jelly is fully launched in
+    // Emoji Picker.
+    if (base::FeatureList::IsEnabled(
+            ash::features::kImeSystemEmojiPickerJellySupport)) {
+      set_corner_radius(16);
+    }
   }
 
  private:
diff --git a/chrome/browser/ui/webui/ash/scalable_iph/scalable_iph_debug_ui.cc b/chrome/browser/ui/webui/ash/scalable_iph/scalable_iph_debug_ui.cc
index a81b179..670df193 100644
--- a/chrome/browser/ui/webui/ash/scalable_iph/scalable_iph_debug_ui.cc
+++ b/chrome/browser/ui/webui/ash/scalable_iph/scalable_iph_debug_ui.cc
@@ -10,6 +10,7 @@
 #include "base/containers/fixed_flat_set.h"
 #include "base/memory/ref_counted_memory.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/strings/strcat.h"
 #include "chrome/browser/scalable_iph/scalable_iph_factory.h"
 #include "chromeos/ash/components/scalable_iph/logger.h"
 #include "chromeos/ash/components/scalable_iph/scalable_iph.h"
@@ -24,9 +25,17 @@
 constexpr char kRecordFiveMinTickEventPath[] = "record-five-min-tick-event";
 constexpr char kRecordedFiveMinTickEvent[] = "Recorded ScalableIphFiveMinTick.";
 
+constexpr char kPreTagBegin[] = "<pre>";
+constexpr char kPreTagEnd[] = "</pre>";
+constexpr char kNewline[] = "\n";
+
 constexpr auto kSupportedPaths = base::MakeFixedFlatSet<base::StringPiece>(
     {kLoggingPath, kRecordFiveMinTickEventPath});
 
+std::string WrapWithPreTags(const std::string& content) {
+  return base::StrCat({kPreTagBegin, kNewline, content, kNewline, kPreTagEnd});
+}
+
 bool ShouldHandleRequest(const std::string& path) {
   return kSupportedPaths.contains(path);
 }
@@ -93,18 +102,18 @@
     // pre-conditions don't get satisfied, querying a service before its
     // initialization, etc.
     std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
-        CollectServiceStartUpDebugLog(browser_context)));
+        WrapWithPreTags(CollectServiceStartUpDebugLog(browser_context))));
     return;
   }
 
   if (path == kRecordFiveMinTickEventPath) {
     scalable_iph->RecordEvent(scalable_iph::ScalableIph::Event::kFiveMinTick);
     std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
-        kRecordedFiveMinTickEvent));
+        WrapWithPreTags(kRecordedFiveMinTickEvent)));
     return;
   } else if (path == kLoggingPath) {
     std::move(callback).Run(base::MakeRefCounted<base::RefCountedString>(
-        scalable_iph->GetLogger()->GenerateLog()));
+        WrapWithPreTags(scalable_iph->GetLogger()->GenerateLog())));
     return;
   }
 }
diff --git a/chrome/browser/ui/webui/ash/sensor_info/sensor_page_handler.h b/chrome/browser/ui/webui/ash/sensor_info/sensor_page_handler.h
index 9d955c6..cb23839 100644
--- a/chrome/browser/ui/webui/ash/sensor_info/sensor_page_handler.h
+++ b/chrome/browser/ui/webui/ash/sensor_info/sensor_page_handler.h
@@ -35,7 +35,8 @@
 // 'OnSensorUpdated' function and in this function forward sensor updates to TS
 // side. SensorPageHandler is also inheriting sensor::mojom::PageHandler, so
 // also responsible of TS side to C++ side communications.
-class SensorPageHandler : public Observer, public sensor::mojom::PageHandler {
+class SensorPageHandler : public SensorObserver,
+                          public sensor::mojom::PageHandler {
  public:
   // Enum class for file open status.
   enum class State {
@@ -80,7 +81,7 @@
   void StartRecordingUpdate() override;
   void StopRecordingUpdate() override;
 
-  // Observer
+  // SensorObserver:
   void OnSensorUpdated(const SensorUpdate& update) override;
 
  private:
diff --git a/chrome/browser/ui/webui/settings/ash/device_keyboard_handler.cc b/chrome/browser/ui/webui/settings/ash/device_keyboard_handler.cc
index f688a446..83d5137 100644
--- a/chrome/browser/ui/webui/settings/ash/device_keyboard_handler.cc
+++ b/chrome/browser/ui/webui/settings/ash/device_keyboard_handler.cc
@@ -44,6 +44,8 @@
         result.has_external_chromeos_keyboard = true;
         break;
       case ui::KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
+      case ui::KeyboardCapability::DeviceType::
+          kDeviceExternalNullTopRowChromeOsKeyboard:
       case ui::KeyboardCapability::DeviceType::kDeviceExternalUnknown:
         result.has_external_generic_keyboard = true;
         break;
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
index fbb8333..40586bf 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.cc
@@ -21,7 +21,7 @@
 namespace {
 
 using ActionTypeVariant =
-    absl::variant<AcceleratorAction, ::ash::mojom::HardCodedAction>;
+    absl::variant<AcceleratorAction, ::ash::mojom::StaticShortcutAction>;
 
 // Used to represent a constant version of the mojom::ActionChoice struct.
 struct ActionChoice {
@@ -41,8 +41,8 @@
     {"Turn on high contrast", AcceleratorAction::kToggleHighContrast},
     {"Turn on magnifier", AcceleratorAction::kToggleFullscreenMagnifier},
     {"Turn on dictation", AcceleratorAction::kEnableOrToggleDictation},
-    {"Copy", ::ash::mojom::HardCodedAction::kCopy},
-    {"Paste", ::ash::mojom::HardCodedAction::kPaste},
+    {"Copy", ::ash::mojom::StaticShortcutAction::kCopy},
+    {"Paste", ::ash::mojom::StaticShortcutAction::kPaste},
 };
 
 // TODO(dpad): Update list to official list of actions.
@@ -57,17 +57,17 @@
     {"Turn on high contrast", AcceleratorAction::kToggleHighContrast},
     {"Turn on magnifier", AcceleratorAction::kToggleFullscreenMagnifier},
     {"Turn on dictation", AcceleratorAction::kEnableOrToggleDictation},
-    {"Copy", ::ash::mojom::HardCodedAction::kCopy},
-    {"Paste", ::ash::mojom::HardCodedAction::kPaste},
+    {"Copy", ::ash::mojom::StaticShortcutAction::kCopy},
+    {"Paste", ::ash::mojom::StaticShortcutAction::kPaste},
 };
 
-mojom::ActionTypePtr GetActionType(AcceleratorAction action) {
-  return mojom::ActionType::NewAcceleratorAction(action);
+mojom::ActionTypePtr GetActionType(AcceleratorAction accelerator_action) {
+  return mojom::ActionType::NewAcceleratorAction(accelerator_action);
 }
 
 mojom::ActionTypePtr GetActionType(
-    ::ash::mojom::HardCodedAction hardcoded_action) {
-  return mojom::ActionType::NewHardcodedAction(hardcoded_action);
+    ::ash::mojom::StaticShortcutAction static_shortcut_action) {
+  return mojom::ActionType::NewStaticShortcutAction(static_shortcut_action);
 }
 
 mojom::ActionTypePtr GetActionTypeFromVariant(ActionTypeVariant variant) {
diff --git a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
index 5720c668..ec9a225 100644
--- a/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
+++ b/chrome/browser/ui/webui/settings/ash/input_device_settings/input_device_settings_provider.mojom
@@ -17,7 +17,7 @@
 // within settings.
 union ActionType {
   ash.mojom.AcceleratorAction accelerator_action;
-  ash.mojom.HardCodedAction hardcoded_action;
+  ash.mojom.StaticShortcutAction static_shortcut_action;
 };
 
 // Interface for observing keyboard settings changes in OSSettings SWA.
diff --git a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
index 1a7c98f..621ae22e 100644
--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc
@@ -77,6 +77,7 @@
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "components/performance_manager/public/features.h"
 #include "components/permissions/features.h"
+#include "components/plus_addresses/features.h"
 #include "components/prefs/pref_service.h"
 #include "components/privacy_sandbox/privacy_sandbox_features.h"
 #include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_utils.h"
@@ -1066,7 +1067,8 @@
     {"migratableCardsInfoSingle", IDS_SETTINGS_SINGLE_MIGRATABLE_CARD_INFO},
     {"migratableCardsInfoMultiple",
      IDS_SETTINGS_MULTIPLE_MIGRATABLE_CARDS_INFO},
-    {"remoteCreditCardLinkLabel", IDS_SETTINGS_REMOTE_CREDIT_CARD_LINK_LABEL},
+    {"remotePaymentMethodsLinkLabel",
+     IDS_SETTINGS_REMOTE_PAYMENT_METHODS_LINK_LABEL},
     {"canMakePaymentToggleLabel", IDS_SETTINGS_CAN_MAKE_PAYMENT_TOGGLE_LABEL},
     {"autofillDetail", IDS_SETTINGS_AUTOFILL_DETAIL},
     {"passwords", IDS_SETTINGS_PASSWORD_MANAGER},
@@ -1128,6 +1130,7 @@
      IDS_SETTINGS_PASSWORDS_BIOMETRIC_AUTHENTICATION_FOR_FILLING_TOGGLE_LABEL_WIN},
     {"managePasskeysSubTitle", IDS_AUTOFILL_MANAGE_PASSKEYS_SUB_TITLE_WIN},
 #endif
+    {"plusAddressSettings", IDS_PLUS_ADDRESS_SETTINGS_LABEL},
   };
 
   GURL google_password_manager_url = GetGooglePasswordManagerURL(
@@ -1219,6 +1222,9 @@
       "syncEnableContactInfoDataTypeInTransportMode",
       base::FeatureList::IsEnabled(
           syncer::kSyncEnableContactInfoDataTypeInTransportMode));
+
+  html_source->AddString("plusAddressManagementUrl",
+                         plus_addresses::kPlusAddressManagementUrl.Get());
 }
 
 void AddSignOutDialogStrings(content::WebUIDataSource* html_source,
diff --git a/chrome/browser/web_applications/manifest_update_manager.cc b/chrome/browser/web_applications/manifest_update_manager.cc
index 69204e6a..b143cceb 100644
--- a/chrome/browser/web_applications/manifest_update_manager.cc
+++ b/chrome/browser/web_applications/manifest_update_manager.cc
@@ -33,6 +33,7 @@
 #include "components/content_settings/core/common/content_settings_types.h"
 #include "components/keep_alive_registry/keep_alive_types.h"
 #include "components/keep_alive_registry/scoped_keep_alive.h"
+#include "components/webapps/browser/features.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_contents_observer.h"
@@ -238,6 +239,15 @@
     return;
   }
 
+  if (provider_->registrar_unsafe().IsShortcutApp(*app_id) &&
+      base::FeatureList::IsEnabled(
+          webapps::features::kCreateShortcutIgnoresManifest)) {
+    // When we create shortcut ignores manifest, we should update manifest for
+    // shortcuts.
+    NotifyResult(url, *app_id, ManifestUpdateResult::kShortcutIgnoresManifest);
+    return;
+  }
+
   if (base::Contains(update_stages_, *app_id)) {
     return;
   }
diff --git a/chrome/browser/web_applications/manifest_update_manager_browsertest.cc b/chrome/browser/web_applications/manifest_update_manager_browsertest.cc
index cb9b101..292324b 100644
--- a/chrome/browser/web_applications/manifest_update_manager_browsertest.cc
+++ b/chrome/browser/web_applications/manifest_update_manager_browsertest.cc
@@ -82,6 +82,7 @@
 #include "components/services/app_service/public/cpp/icon_info.h"
 #include "components/services/app_service/public/cpp/protocol_handler_info.h"
 #include "components/services/app_service/public/cpp/share_target.h"
+#include "components/webapps/browser/features.h"
 #include "components/webapps/browser/install_result_code.h"
 #include "components/webapps/browser/installable/installable_metrics.h"
 #include "components/webapps/browser/uninstall_result_code.h"
@@ -5737,4 +5738,36 @@
                             AppIdTestParam::kWithFlagAppIdDialogForIcon)),
     ManifestUpdateManagerBrowserTest_AppIdentityParameterized::ParamToString);
 
+class ManifestUpdateManagerBrowserTest_CreateShortcutIgnoresManifest
+    : public ManifestUpdateManagerBrowserTest {
+  base::test::ScopedFeatureList scoped_feature_list_{
+      webapps::features::kCreateShortcutIgnoresManifest};
+};
+
+IN_PROC_BROWSER_TEST_F(
+    ManifestUpdateManagerBrowserTest_CreateShortcutIgnoresManifest,
+    CheckUpdateSkipped) {
+  // Install an app with no manifest, trigger an update by navigation.
+  GURL no_manifest_url = GetAppURLWithoutManifest();
+  const webapps::AppId app_id = InstallWebAppWithoutManifest();
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  UpdateCheckResultAwaiter result_awaiter(no_manifest_url);
+
+  // Inject new manifest into the page and load the page to trigger update.
+  EXPECT_TRUE(content::ExecJs(
+      web_contents,
+      "addManifestLinkTag('/banners/manifest_for_no_manifest_page.json')"));
+
+  DidFinishLoadObserver load_observer(web_contents, no_manifest_url);
+  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), no_manifest_url));
+  EXPECT_TRUE(load_observer.AwaitCorrectPageLoaded());
+
+  EXPECT_EQ(ManifestUpdateResult::kShortcutIgnoresManifest,
+            std::move(result_awaiter).AwaitNextResult());
+
+  histogram_tester_.ExpectBucketCount(
+      kUpdateHistogramName, ManifestUpdateResult::kShortcutIgnoresManifest, 1);
+}
+
 }  // namespace web_app
diff --git a/chrome/browser/web_applications/manifest_update_utils.cc b/chrome/browser/web_applications/manifest_update_utils.cc
index 4d150555..7124707b 100644
--- a/chrome/browser/web_applications/manifest_update_utils.cc
+++ b/chrome/browser/web_applications/manifest_update_utils.cc
@@ -56,6 +56,8 @@
       return os << "kAppIsIsolatedWebApp";
     case ManifestUpdateResult::kCancelledDueToMainFrameNavigation:
       return os << "kCancelledDueToMainFrameNavigation";
+    case ManifestUpdateResult::kShortcutIgnoresManifest:
+      return os << "kkShortcutIgnoresManifest";
   }
 }
 
diff --git a/chrome/browser/web_applications/manifest_update_utils.h b/chrome/browser/web_applications/manifest_update_utils.h
index f4980dd..1124527 100644
--- a/chrome/browser/web_applications/manifest_update_utils.h
+++ b/chrome/browser/web_applications/manifest_update_utils.h
@@ -42,7 +42,8 @@
   kAppIdentityUpdateRejectedAndUninstalled = 16,
   kAppIsIsolatedWebApp = 17,
   kCancelledDueToMainFrameNavigation = 18,
-  kMaxValue = kCancelledDueToMainFrameNavigation,
+  kShortcutIgnoresManifest = 19,
+  kMaxValue = kShortcutIgnoresManifest,
 };
 
 std::ostream& operator<<(std::ostream& os, ManifestUpdateResult result);
diff --git a/chrome/browser/web_applications/web_app_registrar.cc b/chrome/browser/web_applications/web_app_registrar.cc
index c5b7b2c0..c3fd25f 100644
--- a/chrome/browser/web_applications/web_app_registrar.cc
+++ b/chrome/browser/web_applications/web_app_registrar.cc
@@ -531,6 +531,9 @@
 }
 
 bool WebAppRegistrar::IsShortcutApp(const webapps::AppId& app_id) const {
+  if (!GetAppById(app_id)) {
+    return false;
+  }
   // TODO(crbug.com/1469482): Record shortcut distinction explicitly instead of
   // using scope.
   return !GetAppScopeInternal(app_id).has_value();
diff --git a/chrome/build/android-arm32.pgo.txt b/chrome/build/android-arm32.pgo.txt
index 0225830..0381f22a 100644
--- a/chrome/build/android-arm32.pgo.txt
+++ b/chrome/build/android-arm32.pgo.txt
@@ -1 +1 @@
-chrome-android32-main-1695923790-bf0bc70f1184b5dd19a64d20328129a4a8135a1a.profdata
+chrome-android32-main-1695967146-11d72fe292686db8da66fa79ac20ff09a2bdfbcf.profdata
diff --git a/chrome/build/android-arm64.pgo.txt b/chrome/build/android-arm64.pgo.txt
index 832a8ba..e1337e28 100644
--- a/chrome/build/android-arm64.pgo.txt
+++ b/chrome/build/android-arm64.pgo.txt
@@ -1 +1 @@
-chrome-android64-main-1695923790-0a4c1f96bf997fa2fee2820e47ef12966466db96.profdata
+chrome-android64-main-1695967146-dd2fbf8fbb81ab78479105c673d05c39359d1c09.profdata
diff --git a/chrome/build/lacros64.pgo.txt b/chrome/build/lacros64.pgo.txt
index ec94c5e1..419de62 100644
--- a/chrome/build/lacros64.pgo.txt
+++ b/chrome/build/lacros64.pgo.txt
@@ -1 +1 @@
-chrome-chromeos-amd64-generic-main-1695902404-5c21040b1221c577513520f51b4d36600f152899.profdata
+chrome-chromeos-amd64-generic-main-1695945611-5f9552a6c6132cf696e9caf7358bb0ff660871c2.profdata
diff --git a/chrome/build/linux.pgo.txt b/chrome/build/linux.pgo.txt
index 6c549312..f3f6213 100644
--- a/chrome/build/linux.pgo.txt
+++ b/chrome/build/linux.pgo.txt
@@ -1 +1 @@
-chrome-linux-main-1695923790-01adceb41ca125edbe2dfbcf0edc9683554ac89f.profdata
+chrome-linux-main-1695967146-442d036dfb39556e6a56805d803577723ac7a035.profdata
diff --git a/chrome/build/mac-arm.pgo.txt b/chrome/build/mac-arm.pgo.txt
index f2e1148..90a0c04 100644
--- a/chrome/build/mac-arm.pgo.txt
+++ b/chrome/build/mac-arm.pgo.txt
@@ -1 +1 @@
-chrome-mac-arm-main-1695923790-af80bd898f22d5e54231bb364fa7dd4b917244b6.profdata
+chrome-mac-arm-main-1695981280-1f0e8b2822b03fab6be0ad808f3bec6249f64ccc.profdata
diff --git a/chrome/build/mac.pgo.txt b/chrome/build/mac.pgo.txt
index 8444c1a..e90f257 100644
--- a/chrome/build/mac.pgo.txt
+++ b/chrome/build/mac.pgo.txt
@@ -1 +1 @@
-chrome-mac-main-1695923790-435ec7e994ed994f45d98028adfd7d50598e6833.profdata
+chrome-mac-main-1695967146-f4099b742b6e2bcd1671994768733632271f799c.profdata
diff --git a/chrome/build/win-arm64.pgo.txt b/chrome/build/win-arm64.pgo.txt
index 677cae5..caa6f6b 100644
--- a/chrome/build/win-arm64.pgo.txt
+++ b/chrome/build/win-arm64.pgo.txt
@@ -1 +1 @@
-chrome-win-arm64-main-1695902280-b4f6373ddaa2a1b997bf4cadc3c45bcbd348405c.profdata
+chrome-win-arm64-main-1695967146-ab9384de8921784e3c08805c9bce04c956609425.profdata
diff --git a/chrome/build/win32.pgo.txt b/chrome/build/win32.pgo.txt
index 26f6183..0b6bd1f6 100644
--- a/chrome/build/win32.pgo.txt
+++ b/chrome/build/win32.pgo.txt
@@ -1 +1 @@
-chrome-win32-main-1695913169-badb0f1f49de03e0d6a27a188aa3478fc52c7321.profdata
+chrome-win32-main-1695967146-837878cb546f1f7e85955c26007c4eb5b17805af.profdata
diff --git a/chrome/build/win64.pgo.txt b/chrome/build/win64.pgo.txt
index 6ef90ff..9c1dcc9 100644
--- a/chrome/build/win64.pgo.txt
+++ b/chrome/build/win64.pgo.txt
@@ -1 +1 @@
-chrome-win64-main-1695913169-9fb44000db670f942ecf4aa1c8df0553a7970621.profdata
+chrome-win64-main-1695967146-964242ea82d1aa39c54c29344721cebf90adb339.profdata
diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc
index e3d79c4..91c7b8e 100644
--- a/chrome/common/chrome_features.cc
+++ b/chrome/common/chrome_features.cc
@@ -35,7 +35,7 @@
 #if BUILDFLAG(IS_CHROMEOS_ASH)
 BASE_FEATURE(kAppManagementAppDetails,
              "AppManagementAppDetails",
-             base::FEATURE_ENABLED_BY_DEFAULT);
+             base::FEATURE_DISABLED_BY_DEFAULT);
 #endif  // BUILDFLAG(IS_CHROMEOS_ASH)
 
 #if BUILDFLAG(IS_CHROMEOS_ASH)
diff --git a/chrome/renderer/autofill/autofill_renderer_browsertest.cc b/chrome/renderer/autofill/autofill_renderer_browsertest.cc
index bcda7058..9769fe7c 100644
--- a/chrome/renderer/autofill/autofill_renderer_browsertest.cc
+++ b/chrome/renderer/autofill/autofill_renderer_browsertest.cc
@@ -201,21 +201,21 @@
   expected.id_attribute = u"firstname";
   expected.name = expected.id_attribute;
   expected.value = std::u16string();
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[0]);
 
   expected.id_attribute = u"middlename";
   expected.name = expected.id_attribute;
   expected.value = std::u16string();
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[1]);
 
   expected.id_attribute = u"lastname";
   expected.name = expected.id_attribute;
   expected.value = std::u16string();
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.autocomplete_attribute = "off";
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[2]);
@@ -224,7 +224,7 @@
   expected.id_attribute = u"state";
   expected.name = expected.id_attribute;
   expected.value = u"?";
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[3]);
 
@@ -260,7 +260,7 @@
   ASSERT_EQ(1UL, forms.size());
   ASSERT_EQ(3UL, forms[0].fields.size());
 
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
 
   expected.id_attribute = u"second_firstname";
@@ -326,14 +326,14 @@
   expected.id_attribute = u"EMAIL_ADDRESS";
   expected.name = expected.id_attribute;
   expected.value.clear();
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[7]);
 
   expected.id_attribute = u"PHONE_HOME_WHOLE_NUMBER";
   expected.name = expected.id_attribute;
   expected.value.clear();
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, forms[0].fields[8]);
 }
diff --git a/chrome/renderer/autofill/form_autofill_browsertest.cc b/chrome/renderer/autofill/form_autofill_browsertest.cc
index b223103..3fa86fa 100644
--- a/chrome/renderer/autofill/form_autofill_browsertest.cc
+++ b/chrome/renderer/autofill/form_autofill_browsertest.cc
@@ -320,7 +320,7 @@
       expected.label = labels[i];
       expected.name = names[i];
       expected.value = values[i];
-      expected.form_control_type = "text";
+      expected.form_control_type = StringToFormControlType("text");
       expected.max_length = WebInputElement::DefaultMaxLength();
       fields.push_back(expected);
     }
@@ -449,15 +449,17 @@
     for (size_t i = 0; i < number_of_field_cases; ++i) {
       SCOPED_TRACE(base::StringPrintf("Verify initial value for field %s",
                                       field_cases[i].id_attribute));
-      expected.form_control_type = field_cases[i].form_control_type;
-      expected.max_length = expected.form_control_type == "text"
-                                ? WebInputElement::DefaultMaxLength()
-                                : 0;
+      expected.form_control_type =
+          StringToFormControlType(field_cases[i].form_control_type);
+      expected.max_length =
+          expected.form_control_type == StringToFormControlType("text")
+              ? WebInputElement::DefaultMaxLength()
+              : 0;
       expected.id_attribute = ASCIIToUTF16(field_cases[i].id_attribute);
       expected.name = expected.id_attribute;
       expected.value = ASCIIToUTF16(field_cases[i].initial_value);
-      if (expected.form_control_type == "text" ||
-          expected.form_control_type == "month") {
+      if (expected.form_control_type == StringToFormControlType("text") ||
+          expected.form_control_type == StringToFormControlType("month")) {
         expected.label = ASCIIToUTF16(field_cases[i].initial_value);
       } else {
         expected.label.clear();
@@ -705,7 +707,7 @@
     ASSERT_EQ(4U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -771,7 +773,7 @@
     expected.name = expected.id_attribute;
     expected.value = u"John";
     expected.label = u"John";
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[0]);
 
@@ -779,7 +781,7 @@
     expected.name = expected.id_attribute;
     expected.value = u"Smith";
     expected.label = u"Smith";
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[1]);
 
@@ -788,7 +790,7 @@
     expected.value = u"john@example.com";
     expected.label = u"john@example.com";
     expected.autocomplete_attribute = "off";
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[2]);
     expected.autocomplete_attribute.clear();
@@ -797,7 +799,7 @@
     expected.name = expected.id_attribute;
     expected.value = u"123 Fantasy Ln.\nApt. 42";
     expected.label.clear();
-    expected.form_control_type = "textarea";
+    expected.form_control_type = StringToFormControlType("textarea");
     expected.max_length = 0;
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[3]);
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, field);
@@ -830,7 +832,7 @@
     ASSERT_EQ(3U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
 
     expected.id_attribute = u"firstname";
     expected.name = expected.id_attribute;
@@ -873,7 +875,7 @@
     const std::vector<FormFieldData>& fields2 = form2.fields;
     ASSERT_EQ(3U, fields2.size());
 
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
 
     expected.id_attribute = u"firstname";
     expected.name = expected.id_attribute;
@@ -924,7 +926,7 @@
     ASSERT_EQ(3U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1002,7 +1004,7 @@
     ASSERT_EQ(3U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1037,7 +1039,7 @@
     const std::vector<FormFieldData>& fields2 = form2.fields;
     ASSERT_EQ(3U, fields2.size());
 
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1085,7 +1087,7 @@
     ASSERT_EQ(unowned_offset + 3, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"apple";
@@ -1181,7 +1183,7 @@
     ASSERT_EQ(3U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1391,7 +1393,7 @@
     ASSERT_EQ(6U, fields2.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1460,7 +1462,7 @@
     expected.is_autofilled = true;
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields2[4]);
 
-    expected.form_control_type = "select-one";
+    expected.form_control_type = StringToFormControlType("select-one");
     expected.id_attribute = u"state";
     expected.name_attribute = u"state";
     expected.name = expected.name_attribute;
@@ -1549,7 +1551,7 @@
     ASSERT_EQ(3U, fields2.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1665,7 +1667,7 @@
     ASSERT_EQ(3U, fields2.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"cc";
@@ -1784,7 +1786,7 @@
     ASSERT_EQ(3U, fields2.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"cc";
@@ -1875,7 +1877,7 @@
     ASSERT_EQ(9U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     expected.id_attribute = u"firstname";
@@ -1902,7 +1904,7 @@
     expected.label.clear();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[3]);
 
-    expected.form_control_type = "month";
+    expected.form_control_type = StringToFormControlType("month");
     expected.max_length = 0;
     expected.id_attribute = u"month";
     expected.name = expected.id_attribute;
@@ -1916,7 +1918,7 @@
     expected.label = u"2012-11";
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[5]);
 
-    expected.form_control_type = "textarea";
+    expected.form_control_type = StringToFormControlType("textarea");
     expected.id_attribute = u"textarea";
     expected.name = expected.id_attribute;
     expected.value.clear();
@@ -2010,7 +2012,7 @@
     ASSERT_EQ(6U, fields.size());
 
     FormFieldData expected;
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
 
     // shipping section
@@ -2096,14 +2098,14 @@
     expected.id_attribute = u"firstname";
     expected.name = expected.id_attribute;
     expected.value.clear();
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[0]);
 
     expected.id_attribute = u"lastname";
     expected.name = expected.id_attribute;
     expected.value.clear();
-    expected.form_control_type = "text";
+    expected.form_control_type = StringToFormControlType("text");
     expected.max_length = WebInputElement::DefaultMaxLength();
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[1]);
 
@@ -2111,7 +2113,7 @@
     expected.name_attribute = u"state";
     expected.name = expected.name_attribute;
     expected.value = u"?";
-    expected.form_control_type = "select-one";
+    expected.form_control_type = StringToFormControlType("select-one");
     expected.max_length = 0;
     EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[2]);
 
@@ -2357,7 +2359,7 @@
                                    EXTRACT_NONE, &result1);
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
@@ -2396,7 +2398,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.value = u"value";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.autocomplete_attribute = "off";
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
@@ -2419,7 +2421,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.value = u"value";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = 5;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
 }
@@ -2441,7 +2443,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.value = u"value";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   expected.is_autofilled = true;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
@@ -2466,7 +2468,7 @@
   expected.id_attribute = u"checkbox";
   expected.name = expected.id_attribute;
   expected.value = u"mail";
-  expected.form_control_type = "checkbox";
+  expected.form_control_type = StringToFormControlType("checkbox");
   expected.is_autofilled = true;
   expected.check_status = FormFieldData::CheckStatus::kChecked;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
@@ -2478,7 +2480,7 @@
   expected.id_attribute = u"radio";
   expected.name = expected.id_attribute;
   expected.value = u"male";
-  expected.form_control_type = "radio";
+  expected.form_control_type = StringToFormControlType("radio");
   expected.is_autofilled = true;
   expected.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
@@ -2503,7 +2505,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.max_length = 0;
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
 
   expected.value = u"CA";
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result1);
@@ -2550,7 +2552,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.max_length = 0;
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   // We check that the extra attributes have been copied to |result1|.
   expected.is_autofilled = true;
   expected.autocomplete_attribute = "off";
@@ -2644,7 +2646,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.max_length = 0;
-  expected.form_control_type = "textarea";
+  expected.form_control_type = StringToFormControlType("textarea");
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result_sans_value);
 
   FormFieldData result_with_value;
@@ -2672,7 +2674,7 @@
   expected.id_attribute = u"element";
   expected.name = expected.id_attribute;
   expected.max_length = 0;
-  expected.form_control_type = "month";
+  expected.form_control_type = StringToFormControlType("month");
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result_sans_value);
 
   FormFieldData result_with_value;
@@ -2700,7 +2702,7 @@
   expected.max_length = WebInputElement::DefaultMaxLength();
   expected.id_attribute = u"password";
   expected.name = expected.id_attribute;
-  expected.form_control_type = "password";
+  expected.form_control_type = StringToFormControlType("password");
   expected.value = u"secret";
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, result);
 }
@@ -2776,7 +2778,8 @@
     FormFieldData expected;
     expected.id_attribute = ASCIIToUTF16(test_case.element_id);
     expected.name = expected.id_attribute;
-    expected.form_control_type = test_case.form_control_type;
+    expected.form_control_type =
+        autofill::StringToFormControlType(test_case.form_control_type);
     expected.max_length = test_case.form_control_type == "text"
                               ? WebInputElement::DefaultMaxLength()
                               : 0;
@@ -2983,7 +2986,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"John";
   expected.label = u"First name:";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[0]);
 
@@ -2991,7 +2994,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"Smith";
   expected.label = u"Last name:";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[1]);
 
@@ -2999,7 +3002,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"123 Fantasy Ln.\nApt. 42";
   expected.label = u"Address:";
-  expected.form_control_type = "textarea";
+  expected.form_control_type = StringToFormControlType("textarea");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[2]);
 
@@ -3007,7 +3010,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"CA";
   expected.label = u"State:";
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[3]);
 
@@ -3015,7 +3018,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"secret";
   expected.label = u"Password:";
-  expected.form_control_type = "password";
+  expected.form_control_type = StringToFormControlType("password");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[4]);
 
@@ -3023,7 +3026,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"2011-12";
   expected.label = u"Card expiration:";
-  expected.form_control_type = "month";
+  expected.form_control_type = StringToFormControlType("month");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[5]);
 
@@ -3280,7 +3283,7 @@
   ASSERT_EQ(3U, fields.size());
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
 
   expected.id_attribute = u"firstname";
@@ -3365,7 +3368,7 @@
   ASSERT_EQ(4U, fields.size());
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
 
   expected.id_attribute = u"firstname";
@@ -4024,7 +4027,7 @@
   expected.label = u"* First Name";
   expected.name = expected.id_attribute;
   expected.value = u"John";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   fields.push_back(expected);
 
@@ -4033,7 +4036,7 @@
   expected.label = u"* Middle Name";
   expected.name = expected.id_attribute;
   expected.value = u"Joe";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   fields.push_back(expected);
 
@@ -4042,7 +4045,7 @@
   expected.label = u"* Last Name";
   expected.name = expected.id_attribute;
   expected.value = u"Smith";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   fields.push_back(expected);
 
@@ -4051,7 +4054,7 @@
   expected.label = u"* Country";
   expected.name = expected.id_attribute;
   expected.value = u"US";
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   expected.max_length = 0;
   fields.push_back(expected);
 
@@ -4060,7 +4063,7 @@
   expected.label = u"* Email";
   expected.name = expected.id_attribute;
   expected.value = u"john@example.com";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   fields.push_back(expected);
 
@@ -4816,7 +4819,7 @@
   ASSERT_EQ(4U, fields.size());
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
 
   expected.label = u"Phone:";
@@ -4872,7 +4875,7 @@
   ASSERT_EQ(6U, fields.size());
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
 
   expected.name_attribute = u"dayphone1";
   expected.label = u"Phone:";
@@ -5421,7 +5424,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"John";
   expected.label = u"John";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[0]);
 
@@ -5429,7 +5432,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"Smith";
   expected.label = u"Smith";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[1]);
 
@@ -5437,7 +5440,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"Albania";
   expected.label.clear();
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[2]);
 
@@ -5454,7 +5457,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"John";
   expected.label = u"John";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[0]);
 
@@ -5462,7 +5465,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"Smith";
   expected.label = u"Smith";
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[1]);
 
@@ -5470,7 +5473,7 @@
   expected.name = expected.id_attribute;
   expected.value = u"AL";
   expected.label.clear();
-  expected.form_control_type = "select-one";
+  expected.form_control_type = StringToFormControlType("select-one");
   expected.max_length = 0;
   EXPECT_FORM_FIELD_DATA_EQUALS(expected, fields[2]);
 }
@@ -5512,7 +5515,7 @@
   ASSERT_EQ(3U, fields.size());
 
   FormFieldData expected;
-  expected.form_control_type = "text";
+  expected.form_control_type = StringToFormControlType("text");
   expected.max_length = WebInputElement::DefaultMaxLength();
 
   expected.id_attribute = u"firstname";
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 0778170..47a70a7 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2532,6 +2532,7 @@
       "../browser/ui/test/test_infobar.cc",
       "../browser/ui/test/test_infobar.h",
       "../browser/ui/thumbnails/thumbnail_readiness_tracker_browsertest.cc",
+      "../browser/ui/toolbar/pinned_toolbar_actions_model_browsertest.cc",
       "../browser/ui/toolbar/toolbar_actions_model_browsertest.cc",
       "../browser/ui/unload_controller_browsertest.cc",
       "../browser/ui/update_chrome_dialog_browsertest.cc",
@@ -3742,6 +3743,7 @@
         "//extensions/renderer",
         "//google_apis/common:test_support",
         "//google_apis/drive",
+        "//ui/actions:actions_headers",
         "//ui/base/idle:test_support",
         "//ui/views:test_support",
       ]
diff --git a/chrome/test/data/extensions/api_test/cookies/api/background.js b/chrome/test/data/extensions/api_test/cookies/api/background.js
index 897a8b2..cabab3b3 100644
--- a/chrome/test/data/extensions/api_test/cookies/api/background.js
+++ b/chrome/test/data/extensions/api_test/cookies/api/background.js
@@ -11,6 +11,7 @@
 var TEST_URL3 = 'https://' + TEST_HOST + '/content.html';
 var TEST_URL4 = 'https://' + TEST_HOST + TEST_PATH + '/content.html';
 var TEST_URL5 = 'http://' + TEST_HOST + TEST_PATH + '/content.html';
+var TEST_OPAQUE_URL = 'file://' + TEST_DOMAIN;
 var TEST_EXPIRATION_DATE = 12345678900;
 var TEST_ODD_DOMAIN_HOST_ONLY = 'strange stuff!!.com';
 var TEST_ODD_DOMAIN = '.' + TEST_ODD_DOMAIN_HOST_ONLY;
@@ -101,6 +102,16 @@
   partitionKey: {topLevelSite: TEST_PARTITION_KEY}
 };
 
+var TEST_PARTITIONED_COOKIE_OPAQUE_TOP_LEVEL_SITE = {
+  url: TEST_PARTITION_KEY,
+  name: 'OPAQUE_TOP_LEVEL_SITE',
+  value: 'partitioned_cookie_val',
+  expirationDate: TEST_EXPIRATION_DATE,
+  secure: false,
+  httpOnly: true,
+  partitionKey: {topLevelSite: TEST_OPAQUE_URL}
+};
+
 function expectValidCookie(cookie) {
   chrome.test.assertNe(null, cookie, 'Expected cookie not set.');
 }
@@ -298,6 +309,16 @@
                 partitionKey: {topLevelSite: TEST_URL4}
               },
               pass(expectNullCookie));
+          // Confirm no cookie retrieved with opaque site.
+          chrome.cookies.get(
+              {
+                url: TEST_PARTITIONED_COOKIE.url,
+                name: TEST_PARTITIONED_COOKIE.name,
+                partitionKey: {topLevelSite: TEST_OPAQUE_URL}
+              },
+              pass(expectNullCookie));
+          // Confirm that a cookie can be retrieved with
+          // correct parameters.
           chrome.cookies.get(
               {
                 url: TEST_PARTITIONED_COOKIE.url,
@@ -367,6 +388,13 @@
                     cookie.partitionKey.topLevelSite);
               }));
         }));
+    // Confirm that setting a cookie with an opaque top_level_site,
+    // does not work but also does not crash.
+    chrome.cookies.set(
+        TEST_PARTITIONED_COOKIE_OPAQUE_TOP_LEVEL_SITE,
+        chrome.test.callbackFail(
+            'Failed to parse or set cookie named "OPAQUE_TOP_LEVEL_SITE".'));
+
     // Confirm that trying to set a partitioned cookie that does not have
     // secure property equal true will result in a fail.
     chrome.cookies.set(
diff --git a/chrome/test/data/webui/BUILD.gn b/chrome/test/data/webui/BUILD.gn
index b27abbe5..929b987d 100644
--- a/chrome/test/data/webui/BUILD.gn
+++ b/chrome/test/data/webui/BUILD.gn
@@ -403,6 +403,7 @@
     "mock_timer_test.ts",
     "mock_timer.ts",
     "polymer_test_util.ts",
+    "test_api.ts",
     "test_browser_proxy.ts",
     "test_mock.ts",
     "test_open_window_proxy.ts",
@@ -413,7 +414,6 @@
 
     # TODO(dpapad): Migrate the files below to TypeScript and remove allowJs
     # from tsconfig_base.json.
-    "mocha_adapter.js",
     "sandbox/gpu_test.js",
     "sandbox/sandbox_test.js",
   ]
@@ -421,6 +421,7 @@
   if (is_chromeos_ash) {
     in_files += [
       "cr_focus_row_behavior_test.ts",
+      "mocha_adapter.js",
       "mojo_webui_test_support.js",
     ]
   }
diff --git a/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js b/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
index ebafd3a0..0b530bf 100644
--- a/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
+++ b/chrome/test/data/webui/chromeos/os_feedback_ui/share_data_page_test.js
@@ -5,7 +5,7 @@
 import 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-lite.js';
 import 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-lite.js';
 
-import {fakeEmptyFeedbackContext, fakeFeedbackContext, fakeInternalUserFeedbackContext} from 'chrome://os-feedback/fake_data.js';
+import {fakeEmptyFeedbackContext, fakeFeedbackContext, fakeInternalUserFeedbackContext, fakeLoginFeedbackContext} from 'chrome://os-feedback/fake_data.js';
 import {FakeFeedbackServiceProvider} from 'chrome://os-feedback/fake_feedback_service_provider.js';
 import {FeedbackFlowState} from 'chrome://os-feedback/feedback_flow.js';
 import {FeedbackAppPreSubmitAction, FeedbackContext} from 'chrome://os-feedback/feedback_types.js';
@@ -196,6 +196,21 @@
         getElementContent('#privacyNote'));
   });
 
+  // Test the privacy note displayed to logged out users.
+  test('privacyNote_loggedOut_users', async () => {
+    await initializePage();
+    page.feedbackContext = fakeLoginFeedbackContext;
+    assertEquals(
+        'Some account and system information may be sent to Google. We use ' +
+            'this information to help address technical issues and improve ' +
+            'our services, subject to our Privacy Policy ' +
+            '(https://policies.google.com/privacy) and Terms of Service ' +
+            '(https://policies.google.com/terms). To request content changes,' +
+            ' go to Legal Help ' +
+            '(https://support.google.com/legal/answer/3110420).',
+        getElementContent('#privacyNote'));
+  });
+
   // Test that the email drop down is populated with two options.
   test('emailDropdownPopulated', async () => {
     await initializePage();
@@ -795,7 +810,7 @@
    */
   test('AdditionalContext_CategoryTag_Bluetooth', async () => {
     await initializePage();
-    page.feedbackContext = fakeFeedbackContext;
+    page.feedbackContext = fakeEmptyFeedbackContext;
 
     // Uncheck the "Link Cross Device Dogfood Feedback" checkbox so that only
     // the Bluetooth-specific categoryTag is added to the report.
@@ -848,7 +863,7 @@
       'AdditionalContext_CategoryTag_LinkCrossDeviceDogfoodFeedback',
       async () => {
         await initializePage();
-        page.feedbackContext = fakeFeedbackContext;
+        page.feedbackContext = fakeEmptyFeedbackContext;
 
         // Uncheck the bluetooth logs checkbox so that only the "Link Cross
         // Device Dogfood Feedback"-specific categoryTag is added to the report.
diff --git a/chrome/test/data/webui/chromeos/scanning/scanning_app_test_utils.js b/chrome/test/data/webui/chromeos/scanning/scanning_app_test_utils.js
index 3f428c5d..7ecad00 100644
--- a/chrome/test/data/webui/chromeos/scanning/scanning_app_test_utils.js
+++ b/chrome/test/data/webui/chromeos/scanning/scanning_app_test_utils.js
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 import './scanning_mojom_imports.js';
 
-import {ColorMode, PageSize, Scanner, ScanSource} from 'chrome://scanning/scanning.mojom-webui.js';
+import {ColorMode, PageSize} from 'chrome://scanning/scanning.mojom-webui.js';
 import {alphabeticalCompare} from 'chrome://scanning/scanning_app_util.js';
 import {assertEquals, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
 import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
diff --git a/chrome/test/data/webui/settings/autofill_page_test.ts b/chrome/test/data/webui/settings/autofill_page_test.ts
index e108746..060f390 100644
--- a/chrome/test/data/webui/settings/autofill_page_test.ts
+++ b/chrome/test/data/webui/settings/autofill_page_test.ts
@@ -3,11 +3,12 @@
 // found in the LICENSE file.
 
 // clang-format off
+import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
 import {DomIf, flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {AutofillManagerImpl, PaymentsManagerImpl, SettingsAutofillSectionElement, SettingsPaymentsSectionElement} from 'chrome://settings/lazy_load.js';
 import {buildRouter, Router} from 'chrome://settings/settings.js';
-import {CrSettingsPrefs, OpenWindowProxyImpl, PasswordManagerImpl, SettingsAutofillPageElement, SettingsPluralStringProxyImpl, SettingsPrefsElement, PasswordManagerPage} from 'chrome://settings/settings.js';
-import {assertEquals, assertDeepEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
+import {CrLinkRowElement, CrSettingsPrefs, OpenWindowProxyImpl, PasswordManagerImpl, SettingsAutofillPageElement, SettingsPluralStringProxyImpl, SettingsPrefsElement, PasswordManagerPage} from 'chrome://settings/settings.js';
+import {assertEquals, assertDeepEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
 import {FakeSettingsPrivate} from 'chrome://webui-test/fake_settings_private.js';
 import {TestPluralStringProxy} from 'chrome://webui-test/test_plural_string_proxy.js';
 import {TestOpenWindowProxy} from 'chrome://webui-test/test_open_window_proxy.js';
@@ -304,3 +305,63 @@
     assertEquals(PasswordManagerPage.PASSWORDS, param);
   });
 });
+
+suite('PlusAddressesUITest', function() {
+  const fakeUrl = 'https://mattwashere';
+  let autofillPage: SettingsAutofillPageElement;
+  let openWindowProxy: TestOpenWindowProxy;
+
+  setup(function() {
+    openWindowProxy = new TestOpenWindowProxy();
+    OpenWindowProxyImpl.setInstance(openWindowProxy);
+    // Override the `plusAddressManagementUrl` by default in this suite. This
+    // property is what drives the dom-if to show (or not) the button.
+    loadTimeData.overrideValues({
+      plusAddressManagementUrl: fakeUrl,
+    });
+    autofillPage = document.createElement('settings-autofill-page');
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+    document.body.appendChild(autofillPage);
+    flush();
+  });
+
+  test('Plus Address Management Existence', function() {
+    // Check that the `loadTimeData` override in the test setup correctly
+    // results in there being a `plusAddressButton`.
+    const plusAddressButton =
+        autofillPage.shadowRoot!.querySelector<CrLinkRowElement>(
+            '#plusAddressManagerButton');
+    assertTrue(!!plusAddressButton);
+  });
+
+  test('Plus Address Management Non-Existence', function() {
+    autofillPage.remove();
+    // Check that the default state (overwriting the override in the setup
+    // function) results in there not being a `plusAddressButton`.
+    loadTimeData.overrideValues({
+      plusAddressManagementUrl: '',
+    });
+    autofillPage = document.createElement('settings-autofill-page');
+    document.body.innerHTML = window.trustedTypes!.emptyHTML;
+    document.body.appendChild(autofillPage);
+    flush();
+
+    const plusAddressButton =
+        autofillPage.shadowRoot!.querySelector<CrLinkRowElement>(
+            '#plusAddressManagerButton');
+    assertFalse(!!plusAddressButton);
+  });
+
+  test('Clicking Plus Address Management item', async function() {
+    const plusAddressButton =
+        autofillPage.shadowRoot!.querySelector<CrLinkRowElement>(
+            '#plusAddressManagerButton');
+    assertTrue(!!plusAddressButton);
+
+    // Validate that, when present, the button results in opening the URL passed
+    // in via the `loadTimeData` override.
+    plusAddressButton.click();
+    const url = await openWindowProxy.whenCalled('openUrl');
+    assertEquals(url, fakeUrl);
+  });
+});
diff --git a/chrome/test/data/webui/settings/chromeos/device_page/customize_button_row_test.ts b/chrome/test/data/webui/settings/chromeos/device_page/customize_button_row_test.ts
index 26be0ba3..8e5e8a4d 100644
--- a/chrome/test/data/webui/settings/chromeos/device_page/customize_button_row_test.ts
+++ b/chrome/test/data/webui/settings/chromeos/device_page/customize_button_row_test.ts
@@ -91,8 +91,9 @@
         expectedRemapping!.name);
     assertEquals(
         getSelectedValue(),
-        'hardcodedAction' +
-            expectedRemapping!.remappingAction?.hardcodedAction!.toString());
+        'staticShortcutAction' +
+            expectedRemapping!.remappingAction?.staticShortcutAction!
+                .toString());
 
     // Change buttonRemapping data to display.
     customizeButtonRow.set('remappingIndex', 1);
@@ -106,19 +107,20 @@
         expectedRemapping!.name);
     assertEquals(
         getSelectedValue(),
-        'action' + expectedRemapping!.remappingAction?.action!.toString());
+        'acceleratorAction' +
+            expectedRemapping!.remappingAction?.acceleratorAction!.toString());
   });
 
   test('update dropdown in mouse will sent events', async () => {
     await initializeMouseCustomizeButtonRow();
-    assertEquals(getSelectedValue(), 'hardcodedAction0');
+    assertEquals(getSelectedValue(), 'staticShortcutAction0');
     assertEquals(buttonRemappingChangedEventCount, 0);
     // Update select to another remapping action.
     const select: HTMLSelectElement|null =
         customizeButtonRow.shadowRoot!.querySelector(
             '#remappingActionDropdown');
     assertTrue(!!select);
-    select.value = 'action0';
+    select.value = 'acceleratorAction0';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
 
@@ -126,7 +128,7 @@
     assertEquals(buttonRemappingChangedEventCount, 1);
     assertDeepEquals(
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, {
-          action: 0,
+          acceleratorAction: 0,
         });
 
     // Update select to no remapping action choice.
@@ -138,17 +140,17 @@
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, undefined);
 
     // Update select from no remapping back to normal remapping action.
-    select.value = 'hardcodedAction1';
+    select.value = 'staticShortcutAction1';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
     assertEquals(buttonRemappingChangedEventCount, 3);
     assertDeepEquals(
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, {
-          hardcodedAction: 1,
+          staticShortcutAction: 1,
         });
 
     // Update select to the same action, no events will be fired.
-    select.value = 'hardcodedAction1';
+    select.value = 'staticShortcutAction1';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
     assertEquals(buttonRemappingChangedEventCount, 3);
@@ -166,7 +168,8 @@
         expectedRemapping!.name);
     assertEquals(
         getSelectedValue(),
-        'action' + expectedRemapping!.remappingAction?.action!.toString());
+        'acceleratorAction' +
+            expectedRemapping!.remappingAction?.acceleratorAction!.toString());
 
     // Change buttonRemapping data to display.
     customizeButtonRow.set('remappingIndex', 1);
@@ -182,7 +185,8 @@
         expectedRemapping!.name);
     assertEquals(
         getSelectedValue(),
-        'action' + expectedRemapping!.remappingAction?.action!.toString());
+        'acceleratorAction' +
+            expectedRemapping!.remappingAction?.acceleratorAction!.toString());
   });
 
   test('Initialize key combination string', async () => {
@@ -209,14 +213,14 @@
 
   test('update dropdown will sent events', async () => {
     await initializeCustomizeButtonRow();
-    assertEquals(getSelectedValue(), 'action2');
+    assertEquals(getSelectedValue(), 'acceleratorAction2');
     assertEquals(buttonRemappingChangedEventCount, 0);
     // Update select to another remapping action.
     const select: HTMLSelectElement|null =
         customizeButtonRow.shadowRoot!.querySelector(
             '#remappingActionDropdown');
     assertTrue(!!select);
-    select.value = 'action1';
+    select.value = 'acceleratorAction1';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
 
@@ -224,7 +228,7 @@
     assertEquals(buttonRemappingChangedEventCount, 1);
     assertDeepEquals(
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, {
-          action: 1,
+          acceleratorAction: 1,
         });
 
     // Update select to no remapping action choice.
@@ -236,17 +240,17 @@
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, undefined);
 
     // Update select from no remapping back to normal remapping action.
-    select.value = 'action2';
+    select.value = 'acceleratorAction2';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
     assertEquals(buttonRemappingChangedEventCount, 3);
     assertDeepEquals(
         customizeButtonRow.get('buttonRemapping_')?.remappingAction, {
-          action: 2,
+          acceleratorAction: 2,
         });
 
     // Update select to the same action, no events will be fired.
-    select.value = 'action2';
+    select.value = 'acceleratorAction2';
     select.dispatchEvent(new Event('change'));
     await flushTasks();
     assertEquals(buttonRemappingChangedEventCount, 3);
@@ -311,7 +315,7 @@
     assertEquals(showKeyCombinationDialogEventCount, 1);
     // Verify that the selected value will change back to
     // the previous selection.
-    assertEquals(select.value, 'action2');
+    assertEquals(select.value, 'acceleratorAction2');
 
     // Verify that when clicking the open key combination value again,
     // the open dialog event will fire again.
@@ -320,6 +324,6 @@
 
     await flushTasks();
     assertEquals(showKeyCombinationDialogEventCount, 2);
-    assertEquals(select.value, 'action2');
+    assertEquals(select.value, 'acceleratorAction2');
   });
 });
diff --git a/chrome/test/data/webui/settings/chromeos/device_page/fake_input_device_settings_provider_test.ts b/chrome/test/data/webui/settings/chromeos/device_page/fake_input_device_settings_provider_test.ts
index 20ac4e3..27cbafa 100644
--- a/chrome/test/data/webui/settings/chromeos/device_page/fake_input_device_settings_provider_test.ts
+++ b/chrome/test/data/webui/settings/chromeos/device_page/fake_input_device_settings_provider_test.ts
@@ -116,7 +116,7 @@
           vkey: 0,
         },
         remappingAction: {
-          action: 0,
+          acceleratorAction: 0,
         },
       },
     ];
diff --git a/chrome/test/data/webui/settings/payments_section_iban_test.ts b/chrome/test/data/webui/settings/payments_section_iban_test.ts
index c7aa2db..d1c772eb 100644
--- a/chrome/test/data/webui/settings/payments_section_iban_test.ts
+++ b/chrome/test/data/webui/settings/payments_section_iban_test.ts
@@ -9,7 +9,7 @@
 import {SettingsSimpleConfirmationDialogElement, CrInputElement, PaymentsManagerImpl, SettingsIbanEditDialogElement} from 'chrome://settings/lazy_load.js';
 import {CrButtonElement, loadTimeData} from 'chrome://settings/settings.js';
 import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
-import {eventToPromise, whenAttributeIs} from 'chrome://webui-test/test_util.js';
+import {eventToPromise, isVisible, whenAttributeIs} from 'chrome://webui-test/test_util.js';
 
 import {createIbanEntry, TestPaymentsManager} from './autofill_fake_data.js';
 import {createPaymentsSection, getDefaultExpectations} from './payments_section_utils.js';
@@ -49,9 +49,9 @@
   }
 
   /**
-   * Returns an array containing all local IBAN items.
+   * Returns an array containing all local and server IBAN items.
    */
-  function getLocalIbanListItems() {
+  function getIbanListItems() {
     return document.body.querySelector('settings-payments-section')!.shadowRoot!
         .querySelector('#paymentsList')!.shadowRoot!.querySelectorAll(
             'settings-iban-list-entry')!;
@@ -113,7 +113,7 @@
     await createPaymentsSection(
         /*creditCards=*/[], [iban1, iban2], /*prefValues=*/ {});
 
-    assertEquals(2, getLocalIbanListItems().length);
+    assertEquals(2, getIbanListItems().length);
   });
 
   test('verifyIbanSummarySublabelWithNickname', async function() {
@@ -122,7 +122,7 @@
     const section = await createPaymentsSection(
         /*creditCards=*/[], [iban], /*prefValues=*/ {});
 
-    assertEquals(1, getLocalIbanListItems().length);
+    assertEquals(1, getIbanListItems().length);
 
     const ibanItemValue = getIbanRowShadowRoot(section.$.paymentsList)
                               .querySelector<HTMLElement>('#value');
@@ -198,7 +198,7 @@
     const section = await createPaymentsSection(
         /*creditCards=*/[], [iban],
         /*prefValues=*/ {});
-    assertEquals(1, getLocalIbanListItems().length);
+    assertEquals(1, getIbanListItems().length);
 
     // Local IBANs will show the 3-dot overflow menu.
     section.$.ibanSharedActionMenu.get();
@@ -221,7 +221,7 @@
 
     const section = await createPaymentsSection(
         /*creditCards=*/[], [iban], /*prefValues=*/ {});
-    assertEquals(1, getLocalIbanListItems().length);
+    assertEquals(1, getIbanListItems().length);
 
     const rowShadowRoot = getIbanRowShadowRoot(section.$.paymentsList);
     assertTrue(!!rowShadowRoot);
@@ -264,7 +264,7 @@
 
     const section = await createPaymentsSection(
         /*creditCards=*/[], [iban], /*prefValues=*/ {});
-    assertEquals(1, getLocalIbanListItems().length);
+    assertEquals(1, getIbanListItems().length);
 
     const rowShadowRoot = getIbanRowShadowRoot(section.$.paymentsList);
     assertTrue(!!rowShadowRoot);
@@ -300,4 +300,29 @@
     expectations.removedIbans = 0;
     paymentsManager.assertExpectations(expectations);
   });
+
+  test('verifyGooglePaymentsIndicatorAppearsForServerIbans', async function() {
+    const iban = createIbanEntry();
+    iban.metadata!.isLocal = false;
+    const section = await createPaymentsSection(
+        /*creditCards=*/[], [iban], /*prefValues=*/ {});
+    assertEquals(1, getIbanListItems().length);
+    assertTrue(
+        isVisible(getIbanRowShadowRoot(section.$.paymentsList)
+                      .querySelector<HTMLElement>('#paymentsIndicator')));
+  });
+
+  test('verifyIbanRowButtonIsOutlinkForServerIbans', async function() {
+    const iban = createIbanEntry();
+    iban.metadata!.isLocal = false;
+    const section = await createPaymentsSection(
+        /*creditCards=*/[], [iban], /*prefValues=*/ {});
+    assertEquals(1, getIbanListItems().length);
+    const rowShadowRoot = getIbanRowShadowRoot(section.$.paymentsList);
+    const menuButton = rowShadowRoot.querySelector('#ibanMenu');
+    assertFalse(!!menuButton);
+    const outlinkButton =
+        rowShadowRoot.querySelector('cr-icon-button.icon-external');
+    assertTrue(!!outlinkButton);
+  });
 });
diff --git a/chrome/test/data/webui/settings/payments_section_interactive_test.ts b/chrome/test/data/webui/settings/payments_section_interactive_test.ts
index 429a36c6..c273c5f 100644
--- a/chrome/test/data/webui/settings/payments_section_interactive_test.ts
+++ b/chrome/test/data/webui/settings/payments_section_interactive_test.ts
@@ -183,8 +183,11 @@
     const firstEntry = section.$.paymentsList.shadowRoot!.querySelector(
         'settings-iban-list-entry');
     assertTrue(!!firstEntry);
-    const menuButton = firstEntry.$.ibanMenu;
+    assertFalse(!!firstEntry.shadowRoot!.querySelector('#remoteIbanLink'));
+    const menuButton =
+        firstEntry.shadowRoot!.querySelector<HTMLElement>('#ibanMenu');
     assertTrue(!!menuButton);
+
     menuButton.click();
     flush();
 
diff --git a/chrome/test/data/webui/test_api.ts b/chrome/test/data/webui/test_api.ts
new file mode 100644
index 0000000..723f58ec
--- /dev/null
+++ b/chrome/test/data/webui/test_api.ts
@@ -0,0 +1,32 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * Make all transitions and animations take 0ms. NOTE: this will completely
+ * disable webkitTransitionEnd events. If your code relies on them firing, it
+ * will break. animationend events should still work.
+ */
+export function disableAnimationsAndTransitions(): void {
+  const all = document.body.querySelectorAll<HTMLElement>('*');
+  const ZERO_MS_IMPORTANT = '0ms !important';
+  for (let i = 0; i < all.length; ++i) {
+    const style = all[i]!.style;
+    style.animationDelay = ZERO_MS_IMPORTANT;
+    style.animationDuration = ZERO_MS_IMPORTANT;
+    style.transitionDelay = ZERO_MS_IMPORTANT;
+    style.transitionDuration = ZERO_MS_IMPORTANT;
+  }
+
+  const realElementAnimate = Element.prototype.animate;
+  Element.prototype.animate = function(
+      keyframes: Keyframe[]|PropertyIndexedKeyframes|null,
+      options?: number|KeyframeAnimationOptions) {
+    if (typeof options === 'object') {
+      options.duration = 0;
+    } else {
+      options = 0;
+    }
+    return realElementAnimate.call(this, keyframes, options);
+  };
+}
diff --git a/chrome/updater/ipc/proxy_impl_base_win.h b/chrome/updater/ipc/proxy_impl_base_win.h
index cf55d3c..ce797d1 100644
--- a/chrome/updater/ipc/proxy_impl_base_win.h
+++ b/chrome/updater/ipc/proxy_impl_base_win.h
@@ -16,6 +16,7 @@
 #include "base/logging.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/sequence_checker.h"
+#include "base/strings/string_util.h"
 #include "base/task/bind_post_task.h"
 #include "base/task/sequenced_task_runner.h"
 #include "base/task/task_traits.h"
@@ -48,7 +49,8 @@
   }
 
  protected:
-  explicit ProxyImplBase(UpdaterScope scope) : scope_(scope) {
+  explicit ProxyImplBase(UpdaterScope scope, const std::vector<IID>& iids = {})
+      : scope_(scope), iids_(iids) {
     DETACH_FROM_SEQUENCE(sequence_checker_);
     VLOG(3) << __func__ << ": Interface: " << typeid(Interface).name()
             << ": iid_user: " << base::win::WStringFromGUID(iid_user)
@@ -110,50 +112,35 @@
         if (dumped_once) {
           return;
         }
+        dumped_once = true;
 
         base::ThreadPool::PostTask(
             FROM_HERE, {base::MayBlock(), base::WithBaseSyncPrimitives()},
             base::BindOnce(
-                [](std::wstring hkey_root, std::wstring interface_iid) {
-                  std::wstring log_string;
-                  for (const auto& reg_key : {
-                           base::StrCat({hkey_root,
-                                         L"\\SOFTWARE\\Classes\\Interface\\",
-                                         interface_iid}),
-                           base::StrCat({hkey_root,
-                                         L"\\SOFTWARE\\Classes\\TypeLib\\",
-                                         interface_iid}),
-                           base::StrCat({hkey_root,
-                                         L"\\SOFTWARE\\WOW6432Node\\Classes"
-                                         L"\\Interface\\",
-                                         interface_iid}),
-                           base::StrCat({hkey_root,
-                                         L"\\SOFTWARE\\WOW6432Node\\Classes"
-                                         L"\\TypeLib\\",
-                                         interface_iid}),
-                           base::StrCat({L"HKCR\\Interface\\", interface_iid}),
-                           base::StrCat({L"HKCR\\TypeLib\\", interface_iid}),
-                           base::StrCat({L"HKCR\\WOW6432Node\\Interface\\",
-                                         interface_iid}),
-                           base::StrCat({L"HKCR\\WOW6432Node\\TypeLib\\",
-                                         interface_iid}),
-                       }) {
+                [](const std::wstring& hkey_root,
+                   const std::vector<IID>& interface_iids) {
+                  for (const auto& iid : interface_iids) {
+                    const std::wstring interface_iid =
+                        base::win::WStringFromGUID(iid);
+                    const auto reg_key =
+                        base::StrCat({hkey_root,
+                                      L"\\SOFTWARE\\WOW6432Node\\Classes"
+                                      L"\\Interface\\",
+                                      interface_iid});
                     absl::optional<std::wstring> contents =
                         GetRegKeyContents(reg_key);
-                    log_string +=
-                        base::StrCat({L"Contents of *", reg_key, L"* = ",
-                                      contents ? *contents : L"", L"\n"});
+                    LOG(ERROR)
+                        << reg_key << ": "
+                        << (contents && !base::ContainsOnlyChars(
+                                            *contents, base::kWhitespaceWide)
+                                ? *contents
+                                : L"*Missing*");
                   }
-                  LOG(ERROR) << "log_string: " << log_string;
-                  DEBUG_ALIAS_FOR_WCHARCSTR(local_log_string,
-                                            log_string.c_str(), 1024 * 4);
                   DUMP_WILL_BE_CHECK(false);
                 },
-                IsSystemInstall(scope_) ? L"HKLM" : L"HKCU",
-                base::win::WStringFromGUID(iid)));
+                IsSystemInstall(scope_) ? L"HKLM" : L"HKCU", iids_));
 
-        base::PlatformThread::Sleep(base::Seconds(5));
-        dumped_once = true;
+        base::PlatformThread::Sleep(base::Seconds(10));
       }();
       return base::unexpected(hr);
     }
@@ -194,6 +181,7 @@
            base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
 
   const UpdaterScope scope_;
+  const std::vector<IID> iids_;
 
   HResultOr<Microsoft::WRL::ComPtr<Interface>> interface_ =
       base::unexpected(S_OK);
diff --git a/chrome/updater/ipc/update_service_internal_proxy_win.cc b/chrome/updater/ipc/update_service_internal_proxy_win.cc
index ca13105f1..bb7dd60b 100644
--- a/chrome/updater/ipc/update_service_internal_proxy_win.cc
+++ b/chrome/updater/ipc/update_service_internal_proxy_win.cc
@@ -22,6 +22,7 @@
 #include "chrome/updater/ipc/update_service_internal_proxy.h"
 #include "chrome/updater/updater_scope.h"
 #include "chrome/updater/util/win_util.h"
+#include "chrome/updater/win/setup/setup_util.h"
 #include "chrome/updater/win/win_constants.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 
@@ -77,7 +78,9 @@
                            __uuidof(IUpdaterInternalSystem)> {
  public:
   explicit UpdateServiceInternalProxyImplImpl(UpdaterScope scope)
-      : ProxyImplBase(scope) {}
+      : ProxyImplBase(scope,
+                      JoinVectors(GetSideBySideInterfaces(scope),
+                                  GetActiveInterfaces(scope))) {}
 
   static auto GetClassGuid(UpdaterScope scope) {
     return IsSystemInstall(scope) ? __uuidof(UpdaterInternalSystemClass)
diff --git a/chrome/updater/test/test_installer/BUILD.gn b/chrome/updater/test/test_installer/BUILD.gn
index 5d0f6b7..e571343 100644
--- a/chrome/updater/test/test_installer/BUILD.gn
+++ b/chrome/updater/test/test_installer/BUILD.gn
@@ -176,14 +176,36 @@
     script_target = "test_script_" + app_name
     copy(script_target) {
       sources = [ invoker.script ]
-      outputs = [ "$target_gen_dir/" + app_name + "/zip_contents/.install" ]
+      outputs =
+          [ "$target_gen_dir/" + app_name + "/zip_contents/" + invoker.script ]
     }
+    install_target = "install_script_" + app_name
+    action(install_target) {
+      script = "generate_install_script.py"
+      inputs = [ script ]
+      outputs = [ "$target_gen_dir/" + app_name + "/zip_contents/.install" ]
+      args = [
+        "--command",
+        invoker.script,
+        "--app",
+        invoker.app,
+        "--version",
+        invoker.version,
+        "--output",
+        rebase_path(outputs[0]),
+      ]
+    }
+
     zip("test_installer_zip_" + app_name) {
-      inputs = get_target_outputs(":" + script_target)
+      inputs = get_target_outputs(":" + script_target) +
+               get_target_outputs(":" + install_target)
       base_dir = "$target_gen_dir/" + app_name + "/zip_contents/"
       output = "$target_gen_dir/" + app_name + "/crx_contents/test_installer_" +
                app_name + ".zip"
-      deps = [ ":" + script_target ]
+      deps = [
+        ":" + script_target,
+        ":" + install_target,
+      ]
     }
     crx3("test_installer_" + app_name) {
       base_dir = "$target_gen_dir/" + app_name + "/crx_contents/"
@@ -196,6 +218,8 @@
   }
 
   test_app_installer("app") {
+    app = "MockApp"
+    version = "1.0.0.0"
     script = "test_app_setup.sh"
   }
 }
diff --git a/chrome/updater/test/test_installer/generate_install_script.py b/chrome/updater/test/test_installer/generate_install_script.py
new file mode 100755
index 0000000..65238b5
--- /dev/null
+++ b/chrome/updater/test/test_installer/generate_install_script.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import argparse
+import os
+import stat
+
+
+def generate_app_install_script(command: str, app: str, company: str,
+                                version: str, output: str):
+    with open(output, "wt") as f:
+        f.write('#/bin/bash\n\n')
+        f.write('SCRIPT_DIR=$(dirname -- "${BASH_SOURCE[0]}")\n')
+        f.write('"${SCRIPT_DIR}/' +
+                f'{command}" --appname={app} --company={company} '
+                f'--product_version={version}\n')
+    os.chmod(output, os.stat(output).st_mode | stat.S_IEXEC)
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument('--command', required=True, help='The command to run.')
+    parser.add_argument('--output',
+                        required=True,
+                        help='The script output path.')
+    parser.add_argument('--app', required=True, help='App to install.')
+    parser.add_argument('--company',
+                        required=False,
+                        default='Chromium',
+                        help='Owner company of the app.')
+    parser.add_argument('--version', required=True, help='Version of the app.')
+    args = parser.parse_args()
+    generate_app_install_script(args.command, args.app, args.company,
+                                args.version, args.output)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/chrome/updater/test/test_installer/test_app_setup.sh b/chrome/updater/test/test_installer/test_app_setup.sh
index fc4c1fe..4e945847 100755
--- a/chrome/updater/test/test_installer/test_app_setup.sh
+++ b/chrome/updater/test/test_installer/test_app_setup.sh
@@ -6,19 +6,13 @@
 # Example script that "installs" an app by writing some values to the install
 # path.
 
-declare appid="{AE098195-B8DB-4A49-8E23-84FCACB61FF1}"
-declare system=0
-declare company="Google"
-declare production_version="1.0.0.0"
-declare install_path="install_result.txt"
-
+declare appname=MockApp
+declare company="Chromium"
+declare product_version="1.0.0.0"
 for i in "$@"; do
   case $i in
-    --system)
-      system=1
-      ;;
-    --appid=*)
-     appid="${i#*=}"
+    --appname=*)
+     appname="${i#*=}"
      ;;
     --company=*)
      company="${i#*=}"
@@ -26,18 +20,25 @@
     --product_version=*)
      product_version="${i#*=}"
      ;;
-    --install_path=*)
-     install_path="${i#*=}"
-     ;;
     *)
       ;;
   esac
 done
 
-mkdir -p $(dirname ${install_path})
-cat << EOF > ${install_path}
-system=${system}
-appid=${appid}
-company=${company}
-product_version=${production_version}
+declare -r install_file="app.json"
+if [[ "${OSTYPE}" =~ ^"darwin" ]]; then
+  declare -r install_path="/Library/Application Support/${company}/${appname}"
+else
+  declare -r install_path="/opt/${company}/${appname}"
+fi
+
+mkdir -p "${install_path}"
+cat << EOF > "${install_path}/${install_file}"
+{
+  "app": "${appname}",
+  "company": "${company}",
+  "pv": "${product_version}"
+}
 EOF
+
+echo "Installed ${appname} version ${product_version} at: ${install_path}."
diff --git a/chromecast/browser/cast_content_browser_client.cc b/chromecast/browser/cast_content_browser_client.cc
index e5da90b..9010c4a0 100644
--- a/chromecast/browser/cast_content_browser_client.cc
+++ b/chromecast/browser/cast_content_browser_client.cc
@@ -113,7 +113,6 @@
 #if BUILDFLAG(IS_ANDROID)
 #include "base/android/build_info.h"
 #include "chromecast/media/audio/cast_audio_manager_android.h"  // nogncheck
-#include "components/cdm/browser/cdm_message_filter_android.h"
 #include "components/crash/core/app/crashpad.h"
 #include "media/audio/android/audio_manager_android.h"
 #include "media/audio/audio_features.h"
@@ -366,21 +365,6 @@
   return main_parts;
 }
 
-void CastContentBrowserClient::RenderProcessWillLaunch(
-    content::RenderProcessHost* host) {
-#if BUILDFLAG(IS_ANDROID)
-  // Cast on Android always allows persisting data.
-  //
-  // Cast on Android build always uses kForceVideoOverlays command line switch
-  // such that secure codecs can always be rendered.
-  //
-  // TODO(yucliu): On Clank, secure codecs support is tied to AndroidOverlay.
-  // Remove kForceVideoOverlays and switch to the Clank model for secure codecs
-  // support.
-  host->AddFilter(new cdm::CdmMessageFilterAndroid(true, true));
-#endif  // BUILDFLAG(IS_ANDROID)
-}
-
 bool CastContentBrowserClient::IsHandledURL(const GURL& url) {
   if (!url.is_valid()) {
     return false;
diff --git a/chromecast/browser/cast_content_browser_client.h b/chromecast/browser/cast_content_browser_client.h
index 27357c5..219649e6 100644
--- a/chromecast/browser/cast_content_browser_client.h
+++ b/chromecast/browser/cast_content_browser_client.h
@@ -176,7 +176,6 @@
   // content::ContentBrowserClient implementation:
   std::unique_ptr<content::BrowserMainParts> CreateBrowserMainParts(
       bool is_integration_test) override;
-  void RenderProcessWillLaunch(content::RenderProcessHost* host) override;
   bool IsHandledURL(const GURL& url) override;
   void SiteInstanceGotProcess(content::SiteInstance* site_instance) override;
   void AppendExtraCommandLineSwitches(base::CommandLine* command_line,
diff --git a/chromecast/common/cast_content_client.cc b/chromecast/common/cast_content_client.cc
index cf203751..7fa579b 100644
--- a/chromecast/common/cast_content_client.cc
+++ b/chromecast/common/cast_content_client.cc
@@ -22,6 +22,7 @@
 #include "components/cast/common/constants.h"
 #include "content/public/common/cdm_info.h"
 #include "media/base/media_switches.h"
+#include "media/cdm/cdm_type.h"
 #include "media/media_buildflags.h"
 #include "mojo/public/cpp/bindings/binder_map.h"
 #include "third_party/widevine/cdm/buildflags.h"
@@ -31,6 +32,8 @@
 
 #if BUILDFLAG(IS_ANDROID)
 #include "chromecast/common/media/cast_media_drm_bridge_client.h"
+#include "components/cdm/common/android_cdm_registration.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 #endif
 
 #if !BUILDFLAG(IS_FUCHSIA)
@@ -202,7 +205,33 @@
     } else {
       DVLOG(1) << "Widevine enabled but no library found";
     }
+#elif BUILDFLAG(IS_ANDROID)
+#if BUILDFLAG(ENABLE_WIDEVINE)
+    cdm::AddAndroidWidevineCdm(cdms);
+#endif  // BUILDFLAG(ENABLE_WIDEVINE)
 
+#if BUILDFLAG(ENABLE_PLAYREADY)
+    // PlayReady may be supported on the devices. Register it anyway without
+    // any capabilities so that it will be checked the first time it is used.
+    // CdmInfo needs a CdmType, but on Android it is not used as the key system
+    // is supported by MediaDrm. Using a random value as something needs to be
+    // specified, and it must be different than other CdmTypes specified.
+    // (On Android the key system is identified by UUID, and that mapping is
+    // maintained by MediaDrmBridge.)
+    const ::media::CdmType kPlayReadyCdmType{0x86eb6b54497627b0ull,
+                                             0xdd48f67486daf152ull};
+    // TODO(jrummell): Move kChromecastPlayreadyKeySystem from
+    // chromecast/media/base/key_systems_common.h to someplace more accessible.
+    const char kChromecastPlayreadyKeySystem[] = "com.chromecast.playready";
+    cdms->push_back(
+        content::CdmInfo(kChromecastPlayreadyKeySystem,
+                         content::CdmInfo::Robustness::kSoftwareSecure,
+                         absl::nullopt, kPlayReadyCdmType));
+    cdms->push_back(
+        content::CdmInfo(kChromecastPlayreadyKeySystem,
+                         content::CdmInfo::Robustness::kHardwareSecure,
+                         absl::nullopt, kPlayReadyCdmType));
+#endif  // BUILDFLAG(ENABLE_PLAYREADY)
 #endif  // BUILDFLAG(BUNDLE_WIDEVINE_CDM) && BUILDFLAG(IS_LINUX)
   }
 }
diff --git a/chromecast/renderer/BUILD.gn b/chromecast/renderer/BUILD.gn
index e16152a..3b770af 100644
--- a/chromecast/renderer/BUILD.gn
+++ b/chromecast/renderer/BUILD.gn
@@ -63,6 +63,7 @@
     "//chromecast/media",
     "//chromecast/media/base:media_codec_support",
     "//chromecast/renderer/media",
+    "//components/cdm/renderer",
     "//components/media_control/renderer",
     "//components/network_hints/renderer",
     "//components/on_load_script_injector/renderer",
diff --git a/chromecast/renderer/cast_content_renderer_client.cc b/chromecast/renderer/cast_content_renderer_client.cc
index 32ef4a2..07d3940 100644
--- a/chromecast/renderer/cast_content_renderer_client.cc
+++ b/chromecast/renderer/cast_content_renderer_client.cc
@@ -32,6 +32,7 @@
 #include "content/public/renderer/render_frame.h"
 #include "content/public/renderer/render_thread.h"
 #include "media/base/audio_parameters.h"
+#include "media/base/key_system_info.h"
 #include "media/base/media.h"
 #include "media/remoting/receiver_controller.h"
 #include "media/remoting/remoting_constants.h"
@@ -50,6 +51,7 @@
 #if BUILDFLAG(IS_ANDROID)
 #include "base/android/bundle_utils.h"
 #include "chromecast/media/audio/cast_audio_device_factory.h"
+#include "components/cdm/renderer/key_system_support_update.h"
 #include "media/base/android/media_codec_util.h"
 #else
 #include "chromecast/renderer/memory_pressure_observer_impl.h"
@@ -170,11 +172,16 @@
 
 void CastContentRendererClient::GetSupportedKeySystems(
     ::media::GetSupportedKeySystemsCB cb) {
+#if BUILDFLAG(IS_ANDROID)
+  cdm::GetSupportedKeySystemsUpdates(
+      /*can_persist_data=*/true, std::move(cb));
+#else
   ::media::KeySystemInfos key_systems;
   media::AddChromecastKeySystems(&key_systems,
                                  false /* enable_persistent_license_support */,
                                  false /* enable_playready */);
   std::move(cb).Run(std::move(key_systems));
+#endif  // BUILDFLAG(IS_ANDROID)
 }
 
 bool CastContentRendererClient::IsSupportedAudioType(
diff --git a/chromecast/renderer/media/key_systems_cast.cc b/chromecast/renderer/media/key_systems_cast.cc
index 75bd08e..38ca955 100644
--- a/chromecast/renderer/media/key_systems_cast.cc
+++ b/chromecast/renderer/media/key_systems_cast.cc
@@ -49,7 +49,7 @@
         persistent_license_support_(persistent_license_support) {
   }
 
-  std::string GetKeySystemName() const override {
+  std::string GetBaseKeySystemName() const override {
     return media::kChromecastPlayreadyKeySystem;
   }
 
@@ -81,20 +81,20 @@
 #if BUILDFLAG(IS_ANDROID)
       return EmeConfig{.hw_secure_codecs = EmeConfigRuleState::kRequired};
 #else
-      return media::EmeConfig::SupportedRule();
+      return EmeConfig::SupportedRule();
 #endif  // BUILDFLAG(IS_ANDROID)
     }
 
     // Cast-specific PlayReady implementation does not currently recognize or
     // support non-empty robustness strings.
-    return media::EmeConfig::UnsupportedRule();
+    return EmeConfig::UnsupportedRule();
   }
 
   EmeConfig::Rule GetPersistentLicenseSessionSupport() const override {
     if (persistent_license_support_) {
-      return media::EmeConfig::SupportedRule();
+      return EmeConfig::SupportedRule();
     } else {
-      return media::EmeConfig::UnsupportedRule();
+      return EmeConfig::UnsupportedRule();
     }
   }
 
@@ -108,9 +108,9 @@
   EmeConfig::Rule GetEncryptionSchemeConfigRule(
       EncryptionScheme encryption_scheme) const override {
     if (encryption_scheme == EncryptionScheme::kCenc) {
-      return media::EmeConfig::SupportedRule();
+      return EmeConfig::SupportedRule();
     } else {
-      return media::EmeConfig::UnsupportedRule();
+      return EmeConfig::UnsupportedRule();
     }
   }
 
@@ -198,41 +198,10 @@
       EmeFeatureSupport::ALWAYS_ENABLED));  // Distinctive identifier.
 #endif                                      // BUILDFLAG(ENABLE_WIDEVINE)
 }
-#elif BUILDFLAG(IS_ANDROID)
-#if BUILDFLAG(ENABLE_PLAYREADY)
-void AddCastPlayreadyKeySystemAndroid(
-    ::media::KeySystemInfos* key_system_infos) {
-  DCHECK(key_system_infos);
-  SupportedKeySystemResponse response =
-      cdm::QueryKeySystemSupport(kChromecastPlayreadyKeySystem);
-
-  if (response.non_secure_codecs == ::media::EME_CODEC_NONE)
-    return;
-
-  key_system_infos->emplace_back(new PlayReadyKeySystemInfo(
-      response.non_secure_codecs, response.secure_codecs,
-      false /* persistent_license_support */));
-}
-#endif  // BUILDFLAG(ENABLE_PLAYREADY)
-
-void AddCastAndroidKeySystems(
-    ::media::KeySystemInfos* key_system_infos,
-    bool enable_playready) {
-#if BUILDFLAG(ENABLE_PLAYREADY)
-  if (enable_playready) {
-    AddCastPlayreadyKeySystemAndroid(key_system_infos);
-  }
-#endif  // BUILDFLAG(ENABLE_PLAYREADY)
-
-#if BUILDFLAG(ENABLE_WIDEVINE)
-  cdm::AddAndroidWidevine(key_system_infos);
-#endif  // BUILDFLAG(ENABLE_WIDEVINE)
-}
-#endif  // BUILDFLAG(IS_ANDROID)
+#endif  // BUILDFLAG(USE_CHROMECAST_CDMS) || BUILDFLAG(ENABLE_LIBRARY_CDMS)
 
 }  // namespace
 
-// TODO(yucliu): Split CMA/Android logics into their own files.
 void AddChromecastKeySystems(
     ::media::KeySystemInfos* key_system_infos,
     bool enable_persistent_license_support,
@@ -240,9 +209,7 @@
 #if BUILDFLAG(USE_CHROMECAST_CDMS) || BUILDFLAG(ENABLE_LIBRARY_CDMS)
   AddCmaKeySystems(key_system_infos, enable_persistent_license_support,
                    enable_playready);
-#elif BUILDFLAG(IS_ANDROID)
-  AddCastAndroidKeySystems(key_system_infos, enable_playready);
-#endif  // BUILDFLAG(IS_ANDROID)
+#endif  // BUILDFLAG(USE_CHROMECAST_CDMS) || BUILDFLAG(ENABLE_LIBRARY_CDMS)
 }
 
 }  // namespace media
diff --git a/chromeos/CHROMEOS_LKGM b/chromeos/CHROMEOS_LKGM
index 5ba46e0..5d7bbbe 100644
--- a/chromeos/CHROMEOS_LKGM
+++ b/chromeos/CHROMEOS_LKGM
@@ -1 +1 @@
-15626.0.0
\ No newline at end of file
+15629.0.0
\ No newline at end of file
diff --git a/chromeos/ash/components/report/BUILD.gn b/chromeos/ash/components/report/BUILD.gn
index 8f699121..fbc5802 100644
--- a/chromeos/ash/components/report/BUILD.gn
+++ b/chromeos/ash/components/report/BUILD.gn
@@ -66,6 +66,8 @@
     "//chromeos/ash/components/report/utils/test_utils.h",
     "//chromeos/ash/components/report/utils/time_utils.cc",
     "//chromeos/ash/components/report/utils/time_utils.h",
+    "//chromeos/ash/components/report/utils/uma_utils.cc",
+    "//chromeos/ash/components/report/utils/uma_utils.h",
   ]
 }
 
@@ -127,6 +129,7 @@
     "//chromeos/ash/components/report/utils/psm_utils_unittest.cc",
     "//chromeos/ash/components/report/utils/test_utils_unittest.cc",
     "//chromeos/ash/components/report/utils/time_utils_unittest.cc",
+    "//chromeos/ash/components/report/utils/uma_utils_unittest.cc",
   ]
 
   deps = [
diff --git a/chromeos/ash/components/report/utils/uma_utils.cc b/chromeos/ash/components/report/utils/uma_utils.cc
new file mode 100644
index 0000000..e00bfa28
--- /dev/null
+++ b/chromeos/ash/components/report/utils/uma_utils.cc
@@ -0,0 +1,56 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/ash/components/report/utils/uma_utils.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/strings/strcat.h"
+
+namespace ash::report::utils {
+
+namespace {
+
+// Convert |PsmRequest| to associated histogram variant name.
+std::string PsmRequestHistogramVariant(PsmRequest psm_request) {
+  switch (psm_request) {
+    case PsmRequest::kImport:
+      return "Import";
+    case PsmRequest::kOprf:
+      return "Oprf";
+    case PsmRequest::kQuery:
+      return "Query";
+  }
+}
+
+// Convert |PsmUseCase| to associated histogram variant name.
+std::string PsmUseCaseHistogramVariant(PsmUseCase psm_use_case) {
+  switch (psm_use_case) {
+    case PsmUseCase::k1DA:
+      return "1DA";
+    case PsmUseCase::k28DA:
+      return "28DA";
+    case PsmUseCase::kCohort:
+      return "Cohort";
+    case PsmUseCase::kObservation:
+      return "Observation";
+  }
+}
+
+}  // namespace
+
+void RecordNetErrorCode(PsmUseCase use_case, PsmRequest request, int net_code) {
+  std::string variant_name = base::StrCat(
+      {"Ash.Report.Psm", PsmUseCaseHistogramVariant(use_case),
+       PsmRequestHistogramVariant(request), "ResponseNetErrorCode"});
+  base::UmaHistogramSparse(variant_name, net_code);
+}
+
+void RecordCheckMembershipCases(PsmUseCase use_case,
+                                CheckMembershipResponseCases response_case) {
+  std::string variant_name =
+      base::StrCat({"Ash.Report.", PsmUseCaseHistogramVariant(use_case),
+                    "CheckMembershipCases"});
+  base::UmaHistogramEnumeration(variant_name, response_case);
+}
+
+}  // namespace ash::report::utils
diff --git a/chromeos/ash/components/report/utils/uma_utils.h b/chromeos/ash/components/report/utils/uma_utils.h
new file mode 100644
index 0000000..045a7a34
--- /dev/null
+++ b/chromeos/ash/components/report/utils/uma_utils.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROMEOS_ASH_COMPONENTS_REPORT_UTILS_UMA_UTILS_H_
+#define CHROMEOS_ASH_COMPONENTS_REPORT_UTILS_UMA_UTILS_H_
+
+namespace ash::report::utils {
+
+// Enum class represents the "PsmRequest" <variants> in histograms.xml.
+// It is used to generate histogram record names, so must stay in sync
+// with the UMA variants definition.
+enum class PsmRequest { kImport = 0, kOprf = 1, kQuery = 2 };
+
+// Enum class represents the "PsmUseCase" <variants> in histograms.xml.
+// It is used to generate histogram record names, so must stay in sync
+// with the UMA variants definition.
+enum class PsmUseCase { k1DA = 0, k28DA = 1, kCohort = 2, kObservation = 3 };
+
+// Records UMA histogram for different failed check membership cases.
+enum class CheckMembershipResponseCases {
+  kUnknown = 0,
+  kCreateOprfRequestFailed = 1,
+  kOprfResponseBodyFailed = 2,
+  kNotHasRlweOprfResponse = 3,
+  kCreateQueryRequestFailed = 4,
+  kQueryResponseBodyFailed = 5,
+  kNotHasRlweQueryResponse = 6,
+  kProcessQueryResponseFailed = 7,
+  kMembershipResponsesSizeIsNotOne = 8,
+  kIsNotPsmIdMember = 9,
+  kSuccessfullySetLocalState = 10,
+  kMaxValue = kSuccessfullySetLocalState,
+};
+
+// Record UMA net error code histogram for a given use case and request.
+void RecordNetErrorCode(PsmUseCase use_case, PsmRequest request, int net_code);
+
+// Record UMA check membership response enum for the PSM use case.
+void RecordCheckMembershipCases(PsmUseCase use_case,
+                                CheckMembershipResponseCases response_case);
+
+}  // namespace ash::report::utils
+
+#endif  // CHROMEOS_ASH_COMPONENTS_REPORT_UTILS_UMA_UTILS_H_
diff --git a/chromeos/ash/components/report/utils/uma_utils_unittest.cc b/chromeos/ash/components/report/utils/uma_utils_unittest.cc
new file mode 100644
index 0000000..47166d06
--- /dev/null
+++ b/chromeos/ash/components/report/utils/uma_utils_unittest.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromeos/ash/components/report/utils/uma_utils.h"
+
+#include "base/test/metrics/histogram_tester.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace ash::report::utils {
+
+class UmaUtilsTest : public testing::Test {
+ public:
+  UmaUtilsTest() = default;
+  UmaUtilsTest(const UmaUtilsTest&) = delete;
+  UmaUtilsTest& operator=(const UmaUtilsTest&) = delete;
+  ~UmaUtilsTest() override = default;
+
+ protected:
+  base::HistogramTester* histogram_tester() { return &histogram_tester_; }
+
+ private:
+  base::HistogramTester histogram_tester_;
+};
+
+// Test case for RecordNetErrorCode function.
+TEST_F(UmaUtilsTest, RecordNetErrorCode) {
+  int net_code = 0;  // Successful net code
+
+  RecordNetErrorCode(PsmUseCase::k1DA, PsmRequest::kImport, net_code);
+  RecordNetErrorCode(PsmUseCase::k28DA, PsmRequest::kImport, net_code);
+  RecordNetErrorCode(PsmUseCase::kCohort, PsmRequest::kImport, net_code);
+  RecordNetErrorCode(PsmUseCase::kObservation, PsmRequest::kImport, net_code);
+
+  histogram_tester()->ExpectBucketCount(
+      "Ash.Report.Psm1DAImportResponseNetErrorCode", PsmRequest::kImport, 1);
+  histogram_tester()->ExpectBucketCount(
+      "Ash.Report.Psm28DAImportResponseNetErrorCode", PsmRequest::kImport, 1);
+  histogram_tester()->ExpectBucketCount(
+      "Ash.Report.PsmCohortImportResponseNetErrorCode", PsmRequest::kImport, 1);
+  histogram_tester()->ExpectBucketCount(
+      "Ash.Report.PsmObservationImportResponseNetErrorCode",
+      PsmRequest::kImport, 1);
+}
+
+// Test case for RecordCheckMembershipCases function.
+TEST_F(UmaUtilsTest, RecordCheckMembershipCases) {
+  CheckMembershipResponseCases response_case =
+      CheckMembershipResponseCases::kNotHasRlweOprfResponse;
+
+  RecordCheckMembershipCases(PsmUseCase::k1DA, response_case);
+  RecordCheckMembershipCases(PsmUseCase::k28DA, response_case);
+  RecordCheckMembershipCases(PsmUseCase::kCohort, response_case);
+  RecordCheckMembershipCases(PsmUseCase::kObservation, response_case);
+
+  histogram_tester()->ExpectBucketCount("Ash.Report.1DACheckMembershipCases",
+                                        response_case, 1);
+  histogram_tester()->ExpectBucketCount("Ash.Report.28DACheckMembershipCases",
+                                        response_case, 1);
+  histogram_tester()->ExpectBucketCount("Ash.Report.CohortCheckMembershipCases",
+                                        response_case, 1);
+  histogram_tester()->ExpectBucketCount(
+      "Ash.Report.ObservationCheckMembershipCases", response_case, 1);
+}
+
+}  // namespace ash::report::utils
diff --git a/chromeos/chromeos_strings.grd b/chromeos/chromeos_strings.grd
index 723cd46e..689fc6de 100644
--- a/chromeos/chromeos_strings.grd
+++ b/chromeos/chromeos_strings.grd
@@ -3963,6 +3963,9 @@
         <message name="IDS_FEEDBACK_TOOL_PRIVACY_NOTE" desc="Text for the privacy note included in the feedback app">
           Go to the <ph name="BEGIN_LINK1">&lt;a id="legalHelpPageUrl"&gt;</ph>Legal Help page<ph name="END_LINK1">&lt;/a&gt;</ph> to request content changes for legal reasons. Some account and system information may be sent to Google. We will use the information you give us to help address technical issues and to improve our services, subject to our <ph name="BEGIN_LINK2">&lt;a id="privacyPolicyUrl"&gt;</ph>Privacy Policy<ph name="END_LINK2">&lt;/a&gt;</ph> and <ph name="BEGIN_LINK3">&lt;a id="termsOfServiceUrl"&gt;</ph>Terms of Service<ph name="END_LINK3">&lt;/a&gt;</ph>.
         </message>
+        <message name="IDS_FEEDBACK_TOOL_PRIVACY_NOTE_LOGGED_OUT" desc="Text for the privacy note included in the feedback app when users are logged out">
+          Some account and system information may be sent to Google. We use this information to help address technical issues and improve our services, subject to our Privacy Policy (<ph name="privacyPolicyUrl">$1<ex>https://policies.google.com/privacy</ex></ph>) and Terms of Service (<ph name="termsOfServiceUrl">$2<ex>https://policies.google.com/terms</ex></ph>). To request content changes, go to Legal Help (<ph name="legalHelpPageUrl">$3<ex>https://support.google.com/legal/answer/3110420</ex></ph>).
+        </message>
         <message name="IDS_FEEDBACK_TOOL_MAY_BE_SHARED_NOTE" desc="Text to inform all users that their feedback may be shared">
           We may share feedback submitted via this form with our partners to troubleshoot bugs and other issues that you report to us. Don’t include sensitive information such as passwords.
         </message>
diff --git a/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PRIVACY_NOTE_LOGGED_OUT.png.sha1 b/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PRIVACY_NOTE_LOGGED_OUT.png.sha1
new file mode 100644
index 0000000..bfc8d4bc
--- /dev/null
+++ b/chromeos/chromeos_strings_grd/IDS_FEEDBACK_TOOL_PRIVACY_NOTE_LOGGED_OUT.png.sha1
@@ -0,0 +1 @@
+8df5956757978532106295aa98442c352af7bd25
\ No newline at end of file
diff --git a/chromeos/profiles/arm-exp.afdo.newest.txt b/chromeos/profiles/arm-exp.afdo.newest.txt
index 16c825fa..6abe832 100644
--- a/chromeos/profiles/arm-exp.afdo.newest.txt
+++ b/chromeos/profiles/arm-exp.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-arm-exp-118-5978.0-1695637284-benchmark-119.0.6033.0-r1-redacted.afdo.xz
+chromeos-chrome-arm-exp-118-5978.0-1695637284-benchmark-119.0.6034.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/arm.afdo.newest.txt b/chromeos/profiles/arm.afdo.newest.txt
index efb02faa..5737f27 100644
--- a/chromeos/profiles/arm.afdo.newest.txt
+++ b/chromeos/profiles/arm.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-arm-none-118-5938.55-1695641973-benchmark-119.0.6034.0-r1-redacted.afdo.xz
+chromeos-chrome-arm-none-118-5938.55-1695641973-benchmark-119.0.6035.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/atom.afdo.newest.txt b/chromeos/profiles/atom.afdo.newest.txt
index bab4d39..d636cfe 100644
--- a/chromeos/profiles/atom.afdo.newest.txt
+++ b/chromeos/profiles/atom.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-atom-118-5978.0-1695637284-benchmark-119.0.6034.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-atom-118-5978.0-1695637284-benchmark-119.0.6035.0-r1-redacted.afdo.xz
diff --git a/chromeos/profiles/bigcore.afdo.newest.txt b/chromeos/profiles/bigcore.afdo.newest.txt
index 5a78725f..ee3b375 100644
--- a/chromeos/profiles/bigcore.afdo.newest.txt
+++ b/chromeos/profiles/bigcore.afdo.newest.txt
@@ -1 +1 @@
-chromeos-chrome-amd64-bigcore-118-5978.0-1695634679-benchmark-119.0.6034.0-r1-redacted.afdo.xz
+chromeos-chrome-amd64-bigcore-118-5978.0-1695634679-benchmark-119.0.6035.0-r1-redacted.afdo.xz
diff --git a/chromeos/tast_control.gni b/chromeos/tast_control.gni
index ab909a90..a9b1f15 100644
--- a/chromeos/tast_control.gni
+++ b/chromeos/tast_control.gni
@@ -49,6 +49,10 @@
   # still investigating.
   "ui.ChromeCrashNotLoggedIn.browser_crashpad",
 
+  # b/296821415
+  "ui.ChromeCrashLoggedIn.browser_crashpad",
+  "ui.ChromeCrashLoggedIn.browser_crashpad_mock_consent",
+
   # https://crbug.com/1279285: Flaky.
   "policy.AllowWakeLocks",
 
diff --git a/clank b/clank
index 5e96d16..e775e49 160000
--- a/clank
+++ b/clank
@@ -1 +1 @@
-Subproject commit 5e96d16a206e29711039f968c564422b80df7786
+Subproject commit e775e4966d9fa7e59540a94a7d87e2905a54b787
diff --git a/components/android_autofill/browser/form_data_android_unittest.cc b/components/android_autofill/browser/form_data_android_unittest.cc
index bd2a2ba8..1d57f6b 100644
--- a/components/android_autofill/browser/form_data_android_unittest.cc
+++ b/components/android_autofill/browser/form_data_android_unittest.cc
@@ -47,7 +47,7 @@
   f.name = std::move(name);
   f.name_attribute = f.name;
   f.id_attribute = u"some_id";
-  f.form_control_type = "text";
+  f.form_control_type = StringToFormControlType("text");
   f.check_status = FormFieldData::CheckStatus::kChecked;
   f.role = FormFieldData::RoleAttribute::kOther;
   f.is_focusable = true;
diff --git a/components/android_autofill/browser/form_field_data_android_bridge_impl.cc b/components/android_autofill/browser/form_field_data_android_bridge_impl.cc
index 7492146..fa756671 100644
--- a/components/android_autofill/browser/form_field_data_android_bridge_impl.cc
+++ b/components/android_autofill/browser/form_field_data_android_bridge_impl.cc
@@ -76,7 +76,8 @@
       ConvertUTF8ToJavaString(env, field.autocomplete_attribute),
       field.should_autocomplete,
       ConvertUTF16ToJavaString(env, field.placeholder),
-      ConvertUTF8ToJavaString(env, field.form_control_type),
+      ConvertUTF8ToJavaString(env,
+                              FormControlTypeToString(field.form_control_type)),
       ConvertUTF16ToJavaString(env, field.id_attribute),
       /*optionValues=*/ProjectOptions(field.options, &SelectOption::value),
       /*optionContents=*/ProjectOptions(field.options, &SelectOption::content),
diff --git a/components/android_autofill/browser/form_field_data_android_unittest.cc b/components/android_autofill/browser/form_field_data_android_unittest.cc
index 63d50c9..2ea0905 100644
--- a/components/android_autofill/browser/form_field_data_android_unittest.cc
+++ b/components/android_autofill/browser/form_field_data_android_unittest.cc
@@ -43,7 +43,7 @@
   f.name = u"SomeName";
   f.name_attribute = f.name;
   f.id_attribute = u"some_id";
-  f.form_control_type = "text";
+  f.form_control_type = StringToFormControlType("text");
   f.check_status = FormFieldData::CheckStatus::kChecked;
   return f;
 }
@@ -136,7 +136,7 @@
 
   // If form control types differ, they are not similar.
   f2 = f1;
-  f2.form_control_type = "password";
+  f2.form_control_type = StringToFormControlType("password");
   EXPECT_FALSE(af.SimilarFieldAs(f2));
 
   // If global ids differ, they are not similar.
diff --git a/components/autofill/content/renderer/form_autofill_util.cc b/components/autofill/content/renderer/form_autofill_util.cc
index ff838d7..850f6347 100644
--- a/components/autofill/content/renderer/form_autofill_util.cc
+++ b/components/autofill/content/renderer/form_autofill_util.cc
@@ -1980,36 +1980,36 @@
           base::FeatureList::IsEnabled(features::kAutofillEnableSelectList));
 }
 
-std::string_view FormControlTypeToString(Type type) {
+FormControlType ToAutofillFormControlType(WebFormControlElement::Type type) {
   switch (type) {
-    case Type::kInputCheckbox:
-      return "checkbox";
-    case Type::kInputEmail:
-      return "email";
-    case Type::kInputMonth:
-      return "month";
-    case Type::kInputNumber:
-      return "number";
-    case Type::kInputPassword:
-      return "password";
-    case Type::kInputRadio:
-      return "radio";
-    case Type::kInputSearch:
-      return "search";
-    case Type::kInputTelephone:
-      return "tel";
-    case Type::kInputText:
-      return "text";
-    case Type::kInputUrl:
-      return "url";
-    case Type::kSelectOne:
-      return "select-one";
-    case Type::kSelectMultiple:
-      return "select-multiple";
-    case Type::kSelectList:
-      return "selectlist";
-    case Type::kTextArea:
-      return "textarea";
+    case WebFormControlElement::Type::kInputCheckbox:
+      return FormControlType::kInputCheckbox;
+    case WebFormControlElement::Type::kInputEmail:
+      return FormControlType::kInputEmail;
+    case WebFormControlElement::Type::kInputMonth:
+      return FormControlType::kInputMonth;
+    case WebFormControlElement::Type::kInputNumber:
+      return FormControlType::kInputNumber;
+    case WebFormControlElement::Type::kInputPassword:
+      return FormControlType::kInputPassword;
+    case WebFormControlElement::Type::kInputRadio:
+      return FormControlType::kInputRadio;
+    case WebFormControlElement::Type::kInputSearch:
+      return FormControlType::kInputSearch;
+    case WebFormControlElement::Type::kInputTelephone:
+      return FormControlType::kInputTelephone;
+    case WebFormControlElement::Type::kInputText:
+      return FormControlType::kInputText;
+    case WebFormControlElement::Type::kInputUrl:
+      return FormControlType::kInputUrl;
+    case WebFormControlElement::Type::kSelectOne:
+      return FormControlType::kSelectOne;
+    case WebFormControlElement::Type::kSelectMultiple:
+      return FormControlType::kSelectMultiple;
+    case WebFormControlElement::Type::kSelectList:
+      return FormControlType::kSelectList;
+    case WebFormControlElement::Type::kTextArea:
+      return FormControlType::kTextArea;
     default:
       NOTREACHED_NORETURN();
   }
@@ -2122,7 +2122,7 @@
   field->host_form_id = GetFormRendererId(form_element);
   field->form_control_ax_id = element.GetAxId();
   field->form_control_type =
-      FormControlTypeToString(element.FormControlTypeForAutofill());
+      ToAutofillFormControlType(element.FormControlTypeForAutofill());
   field->max_length =
       IsTextInput(input_element) ? input_element.MaxLength() : 0;
   field->autocomplete_attribute = GetAutocompleteAttribute(element);
@@ -2275,7 +2275,7 @@
 
     // The typed value is preserved for all passwords. It is also preserved for
     // potential usernames, as long as the |value| is not deemed acceptable.
-    if (field->form_control_type == "password" ||
+    if (field->form_control_type == StringToFormControlType("password") ||
         !ScriptModifiedUsernameAcceptable(field->value, user_input,
                                           field_data_manager)) {
       field->user_input = user_input;
diff --git a/components/autofill/content/renderer/form_autofill_util.h b/components/autofill/content/renderer/form_autofill_util.h
index f343a75..419b887 100644
--- a/components/autofill/content/renderer/form_autofill_util.h
+++ b/components/autofill/content/renderer/form_autofill_util.h
@@ -181,8 +181,7 @@
 // {Text, Radiobutton, Checkbox, Select, TextArea}.
 bool IsAutofillableElement(const blink::WebFormControlElement& element);
 
-// TODO(crbug.com/1482526): Move to //components/autofill/core/common.
-std::string_view FormControlTypeToString(
+FormControlType ToAutofillFormControlType(
     blink::WebFormControlElement::Type type);
 
 // Returns true iff `element` has a "webauthn" autocomplete attribute.
diff --git a/components/autofill/content/renderer/password_autofill_agent.cc b/components/autofill/content/renderer/password_autofill_agent.cc
index 6a44907..d141d12 100644
--- a/components/autofill/content/renderer/password_autofill_agent.cc
+++ b/components/autofill/content/renderer/password_autofill_agent.cc
@@ -307,8 +307,8 @@
   for (blink::WebFormControlElement& control_element : fields) {
     FieldSignature field_signature = CalculateFieldSignatureByNameAndType(
         control_element.NameForAutofill().Utf16(),
-        form_util::FormControlTypeToString(
-            control_element.FormControlTypeForAutofill()));
+        FormControlTypeToString(form_util::ToAutofillFormControlType(
+            control_element.FormControlTypeForAutofill())));
     SetAttributeAsync(control_element, kDebugAttributeForFieldSignature,
                       base::NumberToString(field_signature.value()));
     SetAttributeAsync(control_element, kDebugAttributeForFormSignature,
@@ -521,7 +521,7 @@
     // block a form submission. Note: Don't use |check_status !=
     // kNotCheckable|, a radio button is considered a "checkable" element too,
     // but it should block a submission.
-    return field.form_control_type == "checkbox";
+    return field.form_control_type == StringToFormControlType("checkbox");
   };
 
   for (size_t i = username_index + 1; i < password_index; ++i) {
@@ -2111,7 +2111,8 @@
 
     FormFieldInfo& field_info = result.fields[i];
     field_info.unique_renderer_id = form_field.unique_renderer_id;
-    field_info.form_control_type = form_field.form_control_type;
+    field_info.form_control_type =
+        FormControlTypeToString(form_field.form_control_type);
     field_info.autocomplete_attribute = form_field.autocomplete_attribute;
     field_info.is_focusable = form_field.is_focusable;
   }
diff --git a/components/autofill/core/browser/autocomplete_history_manager.cc b/components/autofill/core/browser/autocomplete_history_manager.cc
index d0a9e51..632c104 100644
--- a/components/autofill/core/browser/autocomplete_history_manager.cc
+++ b/components/autofill/core/browser/autocomplete_history_manager.cc
@@ -39,12 +39,11 @@
 const int kMaxAutocompleteMenuItems = 6;
 
 bool IsTextField(const FormFieldData& field) {
-  return
-      field.form_control_type == "text" ||
-      field.form_control_type == "search" ||
-      field.form_control_type == "tel" ||
-      field.form_control_type == "url" ||
-      field.form_control_type == "email";
+  return field.form_control_type == StringToFormControlType("text") ||
+         field.form_control_type == StringToFormControlType("search") ||
+         field.form_control_type == StringToFormControlType("tel") ||
+         field.form_control_type == StringToFormControlType("url") ||
+         field.form_control_type == StringToFormControlType("email");
 }
 
 // Returns true if the field has a meaningful name.
@@ -92,7 +91,7 @@
   CancelPendingQueries(handler.get());
 
   if (!IsMeaningfulFieldName(field.name) || !client.IsAutocompleteEnabled() ||
-      field.form_control_type == "textarea" ||
+      field.form_control_type == StringToFormControlType("textarea") ||
       IsInAutofillSuggestionsDisabledExperiment()) {
     SendSuggestions({}, QueryHandler(field.global_id(), trigger_source,
                                      field.value, handler));
diff --git a/components/autofill/core/browser/autocomplete_history_manager_unittest.cc b/components/autofill/core/browser/autocomplete_history_manager_unittest.cc
index 414efdf..4634c26 100644
--- a/components/autofill/core/browser/autocomplete_history_manager_unittest.cc
+++ b/components/autofill/core/browser/autocomplete_history_manager_unittest.cc
@@ -152,7 +152,7 @@
   valid_cc.name = u"ccnum";
   valid_cc.value = u"4012888888881881";
   valid_cc.properties_mask |= kUserTyped;
-  valid_cc.form_control_type = "text";
+  valid_cc.form_control_type = StringToFormControlType("text");
   form.fields.push_back(valid_cc);
 
   EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
@@ -176,7 +176,7 @@
   invalid_cc.name = u"ccnum";
   invalid_cc.value = u"4580123456789012";
   invalid_cc.properties_mask |= kUserTyped;
-  invalid_cc.form_control_type = "text";
+  invalid_cc.form_control_type = StringToFormControlType("text");
   form.fields.push_back(invalid_cc);
 
   EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_));
@@ -197,7 +197,7 @@
   ssn.name = u"ssn";
   ssn.value = u"078-05-1120";
   ssn.properties_mask |= kUserTyped;
-  ssn.form_control_type = "text";
+  ssn.form_control_type = StringToFormControlType("text");
   form.fields.push_back(ssn);
 
   EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
@@ -219,7 +219,7 @@
   search_field.name = u"search";
   search_field.value = u"my favorite query";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_));
@@ -240,7 +240,7 @@
   search_field.name = u"search";
   search_field.value = u"my favorite query";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
@@ -264,7 +264,7 @@
   search_field.name = u"search";
   search_field.value = u"";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   // Single whitespace.
@@ -272,7 +272,7 @@
   search_field.name = u"other search";
   search_field.value = u" ";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   // Multiple whitespaces.
@@ -280,7 +280,7 @@
   search_field.name = u"other search";
   search_field.value = u"      ";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   EXPECT_CALL(*(web_data_service_.get()), AddFormFields(_)).Times(0);
@@ -305,7 +305,7 @@
   field.name = u"esoterica";
   field.value = u"a truly esoteric value, I assure you";
   field.properties_mask |= kUserTyped;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.should_autocomplete = false;
   form.fields.push_back(field);
 
@@ -330,7 +330,7 @@
   search_field.name = u"search";
   search_field.value = u"my favorite query";
   search_field.properties_mask |= kUserTyped;
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   form.fields.push_back(search_field);
 
   EXPECT_CALL(*web_data_service_, AddFormFields(_)).Times(0);
@@ -354,7 +354,7 @@
   search_field.label = u"Search";
   search_field.name = u"search";
   search_field.value = u"my favorite query";
-  search_field.form_control_type = "search";
+  search_field.form_control_type = StringToFormControlType("search");
   search_field.properties_mask |= kUserTyped;
   search_field.is_focusable = false;
   form.fields.push_back(search_field);
@@ -380,7 +380,7 @@
   field.name = u"esoterica";
   field.value = u"a truly esoteric value, I assure you";
   field.properties_mask |= kUserTyped;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.role = FormFieldData::RoleAttribute::kPresentation;
   form.fields.push_back(field);
 
diff --git a/components/autofill/core/browser/autofill_client.cc b/components/autofill/core/browser/autofill_client.cc
index 8324b2c..97ed523 100644
--- a/components/autofill/core/browser/autofill_client.cc
+++ b/components/autofill/core/browser/autofill_client.cc
@@ -68,6 +68,10 @@
   return nullptr;
 }
 
+compose::ComposeManager* AutofillClient::GetComposeManager() {
+  return nullptr;
+}
+
 plus_addresses::PlusAddressService* AutofillClient::GetPlusAddressService() {
   return nullptr;
 }
diff --git a/components/autofill/core/browser/autofill_client.h b/components/autofill/core/browser/autofill_client.h
index 20a1f28..ab6398fc 100644
--- a/components/autofill/core/browser/autofill_client.h
+++ b/components/autofill/core/browser/autofill_client.h
@@ -43,6 +43,14 @@
 
 class PrefService;
 
+namespace compose {
+class ComposeManager;
+}
+
+namespace plus_addresses {
+class PlusAddressService;
+}
+
 namespace signin {
 class IdentityManager;
 }
@@ -65,10 +73,6 @@
 }
 #endif
 
-namespace plus_addresses {
-class PlusAddressService;
-}
-
 namespace autofill {
 
 class AddressNormalizer;
@@ -428,6 +432,9 @@
   // KeyedService that manages that data.
   virtual plus_addresses::PlusAddressService* GetPlusAddressService();
 
+  // Returns the `ComposeManager` instance for the tab of this client.
+  virtual compose::ComposeManager* GetComposeManager();
+
   // Orchestrates UI for enterprise plus address creation; no-op except on
   // supported platforms.
   virtual void OfferPlusAddressCreation(
diff --git a/components/autofill/core/browser/autofill_download_manager_unittest.cc b/components/autofill/core/browser/autofill_download_manager_unittest.cc
index 1368559..16cfa28 100644
--- a/components/autofill/core/browser/autofill_download_manager_unittest.cc
+++ b/components/autofill/core/browser/autofill_download_manager_unittest.cc
@@ -293,37 +293,37 @@
   FormFieldData field;
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"email";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"email2";
   field.name = u"email2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -333,22 +333,22 @@
 
   field.label = u"address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"address2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"city";
   field.name = u"city";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   form_structures.push_back(std::make_unique<FormStructure>(form));
@@ -357,17 +357,17 @@
 
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   form_structures.push_back(std::make_unique<FormStructure>(form));
@@ -484,7 +484,7 @@
   // Modify form structures to miss the cache.
   field.label = u"Address line 2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
   form_structures.push_back(std::make_unique<FormStructure>(form));
 
@@ -539,12 +539,12 @@
 
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -636,7 +636,7 @@
   FormFieldData field;
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -745,12 +745,12 @@
 
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   FormStructure form_structure(form);
@@ -823,11 +823,11 @@
     FormFieldData field;
 
     field.name = u"firstname";
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     form.fields.push_back(field);
 
     field.name = u"lastname";
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     form.fields.push_back(field);
     FormStructure form_structure(form);
     form_structure.set_submission_source(SubmissionSource::FORM_SUBMISSION);
@@ -865,7 +865,8 @@
     if (is_raw_metadata_uploading_enabled) {
       EXPECT_EQ(form.name, UTF8ToUTF16(upload.form_name()));
       EXPECT_EQ(form.fields[0].name, UTF8ToUTF16(upload.field()[0].name()));
-      EXPECT_EQ(form.fields[1].form_control_type, upload.field()[1].type());
+      EXPECT_EQ(form.fields[1].form_control_type,
+                StringToFormControlType(upload.field()[1].type()));
     }
 
     test_url_loader_factory_.SimulateResponseForPendingRequest(
@@ -885,22 +886,22 @@
   FormFieldData field;
   field.label = u"address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"address2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"city";
   field.name = u"city";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -958,22 +959,22 @@
   FormFieldData field;
   field.label = u"address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"address2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"city";
   field.name = u"city";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   auto form_structure = std::make_unique<FormStructure>(form);
@@ -1047,22 +1048,22 @@
   FormFieldData field;
   field.label = u"address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"address2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"city";
   field.name = u"city";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -1123,22 +1124,22 @@
   FormFieldData field;
   field.label = u"address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"address2";
   field.name = u"address2";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"city";
   field.name = u"city";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   form.fields.push_back(field);
 
   base::HistogramTester histogram;
@@ -1209,7 +1210,7 @@
       FormFieldData field;
       field.label = base::NumberToString16(i);
       field.name = base::NumberToString16(i);
-      field.form_control_type = "text";
+      field.form_control_type = StringToFormControlType("text");
       form.fields.push_back(field);
     }
     form_structures.push_back(std::make_unique<FormStructure>(form));
@@ -1229,7 +1230,7 @@
       FormFieldData field;
       field.label = base::NumberToString16(i);
       field.name = base::NumberToString16(i);
-      field.form_control_type = "text";
+      field.form_control_type = StringToFormControlType("text");
       form.fields.push_back(field);
     }
     form_structures.push_back(std::make_unique<FormStructure>(form));
@@ -1243,7 +1244,7 @@
   FormData form;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"username";
   field.name = u"username";
@@ -1596,7 +1597,7 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   std::vector<std::unique_ptr<FormStructure>> form_structures;
@@ -1611,17 +1612,17 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name:";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Email:";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   AutofillDownloadManager download_manager(
@@ -1644,7 +1645,7 @@
   FormFieldData field;
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   FormData form;
   form.fields.push_back(field);
@@ -1684,7 +1685,7 @@
   FormFieldData field;
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   FormData form;
   form.fields.push_back(field);
@@ -1755,7 +1756,7 @@
   FormFieldData field;
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   FormData form;
   form.fields.push_back(field);
@@ -1800,7 +1801,7 @@
   FormFieldData field;
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   FormData form;
   form.fields.push_back(field);
@@ -1862,7 +1863,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-description";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -1874,7 +1875,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-description";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -1886,7 +1887,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-description";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -1949,7 +1950,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-descriptionm";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -1960,7 +1961,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-descriptionm";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -1971,7 +1972,7 @@
   field.label = u"field-label";
   field.aria_label = u"field-aria-label";
   field.aria_description = u"field-aria-descriptionm";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.css_classes = u"field-css-classes";
   field.placeholder = u"field-placeholder";
   form.fields.push_back(field);
@@ -2045,17 +2046,17 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name:";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Email:";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   AutofillDownloadManager download_manager(
@@ -2103,19 +2104,19 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
   small_form.fields.push_back(field);
 
   field.label = u"Last Name:";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
   small_form.fields.push_back(field);
 
   field.label = u"Email:";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   AutofillDownloadManager download_manager(
@@ -2192,17 +2193,17 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name:";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Email:";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   AutofillDownloadManager download_manager(
@@ -2251,17 +2252,17 @@
 
   field.label = u"First Name:";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Last Name:";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   field.label = u"Email:";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   form.fields.push_back(field);
 
   AutofillDownloadManager download_manager(
diff --git a/components/autofill/core/browser/autofill_field.cc b/components/autofill/core/browser/autofill_field.cc
index 77a926b..33bf7c68 100644
--- a/components/autofill/core/browser/autofill_field.cc
+++ b/components/autofill/core/browser/autofill_field.cc
@@ -257,8 +257,8 @@
 
 AutofillField::AutofillField(const FormFieldData& field)
     : FormFieldData(field), parseable_name_(name), parseable_label_(label) {
-  field_signature_ =
-      CalculateFieldSignatureByNameAndType(name, form_control_type);
+  field_signature_ = CalculateFieldSignatureByNameAndType(
+      name, FormControlTypeToString(form_control_type));
   local_type_predictions_.fill(NO_SERVER_DATA);
 }
 
@@ -505,7 +505,8 @@
 FieldSignature AutofillField::GetFieldSignature() const {
   return field_signature_
              ? *field_signature_
-             : CalculateFieldSignatureByNameAndType(name, form_control_type);
+             : CalculateFieldSignatureByNameAndType(
+                   name, FormControlTypeToString(form_control_type));
 }
 
 std::string AutofillField::FieldSignatureAsStr() const {
@@ -570,27 +571,30 @@
   }
 }
 
-FormControlType AutofillField::FormControlType() const {
+DeprecatedFormControlType AutofillField::FormControlType() const {
   // Keep in sync with https://html.spec.whatwg.org/#attr-input-type.
-  if (form_control_type == "text" || form_control_type == "search" ||
-      form_control_type == "tel" || form_control_type == "url" ||
-      form_control_type == "email" || form_control_type == "password" ||
-      form_control_type == "number") {
-    return FormControlType::kText;
-  } else if (form_control_type == "textarea") {
-    return FormControlType::kTextarea;
-  } else if (form_control_type == "checkbox") {
-    return FormControlType::kCheckbox;
-  } else if (form_control_type == "radio") {
-    return FormControlType::kRadio;
-  } else if (form_control_type == "select-one") {
-    return FormControlType::kSelectOne;
-  } else if (form_control_type == "selectlist") {
-    return FormControlType::kSelectlist;
-  } else if (form_control_type == "") {
-    return FormControlType::kEmpty;
+  if (form_control_type == StringToFormControlType("text") ||
+      form_control_type == StringToFormControlType("search") ||
+      form_control_type == StringToFormControlType("tel") ||
+      form_control_type == StringToFormControlType("url") ||
+      form_control_type == StringToFormControlType("email") ||
+      form_control_type == StringToFormControlType("password") ||
+      form_control_type == StringToFormControlType("number")) {
+    return DeprecatedFormControlType::kText;
+  } else if (form_control_type == StringToFormControlType("textarea")) {
+    return DeprecatedFormControlType::kTextarea;
+  } else if (form_control_type == StringToFormControlType("checkbox")) {
+    return DeprecatedFormControlType::kCheckbox;
+  } else if (form_control_type == StringToFormControlType("radio")) {
+    return DeprecatedFormControlType::kRadio;
+  } else if (form_control_type == StringToFormControlType("select-one")) {
+    return DeprecatedFormControlType::kSelectOne;
+  } else if (form_control_type == StringToFormControlType("selectlist")) {
+    return DeprecatedFormControlType::kSelectlist;
+  } else if (form_control_type == StringToFormControlType("")) {
+    return DeprecatedFormControlType::kEmpty;
   } else {
-    return FormControlType::kOther;
+    return DeprecatedFormControlType::kOther;
   }
 }
 
diff --git a/components/autofill/core/browser/autofill_field.h b/components/autofill/core/browser/autofill_field.h
index 9f0f6bc..344fe97 100644
--- a/components/autofill/core/browser/autofill_field.h
+++ b/components/autofill/core/browser/autofill_field.h
@@ -31,7 +31,7 @@
 typedef std::map<ServerFieldType, AutofillDataModel::ValidityState>
     ServerFieldTypeValidityStateMap;
 
-enum class FormControlType {
+enum class DeprecatedFormControlType {
   kEmpty = 0,
   kOther = 1,
   kText = 2,
@@ -319,7 +319,8 @@
     return autofill_source_profile_guid_;
   }
 
-  enum FormControlType FormControlType() const;
+  // Use FormFieldData::form_control_type instead (crbug.com/1482526).
+  enum DeprecatedFormControlType FormControlType() const;
 
  private:
   explicit AutofillField(FieldSignature field_signature);
diff --git a/components/autofill/core/browser/autofill_form_test_utils.cc b/components/autofill/core/browser/autofill_form_test_utils.cc
index 5774bfb..9bec000 100644
--- a/components/autofill/core/browser/autofill_form_test_utils.cc
+++ b/components/autofill/core/browser/autofill_form_test_utils.cc
@@ -18,8 +18,8 @@
   testing::Message result;
   result << "Form contains " << form_data.fields.size() << " fields:\n";
   for (const FormFieldData& field : form_data.fields) {
-    result << "type=" << field.form_control_type << ", name=" << field.name
-           << ", label=" << field.label << "\n";
+    result << "type=" << FormControlTypeToString(field.form_control_type)
+           << ", name=" << field.name << ", label=" << field.label << "\n";
   }
   return result;
 }
@@ -98,9 +98,11 @@
   f.is_form_tag = d.is_form_tag;
   for (const FieldDescription& dd : d.fields) {
     FormFieldData ff = CreateFieldByRole(dd.role);
-    ff.form_control_type = dd.form_control_type;
-    if (ff.form_control_type == "select-one" && !dd.select_options.empty())
+    ff.form_control_type = StringToFormControlType(dd.form_control_type);
+    if (ff.form_control_type == StringToFormControlType("select-one") &&
+        !dd.select_options.empty()) {
       ff.options = dd.select_options;
+    }
     ff.host_frame = dd.host_frame.value_or(f.host_frame);
     ff.unique_renderer_id =
         dd.unique_renderer_id.value_or(MakeFieldRendererId());
diff --git a/components/autofill/core/browser/autofill_merge_unittest.cc b/components/autofill/core/browser/autofill_merge_unittest.cc
index 4b16128..4935a6ad 100644
--- a/components/autofill/core/browser/autofill_merge_unittest.cc
+++ b/components/autofill/core/browser/autofill_merge_unittest.cc
@@ -201,7 +201,7 @@
       field.label = field_type;
       field.name = field_type;
       field.value = value;
-      field.form_control_type = "text";
+      field.form_control_type = StringToFormControlType("text");
       field.is_focusable = true;
       form.fields.push_back(field);
     }
diff --git a/components/autofill/core/browser/browser_autofill_manager_unittest.cc b/components/autofill/core/browser/browser_autofill_manager_unittest.cc
index 8b0e784..f72e1ab 100644
--- a/components/autofill/core/browser/browser_autofill_manager_unittest.cc
+++ b/components/autofill/core/browser/browser_autofill_manager_unittest.cc
@@ -402,7 +402,8 @@
   EXPECT_EQ(UTF8ToUTF16(expected_label), field.label);
   EXPECT_EQ(UTF8ToUTF16(expected_name), field.name);
   EXPECT_EQ(UTF8ToUTF16(expected_value), field.value);
-  EXPECT_EQ(expected_form_control_type, field.form_control_type);
+  EXPECT_EQ(StringToFormControlType(expected_form_control_type),
+            field.form_control_type);
 }
 
 // Verifies that the |filled_form| has been filled with the given data.
@@ -612,14 +613,14 @@
         std::make_unique<MockAutofillDownloadManager>(&autofill_client_));
 
     browser_autofill_manager_->set_touch_to_fill_delegate(
-        std::make_unique<MockTouchToFillDelegate>());
+        std::make_unique<NiceMock<MockTouchToFillDelegate>>());
     ON_CALL(touch_to_fill_delegate(), GetManager())
         .WillByDefault(Return(browser_autofill_manager_.get()));
     ON_CALL(touch_to_fill_delegate(), IsShowingTouchToFill())
         .WillByDefault(Return(false));
 
     browser_autofill_manager_->set_fast_checkout_delegate(
-        std::make_unique<MockFastCheckoutDelegate>());
+        std::make_unique<NiceMock<MockFastCheckoutDelegate>>());
     ON_CALL(fast_checkout_delegate(), IsShowingFastCheckoutUI())
         .WillByDefault(Return(false));
 
@@ -6834,7 +6835,7 @@
   // reject default values for text fields.
   FormFieldData* state_field = form.FindFieldByName(u"state");
   ASSERT_TRUE(state_field != nullptr);
-  state_field->form_control_type = form_control_type;
+  state_field->form_control_type = StringToFormControlType(form_control_type);
   state_field->value = base::UTF8ToUTF16(kElvisAddressFillData.state);
 
   test->FormsSeen({form});
diff --git a/components/autofill/core/browser/field_filler.cc b/components/autofill/core/browser/field_filler.cc
index 7050df0..5ec31bec 100644
--- a/components/autofill/core/browser/field_filler.cc
+++ b/components/autofill/core/browser/field_filler.cc
@@ -678,8 +678,9 @@
     const std::u16string& address_value,
     const std::string& address_language_code,
     FormFieldData* field) {
-  if (field->form_control_type == "textarea")
+  if (field->form_control_type == StringToFormControlType("textarea")) {
     return address_value;
+  }
 
   ::i18n::addressinput::AddressData address_data;
   address_data.language_code = address_language_code;
@@ -902,7 +903,7 @@
     std::string* failure_to_fill) {
   ServerFieldType storable_type = field.Type().GetStorableType();
 
-  if (field.form_control_type == "month") {
+  if (field.form_control_type == StringToFormControlType("month")) {
     return GetExpirationForMonthControl(credit_card);
   } else {
     switch (storable_type) {
diff --git a/components/autofill/core/browser/field_filler_unittest.cc b/components/autofill/core/browser/field_filler_unittest.cc
index e9b4c50..2f2c463 100644
--- a/components/autofill/core/browser/field_filler_unittest.cc
+++ b/components/autofill/core/browser/field_filler_unittest.cc
@@ -311,7 +311,7 @@
   AutofillQueryResponse::FormSuggestion::FieldSuggestion::FieldPrediction
       prediction;
   ASSERT_EQ(std::u16string(), field.name);
-  ASSERT_EQ(std::string(), field.form_control_type);
+  ASSERT_EQ(FormControlType::kEmpty, field.form_control_type);
 
   // Signature is empty.
   EXPECT_EQ("2085434232", field.FieldSignatureAsStr());
@@ -321,7 +321,7 @@
   EXPECT_EQ("1606968241", field.FieldSignatureAsStr());
 
   // Field form control type is set.
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   EXPECT_EQ("502192749", field.FieldSignatureAsStr());
 
   // Heuristic type does not affect FieldSignature.
@@ -710,7 +710,7 @@
 TEST_P(ExpirationYearTest, FillExpirationYearInput) {
   auto test_case = GetParam();
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.SetHtmlType(test_case.field_type, HtmlFieldMode());
   field.max_length = test_case.field_max_length;
 
@@ -817,7 +817,7 @@
   }
 
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.SetHtmlType(test_case.field_type, HtmlFieldMode());
   field.max_length = test_case.field_max_length;
 
@@ -1517,7 +1517,7 @@
 
 TEST_F(AutofillFieldFillerTest, FillMonthControl) {
   AutofillField field;
-  field.form_control_type = "month";
+  field.form_control_type = StringToFormControlType("month");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_EXP_4_DIGIT_YEAR);
@@ -1540,7 +1540,7 @@
 
 TEST_F(AutofillFieldFillerTest, FillStreetAddressTextArea) {
   AutofillField field;
-  field.form_control_type = "textarea";
+  field.form_control_type = StringToFormControlType("textarea");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            ADDRESS_HOME_STREET_ADDRESS);
@@ -1564,7 +1564,7 @@
 
 TEST_F(AutofillFieldFillerTest, FillStreetAddressTextField) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.set_server_predictions(
       {::autofill::test::CreateFieldPrediction(ADDRESS_HOME_STREET_ADDRESS)});
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
@@ -2163,7 +2163,7 @@
 
 TEST_F(AutofillFieldFillerTest, PreviewVirtualMonth) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(), CREDIT_CARD_EXP_MONTH);
 
@@ -2187,7 +2187,7 @@
 
 TEST_F(AutofillFieldFillerTest, PreviewVirtualYear) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_EXP_4_DIGIT_YEAR);
@@ -2213,7 +2213,7 @@
   // Test reducing 4 digit year to 2 digits.
   AutofillField field;
   field.max_length = 2;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_EXP_4_DIGIT_YEAR);
@@ -2229,7 +2229,7 @@
 
 TEST_F(AutofillFieldFillerTest, PreviewVirtualDate) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR);
@@ -2265,7 +2265,7 @@
 TEST_F(AutofillFieldFillerTest, PreviewVirtualShortenedDate) {
   // Test reducing dates to various max length field values.
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 4;
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
@@ -2316,7 +2316,7 @@
 
 TEST_F(AutofillFieldFillerTest, PreviewVirtualCVC) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_VERIFICATION_CODE);
@@ -2332,7 +2332,7 @@
 
 TEST_F(AutofillFieldFillerTest, PreviewVirtualCVCAmericanExpress) {
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(),
                            CREDIT_CARD_VERIFICATION_CODE);
@@ -2349,7 +2349,7 @@
 TEST_F(AutofillFieldFillerTest, PreviewVirtualCardNumber) {
   AutofillField field;
   field.set_heuristic_type(GetActiveHeuristicSource(), CREDIT_CARD_NUMBER);
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   CreditCard card = test::GetVirtualCard();
   card.SetNumber(u"5454545454545454");
@@ -2376,7 +2376,7 @@
   AutofillField field;
   field.max_length = 17;
   field.set_credit_card_number_offset(18);
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.set_heuristic_type(GetActiveHeuristicSource(), CREDIT_CARD_NUMBER);
 
   CreditCard card = test::GetVirtualCard();
@@ -2401,7 +2401,7 @@
   std::u16string name = u"Jone Doe";
 
   AutofillField field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   FieldFiller filler(/*app_locale=*/"en-US", /*address_normalizer=*/nullptr);
   field.set_heuristic_type(GetActiveHeuristicSource(), CREDIT_CARD_NAME_FULL);
 
diff --git a/components/autofill/core/browser/form_data_importer.cc b/components/autofill/core/browser/form_data_importer.cc
index f1b0bb5ef..f28982f 100644
--- a/components/autofill/core/browser/form_data_importer.cc
+++ b/components/autofill/core/browser/form_data_importer.cc
@@ -988,7 +988,7 @@
     types_seen.insert(server_field_type);
 
     // If |field| is an HTML5 month input, handle it as a special case.
-    if (base::EqualsCaseInsensitiveASCII(field->form_control_type, "month")) {
+    if (field->form_control_type == StringToFormControlType("month")) {
       DCHECK_EQ(CREDIT_CARD_EXP_DATE_4_DIGIT_YEAR, server_field_type);
       result.card.SetInfoForMonthInputType(value);
       continue;
diff --git a/components/autofill/core/browser/form_parsing/birthdate_field.cc b/components/autofill/core/browser/form_parsing/birthdate_field.cc
index fdc1673..86c50f5 100644
--- a/components/autofill/core/browser/form_parsing/birthdate_field.cc
+++ b/components/autofill/core/browser/form_parsing/birthdate_field.cc
@@ -64,7 +64,7 @@
                                                   int max_value,
                                                   size_t max_options) {
   AutofillField* field = scanner->Cursor();
-  if (!MatchesFormControlType(field->form_control_type,
+  if (!MatchesFormControlType(FormControlTypeToString(field->form_control_type),
                               {MatchFieldType::kSelect})) {
     return false;
   }
@@ -90,7 +90,7 @@
 bool BirthdateField::IsLikelyBirthdateYearSelectField(
     AutofillScanner* scanner) {
   AutofillField* field = scanner->Cursor();
-  if (!MatchesFormControlType(field->form_control_type,
+  if (!MatchesFormControlType(FormControlTypeToString(field->form_control_type),
                               {MatchFieldType::kSelect})) {
     return false;
   }
diff --git a/components/autofill/core/browser/form_parsing/credit_card_field.cc b/components/autofill/core/browser/form_parsing/credit_card_field.cc
index 0934901..3e5f85e5 100644
--- a/components/autofill/core/browser/form_parsing/credit_card_field.cc
+++ b/components/autofill/core/browser/form_parsing/credit_card_field.cc
@@ -276,7 +276,7 @@
 
   AutofillField* field = scanner->Cursor();
   if (!MatchesFormControlType(
-          field->form_control_type,
+          FormControlTypeToString(field->form_control_type),
           {MatchFieldType::kSelect, MatchFieldType::kSearch})) {
     return false;
   }
@@ -311,7 +311,7 @@
 
   AutofillField* field = scanner->Cursor();
   if (!MatchesFormControlType(
-          field->form_control_type,
+          FormControlTypeToString(field->form_control_type),
           {MatchFieldType::kSelect, MatchFieldType::kSearch})) {
     return false;
   }
@@ -388,9 +388,10 @@
   AutofillField* field = scanner->Cursor();
 
   if (!MatchesFormControlType(
-          field->form_control_type,
-          {MatchFieldType::kSelect, MatchFieldType::kSearch}))
+          FormControlTypeToString(field->form_control_type),
+          {MatchFieldType::kSelect, MatchFieldType::kSearch})) {
     return false;
+  }
 
   // We set |ignore_whitespace| to true on these calls because this is actually
   // a pretty common mistake; e.g., "Master card" instead of "Mastercard".
@@ -523,8 +524,8 @@
                                           LogManager* log_manager,
                                           const LanguageCode& page_language,
                                           PatternSource pattern_source) {
-  if (!expiration_date_ && base::EqualsCaseInsensitiveASCII(
-                               scanner->Cursor()->form_control_type, "month")) {
+  if (!expiration_date_ && scanner->Cursor()->form_control_type ==
+                               StringToFormControlType("month")) {
     expiration_date_ = scanner->Cursor();
     expiration_month_ = nullptr;
     expiration_year_ = nullptr;
diff --git a/components/autofill/core/browser/form_parsing/form_field.cc b/components/autofill/core/browser/form_parsing/form_field.cc
index 9b1eb7b..1ea3a8e1 100644
--- a/components/autofill/core/browser/form_parsing/form_field.cc
+++ b/components/autofill/core/browser/form_parsing/form_field.cc
@@ -329,7 +329,7 @@
 
   const AutofillField* field = scanner->Cursor();
 
-  if (!MatchesFormControlType(field->form_control_type,
+  if (!MatchesFormControlType(FormControlTypeToString(field->form_control_type),
                               match_type.field_types)) {
     return false;
   }
@@ -352,8 +352,9 @@
   for (MatchPatternRef pattern_ref : patterns) {
     MatchingPattern pattern =
         projection ? (*projection)(*pattern_ref) : *pattern_ref;
-    if (!MatchesFormControlType(field->form_control_type,
-                                pattern.match_field_input_types)) {
+    if (!MatchesFormControlType(
+            FormControlTypeToString(field->form_control_type),
+            pattern.match_field_input_types)) {
       continue;
     }
 
diff --git a/components/autofill/core/browser/form_parsing/parsing_test_utils.cc b/components/autofill/core/browser/form_parsing/parsing_test_utils.cc
index 8bed0ba..e284097 100644
--- a/components/autofill/core/browser/form_parsing/parsing_test_utils.cc
+++ b/components/autofill/core/browser/form_parsing/parsing_test_utils.cc
@@ -53,7 +53,7 @@
     int max_length,
     ServerFieldType expected_type) {
   FormFieldData field_data;
-  field_data.form_control_type = control_type;
+  field_data.form_control_type = StringToFormControlType(control_type);
   field_data.name = base::UTF8ToUTF16(name);
   field_data.label = base::UTF8ToUTF16(label);
   field_data.max_length = max_length;
diff --git a/components/autofill/core/browser/form_parsing/phone_field.cc b/components/autofill/core/browser/form_parsing/phone_field.cc
index daf26509..9c32a40 100644
--- a/components/autofill/core/browser/form_parsing/phone_field.cc
+++ b/components/autofill/core/browser/form_parsing/phone_field.cc
@@ -142,9 +142,10 @@
   AutofillField* field = scanner->Cursor();
 
   // Return false if the field is not a selection box.
-  if (!MatchesFormControlType(field->form_control_type,
-                              {MatchFieldType::kSelect}))
+  if (!MatchesFormControlType(FormControlTypeToString(field->form_control_type),
+                              {MatchFieldType::kSelect})) {
     return false;
+  }
 
   // If the number of the options is less than the minimum limit or more than
   // the maximum limit, return false.
diff --git a/components/autofill/core/browser/form_parsing/phone_field_unittest.cc b/components/autofill/core/browser/form_parsing/phone_field_unittest.cc
index 5f5a2d64..70639da 100644
--- a/components/autofill/core/browser/form_parsing/phone_field_unittest.cc
+++ b/components/autofill/core/browser/form_parsing/phone_field_unittest.cc
@@ -119,7 +119,7 @@
 autofill::FieldGlobalId PhoneFieldTest::AppendField(
     const TestFieldData& field_data) {
   FormFieldData field;
-  field.form_control_type = field_data.type;
+  field.form_control_type = StringToFormControlType(field_data.type);
   field.label = field_data.label;
   field.name = field_data.name;
   field.max_length = field_data.max_length;
diff --git a/components/autofill/core/browser/form_structure.cc b/components/autofill/core/browser/form_structure.cc
index f5e16cf5..cda304b 100644
--- a/components/autofill/core/browser/form_structure.cc
+++ b/components/autofill/core/browser/form_structure.cc
@@ -298,11 +298,11 @@
                           RandomizedEncoder::FIELD_NAME, field.name_attribute,
                           /*include_checksum=*/false, metadata->mutable_name());
   }
-  if (!field.form_control_type.empty()) {
+  if (!FormControlTypeToString(field.form_control_type).empty()) {
     EncodeRandomizedValue(encoder, form_signature, field_signature,
                           RandomizedEncoder::FIELD_CONTROL_TYPE,
-                          field.form_control_type, /*include_checksum=*/false,
-                          metadata->mutable_type());
+                          FormControlTypeToString(field.form_control_type),
+                          /*include_checksum=*/false, metadata->mutable_type());
   }
   if (!field.label.empty()) {
     EncodeRandomizedValue(encoder, form_signature, field_signature,
@@ -364,10 +364,11 @@
     if (!ShouldSkipField(field))
       ++active_field_count_;
 
-    if (field.form_control_type == "password")
+    if (field.form_control_type == StringToFormControlType("password")) {
       has_password_field_ = true;
-    else
+    } else {
       all_fields_are_passwords_ = false;
+    }
 
     fields_.push_back(std::make_unique<AutofillField>(field));
   }
@@ -1418,7 +1419,8 @@
     }
 
     if (is_raw_metadata_uploading_enabled) {
-      added_field->set_type(field->form_control_type);
+      added_field->set_type(
+          std::string(FormControlTypeToString(field->form_control_type)));
 
       if (!field->name.empty())
         added_field->set_name(base::UTF16ToUTF8(field->name));
diff --git a/components/autofill/core/browser/form_structure_process_query_response_fuzzer.cc b/components/autofill/core/browser/form_structure_process_query_response_fuzzer.cc
index ccffef9..6d925454 100644
--- a/components/autofill/core/browser/form_structure_process_query_response_fuzzer.cc
+++ b/components/autofill/core/browser/form_structure_process_query_response_fuzzer.cc
@@ -26,7 +26,7 @@
   FormFieldData field;
   field.label = ASCIIToUTF16(label);
   field.name = ASCIIToUTF16(name);
-  field.form_control_type = control_type;
+  field.form_control_type = StringToFormControlType(control_type);
 
   form_data->fields.push_back(field);
 }
diff --git a/components/autofill/core/browser/form_structure_rationalizer.cc b/components/autofill/core/browser/form_structure_rationalizer.cc
index 9d4dd00b..cf589932 100644
--- a/components/autofill/core/browser/form_structure_rationalizer.cc
+++ b/components/autofill/core/browser/form_structure_rationalizer.cc
@@ -104,8 +104,9 @@
       field->SetHtmlType(type, field->html_mode());
     };
     // Some of the following rationalization operates only on text fields.
-    bool is_text_field = field->FormControlType() == FormControlType::kText ||
-                         field->FormControlType() == FormControlType::kTextarea;
+    bool is_text_field =
+        field->FormControlType() == DeprecatedFormControlType::kText ||
+        field->FormControlType() == DeprecatedFormControlType::kTextarea;
     switch (field->html_type()) {
       case HtmlFieldType::kAdditionalName:
         if (!is_text_field) {
diff --git a/components/autofill/core/browser/form_structure_rationalizer_unittest.cc b/components/autofill/core/browser/form_structure_rationalizer_unittest.cc
index 4002d24d..32e49fc 100644
--- a/components/autofill/core/browser/form_structure_rationalizer_unittest.cc
+++ b/components/autofill/core/browser/form_structure_rationalizer_unittest.cc
@@ -99,7 +99,8 @@
       field.section = Section::FromAutocomplete(
           {.section = std::string(field_template.section)});
     }
-    field.form_control_type = std::string(field_template.form_control_type);
+    field.form_control_type =
+        StringToFormControlType(field_template.form_control_type);
     field.is_focusable = field_template.is_focusable;
     field.max_length = field_template.max_length;
     field.parsed_autocomplete = field_template.parsed_autocomplete;
diff --git a/components/autofill/core/browser/form_structure_sectioning_util_unittest.cc b/components/autofill/core/browser/form_structure_sectioning_util_unittest.cc
index 4dbfb13f..6dc132f8a 100644
--- a/components/autofill/core/browser/form_structure_sectioning_util_unittest.cc
+++ b/components/autofill/core/browser/form_structure_sectioning_util_unittest.cc
@@ -60,7 +60,7 @@
     const auto& f =
         result.emplace_back(std::make_unique<AutofillField>(FormFieldData()));
     f->unique_renderer_id = test::MakeFieldRendererId();
-    f->form_control_type = t.form_control_type;
+    f->form_control_type = StringToFormControlType(t.form_control_type);
     f->SetTypeTo(AutofillType(t.field_type));
     DCHECK_EQ(f->Type().GetStorableType(), t.field_type);
     if (!t.autocomplete_section.empty() ||
diff --git a/components/autofill/core/browser/form_structure_unittest.cc b/components/autofill/core/browser/form_structure_unittest.cc
index d5d7baa8..90a1d8c 100644
--- a/components/autofill/core/browser/form_structure_unittest.cc
+++ b/components/autofill/core/browser/form_structure_unittest.cc
@@ -338,7 +338,7 @@
   // not by autofill.
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -349,7 +349,7 @@
   // not by autofill.
   field.label = u"password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -359,7 +359,7 @@
   // be picked up by autofill only if there is no minimum field enforcement.
   field.label = u"Full Name";
   field.name = u"fullname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -369,7 +369,7 @@
   // be picked up by autofill only if there is no minimum field enforcement.
   field.label = u"Address Line 1";
   field.name = u"address1";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -378,7 +378,7 @@
   // We now have three auto-fillable fields. It's always autofillable.
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -417,7 +417,7 @@
 
   void AddTextField() {
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     AddField(field);
   }
 
@@ -445,7 +445,7 @@
   {
     FormFieldData field;
     field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
-    field.form_control_type = "radio";
+    field.form_control_type = StringToFormControlType("radio");
     AddField(field);
   }
   EXPECT_FALSE(test_api(form_structure()).ShouldBeParsed());
@@ -456,7 +456,7 @@
   {
     FormFieldData field;
     field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
-    field.form_control_type = "checkbox";
+    field.form_control_type = StringToFormControlType("checkbox");
     AddField(field);
   }
   EXPECT_FALSE(test_api(form_structure()).ShouldBeParsed());
@@ -491,7 +491,7 @@
 TEST_F(FormStructureTestImpl_ShouldBeParsed_Test, FalseIfOnlySelectField) {
   {
     FormFieldData field;
-    field.form_control_type = "select-one";
+    field.form_control_type = StringToFormControlType("select-one");
     AddField(field);
   }
   EXPECT_FALSE(test_api(form_structure()).ShouldBeParsed());
@@ -507,7 +507,7 @@
 TEST_F(FormStructureTestImpl_ShouldBeParsed_Test, FalseIfOnlySelectListField) {
   {
     FormFieldData field;
-    field.form_control_type = "selectlist";
+    field.form_control_type = StringToFormControlType("selectlist");
     AddField(field);
   }
   EXPECT_FALSE(test_api(form_structure()).ShouldBeParsed());
@@ -546,7 +546,7 @@
 TEST_F(FormStructureTestImpl_ShouldBeParsed_Test, TrueIfOnlyPasswordFields) {
   {
     FormFieldData field;
-    field.form_control_type = "password";
+    field.form_control_type = StringToFormControlType("password");
     AddField(field);
   }
   EXPECT_TRUE(test_api(form_structure()).ShouldBeParsed());
@@ -563,7 +563,7 @@
 
   {
     FormFieldData field;
-    field.form_control_type = "password";
+    field.form_control_type = StringToFormControlType("password");
     AddField(field);
   }
   EXPECT_TRUE(test_api(form_structure()).ShouldBeParsed());
@@ -594,7 +594,7 @@
     FormFieldData field;
     field.parsed_autocomplete = AutocompleteParsingResult{
         .section = "my-billing-section", .field_type = HtmlFieldType::kName};
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     AddField(field);
   }
   EXPECT_TRUE(test_api(form_structure()).ShouldBeParsed());
@@ -976,7 +976,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -1046,7 +1046,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Promo Code";
   field.name = u"promocode";
@@ -1243,7 +1243,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Your First Name:";
   field.name = u"bill.first";
@@ -1292,7 +1292,7 @@
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -1332,7 +1332,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"E-mail address";
   field.name = u"email";
@@ -1367,7 +1367,7 @@
   field.label = std::u16string();
   field.name = u"Submit";
   field.value = u"continue";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -1403,7 +1403,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = std::u16string();
@@ -1442,7 +1442,7 @@
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -1478,7 +1478,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Name on Card";
   field.name = u"name_on_card";
@@ -1507,7 +1507,7 @@
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -1540,7 +1540,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Name on Card";
   field.name = u"name_on_card";
@@ -1576,7 +1576,7 @@
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -1611,7 +1611,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Address Line1";
   field.name = u"Address";
@@ -1657,7 +1657,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Address Line1";
   field.name = u"shipping.address.addressLine1";
@@ -1706,7 +1706,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Street:";
   field.name = u"FOPIH_RgWebCC_0_IHAddress_ads1";
@@ -1754,7 +1754,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Address Line1";
   field.name = u"address1";
@@ -1792,7 +1792,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Address Line1";
   field.name = u"Address";
@@ -1831,7 +1831,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name*:";
   field.name = u"editBillingAddress$firstNameBox";
@@ -1915,7 +1915,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Phone:";
   field.name = u"dayphone1";
@@ -1964,7 +1964,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Name on Card";
   field.name = u"name_on_card";
@@ -2021,7 +2021,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Card number";
   field.name = u"ccnumber";
@@ -2087,7 +2087,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Cardholder Name";
   field.name = u"cc_first_name";
@@ -2151,7 +2151,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Name on Card";
   field.name = u"name_on_card";
@@ -2432,7 +2432,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -2452,7 +2452,7 @@
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities, {EMAIL_ADDRESS},
       {AutofillProfile::INVALID});
@@ -2461,7 +2461,7 @@
 
   field.label = u"Phone";
   field.name = u"phone";
-  field.form_control_type = "number";
+  field.form_control_type = StringToFormControlType("number");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {PHONE_HOME_WHOLE_NUMBER}, {AutofillProfile::EMPTY});
@@ -2470,7 +2470,7 @@
 
   field.label = u"Country";
   field.name = u"country";
-  field.form_control_type = "select-one";
+  field.form_control_type = StringToFormControlType("select-one");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {ADDRESS_HOME_COUNTRY}, {AutofillProfile::VALID});
@@ -2561,7 +2561,7 @@
   for (size_t i = 0; i < 2; ++i) {
     field.label = u"Address";
     field.name = u"address";
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.unique_renderer_id = test::MakeFieldRendererId();
     form.fields.push_back(field);
     test::InitializePossibleTypesAndValidities(
@@ -2612,7 +2612,7 @@
   form.url = GURL("http://www.foo.com/");
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -2632,7 +2632,7 @@
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities, {EMAIL_ADDRESS},
       {AutofillProfile::INVALID});
@@ -2641,7 +2641,7 @@
 
   field.label = u"Phone";
   field.name = u"phone";
-  field.form_control_type = "number";
+  field.form_control_type = StringToFormControlType("number");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {PHONE_HOME_WHOLE_NUMBER}, {AutofillProfile::EMPTY});
@@ -2650,7 +2650,7 @@
 
   field.label = u"Country";
   field.name = u"country";
-  field.form_control_type = "select-one";
+  field.form_control_type = StringToFormControlType("select-one");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {ADDRESS_HOME_COUNTRY}, {AutofillProfile::VALID});
@@ -2736,7 +2736,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -2756,7 +2756,7 @@
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities, {EMAIL_ADDRESS},
       {AutofillProfile::INVALID, AutofillProfile::VALID});
@@ -2765,7 +2765,7 @@
 
   field.label = u"Phone";
   field.name = u"phone";
-  field.form_control_type = "number";
+  field.form_control_type = StringToFormControlType("number");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {PHONE_HOME_WHOLE_NUMBER},
@@ -2775,7 +2775,7 @@
 
   field.label = u"Country";
   field.name = u"country";
-  field.form_control_type = "select-one";
+  field.form_control_type = StringToFormControlType("select-one");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities,
       {ADDRESS_HOME_COUNTRY}, {AutofillProfile::VALID, AutofillProfile::VALID});
@@ -2860,7 +2860,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -2878,7 +2878,7 @@
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   test::InitializePossibleTypesAndValidities(
       possible_field_types, possible_field_types_validities, {EMAIL_ADDRESS});
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -2886,7 +2886,7 @@
 
   field.label = u"Phone";
   field.name = u"phone";
-  field.form_control_type = "number";
+  field.form_control_type = StringToFormControlType("number");
   test::InitializePossibleTypesAndValidities(possible_field_types,
                                              possible_field_types_validities,
                                              {PHONE_HOME_WHOLE_NUMBER});
@@ -2895,7 +2895,7 @@
 
   field.label = u"Country";
   field.name = u"country";
-  field.form_control_type = "select-one";
+  field.form_control_type = StringToFormControlType("select-one");
   test::InitializePossibleTypesAndValidities(possible_field_types,
                                              possible_field_types_validities,
                                              {ADDRESS_HOME_COUNTRY});
@@ -2980,7 +2980,7 @@
   for (size_t i = 0; i < 2; ++i) {
     field.label = u"Address";
     field.name = u"address";
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.unique_renderer_id = test::MakeFieldRendererId();
     form.fields.push_back(field);
     test::InitializePossibleTypesAndValidities(
@@ -3031,7 +3031,7 @@
   for (size_t i = 0; i < 300; ++i) {
     field.label = u"Address";
     field.name = u"address";
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.unique_renderer_id = test::MakeFieldRendererId();
     form.fields.push_back(field);
     test::InitializePossibleTypesAndValidities(
@@ -3321,7 +3321,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"firstname";
@@ -3342,7 +3342,7 @@
   field.label = u"Email";
   field.name = u"email";
   field.name_attribute = field.name;
-  field.form_control_type = "email";
+  field.form_control_type = StringToFormControlType("email");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
   test::InitializePossibleTypesAndValidities(
@@ -3403,7 +3403,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   // No label for the first field.
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -3476,7 +3476,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -3563,7 +3563,7 @@
   form.name = u"myform";
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -3789,7 +3789,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Cardholder name";
   field.name = u"cc-name";
@@ -3930,7 +3930,7 @@
   form.is_form_tag = true;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"First Name";
   field.name = u"first";
@@ -4188,7 +4188,7 @@
   form.is_form_tag = false;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"email";
   field.name = u"email";
@@ -4494,13 +4494,14 @@
                                          RandomizedEncoder::FIELD_NAME,
                                          field.name_attribute));
     }
-    if (field.form_control_type.empty()) {
+    if (FormControlTypeToString(field.form_control_type).empty()) {
       EXPECT_FALSE(metadata.has_type());
     } else {
-      EXPECT_EQ(metadata.type().encoded_bits(),
-                encoder.Encode(form_signature, field_signature,
-                               RandomizedEncoder::FIELD_CONTROL_TYPE,
-                               field.form_control_type));
+      EXPECT_EQ(
+          metadata.type().encoded_bits(),
+          encoder.Encode(form_signature, field_signature,
+                         RandomizedEncoder::FIELD_CONTROL_TYPE,
+                         FormControlTypeToString(field.form_control_type)));
     }
     if (field.label.empty()) {
       EXPECT_FALSE(metadata.has_label());
@@ -4565,7 +4566,7 @@
 
     // One form field needed to be valid form.
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.label = u"email";
     field.name = u"email";
     field.unique_renderer_id = test::MakeFieldRendererId();
@@ -4597,7 +4598,7 @@
   FormData form;
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"email";
   field.name = u"email";
@@ -4612,7 +4613,7 @@
   // Checkable fields shouldn't affect the signature.
   field.label = u"Select";
   field.name = u"Select";
-  field.form_control_type = "checkbox";
+  field.form_control_type = StringToFormControlType("checkbox");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -4644,7 +4645,7 @@
   field.check_status = FormFieldData::CheckStatus::kNotCheckable;
   field.label = u"Random Field label";
   field.name = u"random1234";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -4675,19 +4676,19 @@
   large_form.url = GURL("http://foo.com/login?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   large_form.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   large_form.fields.push_back(field2);
 
   FormFieldData field3;
-  field3.form_control_type = "email";
+  field3.form_control_type = StringToFormControlType("email");
   large_form.fields.push_back(field3);
 
   FormFieldData field4;
-  field4.form_control_type = "tel";
+  field4.form_control_type = StringToFormControlType("tel");
   large_form.fields.push_back(field4);
 
   // Alternative form signature string of a form with more than two fields
@@ -4703,11 +4704,11 @@
   small_form_path.url = GURL("http://foo.com/login?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_path.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_path.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less should
@@ -4723,11 +4724,11 @@
   small_form_ref.url = GURL("http://foo.com?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_ref.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_ref.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less and
@@ -4744,11 +4745,11 @@
   small_form_query.url = GURL("http://foo.com?q=a");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_query.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_query.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less and
@@ -4769,19 +4770,19 @@
   FormFieldData field;
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"Submit";
-  field.form_control_type = "submit";
+  field.form_control_type = StringToFormControlType("submit");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -4797,20 +4798,20 @@
   FormFieldData field;
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"select";
   field.name = u"select";
-  field.form_control_type = "checkbox";
+  field.form_control_type = StringToFormControlType("checkbox");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = std::u16string();
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.check_status = FormFieldData::CheckStatus::kNotCheckable;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -4850,19 +4851,19 @@
   FormFieldData field;
   // No label on the first field.
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Enter your Email address";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Enter your Password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -4898,7 +4899,7 @@
   FormFieldData field;
   // No label on the first field.
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -4909,13 +4910,13 @@
       u"What Marketers Do! We Know That Your Email Address Has The Possibility "
       u"Of Exceeding A Certain Number Of Characters...";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Enter your Password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -4952,14 +4953,14 @@
   FormFieldData field;
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = std::u16string();
   // No name set for this field.
   field.name = u"";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.check_status = FormFieldData::CheckStatus::kNotCheckable;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -5116,7 +5117,7 @@
   FormData form_data;
   FormFieldData field;
   form_data.url = GURL("http://foo.com");
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   // First name field.
   field.label = u"Nombre";
@@ -5177,7 +5178,7 @@
   FormData form_data;
   FormFieldData field;
   form_data.url = GURL("http://foo.com");
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   // Field for the name.
   field.label = u"Name";
@@ -5385,7 +5386,7 @@
     // Create an iframe form with a single field.
     std::vector<FormFieldData> fields;
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.name = u"name";
     field.unique_renderer_id = test::MakeFieldRendererId();
     field.host_form_signature = FormSignature(host_form_signature);
@@ -5441,7 +5442,7 @@
   // 12345 or 67890. The first two fields have identical field signatures.
   std::vector<FormFieldData> fields;
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.name = u"name";
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -5558,7 +5559,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"fullname";
   field.name = u"fullname";
@@ -5573,7 +5574,7 @@
   // Checkable fields should be ignored in parsing
   FormFieldData checkable_field;
   checkable_field.label = u"radio_button";
-  checkable_field.form_control_type = "radio";
+  checkable_field.form_control_type = StringToFormControlType("radio");
   checkable_field.check_status =
       FormFieldData::CheckStatus::kCheckableButUnchecked;
   checkable_field.unique_renderer_id = test::MakeFieldRendererId();
@@ -5592,7 +5593,7 @@
 
   field.label = u"password";
   field.name = u"password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form2.fields.push_back(field);
 
@@ -6012,7 +6013,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 10000;
   field.should_autocomplete = false;
 
@@ -6080,7 +6081,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 10000;
   field.should_autocomplete = false;
 
@@ -6148,7 +6149,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 10000;
   field.should_autocomplete = false;
 
@@ -6296,7 +6297,7 @@
   FormFieldData field;
   // Check that the form with 250 fields are processed correctly.
   for (size_t i = 0; i < 250; ++i) {
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.name = u"text" + base::NumberToString16(i);
     field.unique_renderer_id = test::MakeFieldRendererId();
     form.fields.push_back(field);
@@ -6320,7 +6321,7 @@
   FormData form;
   FormFieldData field;
   field.name = u"Password";
-  field.form_control_type = "password";
+  field.form_control_type = StringToFormControlType("password");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -6379,7 +6380,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 10000;
 
   field.label = u"Full Name";
@@ -6719,7 +6720,7 @@
   FormData form;
   form.url = GURL("http://foo.com");
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.max_length = 10000;
 
   field.label = u"Full Name";
@@ -6767,7 +6768,7 @@
   // Add 6 fields.
   for (int i = 0; i < 6; i++) {
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.label = field.name = base::NumberToString16(i);
     field.unique_renderer_id = test::MakeFieldRendererId();
     form_data.fields.push_back(field);
@@ -6902,7 +6903,7 @@
                            LocalFrameToken frame_token,
                            FormRendererId host_form_id) {
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
     field.name = name;
     field.unique_renderer_id = test::MakeFieldRendererId();
     field.host_frame = frame_token;
diff --git a/components/autofill/core/browser/heuristic_classification_unittests.cc b/components/autofill/core/browser/heuristic_classification_unittests.cc
index ef4da30..071650a 100644
--- a/components/autofill/core/browser/heuristic_classification_unittests.cc
+++ b/components/autofill/core/browser/heuristic_classification_unittests.cc
@@ -342,13 +342,13 @@
       "tel",      "text",     "time",   "url",   "week",     "textarea"};
   if (const std::string* type = field_dict.FindString("type_attr")) {
     if (*type == "select") {
-      field.form_control_type = "select-one";
+      field.form_control_type = StringToFormControlType("select-one");
     } else if (*type == "input") {
-      field.form_control_type = "text";
+      field.form_control_type = StringToFormControlType("text");
     } else if (base::Contains(valid_control_types, *type)) {
-      field.form_control_type = *type;
+      field.form_control_type = StringToFormControlType(*type);
     } else {
-      field.form_control_type = "text";
+      field.form_control_type = StringToFormControlType("text");
     }
   }
   if (const std::string* autocomplete =
diff --git a/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc b/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
index 1de5d4e..0d7b92d 100644
--- a/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
+++ b/components/autofill/core/browser/metrics/autofill_metrics_unittest.cc
@@ -655,7 +655,7 @@
   FieldSignature field_signature[2];
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"fullname";
   field.name = u"fullname";
@@ -753,7 +753,7 @@
   FieldSignature field_signature[3];
 
   FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
 
   field.label = u"Country";
   field.name = u"country";
@@ -772,7 +772,7 @@
   field.label = u"State";
   field.name = u"state";
   field.is_focusable = false;
-  field.form_control_type = "select-one";
+  field.form_control_type = StringToFormControlType("select-one");
   form.fields.push_back(field);
   // Regardless of the order of appearance, hidden fields are rationalized
   // before their corresponding visible one.
@@ -6520,7 +6520,7 @@
     form.url = GURL("http://foo.com");
     form.main_frame_origin = url::Origin::Create(GURL("http://foo_root.com"));
     FormFieldData field;
-    field.form_control_type = "text";
+    field.form_control_type = StringToFormControlType("text");
 
     field.label = u"fullname";
     field.name = u"fullname";
@@ -6533,7 +6533,7 @@
     // Checkable fields should be ignored in parsing.
     FormFieldData checkable_field;
     checkable_field.label = u"radio_button";
-    checkable_field.form_control_type = "radio";
+    checkable_field.form_control_type = StringToFormControlType("radio");
     checkable_field.check_status =
         FormFieldData::CheckStatus::kCheckableButUnchecked;
     form.fields.push_back(checkable_field);
@@ -6547,7 +6547,7 @@
 
     field.label = u"password";
     field.name = u"password";
-    field.form_control_type = "password";
+    field.form_control_type = StringToFormControlType("password");
     form.fields.push_back(field);
 
     owned_forms_.push_back(std::make_unique<FormStructure>(form));
@@ -8150,19 +8150,19 @@
   FormFieldData field;
   field.label = u"State";
   field.name = u"state";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Street";
   field.name = u"";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Number";
   field.name = u"";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -8210,7 +8210,7 @@
           {UFIT::kFieldSignatureName,
            Collapse(CalculateFieldSignatureForField(form.fields[i])).value()},
           {UFIT::kFormControlTypeName,
-           base::to_underlying(FormControlType::kText)},
+           base::to_underlying(DeprecatedFormControlType::kText)},
           {UFIT::kAutocompleteStateName,
            base::to_underlying(AutofillMetrics::AutocompleteState::kNone)},
           {UFIT::kAutofillStatusVectorName, autofill_status_vector.data()[0]},
@@ -8239,19 +8239,19 @@
   FormFieldData field;
   field.label = u"State";
   field.name = u"state";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Street";
   field.name = u"";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Number";
   field.name = u"";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -8327,7 +8327,7 @@
           {UFIT::kAutofillSkippedStatusName,
            DenseSet<FieldFillingSkipReason>{status}.data()[0]},
           {UFIT::kFormControlTypeName,
-           base::to_underlying(FormControlType::kText)},
+           base::to_underlying(DeprecatedFormControlType::kText)},
           {UFIT::kAutocompleteStateName,
            base::to_underlying(AutofillMetrics::AutocompleteState::kNone)},
           {UFIT::kAutofillStatusVectorName, autofill_status_vector.data()[0]},
@@ -8435,7 +8435,7 @@
   FormFieldData field;
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.autocomplete_attribute = "family-name";
   field.parsed_autocomplete = ParseAutocompleteAttribute("family-name");
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -8444,7 +8444,7 @@
   // Heuristic value will NOT match with Autocomplete attribute.
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.autocomplete_attribute = "additional-name";
   field.parsed_autocomplete = ParseAutocompleteAttribute("additional-name");
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -8453,7 +8453,7 @@
   // No autocomplete attribute.
   field.label = u"Address";
   field.name = u"address";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.autocomplete_attribute = "off";
   field.parsed_autocomplete = ParseAutocompleteAttribute("off");
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -8462,7 +8462,7 @@
   // Heuristic value will be unknown.
   field.label = u"Garbage label";
   field.name = u"garbage";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.autocomplete_attribute = "postal-code";
   field.parsed_autocomplete = ParseAutocompleteAttribute("postal-code");
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -8470,7 +8470,7 @@
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.autocomplete_attribute = "garbage";
   field.parsed_autocomplete = ParseAutocompleteAttribute("garbage");
   field.unique_renderer_id = test::MakeFieldRendererId();
@@ -8568,7 +8568,7 @@
         {UFIT::kTypeChangedByRationalizationName, false},
         {UFIT::kRankInFieldSignatureGroupName, 1},
         {UFIT::kFormControlTypeName,
-         base::to_underlying(FormControlType::kText)},
+         base::to_underlying(DeprecatedFormControlType::kText)},
         {UFIT::kAutocompleteStateName,
          base::to_underlying(autocomplete_states[i])},
         {UFIT::kAutofillStatusVectorName, autofill_status_vector.data()[0]},
@@ -8728,7 +8728,7 @@
         {UFIT::kFieldSignatureName,
          Collapse(CalculateFieldSignatureForField(form.fields[i])).value()},
         {UFIT::kFormControlTypeName,
-         base::to_underlying(FormControlType::kText)},
+         base::to_underlying(DeprecatedFormControlType::kText)},
         {UFIT::kAutocompleteStateName,
          base::to_underlying(AutofillMetrics::AutocompleteState::kNone)},
         {UFIT::kAutofillStatusVectorName, autofill_status_vector.data()[0]},
@@ -8871,14 +8871,14 @@
   FormFieldData field;
   field.label = u"Option 1";
   field.name = u"Option 1";
-  field.form_control_type = "checkbox";
+  field.form_control_type = StringToFormControlType("checkbox");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Option 2";
   field.name = u"Option 2";
-  field.form_control_type = "checkbox";
+  field.form_control_type = StringToFormControlType("checkbox");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -8909,21 +8909,21 @@
   FormFieldData field;
   field.label = u"username";
   field.name = u"username";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   // Two checkable radio buttons.
   field.label = u"female";
   field.name = u"female";
-  field.form_control_type = "radio";
+  field.form_control_type = StringToFormControlType("radio");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"male";
   field.name = u"male";
-  field.form_control_type = "radio";
+  field.form_control_type = StringToFormControlType("radio");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -8931,7 +8931,7 @@
   // One checkable checkbox.
   field.label = u"save";
   field.name = u"save";
-  field.form_control_type = "checkbox";
+  field.form_control_type = StringToFormControlType("checkbox");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -8965,27 +8965,27 @@
   FormFieldData field;
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   // Two checkable radio buttons.
   field.label = u"female";
   field.name = u"female";
-  field.form_control_type = "radio";
+  field.form_control_type = StringToFormControlType("radio");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"male";
   field.name = u"male";
-  field.form_control_type = "radio";
+  field.form_control_type = StringToFormControlType("radio");
   field.check_status = FormFieldData::CheckStatus::kCheckableButUnchecked;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -9007,9 +9007,9 @@
   auto entries =
       test_ukm_recorder().GetEntriesByName(UkmFieldInfoType::kEntryName);
   ASSERT_EQ(4u, entries.size());
-  std::vector<FormControlType> form_control_types = {
-      FormControlType::kText, FormControlType::kText, FormControlType::kRadio,
-      FormControlType::kRadio};
+  std::vector<DeprecatedFormControlType> form_control_types = {
+      DeprecatedFormControlType::kText, DeprecatedFormControlType::kText,
+      DeprecatedFormControlType::kRadio, DeprecatedFormControlType::kRadio};
   for (size_t i = 0; i < entries.size(); ++i) {
     SCOPED_TRACE(testing::Message() << i);
     DenseSet<AutofillStatus> autofill_status_vector = {
@@ -9095,20 +9095,20 @@
   FormFieldData field;
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   // Selectlist.
   field.label = u"Country";
   field.name = u"country";
-  field.form_control_type = "selectlist";
+  field.form_control_type = StringToFormControlType("selectlist");
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
@@ -9130,13 +9130,13 @@
   ASSERT_EQ(3u, entries.size());
   test_ukm_recorder().ExpectEntryMetric(
       entries[0], UkmFieldInfoType::kFormControlTypeName,
-      base::to_underlying(FormControlType::kText));
+      base::to_underlying(DeprecatedFormControlType::kText));
   test_ukm_recorder().ExpectEntryMetric(
       entries[1], UkmFieldInfoType::kFormControlTypeName,
-      base::to_underlying(FormControlType::kText));
+      base::to_underlying(DeprecatedFormControlType::kText));
   test_ukm_recorder().ExpectEntryMetric(
       entries[2], UkmFieldInfoType::kFormControlTypeName,
-      base::to_underlying(FormControlType::kSelectlist));
+      base::to_underlying(DeprecatedFormControlType::kSelectlist));
 }
 
 // Tests that the field which is in a different frame than its form is recorded
@@ -9155,21 +9155,21 @@
   FormFieldData field;
   field.label = u"First Name";
   field.name = u"firstname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.host_frame = form.host_frame;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Last Name";
   field.name = u"lastname";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.host_frame = test::MakeLocalFrameToken(test::RandomizeFrame(true));
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
 
   field.label = u"Email";
   field.name = u"email";
-  field.form_control_type = "text";
+  field.form_control_type = StringToFormControlType("text");
   field.host_frame = form.host_frame;
   field.unique_renderer_id = test::MakeFieldRendererId();
   form.fields.push_back(field);
@@ -9191,8 +9191,9 @@
   auto entries =
       test_ukm_recorder().GetEntriesByName(UkmFieldInfoType::kEntryName);
   ASSERT_EQ(3u, entries.size());
-  std::vector<FormControlType> form_control_types = {
-      FormControlType::kText, FormControlType::kText, FormControlType::kText};
+  std::vector<DeprecatedFormControlType> form_control_types = {
+      DeprecatedFormControlType::kText, DeprecatedFormControlType::kText,
+      DeprecatedFormControlType::kText};
   for (size_t i = 0; i < entries.size(); ++i) {
     SCOPED_TRACE(testing::Message() << i);
 
diff --git a/components/autofill/core/browser/metrics/quality_metrics_unittest.cc b/components/autofill/core/browser/metrics/quality_metrics_unittest.cc
index 012f658dfe..593a0ce9 100644
--- a/components/autofill/core/browser/metrics/quality_metrics_unittest.cc
+++ b/components/autofill/core/browser/metrics/quality_metrics_unittest.cc
@@ -535,7 +535,7 @@
 
   FormData form = CreateForm(
       {CreateTestFormField("first", "first", ValueForType(NAME_FIRST), "text"),
-       CreateTestFormField("last", "last", ValueForType(NAME_LAST), "test"),
+       CreateTestFormField("last", "last", ValueForType(NAME_LAST), "text"),
        CreateTestFormField("Unknown", "Unknown",
                            ValueForType(actual_field_type), "text")});
 
diff --git a/components/autofill/core/browser/personal_data_manager.cc b/components/autofill/core/browser/personal_data_manager.cc
index dc9dc9b..6cecec3 100644
--- a/components/autofill/core/browser/personal_data_manager.cc
+++ b/components/autofill/core/browser/personal_data_manager.cc
@@ -1815,83 +1815,6 @@
   return nullptr;
 }
 
-void PersonalDataManager::SetProfilesForAllSources(
-    std::vector<AutofillProfile>* new_profiles) {
-  if (!database_helper_->GetLocalDatabase()) {
-    return;
-  }
-
-  ClearOnGoingProfileChanges();
-
-  const auto split_point = base::ranges::partition(
-      *new_profiles, [](const AutofillProfile& profile) {
-        return profile.source() == AutofillProfile::Source::kLocalOrSyncable;
-      });
-  bool change_happened =
-      SetProfilesForSource({new_profiles->begin(), split_point},
-                           AutofillProfile::Source::kLocalOrSyncable);
-  change_happened |= SetProfilesForSource({split_point, new_profiles->end()},
-                                          AutofillProfile::Source::kAccount);
-
-  if (!change_happened) {
-    // When a change happens (add, update, remove), we would consequently call
-    // `NotifyPersonalDataChanged()` which notifies the tests to stop waiting.
-    // Otherwise, we need to stop them by calling the function directly.
-    NotifyPersonalDataObserver();
-  }
-}
-
-bool PersonalDataManager::SetProfilesForSource(
-    base::span<const AutofillProfile> new_profiles,
-    AutofillProfile::Source source) {
-  DCHECK(
-      base::ranges::all_of(new_profiles, [&](const AutofillProfile& profile) {
-        return profile.source() == source;
-      }));
-
-  // Means that a profile was added, removed or updated.
-  bool change_happened = false;
-
-  std::vector<std::unique_ptr<AutofillProfile>>& profiles =
-      GetProfileStorage(source);
-
-  // Any profiles that are not in the new profile list should be removed from
-  // the web database
-  for (const auto& it : profiles) {
-    if (!FindByGUID(new_profiles, it->guid())) {
-      RemoveProfileFromDB(it->guid());
-      change_happened = true;
-    }
-  }
-
-  // Update the web database with the new and existing profiles.
-  for (const AutofillProfile& it : new_profiles) {
-    const auto* existing_profile = GetProfileByGUID(it.guid());
-    // In `SetProfilesForSource()`, exceptionally, profiles are directly added/
-    // updated on the `profiles` before they are ready to be added or get
-    // updated in the database. Enforce the changes to make sure the database is
-    // also updated.
-    if (existing_profile) {
-      if (!existing_profile->EqualsForUpdatePurposes(it)) {
-        UpdateProfileInDB(it, /*enforced=*/true);
-        change_happened = true;
-      }
-    } else if (!FindByContents(profiles, it)) {
-      AddProfileToDB(it, /*enforced=*/true);
-      change_happened = true;
-    }
-  }
-
-  if (change_happened) {
-    // Copy in the new profiles.
-    profiles.clear();
-    for (const AutofillProfile& it : new_profiles) {
-      profiles.push_back(std::make_unique<AutofillProfile>(it));
-    }
-  }
-  return change_happened;
-}
-
 bool PersonalDataManager::IsProfileMigrationBlocked(
     const std::string& guid) const {
   AutofillProfile* profile = GetProfileByGUID(guid);
@@ -2555,8 +2478,7 @@
   AutofillAddressConversionCompleted();
 }
 
-void PersonalDataManager::AddProfileToDB(const AutofillProfile& profile,
-                                         bool enforced) {
+void PersonalDataManager::AddProfileToDB(const AutofillProfile& profile) {
   if (profile.IsEmpty(app_locale_)) {
     NotifyPersonalDataObserver();
     return;
@@ -2565,16 +2487,14 @@
   if (!ProfileChangesAreOngoing(profile.guid())) {
     const std::vector<std::unique_ptr<AutofillProfile>>& profiles =
         GetProfileStorage(profile.source());
-    if (!enforced && (FindByGUID(profiles, profile.guid()) ||
-                      FindByContents(profiles, profile))) {
+    if (FindByGUID(profiles, profile.guid()) ||
+        FindByContents(profiles, profile)) {
       NotifyPersonalDataObserver();
       return;
     }
   }
   ongoing_profile_changes_[profile.guid()].emplace_back(
       AutofillProfileChange::ADD, profile);
-  if (enforced)
-    ongoing_profile_changes_[profile.guid()].back().set_enforced();
   HandleNextProfileChange(profile.guid());
 }
 
@@ -2650,8 +2570,7 @@
   if (change_type == AutofillProfileChange::ADD) {
     const std::vector<std::unique_ptr<AutofillProfile>>& profiles =
         GetProfileStorage(profile.source());
-    if (!change.enforced() &&
-        (profile_exists || FindByContents(profiles, profile))) {
+    if (profile_exists || FindByContents(profiles, profile)) {
       OnProfileChangeDone(guid);
       return;
     }
@@ -2695,10 +2614,6 @@
   }
 }
 
-void PersonalDataManager::ClearOnGoingProfileChanges() {
-  ongoing_profile_changes_.clear();
-}
-
 bool PersonalDataManager::HasPendingQueries() {
   return pending_synced_local_profiles_query_ != 0 ||
          pending_account_profiles_query_ != 0 ||
diff --git a/components/autofill/core/browser/personal_data_manager.h b/components/autofill/core/browser/personal_data_manager.h
index 3a05ee5..5912f99 100644
--- a/components/autofill/core/browser/personal_data_manager.h
+++ b/components/autofill/core/browser/personal_data_manager.h
@@ -557,16 +557,6 @@
   // Returns whether sync's integration with payments is on.
   virtual bool IsAutofillWalletImportEnabled() const;
 
-  // Partitions `new_profiles` by their sources and sets
-  // `synced_local_profiles_` and `account_profiles_` to the corresponding
-  // profiles. Updates the web database by adding, updating and removing
-  // profiles, depending on the difference of the current state and
-  // `new_profiles`. `synced_local_profiles_` and `account_profiles_` need to be
-  // updated at the end of the function, since some tasks cannot tolerate
-  // database delays.
-  virtual void SetProfilesForAllSources(
-      std::vector<AutofillProfile>* new_profiles);
-
   // Sets |credit_cards_| to the contents of |credit_cards| and updates the web
   // database by adding, updating and removing credit cards.
   void SetCreditCards(std::vector<CreditCard>* credit_cards);
@@ -744,13 +734,6 @@
   virtual const AutofillProfileUpdateStrikeDatabase*
   GetProfileUpdateStrikeDatabase() const;
 
-  // Like `SetProfilesForAllSources()`, but assumes that all profiles in
-  // `new_profiles` have the given `source`.
-  // Returns true if a change happened.
-  virtual bool SetProfilesForSource(
-      base::span<const AutofillProfile> new_profiles,
-      AutofillProfile::Source source);
-
   // Loads the saved profiles from the web database.
   virtual void LoadProfiles();
 
@@ -930,9 +913,10 @@
   void RemoveAutofillProfileByGUIDAndBlankCreditCardReference(
       const std::string& guid);
 
-  // Add/Update/Remove |profile| on DB. |enforced| should be true when the
-  // add/update should happen regardless of an existing/equal profile.
-  void AddProfileToDB(const AutofillProfile& profile, bool enforced = false);
+  // Add/Update/Removes a profile in AutofillTable asynchronously. The changes
+  // only surface in the PDM after the task on the DB sequence has finished.
+  // TODO(crbug.cm/1420547): `enforced` should not be used. Remove it.
+  void AddProfileToDB(const AutofillProfile& profile);
   void UpdateProfileInDB(const AutofillProfile& profile, bool enforced = false);
   void RemoveProfileFromDB(const std::string& guid);
 
@@ -955,8 +939,6 @@
   // Remove the change from the |ongoing_profile_changes_|, handle next task or
   // Refresh.
   void OnProfileChangeDone(const std::string& guid);
-  // Clear |ongoing_profile_changes_|.
-  void ClearOnGoingProfileChanges();
 
   // Returns if there are any pending queries to the web database.
   bool HasPendingQueries();
diff --git a/components/autofill/core/browser/personal_data_manager_unittest.cc b/components/autofill/core/browser/personal_data_manager_unittest.cc
index 6d14c73..0962f00 100644
--- a/components/autofill/core/browser/personal_data_manager_unittest.cc
+++ b/components/autofill/core/browser/personal_data_manager_unittest.cc
@@ -542,49 +542,6 @@
       ElementsAre(Pointee(kLocalOrSyncableProfile), Pointee(kAccountProfile)));
 }
 
-// Tests that `SetProfilesForAllSources()` overwrites profiles with the correct
-// source.
-TEST_F(PersonalDataManagerTest, SetProfiles) {
-  AutofillProfile kAccountProfile = test::GetFullProfile();
-  kAccountProfile.set_source_for_testing(AutofillProfile::Source::kAccount);
-  AutofillProfile kLocalProfile = test::GetFullProfile();
-
-  // Set `kAccount` profiles only.
-  std::vector<AutofillProfile> profiles = {kAccountProfile};
-  personal_data_->SetProfilesForAllSources(&profiles);
-  PersonalDataProfileTaskWaiter(*personal_data_).Wait();
-  EXPECT_THAT(
-      personal_data_->GetProfilesFromSource(AutofillProfile::Source::kAccount),
-      ElementsAre(Pointee(kAccountProfile)));
-  EXPECT_TRUE(
-      personal_data_
-          ->GetProfilesFromSource(AutofillProfile::Source::kLocalOrSyncable)
-          .empty());
-
-  // Set `kLocalOrSyncable` profiles only. This clear the existing `kAccount`
-  // profiles
-  profiles = {kLocalProfile};
-  personal_data_->SetProfilesForAllSources(&profiles);
-  PersonalDataProfileTaskWaiter(*personal_data_).Wait();
-  EXPECT_TRUE(
-      personal_data_->GetProfilesFromSource(AutofillProfile::Source::kAccount)
-          .empty());
-  EXPECT_THAT(personal_data_->GetProfilesFromSource(
-                  AutofillProfile::Source::kLocalOrSyncable),
-              ElementsAre(Pointee(kLocalProfile)));
-
-  // Set profiles of both sources.
-  profiles = {kAccountProfile, kLocalProfile};
-  personal_data_->SetProfilesForAllSources(&profiles);
-  PersonalDataProfileTaskWaiter(*personal_data_).Wait();
-  EXPECT_THAT(
-      personal_data_->GetProfilesFromSource(AutofillProfile::Source::kAccount),
-      ElementsAre(Pointee(kAccountProfile)));
-  EXPECT_THAT(personal_data_->GetProfilesFromSource(
-                  AutofillProfile::Source::kLocalOrSyncable),
-              ElementsAre(Pointee(kLocalProfile)));
-}
-
 // Adding, updating, removing operations without waiting in between.
 TEST_F(PersonalDataManagerTest, AddRemoveUpdateProfileSequence) {
   AutofillProfile profile(test::GetFullProfile());
diff --git a/components/autofill/core/browser/test_personal_data_manager.cc b/components/autofill/core/browser/test_personal_data_manager.cc
index 3413535..f9092b5 100644
--- a/components/autofill/core/browser/test_personal_data_manager.cc
+++ b/components/autofill/core/browser/test_personal_data_manager.cc
@@ -138,24 +138,6 @@
   return default_country_code_;
 }
 
-void TestPersonalDataManager::SetProfilesForAllSources(
-    std::vector<AutofillProfile>* profiles) {
-  // Copy all the profiles. Called by functions like
-  // PersonalDataManager::SaveImportedProfile, which impact metrics.
-  ClearProfiles();
-  for (const auto& profile : *profiles)
-    AddProfile(profile);
-}
-
-bool TestPersonalDataManager::SetProfilesForSource(
-    base::span<const AutofillProfile> new_profiles,
-    AutofillProfile::Source source) {
-  GetProfileStorage(source).clear();
-  for (const AutofillProfile& profile : new_profiles)
-    AddProfile(profile);
-  return true;
-}
-
 void TestPersonalDataManager::LoadProfiles() {
   // Overridden to avoid a trip to the database. This should be a no-op except
   // for the side-effect of logging the profile count.
diff --git a/components/autofill/core/browser/test_personal_data_manager.h b/components/autofill/core/browser/test_personal_data_manager.h
index 6e65331..6430118 100644
--- a/components/autofill/core/browser/test_personal_data_manager.h
+++ b/components/autofill/core/browser/test_personal_data_manager.h
@@ -57,10 +57,6 @@
   void UpdateCreditCard(const CreditCard& credit_card) override;
   void AddFullServerCreditCard(const CreditCard& credit_card) override;
   const std::string& GetDefaultCountryCodeForNewAddress() const override;
-  void SetProfilesForAllSources(
-      std::vector<AutofillProfile>* profiles) override;
-  bool SetProfilesForSource(base::span<const AutofillProfile> new_profiles,
-                            AutofillProfile::Source source) override;
   void LoadProfiles() override;
   void LoadCreditCards() override;
   void LoadCreditCardCloudTokenData() override;
diff --git a/components/autofill/core/common/autofill_data_validation.cc b/components/autofill/core/common/autofill_data_validation.cc
index b13fa41..b3c3bef5 100644
--- a/components/autofill/core/common/autofill_data_validation.cc
+++ b/components/autofill/core/common/autofill_data_validation.cc
@@ -5,6 +5,7 @@
 #include "components/autofill/core/common/autofill_data_validation.h"
 
 #include "base/ranges/algorithm.h"
+#include "base/types/cxx23_to_underlying.h"
 #include "components/autofill/core/common/autofill_constants.h"
 #include "components/autofill/core/common/form_data.h"
 #include "components/autofill/core/common/form_field_data.h"
@@ -32,7 +33,10 @@
 bool IsValidFormFieldData(const FormFieldData& field) {
   return IsValidString16(field.label) && IsValidString16(field.name) &&
          IsValidString16(field.value) &&
-         IsValidString(field.form_control_type) &&
+         base::to_underlying(field.form_control_type) >=
+             base::to_underlying(FormControlType::kMinValue) &&
+         base::to_underlying(field.form_control_type) <=
+             base::to_underlying(FormControlType::kMaxValue) &&
          IsValidString(field.autocomplete_attribute) &&
          IsValidOptionVector(field.options);
 }
diff --git a/components/autofill/core/common/autofill_test_utils.cc b/components/autofill/core/common/autofill_test_utils.cc
index 03cb651..45c77da 100644
--- a/components/autofill/core/common/autofill_test_utils.cc
+++ b/components/autofill/core/common/autofill_test_utils.cc
@@ -144,7 +144,7 @@
   field.label = base::UTF8ToUTF16(label);
   field.name = base::UTF8ToUTF16(name);
   field.value = base::UTF8ToUTF16(value);
-  field.form_control_type = type;
+  field.form_control_type = StringToFormControlType(type);
   field.is_focusable = true;
   return field;
 }
diff --git a/components/autofill/core/common/field_data_manager_unittest.cc b/components/autofill/core/common/field_data_manager_unittest.cc
index 0fba915..0e7f296 100644
--- a/components/autofill/core/common/field_data_manager_unittest.cc
+++ b/components/autofill/core/common/field_data_manager_unittest.cc
@@ -20,13 +20,13 @@
     FormFieldData field1;
     field1.id_attribute = u"name1";
     field1.value = u"first";
-    field1.form_control_type = "text";
+    field1.form_control_type = StringToFormControlType("text");
     field1.unique_renderer_id = FieldRendererId(1);
     control_elements_.push_back(field1);
 
     FormFieldData field2;
     field2.id_attribute = u"name2";
-    field2.form_control_type = "password";
+    field2.form_control_type = StringToFormControlType("password");
     field2.unique_renderer_id = FieldRendererId(2);
     control_elements_.push_back(field2);
   }
diff --git a/components/autofill/core/common/form_data_unittest.cc b/components/autofill/core/common/form_data_unittest.cc
index 43980fc7..96714fd2 100644
--- a/components/autofill/core/common/form_data_unittest.cc
+++ b/components/autofill/core/common/form_data_unittest.cc
@@ -141,7 +141,7 @@
   field_data.label = u"label";
   field_data.name = u"name";
   field_data.value = u"value";
-  field_data.form_control_type = "password";
+  field_data.form_control_type = StringToFormControlType("password");
   field_data.autocomplete_attribute = "off";
   field_data.max_length = 200;
   field_data.is_autofilled = true;
diff --git a/components/autofill/core/common/form_field_data.cc b/components/autofill/core/common/form_field_data.cc
index e90fc8ae..b779dc3 100644
--- a/components/autofill/core/common/form_field_data.cc
+++ b/components/autofill/core/common/form_field_data.cc
@@ -14,6 +14,7 @@
 #include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/types/cxx23_to_underlying.h"
 #include "build/build_config.h"
 #include "components/autofill/core/common/autofill_features.h"
 #include "components/autofill/core/common/autofill_util.h"
@@ -92,13 +93,18 @@
 
 bool DeserializeSection1(base::PickleIterator* iter,
                          FormFieldData* field_data) {
-  return iter->ReadString16(&field_data->label) &&
-         iter->ReadString16(&field_data->name) &&
-         iter->ReadString16(&field_data->value) &&
-         iter->ReadString(&field_data->form_control_type) &&
-         iter->ReadString(&field_data->autocomplete_attribute) &&
-         iter->ReadUInt64(&field_data->max_length) &&
-         iter->ReadBool(&field_data->is_autofilled);
+  std::string form_control_type;
+  bool success = iter->ReadString16(&field_data->label) &&
+                 iter->ReadString16(&field_data->name) &&
+                 iter->ReadString16(&field_data->value) &&
+                 iter->ReadString(&form_control_type) &&
+                 iter->ReadString(&field_data->autocomplete_attribute) &&
+                 iter->ReadUInt64(&field_data->max_length) &&
+                 iter->ReadBool(&field_data->is_autofilled);
+  if (success) {
+    field_data->form_control_type = StringToFormControlType(form_control_type);
+  }
+  return success;
 }
 
 bool DeserializeSection5(base::PickleIterator* iter,
@@ -325,6 +331,10 @@
   return os << section.ToString();
 }
 
+LogBuffer& operator<<(LogBuffer& buffer, FormControlType type) {
+  return buffer << FormControlTypeToString(type);
+}
+
 FormFieldData::FormFieldData() = default;
 
 FormFieldData::FormFieldData(const FormFieldData&) = default;
@@ -342,22 +352,25 @@
 }
 
 bool FormFieldData::IsTextInputElement() const {
-  return form_control_type == "text" || form_control_type == "password" ||
-         form_control_type == "search" || form_control_type == "tel" ||
-         form_control_type == "url" || form_control_type == "email" ||
-         form_control_type == "number";
+  return form_control_type == StringToFormControlType("text") ||
+         form_control_type == StringToFormControlType("password") ||
+         form_control_type == StringToFormControlType("search") ||
+         form_control_type == StringToFormControlType("tel") ||
+         form_control_type == StringToFormControlType("url") ||
+         form_control_type == StringToFormControlType("email") ||
+         form_control_type == StringToFormControlType("number");
 }
 
 bool FormFieldData::IsPasswordInputElement() const {
-  return form_control_type == "password";
+  return form_control_type == StringToFormControlType("password");
 }
 
 bool FormFieldData::IsSelectElement() const {
-  return form_control_type == "select-one";
+  return form_control_type == StringToFormControlType("select-one");
 }
 
 bool FormFieldData::IsSelectListElement() const {
-  return form_control_type == "selectlist";
+  return form_control_type == StringToFormControlType("selectlist");
 }
 
 bool FormFieldData::IsSelectOrSelectListElement() const {
@@ -392,13 +405,98 @@
          IdentityTuple(a) == IdentityTuple(b);
 }
 
+std::string_view FormControlTypeToString(FormControlType type) {
+  switch (type) {
+    case FormControlType::kInputCheckbox:
+      return "checkbox";
+    case FormControlType::kInputEmail:
+      return "email";
+    case FormControlType::kInputMonth:
+      return "month";
+    case FormControlType::kInputNumber:
+      return "number";
+    case FormControlType::kInputPassword:
+      return "password";
+    case FormControlType::kInputRadio:
+      return "radio";
+    case FormControlType::kInputSearch:
+      return "search";
+    case FormControlType::kInputTelephone:
+      return "tel";
+    case FormControlType::kInputText:
+      return "text";
+    case FormControlType::kInputUrl:
+      return "url";
+    case FormControlType::kSelectOne:
+      return "select-one";
+    case FormControlType::kSelectMultiple:
+      return "select-multiple";
+    case FormControlType::kSelectList:
+      return "selectlist";
+    case FormControlType::kTextArea:
+      return "textarea";
+    case FormControlType::kEmpty:
+      return "";
+  }
+  return "invalid";
+}
+
+FormControlType StringToFormControlType(std::string_view type) {
+  if (type == "checkbox") {
+    return FormControlType::kInputCheckbox;
+  }
+  if (type == "email") {
+    return FormControlType::kInputEmail;
+  }
+  if (type == "month") {
+    return FormControlType::kInputMonth;
+  }
+  if (type == "number") {
+    return FormControlType::kInputNumber;
+  }
+  if (type == "password") {
+    return FormControlType::kInputPassword;
+  }
+  if (type == "radio") {
+    return FormControlType::kInputRadio;
+  }
+  if (type == "search") {
+    return FormControlType::kInputSearch;
+  }
+  if (type == "tel") {
+    return FormControlType::kInputTelephone;
+  }
+  if (type == "text") {
+    return FormControlType::kInputText;
+  }
+  if (type == "url") {
+    return FormControlType::kInputUrl;
+  }
+  if (type == "select-one") {
+    return FormControlType::kSelectOne;
+  }
+  if (type == "select-multiple") {
+    return FormControlType::kSelectMultiple;
+  }
+  if (type == "selectlist") {
+    return FormControlType::kSelectList;
+  }
+  if (type == "textarea") {
+    return FormControlType::kTextArea;
+  }
+  if (type == "") {
+    return FormControlType::kEmpty;
+  }
+  return FormControlType::kEmpty;
+}
+
 void SerializeFormFieldData(const FormFieldData& field_data,
                             base::Pickle* pickle) {
   pickle->WriteInt(kFormFieldDataPickleVersion);
   pickle->WriteString16(field_data.label);
   pickle->WriteString16(field_data.name);
   pickle->WriteString16(field_data.value);
-  pickle->WriteString(field_data.form_control_type);
+  pickle->WriteString(FormControlTypeToString(field_data.form_control_type));
   // We don't serialize the `parsed_autocomplete`. See http://crbug.com/1353392.
   pickle->WriteString(field_data.autocomplete_attribute);
   pickle->WriteUInt64(field_data.max_length);
diff --git a/components/autofill/core/common/form_field_data.h b/components/autofill/core/common/form_field_data.h
index 903c26c..c02648c 100644
--- a/components/autofill/core/common/form_field_data.h
+++ b/components/autofill/core/common/form_field_data.h
@@ -9,6 +9,7 @@
 
 #include <limits>
 #include <string>
+#include <string_view>
 #include <type_traits>
 #include <vector>
 
@@ -148,6 +149,10 @@
 LogBuffer& operator<<(LogBuffer& buffer, const Section& section);
 std::ostream& operator<<(std::ostream& os, const Section& section);
 
+using FormControlType = mojom::FormControlType;
+
+LogBuffer& operator<<(LogBuffer& buffer, FormControlType type);
+
 // Stores information about a field in a form. Read more about forms and fields
 // at FormData.
 struct FormFieldData {
@@ -266,7 +271,7 @@
   // substring of `value`.
   uint32_t selection_start = 0;
   uint32_t selection_end = 0;
-  std::string form_control_type;
+  FormControlType form_control_type = FormControlType::kEmpty;
   std::string autocomplete_attribute;
   absl::optional<AutocompleteParsingResult> parsed_autocomplete;
   std::u16string placeholder;
@@ -357,6 +362,14 @@
   bool force_override = false;
 };
 
+// TODO(crbug.com/1482526): Eliminate references to this function where
+// possible.
+std::string_view FormControlTypeToString(FormControlType type);
+
+// TODO(crbug.com/1482526): Eliminate references to this function where
+// possible.
+FormControlType StringToFormControlType(std::string_view type);
+
 // Serialize and deserialize FormFieldData. These are used when FormData objects
 // are serialized and deserialized.
 void SerializeFormFieldData(const FormFieldData& form_field_data,
diff --git a/components/autofill/core/common/form_field_data_unittest.cc b/components/autofill/core/common/form_field_data_unittest.cc
index 6c462fa..91b8a6f 100644
--- a/components/autofill/core/common/form_field_data_unittest.cc
+++ b/components/autofill/core/common/form_field_data_unittest.cc
@@ -18,7 +18,7 @@
   data->label = u"label";
   data->name = u"name";
   data->value = u"value";
-  data->form_control_type = "password";
+  data->form_control_type = StringToFormControlType("password");
   data->autocomplete_attribute = "off";
   data->max_length = 200;
   data->is_autofilled = true;
@@ -59,7 +59,7 @@
   pickle->WriteString16(data.label);
   pickle->WriteString16(data.name);
   pickle->WriteString16(data.value);
-  pickle->WriteString(data.form_control_type);
+  pickle->WriteString(FormControlTypeToString(data.form_control_type));
   pickle->WriteString(data.autocomplete_attribute);
   pickle->WriteUInt64(data.max_length);
   pickle->WriteBool(data.is_autofilled);
@@ -436,8 +436,10 @@
   };
 
   for (const auto& test_case : test_data) {
+    SCOPED_TRACE(testing::Message() << test_case.form_control_type);
     FormFieldData data;
-    data.form_control_type = test_case.form_control_type;
+    data.form_control_type =
+        StringToFormControlType(test_case.form_control_type);
     EXPECT_EQ(test_case.expected, data.IsTextInputElement());
   }
 }
diff --git a/components/autofill/core/common/mojom/autofill_types.mojom b/components/autofill/core/common/mojom/autofill_types.mojom
index 5d9f48ac..1fbfb47 100644
--- a/components/autofill/core/common/mojom/autofill_types.mojom
+++ b/components/autofill/core/common/mojom/autofill_types.mojom
@@ -11,6 +11,31 @@
 import "ui/gfx/geometry/mojom/geometry.mojom";
 import "url/mojom/url.mojom";
 
+// A subset of the WebFormControlElement::Type constants supported by Autofill.
+enum FormControlType {
+  // TODO(crbug.com/1482526): Remove kEmpty. Autofill's browser code currently
+  // assumes there's an empty or unknown form control type, which is not
+  // necessary:
+  // https://html.spec.whatwg.org/multipage/input.html#attr-input-type
+  // https://html.spec.whatwg.org/multipage/form-elements.html#dom-select-type
+  // https://html.spec.whatwg.org/multipage/form-elements.html#dom-textarea-type
+  kEmpty,
+  kInputCheckbox,
+  kInputEmail,
+  kInputMonth,
+  kInputNumber,
+  kInputPassword,
+  kInputRadio,
+  kInputSearch,
+  kInputTelephone,
+  kInputText,
+  kInputUrl,
+  kSelectOne,
+  kSelectMultiple,
+  kSelectList,
+  kTextArea,
+};
+
 // The list of all HTML autocomplete field mode hints supported by Chrome.
 // See [ http://is.gd/whatwg_autocomplete ] for the full list of specced hints.
 enum HtmlFieldMode {
@@ -330,7 +355,7 @@
   mojo_base.mojom.String16 value;
   uint32 selection_start;
   uint32 selection_end;
-  string form_control_type;
+  FormControlType form_control_type;
   string autocomplete_attribute;
   AutocompleteParsingResult? parsed_autocomplete;
   mojo_base.mojom.String16 placeholder;
diff --git a/components/autofill/core/common/mojom/autofill_types_mojom_traits.h b/components/autofill/core/common/mojom/autofill_types_mojom_traits.h
index 0e12cc0..104530e 100644
--- a/components/autofill/core/common/mojom/autofill_types_mojom_traits.h
+++ b/components/autofill/core/common/mojom/autofill_types_mojom_traits.h
@@ -217,7 +217,7 @@
     return r.selection_end;
   }
 
-  static const std::string& form_control_type(
+  static autofill::mojom::FormControlType form_control_type(
       const autofill::FormFieldData& r) {
     return r.form_control_type;
   }
diff --git a/components/autofill/core/common/mojom/autofill_types_mojom_traits_unittest.cc b/components/autofill/core/common/mojom/autofill_types_mojom_traits_unittest.cc
index 7c820b3..d113ecf 100644
--- a/components/autofill/core/common/mojom/autofill_types_mojom_traits_unittest.cc
+++ b/components/autofill/core/common/mojom/autofill_types_mojom_traits_unittest.cc
@@ -300,7 +300,7 @@
   input.value = u"value";
   input.selection_start = 1;
   input.selection_end = 2;
-  input.form_control_type = "text";
+  input.form_control_type = FormControlType::kInputText;
   input.autocomplete_attribute = "on";
   input.parsed_autocomplete =
       AutocompleteParsingResult{.section = "autocomplete_section",
diff --git a/components/autofill/core/common/save_password_progress_logger.cc b/components/autofill/core/common/save_password_progress_logger.cc
index 76601a0d..890039e 100644
--- a/components/autofill/core/common/save_password_progress_logger.cc
+++ b/components/autofill/core/common/save_password_progress_logger.cc
@@ -133,7 +133,9 @@
       "%s: signature=%s, type=%s, renderer_id=%s, %s, %s%s",
       ScrubElementID(field.name).c_str(),
       base::NumberToString(*CalculateFieldSignatureForField(field)).c_str(),
-      ScrubElementID(field.form_control_type).c_str(),
+      ScrubElementID(std::string(autofill::FormControlTypeToString(
+                         field.form_control_type)))
+          .c_str(),
       NumberToString(*field.unique_renderer_id).c_str(), is_visible, is_empty,
       autocomplete.c_str());
 }
diff --git a/components/autofill/core/common/signatures.cc b/components/autofill/core/common/signatures.cc
index 8453bfae..d5e76699 100644
--- a/components/autofill/core/common/signatures.cc
+++ b/components/autofill/core/common/signatures.cc
@@ -123,7 +123,7 @@
     if (!IsCheckable(field.check_status)) {
       // Add all supported form fields' form control types to the signature.
       base::StrAppend(&form_signature_field_types,
-                      {"&", field.form_control_type});
+                      {"&", FormControlTypeToString(field.form_control_type)});
     }
   }
 
@@ -157,8 +157,12 @@
 
 FieldSignature CalculateFieldSignatureForField(
     const FormFieldData& field_data) {
+  FormControlType type = field_data.form_control_type;
+  if (type == FormControlType::kEmpty) {
+    type = FormControlType::kInputText;
+  }
   return CalculateFieldSignatureByNameAndType(field_data.name,
-                                              field_data.form_control_type);
+                                              FormControlTypeToString(type));
 }
 
 uint64_t StrToHash64Bit(base::StringPiece str) {
diff --git a/components/autofill/core/common/signatures_unittest.cc b/components/autofill/core/common/signatures_unittest.cc
index 22b2509..cb1e3e0 100644
--- a/components/autofill/core/common/signatures_unittest.cc
+++ b/components/autofill/core/common/signatures_unittest.cc
@@ -16,12 +16,12 @@
   actual_form.name = u"form_name_12345";
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   field1.name = u"field_name_12345";
   actual_form.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   field2.name = u"field_name_1234";
   actual_form.fields.push_back(field2);
 
@@ -42,19 +42,19 @@
   large_form.url = GURL("http://foo.com/login?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   large_form.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   large_form.fields.push_back(field2);
 
   FormFieldData field3;
-  field3.form_control_type = "email";
+  field3.form_control_type = StringToFormControlType("email");
   large_form.fields.push_back(field3);
 
   FormFieldData field4;
-  field4.form_control_type = "tel";
+  field4.form_control_type = StringToFormControlType("tel");
   large_form.fields.push_back(field4);
 
   // Alternative form signature string of a form with more than two fields
@@ -68,11 +68,11 @@
   small_form_path.url = GURL("http://foo.com/login?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_path.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_path.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less should
@@ -86,11 +86,11 @@
   small_form_ref.url = GURL("http://foo.com?q=a#ref");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_ref.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_ref.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less and
@@ -105,11 +105,11 @@
   small_form_query.url = GURL("http://foo.com?q=a");
 
   FormFieldData field1;
-  field1.form_control_type = "text";
+  field1.form_control_type = StringToFormControlType("text");
   small_form_query.fields.push_back(field1);
 
   FormFieldData field2;
-  field2.form_control_type = "text";
+  field2.form_control_type = StringToFormControlType("text");
   small_form_query.fields.push_back(field2);
 
   // Alternative form signature string of a form with 2 fields or less and
diff --git a/components/autofill/ios/browser/autofill_agent_unittests.mm b/components/autofill/ios/browser/autofill_agent_unittests.mm
index 4f741a91..9174130 100644
--- a/components/autofill/ios/browser/autofill_agent_unittests.mm
+++ b/components/autofill/ios/browser/autofill_agent_unittests.mm
@@ -138,7 +138,7 @@
   form.unique_renderer_id = FormRendererId(1);
 
   autofill::FormFieldData field;
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   field.label = u"Card number";
   field.name = u"number";
   field.name_attribute = field.name;
diff --git a/components/autofill/ios/browser/autofill_util.mm b/components/autofill/ios/browser/autofill_util.mm
index c392d7b..15bd348 100644
--- a/components/autofill/ios/browser/autofill_util.mm
+++ b/components/autofill/ios/browser/autofill_util.mm
@@ -212,7 +212,8 @@
   }
 
   field_data->name = base::UTF8ToUTF16(*name);
-  field_data->form_control_type = *form_control_type;
+  field_data->form_control_type =
+      autofill::StringToFormControlType(*form_control_type);
 
   const std::string* unique_renderer_id =
       field.FindString("unique_renderer_id");
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingException.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingException.java
index 23e41bc..30276e1 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingException.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingException.java
@@ -22,12 +22,12 @@
     private final @ContentSettingsType int mContentSettingType;
     private final String mPrimaryPattern;
     private final String mSecondaryPattern;
-    private final @ContentSettingValues @Nullable Integer mContentSetting;
     // TODO(crbug.com/1344877): Convert {@link #mSource} to enum to enable merging {@link #mSource}
     // and {@link #mIsEmbargoed}.
     private final String mSource;
     private final Integer mExpirationInDays;
     private final boolean mIsEmbargoed;
+    private @ContentSettingValues @Nullable Integer mContentSetting;
 
     /**
      * Construct a ContentSettingException.
@@ -101,6 +101,7 @@
      */
     public void setContentSetting(
             BrowserContextHandle browserContextHandle, @ContentSettingValues int value) {
+        mContentSetting = value;
         WebsitePreferenceBridge.setContentSettingCustomScope(browserContextHandle,
                 mContentSettingType, mPrimaryPattern, getSecondaryPatternSafe(), value);
     }
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingsResources.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingsResources.java
index 4cd8246d..5dcf09f3 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingsResources.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/ContentSettingsResources.java
@@ -288,6 +288,11 @@
                         R.string.website_settings_category_sound_allowed,
                         R.string.website_settings_category_sound_blocked);
 
+            case ContentSettingsType.STORAGE_ACCESS:
+                return new ResourceItem(R.drawable.ic_storage_access_24,
+                        R.string.storage_access_permission_title, ContentSettingValues.ASK,
+                        ContentSettingValues.BLOCK, 0, 0);
+
             case ContentSettingsType.USB_CHOOSER_DATA:
                 return new ResourceItem(R.drawable.gm_filled_usb_24, 0, ContentSettingValues.ASK,
                         ContentSettingValues.BLOCK, 0, 0);
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/PermissionInfo.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/PermissionInfo.java
index bf1276a..324e701 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/PermissionInfo.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/PermissionInfo.java
@@ -21,17 +21,12 @@
     private final String mOrigin;
     private final @ContentSettingsType int mContentSettingsType;
 
-    public PermissionInfo(@ContentSettingsType int type, String origin, String embedder) {
-        this(type, origin, embedder, false);
-    }
-
     public PermissionInfo(
             @ContentSettingsType int type, String origin, String embedder, boolean isEmbargoed) {
         assert WebsitePermissionsFetcher.getPermissionsType(type)
-                        == WebsitePermissionsFetcher.WebsitePermissionsType.PERMISSION_INFO
-                || WebsitePermissionsFetcher.getPermissionsType(type)
-                        == WebsitePermissionsFetcher.WebsitePermissionsType.EMBEDDED_PERMISSION_INFO
-            : "invalid type: " + type;
+                == WebsitePermissionsFetcher.WebsitePermissionsType.PERMISSION_INFO
+            : "invalid type: "
+                        + type;
         mOrigin = origin;
         mEmbedder = embedder;
         mContentSettingsType = type;
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
index 38243d6c..10610382 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SingleWebsiteSettings.java
@@ -43,6 +43,7 @@
 
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -204,9 +205,13 @@
     // The Preference key for chooser object permissions.
     private static final String CHOOSER_PERMISSION_PREFERENCE_KEY = "chooser_permission_list";
 
+    // The Preference key for embedded permissions such as StorageAccess.
+    private static final String EMBEDDED_PERMISSION_PREFERENCE_KEY = "embedded_permission_list";
+
     // The number of user and policy chosen object permissions displayed.
     private int mObjectUserPermissionCount;
     private int mObjectPolicyPermissionCount;
+    private int mEmbeddedPermissionCount;
 
     // Records previous notification permission on Android O+ to allow detection of permission
     // revocation within the Android system permission activity.
@@ -423,13 +428,13 @@
                     merged.setPermissionInfo(info);
                 }
             }
-            for (var infoList : other.getEmbeddedPermissionInfos().values()) {
-                for (var info : infoList) {
+            for (var exceptionList : other.getEmbeddedPermissions().values()) {
+                for (var exception : exceptionList) {
                     boolean matchesOrigin = other.getEmbedder() != null
                             && WebsitePreferenceBridgeJni.get().urlMatchesContentSettingsPattern(
-                                    origin, info.getEmbedder());
+                                    origin, exception.getSecondaryPattern());
                     if (matchesOrigin) {
-                        merged.addEmbeddedPermissionInfo(info);
+                        merged.addEmbeddedPermission(exception);
                     }
                 }
             }
@@ -501,6 +506,7 @@
 
         findPreference(PREF_SITE_TITLE).setTitle(mSite.getTitle());
         setupContentSettingsPreferences();
+        setUpEmbeddedContentSettingPreferences();
         setUpChosenObjectPreferences();
         setupResetSitePreference();
         setUpClearDataPreference();
@@ -837,6 +843,40 @@
         }
     }
 
+    private void setUpEmbeddedContentSettingPreferences() {
+        PreferenceScreen preferenceScreen = getPreferenceScreen();
+        BrowserContextHandle handle = getSiteSettingsDelegate().getBrowserContextHandle();
+
+        for (List<ContentSettingException> entries : mSite.getEmbeddedPermissions().values()) {
+            for (ContentSettingException info : entries) {
+                var preference = new ChromeSwitchPreference(getStyledContext());
+                preference.setKey(EMBEDDED_PERMISSION_PREFERENCE_KEY);
+                preference.setIcon(getContentSettingsIcon(info.getContentSettingType(), null));
+                preference.setTitle(ContentSettingsResources.getTitle(
+                        info.getContentSettingType(), getSiteSettingsDelegate()));
+                var pattern = WebsiteAddress.create(info.getPrimaryPattern());
+                preference.setSummary(pattern.getHost());
+                preference.setChecked(info.getContentSetting() == ContentSettingValues.ALLOW);
+                preference.setOnPreferenceChangeListener((pref, newValue) -> {
+                    info.setContentSetting(handle,
+                            (boolean) newValue ? ContentSettingValues.ALLOW
+                                               : ContentSettingValues.BLOCK);
+                    if (mWebsiteSettingsObserver != null) {
+                        mWebsiteSettingsObserver.onPermissionChanged();
+                    }
+                    return true;
+                });
+                if (info.getContentSettingType() == mHighlightedPermission) {
+                    preference.setBackgroundColor(mHighlightColor);
+                }
+
+                mEmbeddedPermissionCount++;
+                preference.setOrder(++mMaxPermissionOrder);
+                preferenceScreen.addPreference(preference);
+            }
+        }
+    }
+
     private Context getStyledContext() {
         return getPreferenceManager().getContext();
     }
@@ -954,7 +994,10 @@
     }
 
     private boolean hasPermissionsPreferences() {
-        if (mObjectUserPermissionCount > 0 || mObjectPolicyPermissionCount > 0) return true;
+        if (mObjectUserPermissionCount > 0 || mObjectPolicyPermissionCount > 0
+                || mEmbeddedPermissionCount > 0) {
+            return true;
+        }
         PreferenceScreen preferenceScreen = getPreferenceScreen();
         for (int i = 0; i < preferenceScreen.getPreferenceCount(); i++) {
             String key = preferenceScreen.getPreference(i).getKey();
@@ -1201,13 +1244,18 @@
                 removePreferenceSafely(key);
             }
         }
+        var preference = findPreference(EMBEDDED_PERMISSION_PREFERENCE_KEY);
+        while (preference != null) {
+            getPreferenceScreen().removePreference(preference);
+            preference = findPreference(EMBEDDED_PERMISSION_PREFERENCE_KEY);
+        }
 
         // Clearing stored data implies popping back to parent menu if there is nothing left to
         // show. Therefore, we only need to explicitly close the activity if there's no stored data
         // to begin with. The only exception to this is if there are policy managed permissions as
         // those cannot be reset and will always show.
-        boolean finishActivityImmediately =
-                mSite.getTotalUsage() == 0 && mObjectPolicyPermissionCount == 0;
+        boolean finishActivityImmediately = mSite.getTotalUsage() == 0
+                && mObjectPolicyPermissionCount == 0 && mEmbeddedPermissionCount == 0;
 
         SiteDataCleaner.resetPermissions(
                 getSiteSettingsDelegate().getBrowserContextHandle(), mSite);
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SiteDataCleaner.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SiteDataCleaner.java
index 4725b62..1297a48 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SiteDataCleaner.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/SiteDataCleaner.java
@@ -64,6 +64,12 @@
         for (ChosenObjectInfo info : site.getChosenObjectInfo()) {
             info.revoke(browserContextHandle);
         }
+
+        for (var exceptions : site.getEmbeddedPermissions().values()) {
+            for (var exception : exceptions) {
+                exception.setContentSetting(browserContextHandle, ContentSettingValues.DEFAULT);
+            }
+        }
     }
 
     /**
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/Website.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/Website.java
index 7b3485d..a3ab86f 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/Website.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/Website.java
@@ -44,7 +44,7 @@
      * Indexed by ContentSettingsType. For Permissions like the StorageAccess API that are keyed by
      * requesting and embedding site.
      */
-    private Map<Integer, List<PermissionInfo>> mEmbeddedPermissionInfos = new HashMap<>();
+    private Map<Integer, List<ContentSettingException>> mEmbeddedPermissionInfos = new HashMap<>();
 
     private LocalStorageInfo mLocalStorageInfo;
     private FPSCookieInfo mFPSCookieInfo;
@@ -166,15 +166,14 @@
         mPermissionInfos.put(info.getContentSettingsType(), info);
     }
 
-    public Map<Integer, List<PermissionInfo>> getEmbeddedPermissionInfos() {
+    public Map<Integer, List<ContentSettingException>> getEmbeddedPermissions() {
         return mEmbeddedPermissionInfos;
     }
 
-    public void addEmbeddedPermissionInfo(PermissionInfo info) {
-        assert info.getEmbedder() != null;
-        assert !info.getEmbedder().equals("*");
+    public void addEmbeddedPermission(ContentSettingException info) {
+        assert !info.getSecondaryPattern().equals("*");
         var list = mEmbeddedPermissionInfos.computeIfAbsent(
-                info.getContentSettingsType(), k -> new ArrayList<>());
+                info.getContentSettingType(), k -> new ArrayList<>());
         list.add(info);
     }
 
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsiteAddress.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsiteAddress.java
index 49884a1..aaa85cd 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsiteAddress.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsiteAddress.java
@@ -64,7 +64,7 @@
         }
 
         // Origin
-        if (originOrHostOrPattern.indexOf(SCHEME_SUFFIX) != -1) {
+        if (originOrHostOrPattern.contains(SCHEME_SUFFIX)) {
             Uri uri = Uri.parse(originOrHostOrPattern);
             String origin = trimTrailingBackslash(originOrHostOrPattern);
             boolean omitProtocolAndPort = UrlConstants.HTTP_SCHEME.equals(uri.getScheme())
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePermissionsFetcher.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePermissionsFetcher.java
index 9e9ba97..45f342d 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePermissionsFetcher.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePermissionsFetcher.java
@@ -39,7 +39,7 @@
     public enum WebsitePermissionsType {
         CONTENT_SETTING_EXCEPTION,
         PERMISSION_INFO,
-        EMBEDDED_PERMISSION_INFO,
+        EMBEDDED_PERMISSION,
         CHOSEN_OBJECT_INFO
     }
 
@@ -99,7 +99,7 @@
             case ContentSettingsType.STORAGE_ACCESS:
                 if (PermissionsAndroidFeatureMap.isEnabled(
                             PermissionsAndroidFeatureList.PERMISSION_STORAGE_ACCESS)) {
-                    return WebsitePermissionsType.EMBEDDED_PERMISSION_INFO;
+                    return WebsitePermissionsType.EMBEDDED_PERMISSION;
                 }
                 return null;
             case ContentSettingsType.BLUETOOTH_GUARD:
@@ -332,10 +332,10 @@
                     queue.add(new ExceptionInfoFetcher(contentSettingsType));
                     return;
                 case PERMISSION_INFO:
-                    queue.add(new PermissionInfoFetcher(contentSettingsType, false));
+                    queue.add(new PermissionInfoFetcher(contentSettingsType));
                     return;
-                case EMBEDDED_PERMISSION_INFO:
-                    queue.add(new PermissionInfoFetcher(contentSettingsType, true));
+                case EMBEDDED_PERMISSION:
+                    queue.add(new ExceptionInfoFetcher(contentSettingsType));
                     return;
                 case CHOSEN_OBJECT_INFO:
                     queue.add(new ChooserExceptionInfoFetcher(contentSettingsType));
@@ -378,6 +378,8 @@
         }
 
         private void setException(int contentSettingsType) {
+            boolean isEmbeddedPermission = getPermissionsType(contentSettingsType)
+                    == WebsitePermissionsType.EMBEDDED_PERMISSION;
             for (ContentSettingException exception :
                     mWebsitePreferenceBridge.getContentSettingsExceptions(
                             mBrowserContextHandle, contentSettingsType)) {
@@ -393,7 +395,11 @@
                         ? address
                         : WebsiteAddress.create(address).getOrigin();
                 Website site = findOrCreateSite(origin, embedder);
-                site.setContentSettingException(contentSettingsType, exception);
+                if (isEmbeddedPermission) {
+                    site.addEmbeddedPermission(exception);
+                } else {
+                    site.setContentSettingException(contentSettingsType, exception);
+                }
             }
         }
 
@@ -430,10 +436,8 @@
             final @ContentSettingsType int mType;
             private boolean mIsEmbeddedPermission;
 
-            public PermissionInfoFetcher(
-                    @ContentSettingsType int type, boolean isEmbeddedPermission) {
+            public PermissionInfoFetcher(@ContentSettingsType int type) {
                 mType = type;
-                mIsEmbeddedPermission = isEmbeddedPermission;
             }
 
             @Override
@@ -445,11 +449,7 @@
                     String embedder =
                             mType == ContentSettingsType.SENSORS ? null : info.getEmbedder();
                     Website site = findOrCreateSite(origin, embedder);
-                    if (mIsEmbeddedPermission) {
-                        site.addEmbeddedPermissionInfo(info);
-                    } else {
-                        site.setPermissionInfo(info);
-                    }
+                    site.setPermissionInfo(info);
                 }
             }
         }
diff --git a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePreferenceBridge.java b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePreferenceBridge.java
index 791e1a5..b7543ea 100644
--- a/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePreferenceBridge.java
+++ b/components/browser_ui/site_settings/android/java/src/org/chromium/components/browser_ui/site_settings/WebsitePreferenceBridge.java
@@ -379,13 +379,17 @@
     public static void setContentSettingCustomScope(BrowserContextHandle browserContextHandle,
             @ContentSettingsType int contentSettingType, String primaryPattern,
             String secondaryPattern, @ContentSettingValues int setting) {
-        // Currently only Cookie Settings support a non-empty, non-wildcard secondaryPattern.
-        // In addition, if a Cookie Setting uses secondaryPattern, the primaryPattern must be
-        // the wildcard.
-        if (contentSettingType != ContentSettingsType.COOKIES) {
-            assert secondaryPattern.equals(SITE_WILDCARD) || secondaryPattern.isEmpty();
-        } else if (!secondaryPattern.equals(SITE_WILDCARD) && !secondaryPattern.isEmpty()) {
+        if (contentSettingType == ContentSettingsType.STORAGE_ACCESS) {
+            // StorageAccess exceptions should always specify a primary and a secondary pattern.
+            assert !primaryPattern.equals(SITE_WILDCARD) && !secondaryPattern.equals(SITE_WILDCARD);
+        } else if (contentSettingType == ContentSettingsType.COOKIES
+                && !secondaryPattern.equals(SITE_WILDCARD)) {
+            // Currently only Cookie Settings support a non-empty, non-wildcard secondaryPattern.
+            // In addition, if a Cookie Setting uses secondaryPattern, the primaryPattern must be
+            // the wildcard.
             assert primaryPattern.equals(SITE_WILDCARD);
+        } else {
+            assert secondaryPattern.equals(SITE_WILDCARD) || secondaryPattern.isEmpty();
         }
 
         WebsitePreferenceBridgeJni.get().setContentSettingCustomScope(browserContextHandle,
diff --git a/components/browser_ui/strings/android/site_settings.grdp b/components/browser_ui/strings/android/site_settings.grdp
index d094c00..79667881 100644
--- a/components/browser_ui/strings/android/site_settings.grdp
+++ b/components/browser_ui/strings/android/site_settings.grdp
@@ -112,6 +112,9 @@
   <message name="IDS_DESKTOP_SITE_TITLE" desc="Title of the permission to request the desktop view of a site by default [CHAR_LIMIT=32]">
     Desktop site
   </message>
+  <message name="IDS_STORAGE_ACCESS_PERMISSION_TITLE" desc="Title of the permission to allow embedded content to access cookies and site data [CHAR_LIMIT=32]">
+    Embedded content
+  </message>
 
   <!-- Site settings global toggles -->
 
diff --git a/components/browser_ui/strings/android/site_settings_grdp/IDS_STORAGE_ACCESS_PERMISSION_TITLE.png.sha1 b/components/browser_ui/strings/android/site_settings_grdp/IDS_STORAGE_ACCESS_PERMISSION_TITLE.png.sha1
new file mode 100644
index 0000000..2207b74
--- /dev/null
+++ b/components/browser_ui/strings/android/site_settings_grdp/IDS_STORAGE_ACCESS_PERMISSION_TITLE.png.sha1
@@ -0,0 +1 @@
+6625fe55c5336a912a673001ee4a592e94eb31a8
\ No newline at end of file
diff --git a/components/browsing_data/core/counters/passwords_counter.cc b/components/browsing_data/core/counters/passwords_counter.cc
index 32fc331..55519bc6 100644
--- a/components/browsing_data/core/counters/passwords_counter.cc
+++ b/components/browsing_data/core/counters/passwords_counter.cc
@@ -12,9 +12,10 @@
 #include "base/scoped_observation.h"
 #include "base/time/time.h"
 #include "components/browsing_data/core/pref_names.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
 #include "components/password_manager/core/browser/password_store_change.h"
 #include "components/password_manager/core/browser/password_store_interface.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/sync/service/sync_service.h"
 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
 #include "url/gurl.h"
@@ -25,7 +26,7 @@
 bool IsPasswordSyncEnabled(const syncer::SyncService* sync_service) {
   if (!sync_service)
     return false;
-  switch (password_manager_util::GetPasswordSyncState(sync_service)) {
+  switch (password_manager::sync_util::GetPasswordSyncState(sync_service)) {
     case password_manager::SyncState::kNotSyncing:
     case password_manager::SyncState::kAccountPasswordsActiveNormalEncryption:
     case password_manager::SyncState::
diff --git a/components/cast_streaming/browser/control/remoting/remoting_decoder_buffer_factory.cc b/components/cast_streaming/browser/control/remoting/remoting_decoder_buffer_factory.cc
index 41cfa7f..b31c44b 100644
--- a/components/cast_streaming/browser/control/remoting/remoting_decoder_buffer_factory.cc
+++ b/components/cast_streaming/browser/control/remoting/remoting_decoder_buffer_factory.cc
@@ -23,7 +23,7 @@
     FrameContents& frame_contents) {
   auto span = frame_contents.Get();
   scoped_refptr<media::DecoderBuffer> decoder_buffer =
-      media::cast::ByteArrayToDecoderBuffer(span.data(), span.size());
+      media::cast::ByteArrayToDecoderBuffer(span);
   if (!decoder_buffer) {
     DLOG(WARNING) << "Deserialization failed!";
     return nullptr;
diff --git a/components/certificate_transparency/chrome_require_ct_delegate.cc b/components/certificate_transparency/chrome_require_ct_delegate.cc
index dc64bc5..d41f58d 100644
--- a/components/certificate_transparency/chrome_require_ct_delegate.cc
+++ b/components/certificate_transparency/chrome_require_ct_delegate.cc
@@ -175,25 +175,16 @@
     const std::string& hostname,
     const net::X509Certificate* chain,
     const net::HashValueVector& spki_hashes) {
-  bool ct_required = false;
-  if (MatchHostname(hostname, &ct_required) ||
-      MatchSPKI(chain, spki_hashes, &ct_required)) {
-    return ct_required ? CTRequirementLevel::REQUIRED
-                       : CTRequirementLevel::NOT_REQUIRED;
+  if (MatchHostname(hostname) || MatchSPKI(chain, spki_hashes)) {
+    return CTRequirementLevel::NOT_REQUIRED;
   }
 
-  // Compute >= 2018-05-01, rather than deal with possible fractional
-  // seconds.
-  const base::Time kMay_1_2018 =
-      base::Time::UnixEpoch() + base::Seconds(1525132800);
-  if (chain->valid_start() >= kMay_1_2018)
-    return CTRequirementLevel::REQUIRED;
-
-  return CTRequirementLevel::DEFAULT;
+  // CT is required since 2018-05-01, and no certificate issued before that
+  // date could be valid anymore, so CT is unconditionally required.
+  return CTRequirementLevel::REQUIRED;
 }
 
 void ChromeRequireCTDelegate::UpdateCTPolicies(
-    const std::vector<std::string>& required_hosts,
     const std::vector<std::string>& excluded_hosts,
     const std::vector<std::string>& excluded_spkis,
     const std::vector<std::string>& excluded_legacy_spkis) {
@@ -202,8 +193,7 @@
   next_id_ = 0;
 
   url_matcher::URLMatcherConditionSet::Vector all_conditions;
-  AddFilters(true, required_hosts, &all_conditions);
-  AddFilters(false, excluded_hosts, &all_conditions);
+  AddFilters(excluded_hosts, &all_conditions);
 
   url_matcher_->AddConditionSets(all_conditions);
 
@@ -220,8 +210,7 @@
   });
 }
 
-bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname,
-                                            bool* ct_required) const {
+bool ChromeRequireCTDelegate::MatchHostname(const std::string& hostname) const {
   if (url_matcher_->IsEmpty())
     return false;
 
@@ -234,38 +223,18 @@
   if (matching_ids.empty())
     return false;
 
-  // Determine the overall policy by determining the most specific policy.
-  auto it = filters_.begin();
-  const Filter* active_filter = nullptr;
-  for (const auto& match : matching_ids) {
-    // Because both |filters_| and |matching_ids| are sorted on the ID,
-    // treat both as forward-only iterators.
-    while (it != filters_.end() && it->first < match)
-      ++it;
-    if (it == filters_.end()) {
-      NOTREACHED();
-      break;
-    }
-
-    if (!active_filter || FilterTakesPrecedence(it->second, *active_filter))
-      active_filter = &it->second;
-  }
-  CHECK(active_filter);
-
-  *ct_required = active_filter->ct_required;
   return true;
 }
 
-bool ChromeRequireCTDelegate::MatchSPKI(const net::X509Certificate* chain,
-                                        const net::HashValueVector& hashes,
-                                        bool* ct_required) const {
+bool ChromeRequireCTDelegate::MatchSPKI(
+    const net::X509Certificate* chain,
+    const net::HashValueVector& hashes) const {
   // Try to scan legacy SPKIs first, if any, since they will only require
   // comparing hash values.
   if (!legacy_spkis_.empty()) {
     for (const auto& hash : hashes) {
       if (std::binary_search(legacy_spkis_.begin(), legacy_spkis_.end(),
                              hash)) {
-        *ct_required = false;
         return true;
       }
     }
@@ -295,7 +264,6 @@
   net::HashValue hash;
   if (net::x509_util::CalculateSha256SpkiHash(leaf_cert, &hash) &&
       base::Contains(matches, hash)) {
-    *ct_required = false;
     return true;
   }
 
@@ -326,7 +294,6 @@
 
   for (auto* cert : candidates) {
     if (AreCertsSameOrganization(leaf_rdn_sequence, cert)) {
-      *ct_required = false;
       return true;
     }
   }
@@ -335,12 +302,10 @@
 }
 
 void ChromeRequireCTDelegate::AddFilters(
-    bool ct_required,
     const std::vector<std::string>& hosts,
     url_matcher::URLMatcherConditionSet::Vector* conditions) {
   for (const auto& pattern : hosts) {
     Filter filter;
-    filter.ct_required = ct_required;
 
     // Parse the pattern just to the hostname, ignoring all other portions of
     // the URL.
@@ -409,18 +374,4 @@
   std::sort(hashes->begin(), hashes->end());
 }
 
-bool ChromeRequireCTDelegate::FilterTakesPrecedence(const Filter& lhs,
-                                                    const Filter& rhs) const {
-  if (lhs.match_subdomains != rhs.match_subdomains)
-    return !lhs.match_subdomains;  // Prefer the more explicit policy.
-
-  if (lhs.host_length != rhs.host_length)
-    return lhs.host_length > rhs.host_length;  // Prefer the longer host match.
-
-  if (lhs.ct_required != rhs.ct_required)
-    return lhs.ct_required;  // Prefer the policy that requires CT.
-
-  return false;
-}
-
 }  // namespace certificate_transparency
diff --git a/components/certificate_transparency/chrome_require_ct_delegate.h b/components/certificate_transparency/chrome_require_ct_delegate.h
index 665ed37..830c847a 100644
--- a/components/certificate_transparency/chrome_require_ct_delegate.h
+++ b/components/certificate_transparency/chrome_require_ct_delegate.h
@@ -48,40 +48,32 @@
       const net::X509Certificate* chain,
       const net::HashValueVector& spki_hashes) override;
 
-  // Updates the CTDelegate to require CT for |required_hosts|, and exclude
-  // |excluded_hosts| from CT policies.  In addtion, this method updates
-  // |excluded_spkis| and |excluded_legacy_spkis| intended for use within an
-  // Enterprise (see https://crbug.com/824184).
-  void UpdateCTPolicies(const std::vector<std::string>& required_hosts,
-                        const std::vector<std::string>& excluded_hosts,
+  // Updates the CTDelegate to exclude |excluded_hosts| from CT policies.  In
+  // addition, this method updates |excluded_spkis| and |excluded_legacy_spkis|
+  // intended for use within an Enterprise (see https://crbug.com/824184).
+  void UpdateCTPolicies(const std::vector<std::string>& excluded_hosts,
                         const std::vector<std::string>& excluded_spkis,
                         const std::vector<std::string>& excluded_legacy_spkis);
 
  private:
   struct Filter {
-    bool ct_required = false;
     bool match_subdomains = false;
     size_t host_length = 0;
   };
 
-  // Returns true if a policy for |hostname| is found, setting
-  // |*ct_required| to indicate whether or not Certificate Transparency is
-  // required for the host.
-  bool MatchHostname(const std::string& hostname, bool* ct_required) const;
+  // Returns true if a policy to disable Certificate Transparency for |hostname|
+  // is found.
+  bool MatchHostname(const std::string& hostname) const;
 
-  // Returns true if a policy for |chain|, which contains the SPKI hashes
-  // |hashes|, is found, setting |*ct_required| to indicate whether or not
-  // Certificate Transparency is required for the certificate.
+  // Returns true if a policy to disable Certificate Transparency for |chain|,
+  // which contains the SPKI hashes |hashes|, is found.
   bool MatchSPKI(const net::X509Certificate* chain,
-                 const net::HashValueVector& hashes,
-                 bool* ct_required) const;
+                 const net::HashValueVector& hashes) const;
 
   // Parses the filters from |host_patterns|, adding them as filters to
-  // |filters_| (with |ct_required| indicating whether or not CT is required
-  // for that host), and updating |*conditions| with the corresponding
+  // |filters_|, and updating |*conditions| with the corresponding
   // URLMatcher::Conditions to match the host.
-  void AddFilters(bool ct_required,
-                  const std::vector<std::string>& host_patterns,
+  void AddFilters(const std::vector<std::string>& host_patterns,
                   url_matcher::URLMatcherConditionSet::Vector* conditions);
 
   // Parses the SPKIs from |spki_list|, setting |*hashes| to the sorted set of
@@ -89,11 +81,6 @@
   void ParseSpkiHashes(const std::vector<std::string> spki_list,
                        net::HashValueVector* hashes) const;
 
-  // Returns true if |lhs| has greater precedence than |rhs|. Filters of
-  // higher precedence are consulted first when determining if a given filter
-  // matches.
-  bool FilterTakesPrecedence(const Filter& lhs, const Filter& rhs) const;
-
   std::unique_ptr<url_matcher::URLMatcher> url_matcher_;
   base::MatcherStringPattern::ID next_id_;
   std::map<base::MatcherStringPattern::ID, Filter> filters_;
diff --git a/components/certificate_transparency/chrome_require_ct_delegate_unittest.cc b/components/certificate_transparency/chrome_require_ct_delegate_unittest.cc
index 91f40b8..1d485df9 100644
--- a/components/certificate_transparency/chrome_require_ct_delegate_unittest.cc
+++ b/components/certificate_transparency/chrome_require_ct_delegate_unittest.cc
@@ -30,13 +30,15 @@
 class ChromeRequireCTDelegateTest : public ::testing::Test {
  public:
   void SetUp() override {
-    // Use a certificate with a notBefore prior to May 2018, so that CT is not
-    // implicitly required.
     cert_ = net::CreateCertificateChainFromFile(
         net::GetTestCertsDirectory(), "expired_cert.pem",
         net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE);
     ASSERT_TRUE(cert_);
-    hashes_.push_back(net::HashValue(net::SHA256HashValue{0}));
+
+    net::HashValue spki_hash;
+    ASSERT_TRUE(net::x509_util::CalculateSha256SpkiHash(cert_->cert_buffer(),
+                                                        &spki_hash));
+    hashes_.push_back(spki_hash);
   }
 
  protected:
@@ -58,34 +60,34 @@
   EXPECT_NE(registered_prefs, newly_registered_prefs);
 }
 
-TEST_F(ChromeRequireCTDelegateTest, DelegateChecksRequired) {
-  using CTRequirementLevel =
-      net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
-  ChromeRequireCTDelegate delegate;
-
-  // No required host set yet.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
-
-  // Add a required host
-  delegate.UpdateCTPolicies({"google.com"}, {}, {}, {});
-
-  // The new setting should take effect.
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
-            delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
-}
-
-TEST_F(ChromeRequireCTDelegateTest, DelegateChecksExcluded) {
+TEST_F(ChromeRequireCTDelegateTest, DelegateChecksExcludedHosts) {
   using CTRequirementLevel =
       net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
   ChromeRequireCTDelegate delegate;
 
   // No setting should yield the default results.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
 
   // Add a excluded host
-  delegate.UpdateCTPolicies({}, {"google.com"}, {}, {});
+  delegate.UpdateCTPolicies({"google.com"}, {}, {});
+
+  // The new setting should take effect.
+  EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
+            delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
+}
+
+TEST_F(ChromeRequireCTDelegateTest, DelegateChecksExcludedSPKIs) {
+  using CTRequirementLevel =
+      net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
+  ChromeRequireCTDelegate delegate;
+
+  // No setting should yield the default results.
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
+            delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
+
+  // Add a excluded SPKI
+  delegate.UpdateCTPolicies({}, {hashes_.front().ToString()}, {});
 
   // The new setting should take effect.
   EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
@@ -98,7 +100,7 @@
   ChromeRequireCTDelegate delegate;
 
   // No setting should yield the default results.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
 
   // Now setup invalid state (that is, that fail to be parsable as
@@ -106,10 +108,10 @@
   delegate.UpdateCTPolicies(
       {"file:///etc/fstab", "file://withahost/etc/fstab", "file:///c|/Windows",
        "*", "https://*", "example.com", "https://example.test:invalid_port"},
-      {}, {}, {});
+      {}, {});
 
   // Wildcards are ignored (both * and https://*).
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
   // File URL hosts are ignored.
   // TODO(rsleevi): https://crbug.com/841407 - Ensure that file URLs have their
@@ -118,77 +120,10 @@
   //          delegate.IsCTRequiredForHost("withahost", cert_.get(), hashes_));
 
   // While the partially parsed hosts should take effect.
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
+  EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
             delegate.IsCTRequiredForHost("example.test", cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
-            delegate.IsCTRequiredForHost("example.com", cert_.get(), hashes_));
-}
-
-// Make sure the various 'undocumented' priorities apply:
-//   - non-wildcards beat wildcards
-//   - more specific hosts beat less specific hosts
-//   - requiring beats excluding
-TEST_F(ChromeRequireCTDelegateTest, AppliesPriority) {
-  using CTRequirementLevel =
-      net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
-  ChromeRequireCTDelegate delegate;
-
-  // No setting should yield the default results.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("example.com", cert_.get(), hashes_));
-  EXPECT_EQ(
-      CTRequirementLevel::DEFAULT,
-      delegate.IsCTRequiredForHost("sub.example.com", cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("accounts.example.com", cert_.get(),
-                                         hashes_));
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("login.accounts.example.com",
-                                         cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("sub.accounts.example.com",
-                                         cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("login.sub.accounts.example.com",
-                                         cert_.get(), hashes_));
-  EXPECT_EQ(
-      CTRequirementLevel::DEFAULT,
-      delegate.IsCTRequiredForHost("test.example.com", cert_.get(), hashes_));
-
-  // Set up policies that exclude it for a domain and all of its subdomains,
-  // but then require it for a specific host.
-  delegate.UpdateCTPolicies(
-      {"sub.example.com", "accounts.example.com", "test.example.com"},
-      {"example.com", ".sub.example.com", ".sub.accounts.example.com",
-       "test.example.com"},
-      {}, {});
-
   EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
             delegate.IsCTRequiredForHost("example.com", cert_.get(), hashes_));
-  // Non-wildcarding (.sub.example.com) beats wildcarding (sub.example.com).
-  EXPECT_EQ(
-      CTRequirementLevel::NOT_REQUIRED,
-      delegate.IsCTRequiredForHost("sub.example.com", cert_.get(), hashes_));
-  // More specific hosts (accounts.example.com) beat less specific hosts
-  // (example.com + wildcard).
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
-            delegate.IsCTRequiredForHost("accounts.example.com", cert_.get(),
-                                         hashes_));
-  // More specific hosts (accounts.example.com) beat less specific hosts
-  // (example.com).
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
-            delegate.IsCTRequiredForHost("login.accounts.example.com",
-                                         cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
-            delegate.IsCTRequiredForHost("sub.accounts.example.com",
-                                         cert_.get(), hashes_));
-  EXPECT_EQ(CTRequirementLevel::REQUIRED,
-            delegate.IsCTRequiredForHost("login.sub.accounts.example.com",
-                                         cert_.get(), hashes_));
-  // Requiring beats excluding.
-  EXPECT_EQ(
-      CTRequirementLevel::REQUIRED,
-      delegate.IsCTRequiredForHost("test.example.com", cert_.get(), hashes_));
 }
 
 TEST_F(ChromeRequireCTDelegateTest, SupportsOrgRestrictions) {
@@ -310,13 +245,13 @@
       leaf = net::X509Certificate::CreateFromBuffer(
           bssl::UpRef(leaf->cert_buffer()), std::move(intermediates));
     }
-    delegate.UpdateCTPolicies({}, {}, {}, {});
+    delegate.UpdateCTPolicies({}, {}, {});
 
     // The default setting should require CT.
     EXPECT_EQ(CTRequirementLevel::REQUIRED,
               delegate.IsCTRequiredForHost("google.com", leaf.get(), hashes));
 
-    delegate.UpdateCTPolicies({}, {}, {test.spki.ToString()}, {});
+    delegate.UpdateCTPolicies({}, {test.spki.ToString()}, {});
 
     // The new setting should take effect.
     EXPECT_EQ(test.expected,
@@ -339,98 +274,25 @@
   hashes_.push_back(net::HashValue(legacy_spki));
 
   // No setting should yield the default results.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
 
   // Setting to a non-legacy CA should not work.
   std::string leaf_hash_string = hashes_.front().ToString();
-  delegate.UpdateCTPolicies({}, {}, {}, {leaf_hash_string});
+  delegate.UpdateCTPolicies({}, {}, {leaf_hash_string});
 
   // This setting should have no effect, because the hash for |cert_|
   // is not a legacy CA hash.
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
+  EXPECT_EQ(CTRequirementLevel::REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
 
   // Now set to a truly legacy CA, and create a chain that
   // contains that legacy CA hash.
-  delegate.UpdateCTPolicies({}, {}, {}, {hashes_.back().ToString()});
+  delegate.UpdateCTPolicies({}, {}, {hashes_.back().ToString()});
   EXPECT_EQ(CTRequirementLevel::NOT_REQUIRED,
             delegate.IsCTRequiredForHost("google.com", cert_.get(), hashes_));
 }
 
-TEST_F(ChromeRequireCTDelegateTest, RequiresCTAfterApril2018) {
-  using CTRequirementLevel =
-      net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
-  ChromeRequireCTDelegate delegate;
-
-  EXPECT_EQ(CTRequirementLevel::DEFAULT,
-            delegate.IsCTRequiredForHost("example.com", cert_.get(), hashes_));
-
-  scoped_refptr<net::X509Certificate> may_2018 =
-      net::CreateCertificateChainFromFile(
-          net::GetTestCertsDirectory(), "may_2018.pem",
-          net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE);
-  ASSERT_TRUE(may_2018);
-
-  net::HashValueVector new_hashes;
-  new_hashes.push_back(net::HashValue(
-      net::X509Certificate::CalculateFingerprint256(may_2018->cert_buffer())));
-
-  EXPECT_EQ(
-      CTRequirementLevel::REQUIRED,
-      delegate.IsCTRequiredForHost("example.com", may_2018.get(), new_hashes));
-}
-
-TEST_F(ChromeRequireCTDelegateTest,
-       PoliciesCheckedBeforeRequiringCTAfterApril2018) {
-  using CTRequirementLevel =
-      net::TransportSecurityState::RequireCTDelegate::CTRequirementLevel;
-  ChromeRequireCTDelegate delegate;
-
-  scoped_refptr<net::X509Certificate> may_2018 =
-      net::CreateCertificateChainFromFile(
-          net::GetTestCertsDirectory(), "may_2018.pem",
-          net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE);
-  ASSERT_TRUE(may_2018);
-
-  net::HashValueVector new_hashes;
-  net::HashValue leaf_hash;
-  ASSERT_TRUE(net::x509_util::CalculateSha256SpkiHash(may_2018->cert_buffer(),
-                                                      &leaf_hash));
-  new_hashes.push_back(std::move(leaf_hash));
-
-  EXPECT_EQ(
-      CTRequirementLevel::REQUIRED,
-      delegate.IsCTRequiredForHost("example.com", may_2018.get(), new_hashes));
-
-  // Check excluding by hostname.
-  delegate.UpdateCTPolicies({}, {"example.com"}, {}, {});
-  EXPECT_EQ(
-      CTRequirementLevel::NOT_REQUIRED,
-      delegate.IsCTRequiredForHost("example.com", may_2018.get(), new_hashes));
-
-  // Check excluding by leaf hash.
-  delegate.UpdateCTPolicies({}, {}, {new_hashes.front().ToString()}, {});
-  EXPECT_EQ(
-      CTRequirementLevel::NOT_REQUIRED,
-      delegate.IsCTRequiredForHost("example.com", may_2018.get(), new_hashes));
-
-  // Check excluding by legacy CA hash.
-
-  // The hash of a known legacy CA. See
-  // //net/cert/root_cert_list_generated.h
-  net::SHA256HashValue legacy_spki = {{
-      0x00, 0x6C, 0xB2, 0x26, 0xA7, 0x72, 0xC7, 0x18, 0x2D, 0x77, 0x72,
-      0x38, 0x3E, 0x37, 0x3F, 0x0F, 0x22, 0x9E, 0x7D, 0xFE, 0x34, 0x44,
-      0x81, 0x0A, 0x8D, 0x6E, 0x50, 0x90, 0x5D, 0x20, 0xD6, 0x61,
-  }};
-  new_hashes.push_back(net::HashValue(legacy_spki));
-  delegate.UpdateCTPolicies({}, {}, {}, {new_hashes.back().ToString()});
-  EXPECT_EQ(
-      CTRequirementLevel::NOT_REQUIRED,
-      delegate.IsCTRequiredForHost("example.com", may_2018.get(), new_hashes));
-}
-
 }  // namespace
 
 }  // namespace certificate_transparency
diff --git a/components/certificate_transparency/pref_names.cc b/components/certificate_transparency/pref_names.cc
index 1d3d999..6aa88d9 100644
--- a/components/certificate_transparency/pref_names.cc
+++ b/components/certificate_transparency/pref_names.cc
@@ -10,14 +10,11 @@
 namespace prefs {
 
 void RegisterPrefs(PrefRegistrySimple* registry) {
-  registry->RegisterListPref(prefs::kCTRequiredHosts);
   registry->RegisterListPref(prefs::kCTExcludedHosts);
   registry->RegisterListPref(prefs::kCTExcludedSPKIs);
   registry->RegisterListPref(prefs::kCTExcludedLegacySPKIs);
 }
 
-const char kCTRequiredHosts[] = "certificate_transparency.required_hosts";
-
 const char kCTExcludedHosts[] = "certificate_transparency.excluded_hosts";
 
 const char kCTExcludedSPKIs[] = "certificate_transparency.excluded_spkis";
diff --git a/components/certificate_transparency/pref_names.h b/components/certificate_transparency/pref_names.h
index 1bd6bb72..2f1e916f 100644
--- a/components/certificate_transparency/pref_names.h
+++ b/components/certificate_transparency/pref_names.h
@@ -18,10 +18,6 @@
 void RegisterPrefs(PrefRegistrySimple* registry);
 
 // The set of hosts (as URLBlocklist-syntax filters) for which Certificate
-// Transparency is required to be present.
-COMPONENT_EXPORT(CERTIFICATE_TRANSPARENCY) extern const char kCTRequiredHosts[];
-
-// The set of hosts (as URLBlocklist-syntax filters) for which Certificate
 // Transparency information is allowed to be absent, even if it would
 // otherwise be required (e.g. as part of security policy).
 COMPONENT_EXPORT(CERTIFICATE_TRANSPARENCY) extern const char kCTExcludedHosts[];
diff --git a/components/commerce/core/discounts_storage_unittest.cc b/components/commerce/core/discounts_storage_unittest.cc
index c32c1c52..a9e197d 100644
--- a/components/commerce/core/discounts_storage_unittest.cc
+++ b/components/commerce/core/discounts_storage_unittest.cc
@@ -154,8 +154,10 @@
   MOCK_METHOD(
       void,
       UpdateEntries,
-      ((const std::vector<std::pair<std::string, commerce::DiscountsContent>>&
+      ((std::unique_ptr<
+           std::vector<std::pair<std::string, commerce::DiscountsContent>>>
             entries_to_update),
+       std::unique_ptr<std::vector<std::string>> keys_to_remove,
        SessionProtoStorage<commerce::DiscountsContent>::OperationCallback
            callback),
       (override));
diff --git a/components/commerce/core/parcel/parcels_storage.cc b/components/commerce/core/parcel/parcels_storage.cc
index 66b1aa2..6f43e64f 100644
--- a/components/commerce/core/parcel/parcels_storage.cc
+++ b/components/commerce/core/parcel/parcels_storage.cc
@@ -51,7 +51,8 @@
     const std::vector<ParcelStatus>& parcel_status,
     StorageUpdateCallback callback) {
   DCHECK(is_initialized_);
-  std::vector<std::pair<std::string, ParcelTrackingContent>> content_to_insert;
+  auto content_to_insert = std::make_unique<
+      std::vector<std::pair<std::string, ParcelTrackingContent>>>();
   for (const auto& status : parcel_status) {
     std::string key = GetDbKeyFromParcelStatus(status.parcel_identifier());
     ParcelTrackingContent content;
@@ -60,10 +61,12 @@
     *new_status = status;
     content.set_last_update_time_usec(
         clock_->Now().ToDeltaSinceWindowsEpoch().InMicroseconds());
-    content_to_insert.emplace_back(key, content);
+    content_to_insert->emplace_back(key, content);
     parcels_cache_[key] = std::move(content);
   }
-  proto_db_->UpdateEntries(std::move(content_to_insert), std::move(callback));
+  proto_db_->UpdateEntries(std::move(content_to_insert),
+                           std::make_unique<std::vector<std::string>>(),
+                           std::move(callback));
 }
 
 void ParcelsStorage::DeleteParcelStatus(const std::string& tracking_id,
diff --git a/components/commerce/core/parcel/parcels_storage_unittest.cc b/components/commerce/core/parcel/parcels_storage_unittest.cc
index e2cf2b3..c7077c9d 100644
--- a/components/commerce/core/parcel/parcels_storage_unittest.cc
+++ b/components/commerce/core/parcel/parcels_storage_unittest.cc
@@ -99,9 +99,10 @@
   MOCK_METHOD(
       void,
       UpdateEntries,
-      ((const std::vector<
-           std::pair<std::string, parcel_tracking_db::ParcelTrackingContent>>&
+      ((std::unique_ptr<std::vector<
+            std::pair<std::string, parcel_tracking_db::ParcelTrackingContent>>>
             entries_to_update),
+       std::unique_ptr<std::vector<std::string>> keys_to_remove,
        SessionProtoStorage<parcel_tracking_db::ParcelTrackingContent>::
            OperationCallback callback),
       (override));
@@ -237,7 +238,7 @@
 }
 
 TEST_F(ParcelsStorageTest, TestUpdateParcelStatus) {
-  EXPECT_CALL(*proto_db_, UpdateEntries(_, _)).Times(1);
+  EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _)).Times(1);
 
   std::vector<ParcelStatus> status;
   ParcelStatus status1 = CreateParcelStatus(kCarrier1, kTrackingId1,
diff --git a/components/commerce/core/subscriptions/subscriptions_storage.cc b/components/commerce/core/subscriptions/subscriptions_storage.cc
index e6575132..95aaa2f 100644
--- a/components/commerce/core/subscriptions/subscriptions_storage.cc
+++ b/components/commerce/core/subscriptions/subscriptions_storage.cc
@@ -15,6 +15,45 @@
 
 namespace commerce {
 
+absl::optional<CommerceSubscriptionProto> GetCommerceSubscriptionProto(
+    const CommerceSubscription& subscription) {
+  SubscriptionTypeProto subscription_type = commerce_subscription_db::
+      CommerceSubscriptionContentProto_SubscriptionType_TYPE_UNSPECIFIED;
+  bool type_parse_succeeded = commerce_subscription_db::
+      CommerceSubscriptionContentProto_SubscriptionType_Parse(
+          SubscriptionTypeToString(subscription.type), &subscription_type);
+
+  TrackingIdTypeProto tracking_id_type = commerce_subscription_db::
+      CommerceSubscriptionContentProto_TrackingIdType_IDENTIFIER_TYPE_UNSPECIFIED;
+  bool id_type_parse_succeeded = commerce_subscription_db::
+      CommerceSubscriptionContentProto_TrackingIdType_Parse(
+          SubscriptionIdTypeToString(subscription.id_type), &tracking_id_type);
+
+  SubscriptionManagementTypeProto management_type = commerce_subscription_db::
+      CommerceSubscriptionContentProto_SubscriptionManagementType_MANAGE_TYPE_UNSPECIFIED;
+  bool management_type_parse_succeeded = commerce_subscription_db::
+      CommerceSubscriptionContentProto_SubscriptionManagementType_Parse(
+          SubscriptionManagementTypeToString(subscription.management_type),
+          &management_type);
+
+  // TODO(crbug.com/1348024): Record metrics for failed parse.
+  if (!type_parse_succeeded || !id_type_parse_succeeded ||
+      !management_type_parse_succeeded) {
+    VLOG(1) << "Fail to get proto type";
+    return absl::nullopt;
+  }
+
+  const std::string& key = GetStorageKeyForSubscription(subscription);
+  CommerceSubscriptionProto proto;
+  proto.set_key(key);
+  proto.set_tracking_id(subscription.id);
+  proto.set_subscription_type(subscription_type);
+  proto.set_tracking_id_type(tracking_id_type);
+  proto.set_management_type(management_type);
+  proto.set_timestamp(subscription.timestamp);
+  return proto;
+}
+
 SubscriptionsStorage::SubscriptionsStorage(
     SessionProtoStorage<CommerceSubscriptionProto>* subscription_proto_db)
     : proto_db_(subscription_proto_db) {
@@ -104,50 +143,22 @@
 }
 
 void SubscriptionsStorage::SaveSubscription(
-    CommerceSubscription subscription,
+    const CommerceSubscription& subscription,
     base::OnceCallback<void(bool)> callback) {
-  // Get proto types from the object.
-  SubscriptionTypeProto subscription_type = commerce_subscription_db::
-      CommerceSubscriptionContentProto_SubscriptionType_TYPE_UNSPECIFIED;
-  bool type_parse_succeeded = commerce_subscription_db::
-      CommerceSubscriptionContentProto_SubscriptionType_Parse(
-          SubscriptionTypeToString(subscription.type), &subscription_type);
-
-  TrackingIdTypeProto tracking_id_type = commerce_subscription_db::
-      CommerceSubscriptionContentProto_TrackingIdType_IDENTIFIER_TYPE_UNSPECIFIED;
-  bool id_type_parse_succeeded = commerce_subscription_db::
-      CommerceSubscriptionContentProto_TrackingIdType_Parse(
-          SubscriptionIdTypeToString(subscription.id_type), &tracking_id_type);
-
-  SubscriptionManagementTypeProto management_type = commerce_subscription_db::
-      CommerceSubscriptionContentProto_SubscriptionManagementType_MANAGE_TYPE_UNSPECIFIED;
-  bool management_type_parse_succeeded = commerce_subscription_db::
-      CommerceSubscriptionContentProto_SubscriptionManagementType_Parse(
-          SubscriptionManagementTypeToString(subscription.management_type),
-          &management_type);
+  auto proto = GetCommerceSubscriptionProto(subscription);
 
   // TODO(crbug.com/1348024): Record metrics for failed parse.
-  if (!type_parse_succeeded || !id_type_parse_succeeded ||
-      !management_type_parse_succeeded) {
-    VLOG(1) << "Fail to get proto type";
+  if (!proto.has_value()) {
     std::move(callback).Run(false);
     return;
   }
 
-  const std::string& key = GetStorageKeyForSubscription(subscription);
-  CommerceSubscriptionProto proto;
-  proto.set_key(key);
-  proto.set_tracking_id(subscription.id);
-  proto.set_subscription_type(subscription_type);
-  proto.set_tracking_id_type(tracking_id_type);
-  proto.set_management_type(management_type);
-  proto.set_timestamp(subscription.timestamp);
-
-  proto_db_->InsertContent(key, proto, std::move(callback));
+  proto_db_->InsertContent(GetStorageKeyForSubscription(subscription),
+                           proto.value(), std::move(callback));
 }
 
 void SubscriptionsStorage::DeleteSubscription(
-    CommerceSubscription subscription,
+    const CommerceSubscription& subscription,
     base::OnceCallback<void(bool)> callback) {
   proto_db_->DeleteOneEntry(GetStorageKeyForSubscription(subscription),
                             std::move(callback));
@@ -247,23 +258,14 @@
   auto local_map = SubscriptionsListToMap(std::move(local_subscriptions));
   std::vector<CommerceSubscription> added_subscriptions;
   std::vector<CommerceSubscription> removed_subscriptions;
+  auto updated_entries = std::make_unique<
+      std::vector<std::pair<std::string, CommerceSubscriptionProto>>>();
+  auto removed_keys = std::make_unique<std::vector<std::string>>();
 
-  bool all_succeeded = true;
   for (auto& kv : local_map) {
     if (remote_map.find(kv.first) == remote_map.end()) {
-      removed_subscriptions.push_back(kv.second);
-      std::string key = GetStorageKeyForSubscription(kv.second);
-      DeleteSubscription(
-          std::move(kv.second),
-          base::BindOnce(
-              [](base::WeakPtr<SubscriptionsStorage> storage, std::string key,
-                 bool* all_succeeded, bool succeeded) {
-                *all_succeeded = (*all_succeeded) && succeeded;
-                if (storage && succeeded) {
-                  storage->subscriptions_cache_.erase(key);
-                }
-              },
-              weak_ptr_factory_.GetWeakPtr(), key, &all_succeeded));
+      removed_keys->emplace_back(GetStorageKeyForSubscription(kv.second));
+      removed_subscriptions.emplace_back(kv.second);
     }
   }
   for (auto& kv : remote_map) {
@@ -277,31 +279,40 @@
       if (local_it->second.timestamp == kv.second.timestamp) {
         continue;
       }
-      DeleteSubscription(std::move(local_it->second),
-                         base::BindOnce(
-                             [](bool* all_succeeded, bool succeeded) {
-                               *all_succeeded = (*all_succeeded) && succeeded;
-                             },
-                             &all_succeeded));
     }
-    added_subscriptions.push_back(kv.second);
-    std::string key_to_insert = GetStorageKeyForSubscription(kv.second);
-    SaveSubscription(
-        std::move(kv.second),
-        base::BindOnce(
-            [](base::WeakPtr<SubscriptionsStorage> storage, std::string key,
-               bool* all_succeeded, bool succeeded) {
-              *all_succeeded = (*all_succeeded) && succeeded;
-              if (storage && succeeded) {
-                storage->subscriptions_cache_.insert(key);
-              }
-            },
-            weak_ptr_factory_.GetWeakPtr(), key_to_insert, &all_succeeded));
+    auto subscription_proto = GetCommerceSubscriptionProto(kv.second);
+    if (!subscription_proto.has_value()) {
+      continue;
+    }
+    added_subscriptions.emplace_back(kv.second);
+    updated_entries->emplace_back(GetStorageKeyForSubscription(kv.second),
+                                  subscription_proto.value());
   }
-  std::move(callback).Run(
-      all_succeeded ? SubscriptionsRequestStatus::kSuccess
-                    : SubscriptionsRequestStatus::kStorageError,
-      std::move(added_subscriptions), std::move(removed_subscriptions));
+
+  proto_db_->UpdateEntries(
+      std::move(updated_entries), std::move(removed_keys),
+      base::BindOnce(
+          [](base::WeakPtr<SubscriptionsStorage> storage,
+             std::vector<CommerceSubscription> added_subs,
+             std::vector<CommerceSubscription> removed_subs,
+             StorageUpdateCallback update_callback, bool succeeded) {
+            if (storage && succeeded) {
+              for (auto& subscription : added_subs) {
+                storage->subscriptions_cache_.insert(
+                    GetStorageKeyForSubscription(subscription));
+              }
+              for (auto& subscription : removed_subs) {
+                storage->subscriptions_cache_.erase(
+                    GetStorageKeyForSubscription(subscription));
+              }
+            }
+            std::move(update_callback)
+                .Run(succeeded ? SubscriptionsRequestStatus::kSuccess
+                               : SubscriptionsRequestStatus::kStorageError,
+                     std::move(added_subs), std::move(removed_subs));
+          },
+          weak_ptr_factory_.GetWeakPtr(), std::move(added_subscriptions),
+          std::move(removed_subscriptions), std::move(callback)));
 }
 
 void SubscriptionsStorage::IsSubscribed(
diff --git a/components/commerce/core/subscriptions/subscriptions_storage.h b/components/commerce/core/subscriptions/subscriptions_storage.h
index 6a734cd..4224f88d 100644
--- a/components/commerce/core/subscriptions/subscriptions_storage.h
+++ b/components/commerce/core/subscriptions/subscriptions_storage.h
@@ -105,10 +105,10 @@
   SubscriptionsStorage();
 
  private:
-  void SaveSubscription(CommerceSubscription subscription,
+  void SaveSubscription(const CommerceSubscription& subscription,
                         base::OnceCallback<void(bool)> callback);
 
-  void DeleteSubscription(CommerceSubscription subscription,
+  void DeleteSubscription(const CommerceSubscription& subscription,
                           base::OnceCallback<void(bool)> callback);
 
   CommerceSubscription GetSubscriptionFromProto(
diff --git a/components/commerce/core/subscriptions/subscriptions_storage_unittest.cc b/components/commerce/core/subscriptions/subscriptions_storage_unittest.cc
index 31063b0..2ac948ae 100644
--- a/components/commerce/core/subscriptions/subscriptions_storage_unittest.cc
+++ b/components/commerce/core/subscriptions/subscriptions_storage_unittest.cc
@@ -136,14 +136,16 @@
                SessionProtoStorage<commerce::CommerceSubscriptionProto>::
                    OperationCallback callback),
               (override));
-  MOCK_METHOD(void,
-              UpdateEntries,
-              ((const std::vector<
-                   std::pair<std::string, commerce::CommerceSubscriptionProto>>&
-                    entries_to_update),
-               SessionProtoStorage<commerce::CommerceSubscriptionProto>::
-                   OperationCallback callback),
-              (override));
+  MOCK_METHOD(
+      void,
+      UpdateEntries,
+      ((std::unique_ptr<std::vector<
+            std::pair<std::string, commerce::CommerceSubscriptionProto>>>
+            entries_to_update),
+       std::unique_ptr<std::vector<std::string>> keys_to_remove,
+       SessionProtoStorage<
+           commerce::CommerceSubscriptionProto>::OperationCallback callback),
+      (override));
   MOCK_METHOD(
       void,
       DeleteAllContent,
@@ -217,6 +219,17 @@
                     OperationCallback callback) {
               std::move(callback).Run(succeeded);
             });
+    ON_CALL(*this, UpdateEntries)
+        .WillByDefault(
+            [succeeded](
+                std::unique_ptr<std::vector<std::pair<
+                    std::string, commerce::CommerceSubscriptionProto>>>
+                    entries_to_update,
+                std::unique_ptr<std::vector<std::string>> keys_to_remove,
+                SessionProtoStorage<commerce::CommerceSubscriptionProto>::
+                    OperationCallback callback) {
+              std::move(callback).Run(succeeded);
+            });
     ON_CALL(*this, DeleteAllContent)
         .WillByDefault(
             [succeeded](SessionProtoStorage<
@@ -349,10 +362,7 @@
   proto_db_->MockOperationResult(true);
 
   EXPECT_CALL(*proto_db_, LoadContentWithPrefix("PRICE_TRACK", _));
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey3, _)).Times(1);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey1, _, _)).Times(1);
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey2, _)).Times(0);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey2, _, _)).Times(0);
+  EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _)).Times(1);
 
   base::RunLoop run_loop;
   storage_->UpdateStorage(
@@ -393,10 +403,7 @@
   proto_db_->MockOperationResult(true);
 
   EXPECT_CALL(*proto_db_, LoadContentWithPrefix("PRICE_TRACK", _));
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey3, _)).Times(1);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey1, _, _)).Times(1);
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey2, _)).Times(1);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey2, _, _)).Times(1);
+  EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _)).Times(1);
 
   base::RunLoop run_loop;
   storage_->UpdateStorage(
@@ -426,9 +433,7 @@
   {
     InSequence s;
     EXPECT_CALL(*proto_db_, LoadContentWithPrefix("PRICE_TRACK", _));
-    EXPECT_CALL(*proto_db_, DeleteOneEntry).Times(0);
-    EXPECT_CALL(*proto_db_, InsertContent(kKey2, _, _));
-    EXPECT_CALL(*proto_db_, InsertContent(kKey1, _, _));
+    EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _)).Times(1);
   }
 
   base::RunLoop run_loop;
@@ -459,8 +464,7 @@
   {
     InSequence s;
     EXPECT_CALL(*proto_db_, LoadContentWithPrefix("PRICE_TRACK", _));
-    EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey3, _));
-    EXPECT_CALL(*proto_db_, InsertContent(kKey1, _, _));
+    EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _));
   }
 
   base::RunLoop run_loop;
@@ -489,10 +493,7 @@
   proto_db_->MockOperationResult(true);
 
   EXPECT_CALL(*proto_db_, LoadContentWithPrefix("PRICE_TRACK", _));
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey3, _)).Times(1);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey1, _, _)).Times(1);
-  EXPECT_CALL(*proto_db_, DeleteOneEntry(kKey2, _)).Times(0);
-  EXPECT_CALL(*proto_db_, InsertContent(kKey2, _, _)).Times(0);
+  EXPECT_CALL(*proto_db_, UpdateEntries(_, _, _)).Times(1);
 
   base::RunLoop run_loop;
   storage_->UpdateStorageAndNotifyModifiedSubscriptions(
diff --git a/components/gcm_driver/gcm_stats_recorder_impl.cc b/components/gcm_driver/gcm_stats_recorder_impl.cc
index 95fc123f..2b8099c 100644
--- a/components/gcm_driver/gcm_stats_recorder_impl.cc
+++ b/components/gcm_driver/gcm_stats_recorder_impl.cc
@@ -13,6 +13,8 @@
 #include "base/strings/stringprintf.h"
 #include "components/gcm_driver/crypto/gcm_decryption_result.h"
 #include "components/gcm_driver/crypto/gcm_encryption_provider.h"
+#include "google_apis/gcm/engine/mcs_client.h"
+#include "google_apis/gcm/engine/registration_request.h"
 
 namespace gcm {
 
@@ -116,6 +118,12 @@
       return "QUOTA_EXCEEDED";
     case gcm::RegistrationRequest::TOO_MANY_REGISTRATIONS:
       return "TOO_MANY_REGISTRATIONS";
+    case gcm::RegistrationRequest::TOO_MANY_SUBSCRIBERS:
+      return "TOO_MANY_SUBSCRIBERS";
+    case gcm::RegistrationRequest::FIS_AUTH_ERROR:
+      return "FIS_AUTH_ERROR";
+    case gcm::RegistrationRequest::INVALID_TARGET_VERSION:
+      return "INVALID_TARGET_VERSION";
   }
   return "UNKNOWN_STATUS";
 }
diff --git a/components/gwp_asan/buildflags/buildflags.gni b/components/gwp_asan/buildflags/buildflags.gni
index 356046f..a1cc98e6 100644
--- a/components/gwp_asan/buildflags/buildflags.gni
+++ b/components/gwp_asan/buildflags/buildflags.gni
@@ -6,12 +6,13 @@
 import("//base/allocator/partition_allocator/partition_alloc.gni")
 import("//build/config/compiler/compiler.gni")
 
-# Windows/x86 is disabled due to https://crbug.com/969146
+# Windows/32-bit is disabled due to https://crbug.com/969146
 # Android component builds are disabled due to https://crbug.com/976399
 # Android requires frame pointers for unwinding, unwind tables aren't shipped in
 # official builds.
 supported_platform =
-    is_linux || is_chromeos || is_mac || (is_win && current_cpu == "x64") ||
+    is_linux || is_chromeos || is_mac ||
+    (is_win && (current_cpu == "x64" || current_cpu == "arm64")) ||
     (is_android && !is_component_build && enable_frame_pointers)
 
 declare_args() {
diff --git a/components/ip_protection_auth/DIR_METADATA b/components/ip_protection_auth/DIR_METADATA
new file mode 100644
index 0000000..142c8e0
--- /dev/null
+++ b/components/ip_protection_auth/DIR_METADATA
@@ -0,0 +1,13 @@
+# Metadata information for this directory.
+#
+# For more information on DIR_METADATA files, see:
+#   https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/README.md
+#
+# For the schema of this file, see Metadata message:
+#   https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/tools/dirmd/proto/dir_metadata.proto
+
+team_email: "android-webview-dev@chromium.org"
+monorail {
+  component: "Mobile>WebView"
+}
+os: ANDROID
diff --git a/components/ip_protection_auth/OWNERS b/components/ip_protection_auth/OWNERS
new file mode 100644
index 0000000..4cd7704
--- /dev/null
+++ b/components/ip_protection_auth/OWNERS
@@ -0,0 +1,2 @@
+abhijithnair@chromium.org
+edechamps@google.com
diff --git a/components/ip_protection_auth/README.md b/components/ip_protection_auth/README.md
new file mode 100644
index 0000000..c9afe1d
--- /dev/null
+++ b/components/ip_protection_auth/README.md
@@ -0,0 +1,7 @@
+# Android IP Protection Authentication Client Library
+
+As part of IP Protection in Android, Android components like WebView and Cronet
+needs to request for authentication tokens from a system provided
+Android service in order to talk to the IP Protection proxy server.
+This client library provides a wrapper that handles the IPC to this
+authentication service.
diff --git a/components/ip_protection_auth/client/BUILD.gn b/components/ip_protection_auth/client/BUILD.gn
new file mode 100644
index 0000000..509dcb6
--- /dev/null
+++ b/components/ip_protection_auth/client/BUILD.gn
@@ -0,0 +1,18 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/config.gni")
+import("//build/config/android/rules.gni")
+
+android_library("ip_protection_auth_client_java") {
+  sources = [ "java/src/org/chromium/components/ip_protection_auth/IpProtectionAuthClient.java" ]
+  deps = [
+    "//components/ip_protection_auth/common/aidl:ip_protection_aidl_java",
+    "//components/ip_protection_auth/common/proto:ip_protection_auth_proto_java",
+    "//third_party/android_deps:com_google_code_findbugs_jsr305_java",
+    "//third_party/android_deps:com_google_errorprone_error_prone_annotations_java",
+    "//third_party/android_deps:protobuf_lite_runtime_java",
+    "//third_party/androidx:androidx_annotation_annotation_java",
+  ]
+}
diff --git a/components/ip_protection_auth/client/java/src/org/chromium/components/ip_protection_auth/IpProtectionAuthClient.java b/components/ip_protection_auth/client/java/src/org/chromium/components/ip_protection_auth/IpProtectionAuthClient.java
new file mode 100644
index 0000000..d218f3a
--- /dev/null
+++ b/components/ip_protection_auth/client/java/src/org/chromium/components/ip_protection_auth/IpProtectionAuthClient.java
@@ -0,0 +1,214 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.errorprone.annotations.DoNotCall;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.chromium.components.ip_protection_auth.common.IIpProtectionAuthAndSignCallback;
+import org.chromium.components.ip_protection_auth.common.IIpProtectionAuthService;
+import org.chromium.components.ip_protection_auth.common.IIpProtectionGetInitialDataCallback;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignResponse;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataResponse;
+
+/**
+ * Client interface for the IP Protection Auth service.
+ *
+ * The methods in this class are thread-safe (except for close() which should not be called
+ * concurrently with other methods).
+ *
+ * TODO(abhijithnair): Update documentation once production ready.
+ * DO NOT DEPEND. CURRENTLY UNDER DEVELOPMENT.
+ */
+public final class IpProtectionAuthClient implements AutoCloseable {
+    // Only used for testing.
+    private static final String IP_PROTECTION_AUTH_STUB_SERVICE_NAME =
+            "org.chromium.components.ip_protection_auth.mock_service.IpProtectionAuthServiceMock";
+
+    // mService being null signifies that the object has been closed by calling close().
+    @Nullable
+    private IIpProtectionAuthService mService;
+    // We need to store this to unbind from the service.
+    @Nullable
+    private ConnectionSetup mConnection;
+
+    private static final class ConnectionSetup implements ServiceConnection {
+        private final IpProtectionAuthServiceCallback mCallback;
+        private final Context mContext;
+
+        ConnectionSetup(
+                @NonNull Context context, @NonNull IpProtectionAuthServiceCallback callback) {
+            mContext = context;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+            IpProtectionAuthClient ipProtectionClient = new IpProtectionAuthClient(
+                    this, IIpProtectionAuthService.Stub.asInterface(iBinder));
+            mCallback.onResult(ipProtectionClient);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            mContext.unbindService(this);
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            mContext.unbindService(this);
+        }
+
+        @Override
+        public void onNullBinding(ComponentName name) {
+            mContext.unbindService(this);
+        }
+    }
+
+    private static final class IIpProtectionGetInitialDataCallbackStub
+            extends IIpProtectionGetInitialDataCallback.Stub {
+        private final GetInitialDataCallback mCallback;
+
+        IIpProtectionGetInitialDataCallbackStub(GetInitialDataCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void reportResult(byte[] bytes) {
+            try {
+                mCallback.onResult(GetInitialDataResponse.parser().parseFrom(bytes));
+            } catch (InvalidProtocolBufferException ex) {
+                // TODO(abhijithnair): Handle this case correctly.
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        public void reportError(byte[] bytes) {
+            mCallback.onError(bytes);
+        }
+    }
+
+    private static final class IIpProtectionAuthAndSignCallbackStub
+            extends IIpProtectionAuthAndSignCallback.Stub {
+        private final AuthAndSignCallback mCallback;
+
+        IIpProtectionAuthAndSignCallbackStub(AuthAndSignCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void reportResult(byte[] bytes) {
+            try {
+                mCallback.onResult(AuthAndSignResponse.parser().parseFrom(bytes));
+            } catch (InvalidProtocolBufferException ex) {
+                // TODO(abhijithnair): Handle this case correctly.
+                throw new RuntimeException(ex);
+            }
+        }
+
+        @Override
+        public void reportError(byte[] bytes) {
+            mCallback.onError(bytes);
+        }
+    }
+
+    public interface IpProtectionAuthServiceCallback {
+        void onResult(IpProtectionAuthClient client);
+
+        void onError(String error);
+    }
+
+    public interface GetInitialDataCallback {
+        // TODO(abhijithnair): Consider using a non-proto generated class.
+        void onResult(GetInitialDataResponse result);
+
+        // TODO(abhijithnair): Change to using a error specific class.
+        void onError(byte[] error);
+    }
+
+    public interface AuthAndSignCallback {
+        // TODO(abhijithnair): Consider using a non-proto generated class.
+        void onResult(AuthAndSignResponse result);
+
+        // TODO(abhijithnair): Change to using a error specific class.
+        void onError(byte[] error);
+    }
+
+    IpProtectionAuthClient(@NonNull ConnectionSetup connectionSetup,
+            @NonNull IIpProtectionAuthService ipProtectionAuthService) {
+        mConnection = connectionSetup;
+        mService = ipProtectionAuthService;
+    }
+
+    @VisibleForTesting
+    public static void createConnectedInstanceForTestingAsync(
+            @NonNull Context context, @NonNull IpProtectionAuthServiceCallback callback) {
+        ComponentName componentName =
+                new ComponentName(context, IP_PROTECTION_AUTH_STUB_SERVICE_NAME);
+        Intent intent = new Intent();
+        intent.setComponent(componentName);
+        ConnectionSetup connectionSetup = new ConnectionSetup(context, callback);
+        context.bindService(intent, connectionSetup, Context.BIND_AUTO_CREATE);
+    }
+
+    @DoNotCall
+    public static void createConnectedInstance(
+            @NonNull Context context, @NonNull IpProtectionAuthServiceCallback callback) {
+        // TODO(abhijithnair): Implement!
+        throw new UnsupportedOperationException("unimplemented");
+    }
+
+    public void getInitialData(GetInitialDataRequest request, GetInitialDataCallback callback) {
+        if (mService == null) {
+            // This denotes a coding error by the caller so it makes sense to throw an unchecked
+            // exception.
+            throw new IllegalStateException("Already closed");
+        }
+
+        IIpProtectionGetInitialDataCallbackStub callbackStub =
+                new IIpProtectionGetInitialDataCallbackStub(callback);
+        try {
+            mService.getInitialData(request.toByteArray(), callbackStub);
+        } catch (RemoteException ex) {
+            // TODO(abhijithnair): Handle this case correctly.
+        }
+    }
+
+    public void authAndSign(AuthAndSignRequest request, AuthAndSignCallback callback) {
+        if (mService == null) {
+            // This denotes a coding error by the caller so it makes sense to throw an unchecked
+            // exception.
+            throw new IllegalStateException("Already closed");
+        }
+        IIpProtectionAuthAndSignCallbackStub callbackStub =
+                new IIpProtectionAuthAndSignCallbackStub(callback);
+        try {
+            mService.authAndSign(request.toByteArray(), callbackStub);
+        } catch (RemoteException ex) {
+            // TODO(abhijithnair): Handle this case correctly.
+        }
+    }
+
+    @Override
+    public void close() {
+        mConnection.mContext.unbindService(mConnection);
+        mConnection = null;
+        mService = null;
+    }
+}
diff --git a/components/ip_protection_auth/common/aidl/BUILD.gn b/components/ip_protection_auth/common/aidl/BUILD.gn
new file mode 100644
index 0000000..e2ffc26
--- /dev/null
+++ b/components/ip_protection_auth/common/aidl/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//testing/test.gni")
+
+android_aidl("ip_protection_aidl") {
+  import_include = [ "src" ]
+  sources = [
+    "src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthAndSignCallback.aidl",
+    "src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthService.aidl",
+    "src/org/chromium/components/ip_protection_auth/common/IIpProtectionGetInitialDataCallback.aidl",
+  ]
+}
+
+android_library("ip_protection_aidl_java") {
+  srcjar_deps = [ ":ip_protection_aidl" ]
+}
diff --git a/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthAndSignCallback.aidl b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthAndSignCallback.aidl
new file mode 100644
index 0000000..47f0f7f
--- /dev/null
+++ b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthAndSignCallback.aidl
@@ -0,0 +1,15 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth.common;
+
+/**
+ * Used for communicating the result of authAndSign() to the caller.
+ */
+oneway interface IIpProtectionAuthAndSignCallback {
+    // Parameter is the serialized form of AuthAndSignResponse proto
+    void reportResult(in byte[] response) = 0;
+    // Parameter is the serialized form of AuthAndSignError proto
+    void reportError(in byte[] error) = 1;
+}
diff --git a/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthService.aidl b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthService.aidl
new file mode 100644
index 0000000..4db9c91d
--- /dev/null
+++ b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionAuthService.aidl
@@ -0,0 +1,18 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth.common;
+
+import org.chromium.components.ip_protection_auth.common.IIpProtectionAuthAndSignCallback;
+import org.chromium.components.ip_protection_auth.common.IIpProtectionGetInitialDataCallback;
+
+/**
+ * Used for conversing with the IP Protection Service.
+ */
+interface IIpProtectionAuthService {
+  // Request should be the serialized form of GetInitialDataRequest proto
+  void getInitialData(in byte[] request, in IIpProtectionGetInitialDataCallback callback) = 0;
+  // Request should be the serialized form of AuthAndSignRequest proto
+  void authAndSign(in byte[] request, in IIpProtectionAuthAndSignCallback callback) = 1;
+}
diff --git a/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionGetInitialDataCallback.aidl b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionGetInitialDataCallback.aidl
new file mode 100644
index 0000000..7fe3db0c
--- /dev/null
+++ b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/IIpProtectionGetInitialDataCallback.aidl
@@ -0,0 +1,15 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth.common;
+
+/**
+ * Used for communicating the result of getInitialData() to the caller.
+ */
+oneway interface IIpProtectionGetInitialDataCallback {
+    // Parameter is the serialized form of GetInitialDataResponse proto
+    void reportResult(in byte[] response) = 0;
+    // Parameter is the serialized form of GetInitialDataError proto
+    void reportError(in byte[] error) = 1;
+}
diff --git a/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/OWNERS b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/OWNERS
new file mode 100644
index 0000000..8f094e0
--- /dev/null
+++ b/components/ip_protection_auth/common/aidl/src/org/chromium/components/ip_protection_auth/common/OWNERS
@@ -0,0 +1,2 @@
+per-file *.aidl=set noparent
+per-file *.aidl=file://ipc/SECURITY_OWNERS
diff --git a/components/ip_protection_auth/common/proto/BUILD.gn b/components/ip_protection_auth/common/proto/BUILD.gn
new file mode 100644
index 0000000..1b14213
--- /dev/null
+++ b/components/ip_protection_auth/common/proto/BUILD.gn
@@ -0,0 +1,11 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//third_party/protobuf/proto_library.gni")
+
+proto_java_library("ip_protection_auth_proto_java") {
+  proto_path = "."
+  sources = [ "ip_protection_auth.proto" ]
+}
diff --git a/components/ip_protection_auth/common/proto/ip_protection_auth.proto b/components/ip_protection_auth/common/proto/ip_protection_auth.proto
new file mode 100644
index 0000000..846076c
--- /dev/null
+++ b/components/ip_protection_auth/common/proto/ip_protection_auth.proto
@@ -0,0 +1,38 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+syntax = "proto3";
+package org.chromium.components.ip_protection_auth.common.proto;
+
+option optimize_for = LITE_RUNTIME;  // TODO(crbug/800281): Remove this after proto 4.0
+option java_package = "org.chromium.components.ip_protection_auth.common.proto";
+option java_outer_classname = "IpProtectionAuthProtos";
+
+// TODO(abhijithnair): Currently this is a sample used to get the end to end
+// data flow set up. This will be modified with real fields.
+// Next tag: 2
+message GetInitialDataRequest {
+  string test_payload = 1;
+}
+
+// TODO(abhijithnair): Currently this is a sample used to get the end to end
+// data flow set up. This will be modified with real fields.
+// Next tag: 2
+message GetInitialDataResponse {
+  string test_payload = 1;
+}
+
+// TODO(abhijithnair): Currently this is a sample used to get the end to end
+// data flow set up. This will be modified with real fields.
+// Next tag: 2
+message AuthAndSignRequest {
+  string test_payload = 1;
+}
+
+// TODO(abhijithnair): Currently this is a sample used to get the end to end
+// data flow set up. This will be modified with real fields.
+// Next tag: 2
+message AuthAndSignResponse {
+  string test_payload = 1;
+}
diff --git a/components/ip_protection_auth/javatests/AndroidManifest.xml b/components/ip_protection_auth/javatests/AndroidManifest.xml
new file mode 100644
index 0000000..95ff6286
--- /dev/null
+++ b/components/ip_protection_auth/javatests/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Chromium Authors
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.chromium.components.ip_protection_auth">
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+  <application android:label="Ip Protection Auth">
+    <service android:name="org.chromium.components.ip_protection_auth.mock_service.IpProtectionAuthServiceMock"
+          android:exported="true" />
+  </application>
+</manifest>
diff --git a/components/ip_protection_auth/javatests/BUILD.gn b/components/ip_protection_auth/javatests/BUILD.gn
new file mode 100644
index 0000000..4dc9c6b
--- /dev/null
+++ b/components/ip_protection_auth/javatests/BUILD.gn
@@ -0,0 +1,30 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//testing/test.gni")
+
+instrumentation_test_apk("ip_protection_auth_test_apk") {
+  apk_name = "IpProtectionAuthTest"
+  apk_under_test = ":ip_protection_auth_apk"
+  android_manifest = "InstrumentationTestAndroidManifest.xml"
+  sources = [ "src/org/chromium/components/ip_protection_auth/test/IpProtectionAuthTest.java" ]
+  deps = [
+    "../client:ip_protection_auth_client_java",
+    "//base:base_java_test_support",
+    "//components/ip_protection_auth/common/proto:ip_protection_auth_proto_java",
+    "//third_party/android_deps:protobuf_lite_runtime_java",
+    "//third_party/androidx:androidx_test_core_java",
+    "//third_party/androidx:androidx_test_ext_junit_java",
+    "//third_party/androidx:androidx_test_runner_java",
+    "//third_party/google-truth:google_truth_java",
+    "//third_party/junit:junit",
+  ]
+}
+
+android_apk("ip_protection_auth_apk") {
+  apk_name = "IpProtectionAuth"
+  android_manifest = "AndroidManifest.xml"
+  deps = [ "../mock_service:ip_protection_auth_service_mock_java" ]
+}
diff --git a/components/ip_protection_auth/javatests/InstrumentationTestAndroidManifest.xml b/components/ip_protection_auth/javatests/InstrumentationTestAndroidManifest.xml
new file mode 100644
index 0000000..dfb9da15
--- /dev/null
+++ b/components/ip_protection_auth/javatests/InstrumentationTestAndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2023 The Chromium Authors
+     Use of this source code is governed by a BSD-style license that can be
+     found in the LICENSE file. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.chromium.components.ip_protection_auth.test">
+  <instrumentation android:name="org.chromium.base.test.BaseChromiumAndroidJUnitRunner"
+      android:targetPackage="org.chromium.components.ip_protection_auth"
+      android:label="Tests for org.chromium.chromium.components.ip_protection_auth"/>
+  <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
+</manifest>
\ No newline at end of file
diff --git a/components/ip_protection_auth/javatests/src/org/chromium/components/ip_protection_auth/test/IpProtectionAuthTest.java b/components/ip_protection_auth/javatests/src/org/chromium/components/ip_protection_auth/test/IpProtectionAuthTest.java
new file mode 100644
index 0000000..8469879f
--- /dev/null
+++ b/components/ip_protection_auth/javatests/src/org/chromium/components/ip_protection_auth/test/IpProtectionAuthTest.java
@@ -0,0 +1,112 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth.test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.ConditionVariable;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.util.Batch;
+import org.chromium.components.ip_protection_auth.IpProtectionAuthClient;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignResponse;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataResponse;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+@Batch(Batch.UNIT_TESTS)
+public final class IpProtectionAuthTest {
+    IpProtectionAuthClient mIpProtectionClient;
+
+    @Before
+    public void setUp() throws Exception {
+        Context context = ApplicationProvider.getApplicationContext();
+
+        final ConditionVariable conditionVariable = new ConditionVariable();
+        IpProtectionAuthClient.IpProtectionAuthServiceCallback callback =
+                new IpProtectionAuthClient.IpProtectionAuthServiceCallback() {
+                    @Override
+                    public void onResult(IpProtectionAuthClient client) {
+                        mIpProtectionClient = client;
+                        conditionVariable.open();
+                    }
+
+                    @Override
+                    public void onError(String error) {
+                        fail("onError should not be called");
+                    }
+                };
+        IpProtectionAuthClient.createConnectedInstanceForTestingAsync(context, callback);
+        assertThat(conditionVariable.block(5000)).isTrue();
+    }
+
+    @After
+    public void tearDown() {
+        mIpProtectionClient.close();
+    }
+
+    @Test
+    public void getInitialDataTest() throws Exception {
+        final ConditionVariable conditionVariable = new ConditionVariable();
+        // Using a 1-element array so that the reference is final and can be passed into the lambda.
+        final GetInitialDataResponse[] getInitialDataResponse = new GetInitialDataResponse[1];
+        IpProtectionAuthClient.GetInitialDataCallback getInitialDataCallback =
+                new IpProtectionAuthClient.GetInitialDataCallback() {
+                    @Override
+                    public void onResult(GetInitialDataResponse response) {
+                        getInitialDataResponse[0] = response;
+                        conditionVariable.open();
+                    }
+
+                    @Override
+                    public void onError(byte[] error) {
+                        fail("onError should not be called");
+                    }
+                };
+        GetInitialDataRequest request =
+                GetInitialDataRequest.newBuilder().setTestPayload("get initial data").build();
+        mIpProtectionClient.getInitialData(request, getInitialDataCallback);
+        assertThat(conditionVariable.block(5000)).isTrue();
+        assertThat(getInitialDataResponse[0].getTestPayload()).isEqualTo("get initial data");
+    }
+
+    @Test
+    public void authAndSignTest() throws Exception {
+        final ConditionVariable conditionVariable = new ConditionVariable();
+        // Using a 1-element array so that the reference is final and can be passed into the lambda.
+        final AuthAndSignResponse[] authAndSignResponse = new AuthAndSignResponse[1];
+        IpProtectionAuthClient.AuthAndSignCallback authAndSignCallback =
+                new IpProtectionAuthClient.AuthAndSignCallback() {
+                    @Override
+                    public void onResult(AuthAndSignResponse response) {
+                        authAndSignResponse[0] = response;
+                        conditionVariable.open();
+                    }
+
+                    @Override
+                    public void onError(byte[] bytes) {
+                        fail("onError should not be called");
+                    }
+                };
+        AuthAndSignRequest authAndSignRequest =
+                AuthAndSignRequest.newBuilder().setTestPayload("auth and sign").build();
+        mIpProtectionClient.authAndSign(authAndSignRequest, authAndSignCallback);
+        assertThat(conditionVariable.block(5000)).isTrue();
+        assertThat(authAndSignResponse[0].getTestPayload()).isEqualTo("auth and sign");
+    }
+}
diff --git a/components/ip_protection_auth/mock_service/BUILD.gn b/components/ip_protection_auth/mock_service/BUILD.gn
new file mode 100644
index 0000000..5590c1f
--- /dev/null
+++ b/components/ip_protection_auth/mock_service/BUILD.gn
@@ -0,0 +1,13 @@
+import("//build/config/android/config.gni")
+import("//build/config/android/rules.gni")
+
+android_library("ip_protection_auth_service_mock_java") {
+  sources = [ "java/src/org/chromium/components/ip_protection_auth/mock_service/IpProtectionAuthServiceMock.java" ]
+
+  deps = [
+    "//components/ip_protection_auth/common/aidl:ip_protection_aidl_java",
+    "//components/ip_protection_auth/common/proto:ip_protection_auth_proto_java",
+    "//third_party/android_deps:protobuf_lite_runtime_java",
+    "//third_party/androidx:androidx_annotation_annotation_java",
+  ]
+}
diff --git a/components/ip_protection_auth/mock_service/java/src/org/chromium/components/ip_protection_auth/mock_service/IpProtectionAuthServiceMock.java b/components/ip_protection_auth/mock_service/java/src/org/chromium/components/ip_protection_auth/mock_service/IpProtectionAuthServiceMock.java
new file mode 100644
index 0000000..ba4abb74
--- /dev/null
+++ b/components/ip_protection_auth/mock_service/java/src/org/chromium/components/ip_protection_auth/mock_service/IpProtectionAuthServiceMock.java
@@ -0,0 +1,74 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.ip_protection_auth.mock_service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.Nullable;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.chromium.components.ip_protection_auth.common.IIpProtectionAuthAndSignCallback;
+import org.chromium.components.ip_protection_auth.common.IIpProtectionAuthService;
+import org.chromium.components.ip_protection_auth.common.IIpProtectionGetInitialDataCallback;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.AuthAndSignResponse;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataRequest;
+import org.chromium.components.ip_protection_auth.common.proto.IpProtectionAuthProtos.GetInitialDataResponse;
+
+/**
+ * Mock implementation of the IP Protection Auth Service
+ */
+public final class IpProtectionAuthServiceMock extends Service {
+    private static final String TAG = "IpProtectionAuthServiceMock";
+    private final IIpProtectionAuthService.Stub mBinder = new IIpProtectionAuthService.Stub() {
+        // TODO(abhijithnair): Currently this method just passes the same request byte[] back as the
+        // result. Use a mock result instead.
+        @Override
+        public void getInitialData(byte[] bytes, IIpProtectionGetInitialDataCallback callback) {
+            try {
+                GetInitialDataRequest request = GetInitialDataRequest.parser().parseFrom(bytes);
+                GetInitialDataResponse response = GetInitialDataResponse.newBuilder()
+                                                          .setTestPayload(request.getTestPayload())
+                                                          .build();
+                callback.reportResult(response.toByteArray());
+            } catch (RemoteException ex) {
+                // TODO(abhijithnair): Handle this exception correctly.
+                throw new RuntimeException(ex);
+            } catch (InvalidProtocolBufferException ex) {
+                // TODO(abhijithnair): Handle this exception correctly.
+                throw new RuntimeException(ex);
+            }
+        }
+
+        // TODO(abhijithnair): Currently this method just passes the same request byte[] back as the
+        // result. Use a mock result instead.
+        @Override
+        public void authAndSign(byte[] bytes, IIpProtectionAuthAndSignCallback callback) {
+            try {
+                AuthAndSignRequest request = AuthAndSignRequest.parser().parseFrom(bytes);
+                AuthAndSignResponse response = AuthAndSignResponse.newBuilder()
+                                                       .setTestPayload(request.getTestPayload())
+                                                       .build();
+                callback.reportResult(response.toByteArray());
+            } catch (RemoteException ex) {
+                // TODO(abhijithnair): Handle this exception correctly.
+                throw new RuntimeException(ex);
+            } catch (InvalidProtocolBufferException ex) {
+                // TODO(abhijithnair): Handle this exception correctly.
+                throw new RuntimeException(ex);
+            }
+        }
+    };
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mBinder;
+    }
+}
diff --git a/components/js_injection/browser/js_communication_host.cc b/components/js_injection/browser/js_communication_host.cc
index 9a07cee..96299ef 100644
--- a/components/js_injection/browser/js_communication_host.cc
+++ b/components/js_injection/browser/js_communication_host.cc
@@ -67,23 +67,13 @@
   std::unique_ptr<WebMessageHostFactory> factory;
 };
 
-struct DocumentStartJavaScript {
-  DocumentStartJavaScript(std::u16string script,
-                          OriginMatcher allowed_origin_rules,
-                          int32_t script_id)
-      : script_(std::move(script)),
-        allowed_origin_rules_(allowed_origin_rules),
-        script_id_(script_id) {}
-
-  DocumentStartJavaScript(DocumentStartJavaScript&) = delete;
-  DocumentStartJavaScript& operator=(DocumentStartJavaScript&) = delete;
-  DocumentStartJavaScript(DocumentStartJavaScript&&) = default;
-  DocumentStartJavaScript& operator=(DocumentStartJavaScript&&) = default;
-
-  std::u16string script_;
-  OriginMatcher allowed_origin_rules_;
-  int32_t script_id_;
-};
+DocumentStartJavaScript::DocumentStartJavaScript(
+    std::u16string script,
+    OriginMatcher allowed_origin_rules,
+    int32_t script_id)
+    : script_(std::move(script)),
+      allowed_origin_rules_(allowed_origin_rules),
+      script_id_(script_id) {}
 
 JsCommunicationHost::AddScriptResult::AddScriptResult() = default;
 JsCommunicationHost::AddScriptResult::AddScriptResult(
@@ -139,6 +129,11 @@
   return false;
 }
 
+const std::vector<DocumentStartJavaScript>&
+JsCommunicationHost::GetDocumentStartJavascripts() const {
+  return scripts_;
+}
+
 std::u16string JsCommunicationHost::AddWebMessageHostFactory(
     std::unique_ptr<WebMessageHostFactory> factory,
     const std::u16string& js_object_name,
diff --git a/components/js_injection/browser/js_communication_host.h b/components/js_injection/browser/js_communication_host.h
index c746850..83fb2dbf 100644
--- a/components/js_injection/browser/js_communication_host.h
+++ b/components/js_injection/browser/js_communication_host.h
@@ -22,11 +22,25 @@
 namespace js_injection {
 
 class OriginMatcher;
-struct DocumentStartJavaScript;
 struct JsObject;
 class JsToBrowserMessaging;
 class WebMessageHostFactory;
 
+struct DocumentStartJavaScript {
+  DocumentStartJavaScript(std::u16string script,
+                          OriginMatcher allowed_origin_rules,
+                          int32_t script_id);
+
+  DocumentStartJavaScript(DocumentStartJavaScript&) = delete;
+  DocumentStartJavaScript& operator=(DocumentStartJavaScript&) = delete;
+  DocumentStartJavaScript(DocumentStartJavaScript&&) = default;
+  DocumentStartJavaScript& operator=(DocumentStartJavaScript&&) = default;
+
+  std::u16string script_;
+  OriginMatcher allowed_origin_rules_;
+  int32_t script_id_;
+};
+
 // This class is 1:1 with WebContents, when AddWebMessageListener() is called,
 // it stores the information in this class and send them to renderer side
 // JsCommunication if there is any. When RenderFrameCreated() gets called, it
@@ -62,6 +76,9 @@
 
   bool RemoveDocumentStartJavaScript(int script_id);
 
+  const std::vector<DocumentStartJavaScript>& GetDocumentStartJavascripts()
+      const;
+
   // Adds a new WebMessageHostFactory. For any urls that match
   // |allowed_origin_rules|, |js_object_name| is registered as a JS object that
   // can be used by script on the page to send and receive messages. Returns
diff --git a/components/mirroring/browser/single_client_video_capture_host.cc b/components/mirroring/browser/single_client_video_capture_host.cc
index bbf876f..baa472a 100644
--- a/components/mirroring/browser/single_client_video_capture_host.cc
+++ b/components/mirroring/browser/single_client_video_capture_host.cc
@@ -184,13 +184,6 @@
   // Ignore this call.
 }
 
-void SingleClientVideoCaptureHost::OnFrameDropped(
-    const base::UnguessableToken& device_id,
-    media::VideoCaptureFrameDropReason reason) {
-  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  // Ignore this call.
-}
-
 void SingleClientVideoCaptureHost::OnNewCropVersion(uint32_t crop_version) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   // Ignore this call.
diff --git a/components/mirroring/browser/single_client_video_capture_host.h b/components/mirroring/browser/single_client_video_capture_host.h
index 7c3b8d8..97a72c46 100644
--- a/components/mirroring/browser/single_client_video_capture_host.h
+++ b/components/mirroring/browser/single_client_video_capture_host.h
@@ -74,8 +74,6 @@
   void GetDeviceFormatsInUse(const base::UnguessableToken& device_id,
                              const base::UnguessableToken& session_id,
                              GetDeviceFormatsInUseCallback callback) override;
-  void OnFrameDropped(const base::UnguessableToken& device_id,
-                      media::VideoCaptureFrameDropReason reason) override;
   void OnLog(const base::UnguessableToken& device_id,
              const std::string& message) override;
 
diff --git a/components/mirroring/browser/single_client_video_capture_host_unittest.cc b/components/mirroring/browser/single_client_video_capture_host_unittest.cc
index 8d5102e..a4816711 100644
--- a/components/mirroring/browser/single_client_video_capture_host_unittest.cc
+++ b/components/mirroring/browser/single_client_video_capture_host_unittest.cc
@@ -157,8 +157,7 @@
     buffers_.erase(iter);
     OnBufferDestroyedCall(buffer_id);
   }
-  MOCK_METHOD1(OnFrameDroppedEarly,
-               void(media::VideoCaptureFrameDropReason reason));
+  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason reason));
 
   MOCK_METHOD1(OnNewCropVersion, void(uint32_t crop_version));
 
diff --git a/components/mirroring/service/fake_video_capture_host.h b/components/mirroring/service/fake_video_capture_host.h
index fcff8a4..223afd72 100644
--- a/components/mirroring/service/fake_video_capture_host.h
+++ b/components/mirroring/service/fake_video_capture_host.h
@@ -36,9 +36,6 @@
                     const media::VideoCaptureFeedback&));
   MOCK_METHOD0(OnStopped, void());
   MOCK_METHOD2(OnLog, void(const base::UnguessableToken&, const std::string&));
-  MOCK_METHOD2(OnFrameDropped,
-               void(const base::UnguessableToken&,
-                    media::VideoCaptureFrameDropReason));
 
   void Start(const base::UnguessableToken& device_id,
              const base::UnguessableToken& session_id,
diff --git a/components/mirroring/service/remoting_sender_unittest.cc b/components/mirroring/service/remoting_sender_unittest.cc
index cfa6c104d2..18ddaa9 100644
--- a/components/mirroring/service/remoting_sender_unittest.cc
+++ b/components/mirroring/service/remoting_sender_unittest.cc
@@ -46,8 +46,7 @@
 
   scoped_refptr<media::DecoderBuffer> received_buffer =
       media::cast::ByteArrayToDecoderBuffer(
-          reinterpret_cast<const uint8_t*>(frame.data.data()),
-          frame.data.size());
+          base::as_bytes(base::make_span(frame.data)));
   EXPECT_EQ(std::string(reinterpret_cast<const char*>(buffer.data()),
                         buffer.data_size()),
             std::string(reinterpret_cast<const char*>(received_buffer->data()),
diff --git a/components/mirroring/service/video_capture_client.cc b/components/mirroring/service/video_capture_client.cc
index f762c98..349cff3 100644
--- a/components/mirroring/service/video_capture_client.cc
+++ b/components/mirroring/service/video_capture_client.cc
@@ -314,7 +314,7 @@
     client_buffers_.erase(buffer_iter);
 }
 
-void VideoCaptureClient::OnFrameDroppedEarly(
+void VideoCaptureClient::OnFrameDropped(
     media::VideoCaptureFrameDropReason reason) {}
 
 void VideoCaptureClient::OnNewCropVersion(uint32_t crop_version) {}
diff --git a/components/mirroring/service/video_capture_client.h b/components/mirroring/service/video_capture_client.h
index 184753f..5bd938a 100644
--- a/components/mirroring/service/video_capture_client.h
+++ b/components/mirroring/service/video_capture_client.h
@@ -68,7 +68,7 @@
       media::mojom::ReadyBufferPtr buffer,
       std::vector<media::mojom::ReadyBufferPtr> scaled_buffers) override;
   void OnBufferDestroyed(int32_t buffer_id) override;
-  void OnFrameDroppedEarly(media::VideoCaptureFrameDropReason reason) override;
+  void OnFrameDropped(media::VideoCaptureFrameDropReason reason) override;
   void OnNewCropVersion(uint32_t crop_version) override;
 
   void SwitchVideoCaptureHost(
diff --git a/components/omnibox/browser/autocomplete_match_type.cc b/components/omnibox/browser/autocomplete_match_type.cc
index 5e2d223..4a00318 100644
--- a/components/omnibox/browser/autocomplete_match_type.cc
+++ b/components/omnibox/browser/autocomplete_match_type.cc
@@ -8,6 +8,7 @@
 #include "base/notreached.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/utf_string_conversions.h"
+#include "components/omnibox/browser/actions/omnibox_action.h"
 #include "components/omnibox/browser/autocomplete_match.h"
 #include "components/omnibox/browser/omnibox_field_trial.h"
 #include "components/omnibox/browser/omnibox_popup_selection.h"
@@ -163,6 +164,12 @@
     return doc_string;
   }
 
+  // Standalone action suggestions must use the associated accessibility hint.
+  if (match.type == AutocompleteMatchType::PEDAL) {
+    DCHECK(match.takeover_action);
+    return match.takeover_action->GetLabelStrings().accessibility_hint;
+  }
+
   int message = message_ids[match.type];
   if (!message)
     return match_text;
diff --git a/components/page_image_service/image_service_consent_helper.cc b/components/page_image_service/image_service_consent_helper.cc
index a66c54ae..49bfa2c 100644
--- a/components/page_image_service/image_service_consent_helper.cc
+++ b/components/page_image_service/image_service_consent_helper.cc
@@ -16,8 +16,6 @@
 
 namespace {
 
-constexpr base::TimeDelta kTimeout = base::Seconds(10);
-
 void RunConsentThrottleCallback(
     base::OnceCallback<void(PageImageServiceConsentStatus)> callback,
     bool success) {
@@ -30,10 +28,12 @@
 ImageServiceConsentHelper::ImageServiceConsentHelper(
     syncer::SyncService* sync_service,
     syncer::ModelType model_type)
-    : sync_service_(sync_service), model_type_(model_type) {
-  base::TimeDelta timeout_duration =
-      base::Seconds(GetFieldTrialParamByFeatureAsInt(
-          kImageServiceObserveSyncDownloadStatus, "timeout_seconds", 10));
+    : sync_service_(sync_service),
+      model_type_(model_type),
+      timeout_duration_(base::Seconds(GetFieldTrialParamByFeatureAsInt(
+          kImageServiceObserveSyncDownloadStatus,
+          "timeout_seconds",
+          10))) {
   if (base::FeatureList::IsEnabled(kImageServiceObserveSyncDownloadStatus)) {
     sync_service_observer_.Observe(sync_service);
   } else if (model_type == syncer::ModelType::BOOKMARKS) {
@@ -43,12 +43,12 @@
             NewPersonalizedBookmarksDataCollectionConsentHelper(
                 sync_service,
                 /*require_sync_feature_enabled=*/true),
-        timeout_duration);
+        timeout_duration_);
   } else if (model_type == syncer::ModelType::HISTORY_DELETE_DIRECTIVES) {
     consent_throttle_ = std::make_unique<unified_consent::ConsentThrottle>(
         unified_consent::UrlKeyedDataCollectionConsentHelper::
             NewPersonalizedDataCollectionConsentHelper(sync_service),
-        timeout_duration);
+        timeout_duration_);
   } else {
     NOTREACHED();
   }
@@ -75,7 +75,7 @@
   enqueued_request_callbacks_.emplace_back(std::move(callback));
   if (!request_processing_timer_.IsRunning()) {
     request_processing_timer_.Start(
-        FROM_HERE, kTimeout,
+        FROM_HERE, timeout_duration_,
         base::BindOnce(&ImageServiceConsentHelper::OnTimeoutExpired,
                        weak_ptr_factory_.GetWeakPtr()));
   }
diff --git a/components/page_image_service/image_service_consent_helper.h b/components/page_image_service/image_service_consent_helper.h
index a3db184..c1433f52 100644
--- a/components/page_image_service/image_service_consent_helper.h
+++ b/components/page_image_service/image_service_consent_helper.h
@@ -70,6 +70,9 @@
   // Consent throttle to be used if sync service is not being directly observed.
   std::unique_ptr<unified_consent::ConsentThrottle> consent_throttle_;
 
+  // The duration to wait before returning some answer back for the request.
+  const base::TimeDelta timeout_duration_;
+
   base::ScopedObservation<syncer::SyncService, syncer::SyncServiceObserver>
       sync_service_observer_{this};
 
diff --git a/components/password_manager/core/browser/browser_save_password_progress_logger_unittest.cc b/components/password_manager/core/browser/browser_save_password_progress_logger_unittest.cc
index 6afeff16..2d839fb 100644
--- a/components/password_manager/core/browser/browser_save_password_progress_logger_unittest.cc
+++ b/components/password_manager/core/browser/browser_save_password_progress_logger_unittest.cc
@@ -61,7 +61,7 @@
     // Add a password field.
     autofill::FormFieldData field;
     field.name = u"password";
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.is_focusable = true;
     field.autocomplete_attribute = "new-password";
     field.unique_renderer_id = autofill::FieldRendererId(10);
@@ -69,7 +69,7 @@
 
     // Add a text field.
     field.name = u"email";
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.is_focusable = false;
     field.unique_renderer_id = autofill::FieldRendererId(42);
     field.value = u"a@example.com";
diff --git a/components/password_manager/core/browser/form_parsing/form_data_parser.cc b/components/password_manager/core/browser/form_parsing/form_data_parser.cc
index 0399681..c29d025 100644
--- a/components/password_manager/core/browser/form_parsing/form_data_parser.cc
+++ b/components/password_manager/core/browser/form_parsing/form_data_parser.cc
@@ -950,7 +950,8 @@
       continue;
     }
 
-    const bool is_password = field.form_control_type == "password";
+    const bool is_password = field.form_control_type ==
+                             autofill::StringToFormControlType("password");
 
     if (!field_value.empty()) {
       std::set<base::StringPiece16>& seen_values =
@@ -1048,7 +1049,8 @@
       significant_fields.accepts_webauthn_credentials;
 
   for (const FormFieldData& field : form_data.fields) {
-    if (field.form_control_type == "password" &&
+    if (field.form_control_type ==
+            autofill::StringToFormControlType("password") &&
         (field.properties_mask & FieldPropertiesFlags::kAutofilled)) {
       result->form_has_autofilled_value = true;
     }
diff --git a/components/password_manager/core/browser/form_parsing/form_data_parser.h b/components/password_manager/core/browser/form_parsing/form_data_parser.h
index 1c0ac96..5ebdf3c 100644
--- a/components/password_manager/core/browser/form_parsing/form_data_parser.h
+++ b/components/password_manager/core/browser/form_parsing/form_data_parser.h
@@ -54,7 +54,8 @@
   // The flag derived from field->autocomplete_attribute.
   AutocompleteFlag autocomplete_flag = AutocompleteFlag::kNone;
 
-  // True if field->form_control_type == "password".
+  // True if field->form_control_type ==
+  // autofill::StringToFormControlType("password").
   bool is_password = false;
 
   // True if field is predicted to be a password.
diff --git a/components/password_manager/core/browser/form_parsing/form_data_parser_unittest.cc b/components/password_manager/core/browser/form_parsing/form_data_parser_unittest.cc
index 5f108b8..be5d4ee 100644
--- a/components/password_manager/core/browser/form_parsing/form_data_parser_unittest.cc
+++ b/components/password_manager/core/browser/form_parsing/form_data_parser_unittest.cc
@@ -283,7 +283,8 @@
         field.name = std::u16string(field_description.name);
       }
       field.name_attribute = field.name;
-      field.form_control_type = field_description.form_control_type;
+      field.form_control_type = autofill::StringToFormControlType(
+          field_description.form_control_type);
       field.is_focusable = field_description.is_focusable;
       field.is_enabled = field_description.is_enabled;
       field.is_readonly = field_description.is_readonly;
diff --git a/components/password_manager/core/browser/form_parsing/fuzzer/form_data_producer.cc b/components/password_manager/core/browser/form_parsing/fuzzer/form_data_producer.cc
index 68fe1d5..fa5e87f 100644
--- a/components/password_manager/core/browser/form_parsing/fuzzer/form_data_producer.cc
+++ b/components/password_manager/core/browser/form_parsing/fuzzer/form_data_producer.cc
@@ -125,8 +125,8 @@
 
   // And finally do the same for all the fields.
   for (size_t i = 0; i < number_of_fields; ++i) {
-    result.fields[i].form_control_type =
-        accessor->ConsumeString(field_params[i].form_control_type_length);
+    result.fields[i].form_control_type = autofill::StringToFormControlType(
+        accessor->ConsumeString(field_params[i].form_control_type_length));
     result.fields[i].autocomplete_attribute =
         accessor->ConsumeString(field_params[i].autocomplete_attribute_length);
     result.fields[i].label =
diff --git a/components/password_manager/core/browser/form_parsing/fuzzer/form_data_proto_producer.cc b/components/password_manager/core/browser/form_parsing/fuzzer/form_data_proto_producer.cc
index b0b0a69d..e6a7b51 100644
--- a/components/password_manager/core/browser/form_parsing/fuzzer/form_data_proto_producer.cc
+++ b/components/password_manager/core/browser/form_parsing/fuzzer/form_data_proto_producer.cc
@@ -47,7 +47,8 @@
     result.fields[i].id_attribute = UTF8ToUTF16(form_data_proto.id());
     result.fields[i].name_attribute = UTF8ToUTF16(form_data_proto.name());
     result.fields[i].is_focusable = form_data_proto.is_focusable();
-    result.fields[i].form_control_type = form_data_proto.form_control_type();
+    result.fields[i].form_control_type =
+        autofill::StringToFormControlType(form_data_proto.form_control_type());
     result.fields[i].autocomplete_attribute =
         form_data_proto.autocomplete_attribute();
     result.fields[i].label = UTF8ToUTF16(form_data_proto.label());
diff --git a/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc b/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
index c01ce440..c0bc1f7 100644
--- a/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
+++ b/components/password_manager/core/browser/form_parsing/password_field_prediction_unittest.cc
@@ -84,7 +84,8 @@
     FormFieldData field;
     field.unique_renderer_id = autofill::FieldRendererId(i + 1000);
     field.name = ASCIIToUTF16(test_fields[i].name);
-    field.form_control_type = test_fields[i].form_control_type;
+    field.form_control_type =
+        autofill::StringToFormControlType(test_fields[i].form_control_type);
 
     AutofillType::ServerPrediction prediction;
     prediction.server_predictions.push_back(
@@ -158,7 +159,8 @@
       FormFieldData field;
       field.unique_renderer_id = autofill::FieldRendererId(i + 1000);
       field.name = ASCIIToUTF16(test_form[i].name);
-      field.form_control_type = test_form[i].form_control_type;
+      field.form_control_type =
+          autofill::StringToFormControlType(test_form[i].form_control_type);
 
       AutofillType::ServerPrediction new_prediction;
       new_prediction.server_predictions = {
diff --git a/components/password_manager/core/browser/form_saver_impl_unittest.cc b/components/password_manager/core/browser/form_saver_impl_unittest.cc
index 0f3993f..6a3801d 100644
--- a/components/password_manager/core/browser/form_saver_impl_unittest.cc
+++ b/components/password_manager/core/browser/form_saver_impl_unittest.cc
@@ -284,7 +284,7 @@
   PasswordForm pending = CreatePending(u"nameofuser", u"wordToP4a55");
   FormFieldData field;
   field.name = u"name";
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.value = u"value";
   field.label = u"label";
   field.placeholder = u"placeholder";
@@ -312,7 +312,8 @@
   ASSERT_EQ(1u, saved.form_data.fields.size());
   const FormFieldData& saved_field = saved.form_data.fields[0];
   EXPECT_EQ(u"name", saved_field.name);
-  EXPECT_EQ("password", saved_field.form_control_type);
+  EXPECT_EQ(autofill::StringToFormControlType("password"),
+            saved_field.form_control_type);
   EXPECT_TRUE(saved_field.value.empty());
   EXPECT_TRUE(saved_field.label.empty());
   EXPECT_TRUE(saved_field.placeholder.empty());
diff --git a/components/password_manager/core/browser/login_database.cc b/components/password_manager/core/browser/login_database.cc
index 7ba56f9..a0ccc8a 100644
--- a/components/password_manager/core/browser/login_database.cc
+++ b/components/password_manager/core/browser/login_database.cc
@@ -1094,6 +1094,21 @@
   return entity_metadata;
 }
 
+LoginDatabase::EncryptionResult DecryptPasswordFromStatement(
+    sql::Statement& s,
+    std::u16string* plaintext_password) {
+  CHECK(plaintext_password);
+  std::string encrypted_password;
+  s.ColumnBlobAsString(COLUMN_PASSWORD_VALUE, &encrypted_password);
+  LoginDatabase::EncryptionResult encryption_result =
+      LoginDatabase::DecryptedString(encrypted_password, plaintext_password);
+  if (encryption_result != LoginDatabase::ENCRYPTION_RESULT_SUCCESS) {
+    LOG(ERROR) << "Password decryption failed, encryption_result is "
+               << encryption_result;
+  }
+  return encryption_result;
+}
+
 }  // namespace
 
 struct LoginDatabase::PrimaryKeyAndPassword {
@@ -1409,7 +1424,7 @@
   const bool success = s.Run();
   if (success) {
     // If success, the row never existed so password was not changed.
-    FillFormInStore(&form_to_add);
+    form_to_add.in_store = GetStore();
     FormPrimaryKey primary_key = FormPrimaryKey(db_.GetLastInsertRowId());
     form_to_add.primary_key = primary_key;
     if (!form_to_add.password_issues.empty()) {
@@ -1431,12 +1446,11 @@
       db_.GetCachedStatement(SQL_FROM_HERE, add_replace_statement_.c_str()));
   BindAddStatement(form_to_add, &s, encrypted_password);
   if (s.Run()) {
+    form_to_add.in_store = GetStore();
     PasswordForm removed_form = form_to_add;
-    FillFormInStore(&removed_form);
     removed_form.primary_key =
         FormPrimaryKey(old_primary_key_password.primary_key);
     list.emplace_back(PasswordStoreChange::REMOVE, removed_form);
-    FillFormInStore(&form_to_add);
 
     FormPrimaryKey primary_key = FormPrimaryKey(db_.GetLastInsertRowId());
     form_to_add.primary_key = primary_key;
@@ -1576,7 +1590,7 @@
                       form.notes);
 
   PasswordStoreChangeList list;
-  FillFormInStore(&form_with_encrypted_password);
+  form_with_encrypted_password.in_store = GetStore();
   form_with_encrypted_password.primary_key =
       FormPrimaryKey(old_primary_key_password.primary_key);
   list.emplace_back(PasswordStoreChange::UPDATE,
@@ -1613,7 +1627,7 @@
   }
   if (changes) {
     PasswordForm removed_form = form;
-    FillFormInStore(&removed_form);
+    removed_form.in_store = GetStore();
     removed_form.primary_key =
         FormPrimaryKey(old_primary_key_password.primary_key);
     changes->emplace_back(PasswordStoreChange::REMOVE, removed_form,
@@ -1634,11 +1648,8 @@
   if (!s1.Step()) {
     return false;
   }
-  PasswordForm form;
-  EncryptionResult result = InitPasswordFormFromStatement(
-      s1, /*decrypt_and_fill_password_value=*/false, &form);
-  DCHECK_EQ(result, ENCRYPTION_RESULT_SUCCESS);
-  DCHECK_EQ(form.primary_key.value(), primary_key);
+  PasswordForm form = GetFormWithoutPasswordFromStatement(s1);
+  CHECK_EQ(form.primary_key.value(), primary_key);
 
 #if BUILDFLAG(IS_IOS)
   DeleteEncryptedPasswordFromKeychain(form.keychain_identifier);
@@ -1651,7 +1662,7 @@
     return false;
   }
   if (changes) {
-    FillFormInStore(&form);
+    form.in_store = GetStore();
     changes->emplace_back(PasswordStoreChange::REMOVE, std::move(form),
                           /*password_changed=*/true);
   }
@@ -1666,7 +1677,7 @@
   if (changes) {
     changes->clear();
   }
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   ScopedTransaction transaction(this);
   if (!GetLoginsCreatedBetween(delete_begin, delete_end, &forms)) {
     return false;
@@ -1675,7 +1686,7 @@
 #if BUILDFLAG(IS_IOS)
   base::Time start = base::Time::Now();
   for (const auto& form : forms) {
-    DeleteEncryptedPasswordFromKeychain(form->keychain_identifier);
+    DeleteEncryptedPasswordFromKeychain(form.keychain_identifier);
   }
   base::UmaHistogramMediumTimes(
       "PasswordManager.PasswordStoreBuiltInBackend.RemoveLoginsCreatedBetween."
@@ -1695,18 +1706,17 @@
   }
   if (changes) {
     for (auto& form : forms) {
-      changes->emplace_back(PasswordStoreChange::REMOVE, *form,
+      changes->emplace_back(PasswordStoreChange::REMOVE, std::move(form),
                             /*password_changed=*/true);
     }
   }
   return true;
 }
 
-bool LoginDatabase::GetAutoSignInLogins(
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+bool LoginDatabase::GetAutoSignInLogins(std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetAutoSignInLogins");
-  DCHECK(forms);
-  DCHECK(!autosignin_statement_.empty());
+  CHECK(forms);
+  CHECK(!autosignin_statement_.empty());
   forms->clear();
 
   sql::Statement s(
@@ -1726,98 +1736,84 @@
   return s.Run();
 }
 
-LoginDatabase::EncryptionResult LoginDatabase::InitPasswordFormFromStatement(
-    sql::Statement& s,
-    bool decrypt_and_fill_password_value,
-    PasswordForm* form) const {
-  std::string encrypted_password;
-  s.ColumnBlobAsString(COLUMN_PASSWORD_VALUE, &encrypted_password);
-  std::u16string decrypted_password;
-  if (decrypt_and_fill_password_value) {
-    EncryptionResult encryption_result =
-        DecryptedString(encrypted_password, &decrypted_password);
-    if (encryption_result != ENCRYPTION_RESULT_SUCCESS) {
-      VLOG(0) << "Password decryption failed, encryption_result is "
-              << encryption_result;
-      return encryption_result;
-    }
-  }
-
-  form->primary_key = FormPrimaryKey(s.ColumnInt(COLUMN_ID));
+PasswordForm LoginDatabase::GetFormWithoutPasswordFromStatement(
+    sql::Statement& s) const {
+  PasswordForm form;
+  form.primary_key = FormPrimaryKey(s.ColumnInt(COLUMN_ID));
   std::string tmp = s.ColumnString(COLUMN_ORIGIN_URL);
-  form->url = GURL(tmp);
+  form.url = GURL(tmp);
   tmp = s.ColumnString(COLUMN_ACTION_URL);
-  form->action = GURL(tmp);
-  form->username_element = s.ColumnString16(COLUMN_USERNAME_ELEMENT);
-  form->username_value = s.ColumnString16(COLUMN_USERNAME_VALUE);
-  form->password_element = s.ColumnString16(COLUMN_PASSWORD_ELEMENT);
-  form->password_value = decrypted_password;
-  s.ColumnBlobAsString(COLUMN_KEYCHAIN_IDENTIFIER, &form->keychain_identifier);
-  form->submit_element = s.ColumnString16(COLUMN_SUBMIT_ELEMENT);
+  form.action = GURL(tmp);
+  form.username_element = s.ColumnString16(COLUMN_USERNAME_ELEMENT);
+  form.username_value = s.ColumnString16(COLUMN_USERNAME_VALUE);
+  form.password_element = s.ColumnString16(COLUMN_PASSWORD_ELEMENT);
+  s.ColumnBlobAsString(COLUMN_KEYCHAIN_IDENTIFIER, &form.keychain_identifier);
+  form.submit_element = s.ColumnString16(COLUMN_SUBMIT_ELEMENT);
   tmp = s.ColumnString(COLUMN_SIGNON_REALM);
-  form->signon_realm = tmp;
-  form->date_created = s.ColumnTime(COLUMN_DATE_CREATED);
-  form->blocked_by_user = (s.ColumnInt(COLUMN_BLOCKLISTED_BY_USER) > 0);
+  form.signon_realm = tmp;
+  form.date_created = s.ColumnTime(COLUMN_DATE_CREATED);
+  form.blocked_by_user = (s.ColumnInt(COLUMN_BLOCKLISTED_BY_USER) > 0);
   // TODO(crbug.com/1151214): Add metrics to capture how often these values fall
   // out of the valid enum range.
-  form->scheme = static_cast<PasswordForm::Scheme>(s.ColumnInt(COLUMN_SCHEME));
-  form->type =
+  form.scheme = static_cast<PasswordForm::Scheme>(s.ColumnInt(COLUMN_SCHEME));
+  form.type =
       static_cast<PasswordForm::Type>(s.ColumnInt(COLUMN_PASSWORD_TYPE));
   base::span<const uint8_t> possible_username_pairs_blob =
       s.ColumnBlob(COLUMN_POSSIBLE_USERNAME_PAIRS);
   if (!possible_username_pairs_blob.empty()) {
     base::Pickle pickle = PickleFromSpan(possible_username_pairs_blob);
-    form->all_alternative_usernames =
+    form.all_alternative_usernames =
         DeserializeAlternativeElementVector(pickle);
   }
-  form->times_used_in_html_form = s.ColumnInt(COLUMN_TIMES_USED);
+  form.times_used_in_html_form = s.ColumnInt(COLUMN_TIMES_USED);
   base::span<const uint8_t> form_data_blob = s.ColumnBlob(COLUMN_FORM_DATA);
   if (!form_data_blob.empty()) {
     base::Pickle form_data_pickle = PickleFromSpan(form_data_blob);
     base::PickleIterator form_data_iter(form_data_pickle);
-    autofill::DeserializeFormData(&form_data_iter, &form->form_data);
+    autofill::DeserializeFormData(&form_data_iter, &form.form_data);
   }
-  form->display_name = s.ColumnString16(COLUMN_DISPLAY_NAME);
-  form->icon_url = GURL(s.ColumnString(COLUMN_ICON_URL));
-  form->federation_origin =
+  form.display_name = s.ColumnString16(COLUMN_DISPLAY_NAME);
+  form.icon_url = GURL(s.ColumnString(COLUMN_ICON_URL));
+  form.federation_origin =
       url::Origin::Create(GURL(s.ColumnString(COLUMN_FEDERATION_URL)));
-  form->skip_zero_click = (s.ColumnInt(COLUMN_SKIP_ZERO_CLICK) > 0);
-  form->generation_upload_status =
+  form.skip_zero_click = (s.ColumnInt(COLUMN_SKIP_ZERO_CLICK) > 0);
+  form.generation_upload_status =
       static_cast<PasswordForm::GenerationUploadStatus>(
           s.ColumnInt(COLUMN_GENERATION_UPLOAD_STATUS));
-  form->date_last_used = s.ColumnTime(COLUMN_DATE_LAST_USED);
+  form.date_last_used = s.ColumnTime(COLUMN_DATE_LAST_USED);
   base::span<const uint8_t> moving_blocked_for_blob =
       s.ColumnBlob(COLUMN_MOVING_BLOCKED_FOR);
   if (!moving_blocked_for_blob.empty()) {
     base::Pickle pickle = PickleFromSpan(moving_blocked_for_blob);
-    form->moving_blocked_for_list = DeserializeGaiaIdHashVector(pickle);
+    form.moving_blocked_for_list = DeserializeGaiaIdHashVector(pickle);
   }
-  form->date_password_modified = s.ColumnTime(COLUMN_DATE_PASSWORD_MODIFIED);
-  form->sender_email = s.ColumnString16(COLUMN_SENDER_EMAIL);
-  form->sender_name = s.ColumnString16(COLUMN_SENDER_NAME);
-  form->date_received = s.ColumnTime(COLUMN_DATE_RECEIVED);
-  form->sharing_notification_displayed =
+  form.date_password_modified = s.ColumnTime(COLUMN_DATE_PASSWORD_MODIFIED);
+  form.sender_email = s.ColumnString16(COLUMN_SENDER_EMAIL);
+  form.sender_name = s.ColumnString16(COLUMN_SENDER_NAME);
+  form.date_received = s.ColumnTime(COLUMN_DATE_RECEIVED);
+  form.sharing_notification_displayed =
       s.ColumnBool(COLUMN_SHARING_NOTIFICATION_DISPLAYED);
-  PopulateFormWithPasswordIssues(form);
-  PopulateFormWithNotes(form);
 
-  return ENCRYPTION_RESULT_SUCCESS;
+  CHECK(form.primary_key.has_value());
+  form.password_issues = GetPasswordIssues(form.primary_key.value());
+  form.notes = GetPasswordNotes(form.primary_key.value());
+
+  return form;
 }
 
-bool LoginDatabase::GetLogins(
-    const PasswordFormDigest& form,
-    bool should_PSL_matching_apply,
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+bool LoginDatabase::GetLogins(const PasswordFormDigest& form,
+                              bool should_PSL_matching_apply,
+                              std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetLogins");
-  DCHECK(forms);
+  CHECK(forms);
   forms->clear();
 
   const bool should_federated_apply =
       form.scheme == PasswordForm::Scheme::kHtml;
-  DCHECK(!get_statement_.empty());
-  DCHECK(!get_statement_psl_.empty());
-  DCHECK(!get_statement_federated_.empty());
-  DCHECK(!get_statement_psl_federated_.empty());
+  CHECK(!get_statement_.empty());
+  CHECK(!get_statement_psl_.empty());
+  CHECK(!get_statement_federated_.empty());
+  CHECK(!get_statement_psl_federated_.empty());
   const std::string* sql_query = &get_statement_;
   if (should_PSL_matching_apply && should_federated_apply) {
     sql_query = &get_statement_psl_federated_;
@@ -1859,13 +1855,12 @@
   return true;
 }
 
-bool LoginDatabase::GetLoginsCreatedBetween(
-    const base::Time begin,
-    const base::Time end,
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+bool LoginDatabase::GetLoginsCreatedBetween(const base::Time begin,
+                                            const base::Time end,
+                                            std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetLoginsCreatedBetween");
-  DCHECK(forms);
-  DCHECK(!created_statement_.empty());
+  CHECK(forms);
+  CHECK(!created_statement_.empty());
   sql::Statement s(
       db_.GetCachedStatement(SQL_FROM_HERE, created_statement_.c_str()));
   s.BindTime(0, begin);
@@ -1875,7 +1870,7 @@
 }
 
 FormRetrievalResult LoginDatabase::GetAllLogins(
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+    std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetAllLogins");
   DCHECK(forms);
   forms->clear();
@@ -1889,7 +1884,7 @@
 FormRetrievalResult LoginDatabase::GetLoginsBySignonRealmAndUsername(
     const std::string& signon_realm,
     const std::u16string& username,
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+    std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetLoginsBySignonRealmAndUsername");
   forms->clear();
 
@@ -1901,21 +1896,19 @@
   return StatementToForms(&s, nullptr, forms);
 }
 
-bool LoginDatabase::GetAutofillableLogins(
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+bool LoginDatabase::GetAutofillableLogins(std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetAutofillableLogins");
   return GetAllLoginsWithBlocklistSetting(false, forms);
 }
 
-bool LoginDatabase::GetBlocklistLogins(
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+bool LoginDatabase::GetBlocklistLogins(std::vector<PasswordForm>* forms) {
   TRACE_EVENT0("passwords", "LoginDatabase::GetBlocklistLogins");
   return GetAllLoginsWithBlocklistSetting(true, forms);
 }
 
 bool LoginDatabase::GetAllLoginsWithBlocklistSetting(
     bool blocklisted,
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+    std::vector<PasswordForm>* forms) {
   DCHECK(forms);
   DCHECK(!blocklisted_statement_.empty());
   forms->clear();
@@ -2352,16 +2345,14 @@
 FormRetrievalResult LoginDatabase::StatementToForms(
     sql::Statement* statement,
     const PasswordFormDigest* matched_form,
-    std::vector<std::unique_ptr<PasswordForm>>* forms) {
+    std::vector<PasswordForm>* forms) {
   DCHECK(forms);
   forms->clear();
   bool has_service_failure = false;
   while (statement->Step()) {
-    auto new_form = std::make_unique<PasswordForm>();
-    FillFormInStore(new_form.get());
-
-    EncryptionResult result = InitPasswordFormFromStatement(
-        *statement, /*decrypt_and_fill_password_value=*/true, new_form.get());
+    std::u16string plaintext_password;
+    EncryptionResult result =
+        DecryptPasswordFromStatement(*statement, &plaintext_password);
     if (result == ENCRYPTION_RESULT_SERVICE_FAILURE) {
       has_service_failure = true;
       continue;
@@ -2371,12 +2362,16 @@
     }
     DCHECK_EQ(ENCRYPTION_RESULT_SUCCESS, result);
 
+    PasswordForm form = GetFormWithoutPasswordFromStatement(*statement);
+    form.password_value = std::move(plaintext_password);
+    form.in_store = GetStore();
+
     if (matched_form &&
-        GetMatchResult(*new_form, *matched_form) == MatchResult::NO_MATCH) {
+        GetMatchResult(form, *matched_form) == MatchResult::NO_MATCH) {
       continue;
     }
 
-    forms->emplace_back(std::move(new_form));
+    forms->push_back(std::move(form));
   }
 
   if (!statement->Succeeded()) {
@@ -2459,22 +2454,22 @@
       all_unique_key_column_names;
 }
 
-void LoginDatabase::FillFormInStore(PasswordForm* form) const {
-  form->in_store = is_account_store() ? PasswordForm::Store::kAccountStore
-                                      : PasswordForm::Store::kProfileStore;
+PasswordForm::Store LoginDatabase::GetStore() const {
+  return is_account_store() ? PasswordForm::Store::kAccountStore
+                            : PasswordForm::Store::kProfileStore;
 }
 
-void LoginDatabase::PopulateFormWithPasswordIssues(PasswordForm* form) const {
-  DCHECK(form->primary_key.has_value());
+base::flat_map<InsecureType, InsecurityMetadata>
+LoginDatabase::GetPasswordIssues(FormPrimaryKey primary_key) const {
   std::vector<InsecureCredential> insecure_credentials =
-      insecure_credentials_table_.GetRows(form->primary_key.value());
+      insecure_credentials_table_.GetRows(primary_key);
   base::flat_map<InsecureType, InsecurityMetadata> issues;
   for (const auto& insecure_credential : insecure_credentials) {
     issues[insecure_credential.insecure_type] = InsecurityMetadata(
         insecure_credential.create_time, insecure_credential.is_muted,
         insecure_credential.trigger_notification_from_backend);
   }
-  form->password_issues = std::move(issues);
+  return issues;
 }
 
 InsecureCredentialsChanged LoginDatabase::UpdateInsecureCredentials(
@@ -2500,13 +2495,12 @@
   return InsecureCredentialsChanged(changed);
 }
 
-void LoginDatabase::PopulateFormWithNotes(PasswordForm* form) const {
-  DCHECK(form->primary_key.has_value());
+std::vector<PasswordNote> LoginDatabase::GetPasswordNotes(
+    FormPrimaryKey primary_key) const {
   if (!base::FeatureList::IsEnabled(syncer::kPasswordNotesWithBackup)) {
-    return;
+    return {};
   }
-  form->notes =
-      password_notes_table_.GetPasswordNotes(form->primary_key.value());
+  return password_notes_table_.GetPasswordNotes(primary_key);
 }
 
 void LoginDatabase::UpdatePasswordNotes(
diff --git a/components/password_manager/core/browser/login_database.h b/components/password_manager/core/browser/login_database.h
index 0c730a1..8d10f4b 100644
--- a/components/password_manager/core/browser/login_database.h
+++ b/components/password_manager/core/browser/login_database.h
@@ -114,41 +114,36 @@
   // and federated credentials.
   // |should_PSL_matching_apply| controls if the PSL matches are included or
   // only the exact matches.
-  [[nodiscard]] bool GetLogins(
-      const PasswordFormDigest& form,
-      bool should_PSL_matching_apply,
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  [[nodiscard]] bool GetLogins(const PasswordFormDigest& form,
+                               bool should_PSL_matching_apply,
+                               std::vector<PasswordForm>* forms);
 
   // Gets all logins created from |begin| onwards (inclusive) and before |end|.
   // You may use a null Time value to do an unbounded search in either
   // direction. |forms| must not be null and will be used to return
   // the results.
-  [[nodiscard]] bool GetLoginsCreatedBetween(
-      base::Time begin,
-      base::Time end,
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  [[nodiscard]] bool GetLoginsCreatedBetween(base::Time begin,
+                                             base::Time end,
+                                             std::vector<PasswordForm>* forms);
 
   // Gets the complete list of all credentials.
   [[nodiscard]] FormRetrievalResult GetAllLogins(
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+      std::vector<PasswordForm>* forms);
 
   // Gets list of logins which match |signon_realm| and |username|.
   [[nodiscard]] FormRetrievalResult GetLoginsBySignonRealmAndUsername(
       const std::string& signon_realm,
       const std::u16string& username,
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+      std::vector<PasswordForm>* forms);
 
   // Gets the complete list of not blocklisted credentials.
-  [[nodiscard]] bool GetAutofillableLogins(
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  [[nodiscard]] bool GetAutofillableLogins(std::vector<PasswordForm>* forms);
 
   // Gets the complete list of blocklisted credentials.
-  [[nodiscard]] bool GetBlocklistLogins(
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  [[nodiscard]] bool GetBlocklistLogins(std::vector<PasswordForm>* forms);
 
   // Gets the list of auto-sign-inable credentials.
-  [[nodiscard]] bool GetAutoSignInLogins(
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  [[nodiscard]] bool GetAutoSignInLogins(std::vector<PasswordForm>* forms);
 
   // Deletes the login database file on disk, and creates a new, empty database.
   // This can be used after migrating passwords to some other store, to ensure
@@ -291,23 +286,17 @@
   void ReportDuplicateCredentialsMetrics();
 
   // Fills |form| from the values in the given statement (which is assumed to be
-  // of the form used by the Get*Logins methods). If
-  // |decrypt_and_fill_password_value| is set to true, it tries to decrypt the
-  // stored password and returns the EncryptionResult from decrypting the
-  // password in |s|; if not ENCRYPTION_RESULT_SUCCESS, |form| is not filled. If
-  // |decrypt_and_fill_password_value| is set to false, it always returns
-  // ENCRYPTION_RESULT_SUCCESS.
-  [[nodiscard]] EncryptionResult InitPasswordFormFromStatement(
-      sql::Statement& s,
-      bool decrypt_and_fill_password_value,
-      PasswordForm* form) const;
+  // of the form used by the Get*Logins methods).
+  // WARNING: Password value itself is absent, callers have to decrypt it if
+  // needed.
+  [[nodiscard]] PasswordForm GetFormWithoutPasswordFromStatement(
+      sql::Statement& s) const;
 
   // Gets all blocklisted or all non-blocklisted (depending on |blocklisted|)
   // credentials. On success returns true and overwrites |forms| with the
   // result.
-  bool GetAllLoginsWithBlocklistSetting(
-      bool blocklisted,
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+  bool GetAllLoginsWithBlocklistSetting(bool blocklisted,
+                                        std::vector<PasswordForm>* forms);
 
   // Returns the DB primary key for the specified |form| and decrypted/encrypted
   // password. Returns {-1, "", ""} if the row for this |form| is not found.
@@ -324,19 +313,19 @@
   [[nodiscard]] FormRetrievalResult StatementToForms(
       sql::Statement* statement,
       const PasswordFormDigest* matched_form,
-      std::vector<std::unique_ptr<PasswordForm>>* forms);
+      std::vector<PasswordForm>* forms);
 
   // Initializes all the *_statement_ data members with appropriate SQL
   // fragments based on |builder|.
   void InitializeStatementStrings(const SQLTableBuilder& builder);
 
-  // Sets the `in_store` member of `form` to either kProfileStore or
-  // kAccountStore depending on the value of `is_account_store_`.
-  void FillFormInStore(PasswordForm* form) const;
+  // Returns either kProfileStore or kAccountStore depending on the value of
+  // `is_account_store_`.
+  PasswordForm::Store GetStore() const;
 
-  // Reads the insecure credentials corresponding to the `form->primary_key`
-  // from the database and fills them into `form->password_issues`.
-  void PopulateFormWithPasswordIssues(PasswordForm* form) const;
+  // Returns insecure credentials corresponding to the `primary_key`.
+  base::flat_map<InsecureType, InsecurityMetadata> GetPasswordIssues(
+      FormPrimaryKey primary_key) const;
 
   // Updates data in the `insecure_credentials_table_` with the password issues
   // data from `password_issues`. Returns whether any insecure credential entry
@@ -345,10 +334,8 @@
       FormPrimaryKey primary_key,
       const base::flat_map<InsecureType, InsecurityMetadata>& password_issues);
 
-  // Reads the `password_notes` table for the notes with `form->primary_key` and
-  // fills the `form->notes` field. If there are no notes for
-  // `form->primary_key`, the form is set to empty notes.
-  void PopulateFormWithNotes(PasswordForm* form) const;
+  // Returns password notes corresponding to `primary_key`.
+  std::vector<PasswordNote> GetPasswordNotes(FormPrimaryKey primary_key) const;
 
   // Updates the `password_notes` table if `notes` changed for `primary_key`.
   void UpdatePasswordNotes(FormPrimaryKey primary_key,
diff --git a/components/password_manager/core/browser/login_database_async_helper.cc b/components/password_manager/core/browser/login_database_async_helper.cc
index b9a0d9c..023139c 100644
--- a/components/password_manager/core/browser/login_database_async_helper.cc
+++ b/components/password_manager/core/browser/login_database_async_helper.cc
@@ -22,6 +22,16 @@
 
 constexpr base::TimeDelta kSyncTaskTimeout = base::Seconds(30);
 
+std::vector<std::unique_ptr<PasswordForm>> ConvertToUniquePtr(
+    std::vector<PasswordForm> forms) {
+  std::vector<std::unique_ptr<PasswordForm>> result;
+  result.reserve(forms.size());
+  for (auto& form : forms) {
+    result.push_back(std::make_unique<PasswordForm>(std::move(form)));
+  }
+  return result;
+}
+
 }  // namespace
 
 LoginDatabaseAsyncHelper::LoginDatabaseAsyncHelper(
@@ -98,7 +108,7 @@
 
 LoginsResultOrError LoginDatabaseAsyncHelper::GetAllLogins() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
 
   if (!login_db_) {
     return PasswordStoreBackendError(
@@ -112,27 +122,27 @@
         PasswordStoreBackendErrorType::kUncategorized,
         PasswordStoreBackendErrorRecoveryType::kUnrecoverable);
   }
-  return forms;
+  return ConvertToUniquePtr(std::move(forms));
 }
 
 LoginsResultOrError LoginDatabaseAsyncHelper::GetAutofillableLogins() {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  std::vector<std::unique_ptr<PasswordForm>> results;
+  std::vector<PasswordForm> results;
   if (!login_db_ || !login_db_->GetAutofillableLogins(&results)) {
     return PasswordStoreBackendError(
         PasswordStoreBackendErrorType::kUncategorized,
         PasswordStoreBackendErrorRecoveryType::kUnrecoverable);
   }
-  return results;
+  return ConvertToUniquePtr(std::move(results));
 }
 
 LoginsResultOrError LoginDatabaseAsyncHelper::FillMatchingLogins(
     const std::vector<PasswordFormDigest>& forms,
     bool include_psl) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  std::vector<std::unique_ptr<PasswordForm>> results;
+  std::vector<PasswordForm> results;
   for (const auto& form : forms) {
-    std::vector<std::unique_ptr<PasswordForm>> matched_forms;
+    std::vector<PasswordForm> matched_forms;
     if (!login_db_ || !login_db_->GetLogins(form, include_psl, &matched_forms))
       return PasswordStoreBackendError(
           PasswordStoreBackendErrorType::kUncategorized,
@@ -141,7 +151,7 @@
                    std::make_move_iterator(matched_forms.begin()),
                    std::make_move_iterator(matched_forms.end()));
   }
-  return results;
+  return ConvertToUniquePtr(std::move(results));
 }
 
 PasswordChangesOrError LoginDatabaseAsyncHelper::AddLogin(
@@ -233,15 +243,15 @@
     base::OnceCallback<void(bool)> sync_completion) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   BeginTransaction();
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   PasswordStoreChangeList changes;
   bool success = login_db_ && login_db_->GetLoginsCreatedBetween(
                                   delete_begin, delete_end, &forms);
   if (success) {
     for (const auto& form : forms) {
       PasswordStoreChangeList remove_changes;
-      if (url_filter.Run(form->url) &&
-          login_db_->RemoveLogin(*form, &remove_changes)) {
+      if (url_filter.Run(form.url) &&
+          login_db_->RemoveLogin(form, &remove_changes)) {
         std::move(remove_changes.begin(), remove_changes.end(),
                   std::back_inserter(changes));
       }
@@ -281,15 +291,16 @@
 PasswordStoreChangeList LoginDatabaseAsyncHelper::DisableAutoSignInForOrigins(
     const base::RepeatingCallback<bool(const GURL&)>& origin_filter) {
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   PasswordStoreChangeList changes;
   if (!login_db_ || !login_db_->GetAutoSignInLogins(&forms))
     return changes;
 
   std::set<GURL> origins_to_update;
   for (const auto& form : forms) {
-    if (origin_filter.Run(form->url))
-      origins_to_update.insert(form->url);
+    if (origin_filter.Run(form.url)) {
+      origins_to_update.insert(form.url);
+    }
   }
 
   std::set<GURL> origins_updated;
@@ -299,8 +310,8 @@
   }
 
   for (const auto& form : forms) {
-    if (origins_updated.count(form->url)) {
-      changes.emplace_back(PasswordStoreChange::UPDATE, *form);
+    if (origins_updated.count(form.url)) {
+      changes.emplace_back(PasswordStoreChange::UPDATE, std::move(form));
     }
   }
   return changes;
@@ -420,14 +431,14 @@
   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
   if (!login_db_)
     return FormRetrievalResult::kDbError;
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   FormRetrievalResult result = login_db_->GetAllLogins(&forms);
   for (const auto& form : forms) {
-    DCHECK(form->primary_key.has_value());
+    CHECK(form.primary_key.has_value());
     key_to_specifics_map->emplace(
-        form->primary_key->value(),
+        form.primary_key->value(),
         std::make_unique<sync_pb::PasswordSpecificsData>(
-            SpecificsDataFromPassword(*form, /*base_password_data=*/{})));
+            SpecificsDataFromPassword(form, /*base_password_data=*/{})));
   }
 
   return result;
diff --git a/components/password_manager/core/browser/login_database_ios_unittest.cc b/components/password_manager/core/browser/login_database_ios_unittest.cc
index de45156..f8b6a60 100644
--- a/components/password_manager/core/browser/login_database_ios_unittest.cc
+++ b/components/password_manager/core/browser/login_database_ios_unittest.cc
@@ -135,11 +135,11 @@
 
   ASSERT_THAT(login_db_->UpdateLogin(form), testing::SizeIs(1));
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   EXPECT_TRUE(login_db_->GetLogins(PasswordFormDigest(form), true, &forms));
 
   ASSERT_EQ(1U, forms.size());
-  std::string keychain_identifier = forms[0]->keychain_identifier;
+  std::string keychain_identifier = forms[0].keychain_identifier;
   ASSERT_FALSE(keychain_identifier.empty());
 
   std::u16string password_value;
@@ -202,15 +202,15 @@
 
   PasswordFormDigest form = {PasswordForm::Scheme::kHtml,
                              "http://www.example.com", GURL()};
-  std::vector<std::unique_ptr<PasswordForm>> logins;
+  std::vector<PasswordForm> logins;
   EXPECT_TRUE(login_db_->GetLogins(form, true, &logins));
   ASSERT_EQ(3U, logins.size());
   // Verify that for each password exist a keychain item with a password.
   for (const auto& login : logins) {
     std::u16string password_value;
     EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
-                                 login->keychain_identifier, &password_value));
-    EXPECT_EQ(login->password_value, password_value);
+                                 login.keychain_identifier, &password_value));
+    EXPECT_EQ(login.password_value, password_value);
   }
 
   login_db_->RemoveLoginsCreatedBetween(base::Time::FromDoubleT(150),
@@ -218,28 +218,26 @@
                                         /*changes=*/nullptr);
 
   // Verify that one password is removed.
-  std::vector<std::unique_ptr<PasswordForm>> remaining_logins;
+  std::vector<PasswordForm> remaining_logins;
   EXPECT_TRUE(login_db_->GetLogins(form, true, &remaining_logins));
-  EXPECT_THAT(remaining_logins, testing::UnorderedElementsAre(
-                                    testing::Pointee(testing::Eq(forms[0])),
-                                    testing::Pointee(testing::Eq(forms[2]))));
+  EXPECT_THAT(remaining_logins,
+              testing::UnorderedElementsAre(testing::Eq(forms[0]),
+                                            testing::Eq(forms[2])));
 
   // Verify that keychain entry is removed.
   std::u16string password_value;
-  EXPECT_EQ(errSecSuccess,
-            GetTextFromKeychainIdentifier(logins[0]->keychain_identifier,
-                                          &password_value));
+  EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
+                               logins[0].keychain_identifier, &password_value));
   EXPECT_EQ(errSecItemNotFound,
-            GetTextFromKeychainIdentifier(logins[1]->keychain_identifier,
+            GetTextFromKeychainIdentifier(logins[1].keychain_identifier,
                                           &password_value));
-  EXPECT_EQ(errSecSuccess,
-            GetTextFromKeychainIdentifier(logins[2]->keychain_identifier,
-                                          &password_value));
+  EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
+                               logins[2].keychain_identifier, &password_value));
 
   // Clear item from the keychain to ensure this test doesn't affect other
   // tests.
-  DeleteEncryptedPasswordFromKeychain(logins[0]->keychain_identifier);
-  DeleteEncryptedPasswordFromKeychain(logins[2]->keychain_identifier);
+  DeleteEncryptedPasswordFromKeychain(logins[0].keychain_identifier);
+  DeleteEncryptedPasswordFromKeychain(logins[2].keychain_identifier);
 }
 
 TEST_F(LoginDatabaseIOSTest, DeleteAndRecreateDatabaseFile) {
@@ -271,39 +269,39 @@
 
   PasswordFormDigest form = {PasswordForm::Scheme::kHtml,
                              "http://www.example.com", GURL()};
-  std::vector<std::unique_ptr<PasswordForm>> logins;
+  std::vector<PasswordForm> logins;
   EXPECT_TRUE(login_db_->GetLogins(form, true, &logins));
   ASSERT_EQ(3U, logins.size());
   // Verify that for each password exist a keychain item with a password.
   for (const auto& login : logins) {
     std::u16string password_value;
     EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
-                                 login->keychain_identifier, &password_value));
-    EXPECT_EQ(login->password_value, password_value);
+                                 login.keychain_identifier, &password_value));
+    EXPECT_EQ(login.password_value, password_value);
   }
 
   // Delete one keychain item to verify that nothing happens when trying to
   // delete it again.
-  DeleteEncryptedPasswordFromKeychain(logins[0]->keychain_identifier);
+  DeleteEncryptedPasswordFromKeychain(logins[0].keychain_identifier);
 
   base::HistogramTester histogram_tester;
   login_db_->DeleteAndRecreateDatabaseFile();
 
   // Verify that all passwords are gone.
-  std::vector<std::unique_ptr<PasswordForm>> remaining_logins;
+  std::vector<PasswordForm> remaining_logins;
   EXPECT_TRUE(login_db_->GetLogins(form, true, &remaining_logins));
   EXPECT_THAT(remaining_logins, testing::IsEmpty());
 
   // Verify that keychain entry is removed.
   std::u16string password_value;
   EXPECT_EQ(errSecItemNotFound,
-            GetTextFromKeychainIdentifier(logins[0]->keychain_identifier,
+            GetTextFromKeychainIdentifier(logins[0].keychain_identifier,
                                           &password_value));
   EXPECT_EQ(errSecItemNotFound,
-            GetTextFromKeychainIdentifier(logins[1]->keychain_identifier,
+            GetTextFromKeychainIdentifier(logins[1].keychain_identifier,
                                           &password_value));
   EXPECT_EQ(errSecItemNotFound,
-            GetTextFromKeychainIdentifier(logins[2]->keychain_identifier,
+            GetTextFromKeychainIdentifier(logins[2].keychain_identifier,
                                           &password_value));
 
   EXPECT_THAT(
@@ -434,7 +432,7 @@
   AddItemToKeychain(u"password note", note_keychain_identifier);
 
   CreateDatabase("login_db_v38_with_keychain_id.sql");
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   {
     // Assert that the database was successfully opened and updated to current
     // version.
@@ -451,11 +449,11 @@
     EXPECT_EQ(db.GetAllLogins(&forms), FormRetrievalResult::kSuccess);
     // Verify that |encrypted_password| is still corresponding to keychain
     // identifier.
-    EXPECT_EQ(forms[0]->keychain_identifier, password_keychain_identifier);
-    EXPECT_EQ(forms[0]->password_value, u"test1");
+    EXPECT_EQ(forms[0].keychain_identifier, password_keychain_identifier);
+    EXPECT_EQ(forms[0].password_value, u"test1");
     // Verify that the password note is still readable.
-    ASSERT_EQ(forms[0]->notes.size(), 1u);
-    EXPECT_EQ(forms[0]->notes[0].value, u"password note");
+    ASSERT_EQ(forms[0].notes.size(), 1u);
+    EXPECT_EQ(forms[0].notes[0].value, u"password note");
   }
   {
     // Verify that password_value in the database is now encrypted with OSCrypt
@@ -614,10 +612,10 @@
   LoginDatabase login_db(get_database_path(), IsAccountStore(false));
   ASSERT_TRUE(login_db.Init());
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   EXPECT_EQ(login_db.GetAllLogins(&forms), FormRetrievalResult::kSuccess);
   EXPECT_EQ(1u, forms.size());
-  EXPECT_EQ(u"password", forms[0]->password_value);
+  EXPECT_EQ(u"password", forms[0].password_value);
 
   ExpectSuccessMetricsRecorded(histogram_tester, IsAccountStore(false));
   histogram_tester.ExpectUniqueSample(
@@ -642,7 +640,7 @@
   AddItemToKeychain(u"password note", note_keychain_identifier);
 
   CreateDatabase("login_db_v39_with_note_keychain_id.sql");
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   {
     // Assert that the database was successfully opened and updated to current
     // version.
@@ -668,8 +666,8 @@
     EXPECT_EQ(login_db.GetAllLogins(&forms), FormRetrievalResult::kSuccess);
     ASSERT_EQ(forms.size(), 1u);
     // Verify that the password note is still readable.
-    ASSERT_EQ(forms[0]->notes.size(), 1u);
-    EXPECT_EQ(forms[0]->notes[0].value, u"password note");
+    ASSERT_EQ(forms[0].notes.size(), 1u);
+    EXPECT_EQ(forms[0].notes[0].value, u"password note");
   }
   {
     // Verify that note value in the database is now encrypted with OSCrypt and
@@ -694,7 +692,7 @@
   CreateDatabase("login_db_v39_with_note_keychain_id.sql");
   ReplaceNoteValue(note_keychain_identifier);
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   {
     // Assert that the database was successfully opened and updated to current
     // version.
@@ -720,8 +718,8 @@
     EXPECT_EQ(login_db.GetAllLogins(&forms), FormRetrievalResult::kSuccess);
     ASSERT_EQ(forms.size(), 1u);
     // Verify that the password note is still readable.
-    ASSERT_EQ(forms[0]->notes.size(), 1u);
-    EXPECT_EQ(forms[0]->notes[0].value, u"test_note");
+    ASSERT_EQ(forms[0].notes.size(), 1u);
+    EXPECT_EQ(forms[0].notes[0].value, u"test_note");
   }
   {
     // Verify that note value in the database is now encrypted with OSCrypt and
diff --git a/components/password_manager/core/browser/login_database_unittest.cc b/components/password_manager/core/browser/login_database_unittest.cc
index e592b34..e856c730 100644
--- a/components/password_manager/core/browser/login_database_unittest.cc
+++ b/components/password_manager/core/browser/login_database_unittest.cc
@@ -280,11 +280,10 @@
   blocklisted.in_store = PasswordForm::Store::kProfileStore;
   ASSERT_EQ(AddChangeForForm(blocklisted), db().AddLogin(blocklisted));
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   EXPECT_EQ(db().GetAllLogins(&forms), FormRetrievalResult::kSuccess);
-  EXPECT_THAT(forms, UnorderedElementsAre(
-                         Pointee(HasPrimaryKeyAndEquals(form)),
-                         Pointee(HasPrimaryKeyAndEquals(blocklisted))));
+  EXPECT_THAT(forms, UnorderedElementsAre(HasPrimaryKeyAndEquals(form),
+                                          HasPrimaryKeyAndEquals(blocklisted)));
 }
 
 TEST_F(LoginDatabaseTest, GetLogins_Self) {
@@ -292,10 +291,10 @@
   ASSERT_EQ(AddChangeForForm(form), db().AddLogin(form));
 
   // Match against an exact copy.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/false, &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, GetLogins_InexactCopy) {
@@ -307,10 +306,10 @@
       GURL("http://www.google.com/new/accounts/LoginAuth"));
 
   // Match against an inexact copy
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(
       db().GetLogins(digest, /*should_PSL_matching_apply=*/true, &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, GetLogins_ProtocolMismatch_HTTP) {
@@ -323,7 +322,7 @@
       GURL("https://www.google.com/new/accounts/LoginAuth"));
 
   // We have only an http record, so no match for this.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(
       db().GetLogins(digest, /*should_PSL_matching_apply=*/true, &result));
   EXPECT_THAT(result, IsEmpty());
@@ -340,14 +339,14 @@
       GURL("http://accounts.google.com/new/accounts/LoginAuth"));
 
   // We have only an https record, so no match for this.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(
       db().GetLogins(digest, /*should_PSL_matching_apply=*/true, &result));
   EXPECT_THAT(result, IsEmpty());
 }
 
 TEST_F(LoginDatabaseTest, AddLoginReturnsPrimaryKey) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Verify the database is empty.
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
@@ -365,7 +364,7 @@
 }
 
 TEST_F(LoginDatabaseTest, RemoveLoginsByPrimaryKey) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Verify the database is empty.
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
@@ -381,9 +380,7 @@
   FormPrimaryKey primary_key = change_list[0].form().primary_key.value();
   EXPECT_EQ(AddChangeForForm(form), change_list);
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
-  result.clear();
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 
   // RemoveLoginByPrimaryKey() doesn't decrypt or fill the password value.
   form.password_value = u"";
@@ -391,12 +388,10 @@
   EXPECT_TRUE(db().RemoveLoginByPrimaryKey(primary_key, &change_list));
   EXPECT_EQ(RemoveChangeForForm(form), change_list);
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
-  EXPECT_EQ(0U, result.size());
+  EXPECT_THAT(result, IsEmpty());
 }
 
 TEST_F(LoginDatabaseTest, ShouldNotRecyclePrimaryKeys) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
-
   // Example password form.
   PasswordForm form = GenerateExamplePasswordForm();
 
@@ -435,11 +430,11 @@
                            GURL("https://mobile.foo.com/login"));
 
   // Match against the mobile site.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form2),
                              /*should_PSL_matching_apply=*/true, &result));
   ASSERT_EQ(1U, result.size());
-  EXPECT_EQ("https://foo.com/", result[0]->signon_realm);
+  EXPECT_EQ("https://foo.com/", result[0].signon_realm);
 
   // Do an exact match by excluding psl matches.
   result.clear();
@@ -449,7 +444,7 @@
 }
 
 TEST_F(LoginDatabaseTest, TestFederatedMatching) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Example password form.
   PasswordForm form;
@@ -485,18 +480,16 @@
                                      GURL("https://foo.com/")};
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/true,
                              &result));
-  EXPECT_THAT(result,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form)),
-                                   Pointee(HasPrimaryKeyAndEquals(form2))));
+  EXPECT_THAT(result, UnorderedElementsAre(HasPrimaryKeyAndEquals(form),
+                                           HasPrimaryKeyAndEquals(form2)));
 
   // Match against the mobile site.
   form_request.url = GURL("https://mobile.foo.com/");
   form_request.signon_realm = "https://mobile.foo.com/";
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/true,
                              &result));
-  EXPECT_THAT(result,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form)),
-                                   Pointee(HasPrimaryKeyAndEquals(form2))));
+  EXPECT_THAT(result, UnorderedElementsAre(HasPrimaryKeyAndEquals(form),
+                                           HasPrimaryKeyAndEquals(form2)));
 }
 
 TEST_F(LoginDatabaseTest, TestFederatedMatchingLocalhost) {
@@ -524,18 +517,16 @@
   PasswordFormDigest form_request(PasswordForm::Scheme::kHtml,
                                   "http://localhost/",
                                   GURL("http://localhost/"));
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/false,
                              &result));
-  EXPECT_THAT(result,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 
   form_request.url = GURL("http://localhost:8080/");
   form_request.signon_realm = "http://localhost:8080/";
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/false,
                              &result));
-  EXPECT_THAT(result, UnorderedElementsAre(
-                          Pointee(HasPrimaryKeyAndEquals(form_with_port))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form_with_port)));
 }
 
 class LoginDatabaseSchemesTest
@@ -572,7 +563,7 @@
                                              GURL("http://second.example.com")};
 
   // This shouldn't match anything.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(second_non_html_auth,
                              /*should_PSL_matching_apply=*/false, &result));
   EXPECT_EQ(0U, result.size());
@@ -580,8 +571,7 @@
   // non-html auth still matches against itself.
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(non_html_auth),
                              /*should_PSL_matching_apply=*/false, &result));
-  EXPECT_THAT(result,
-              ElementsAre(Pointee(HasPrimaryKeyAndEquals(non_html_auth))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(non_html_auth)));
 }
 
 TEST_P(LoginDatabaseSchemesTest, TestIPAddressMatches) {
@@ -596,11 +586,10 @@
   ip_form.scheme = GetParam();
 
   EXPECT_EQ(AddChangeForForm(ip_form), db().AddLogin(ip_form));
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(ip_form),
                              /*should_PSL_matching_apply=*/false, &result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(ip_form)));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(ip_form)));
 }
 
 INSTANTIATE_TEST_SUITE_P(Schemes,
@@ -611,7 +600,7 @@
                                          PasswordForm::Scheme::kOther));
 
 TEST_F(LoginDatabaseTest, TestPublicSuffixDomainGoogle) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Saved password form on Google sign-in page.
   PasswordForm form;
@@ -631,7 +620,7 @@
   EXPECT_TRUE(
       db().GetLogins(form2, /*should_PSL_matching_apply=*/true, &result));
   ASSERT_EQ(1U, result.size());
-  EXPECT_EQ(form.signon_realm, result[0]->signon_realm);
+  EXPECT_EQ(form.signon_realm, result[0].signon_realm);
 
   // There should be no PSL match on other subdomains.
   PasswordFormDigest form3 = {PasswordForm::Scheme::kHtml,
@@ -644,7 +633,7 @@
 }
 
 TEST_F(LoginDatabaseTest, TestFederatedMatchingWithoutPSLMatching) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Example password form.
   PasswordForm form;
@@ -677,14 +666,14 @@
                                      form.signon_realm, form.url};
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/false,
                              &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 
   // Match against the second one.
   form_request.url = form2.url;
   form_request.signon_realm = form2.signon_realm;
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/false,
                              &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form2))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form2)));
 }
 
 TEST_F(LoginDatabaseTest, TestFederatedPSLMatching) {
@@ -707,17 +696,17 @@
   PasswordFormDigest form_request = {PasswordForm::Scheme::kHtml,
                                      "https://example.com/",
                                      GURL("https://example.com/login")};
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(form_request, /*should_PSL_matching_apply=*/true,
                              &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 // This test fails if the implementation of GetLogins uses GetCachedStatement
 // instead of GetUniqueStatement, since REGEXP is in use. See
 // http://crbug.com/248608.
 TEST_F(LoginDatabaseTest, TestPublicSuffixDomainMatchingDifferentSites) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Example password form.
   PasswordForm form;
@@ -742,7 +731,7 @@
   EXPECT_TRUE(
       db().GetLogins(form2, /*should_PSL_matching_apply=*/true, &result));
   EXPECT_EQ(1U, result.size());
-  EXPECT_EQ("https://foo.com/", result[0]->signon_realm);
+  EXPECT_EQ("https://foo.com/", result[0].signon_realm);
   result.clear();
 
   // Add baz.com desktop site.
@@ -767,7 +756,7 @@
   EXPECT_TRUE(
       db().GetLogins(form3, /*should_PSL_matching_apply=*/true, &result));
   EXPECT_EQ(1U, result.size());
-  EXPECT_EQ("https://baz.com/", result[0]->signon_realm);
+  EXPECT_EQ("https://baz.com/", result[0].signon_realm);
 }
 
 PasswordForm GetFormWithNewSignonRealm(PasswordForm form,
@@ -780,7 +769,7 @@
 }
 
 TEST_F(LoginDatabaseTest, TestPublicSuffixDomainMatchingRegexp) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Example password form.
   PasswordForm form;
@@ -907,7 +896,7 @@
 }
 
 TEST_F(LoginDatabaseTest, ClearPrivateData_SavedPasswords) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Verify the database is empty.
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
@@ -937,7 +926,7 @@
   result.clear();
 
   // Get everything from today's date and on.
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   EXPECT_TRUE(db().GetLoginsCreatedBetween(now, base::Time(), &forms));
   EXPECT_EQ(2U, forms.size());
   forms.clear();
@@ -984,7 +973,7 @@
 }
 
 TEST_F(LoginDatabaseTest, GetAutoSignInLogins) {
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
 
   GURL origin("https://example.com");
   EXPECT_TRUE(AddZeroClickableLogin(&db(), "foo1", origin));
@@ -995,7 +984,7 @@
   EXPECT_TRUE(db().GetAutoSignInLogins(&forms));
   EXPECT_EQ(4U, forms.size());
   for (const auto& form : forms)
-    EXPECT_FALSE(form->skip_zero_click);
+    EXPECT_FALSE(form.skip_zero_click);
 
   EXPECT_TRUE(db().DisableAutoSignInForOrigin(origin));
   EXPECT_TRUE(db().GetAutoSignInLogins(&forms));
@@ -1003,7 +992,7 @@
 }
 
 TEST_F(LoginDatabaseTest, DisableAutoSignInForOrigin) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   GURL origin1("https://google.com");
   GURL origin2("https://chrome.com");
@@ -1016,21 +1005,22 @@
 
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
   for (const auto& form : result)
-    EXPECT_FALSE(form->skip_zero_click);
+    EXPECT_FALSE(form.skip_zero_click);
 
   EXPECT_TRUE(db().DisableAutoSignInForOrigin(origin1));
   EXPECT_TRUE(db().DisableAutoSignInForOrigin(origin3));
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
   for (const auto& form : result) {
-    if (form->url == origin1 || form->url == origin3)
-      EXPECT_TRUE(form->skip_zero_click);
-    else
-      EXPECT_FALSE(form->skip_zero_click);
+    if (form.url == origin1 || form.url == origin3) {
+      EXPECT_TRUE(form.skip_zero_click);
+    } else {
+      EXPECT_FALSE(form.skip_zero_click);
+    }
   }
 }
 
 TEST_F(LoginDatabaseTest, BlocklistedLogins) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   // Verify the database is empty.
   EXPECT_TRUE(db().GetBlocklistLogins(&result));
@@ -1064,15 +1054,11 @@
   // GetLogins should give the blocklisted result.
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
-  result.clear();
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 
   // So should GetBlocklistedLogins.
   EXPECT_TRUE(db().GetBlocklistLogins(&result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
-  result.clear();
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, VectorSerialization) {
@@ -1129,7 +1115,7 @@
 }
 
 TEST_F(LoginDatabaseTest, UpdateIncompleteCredentials) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   // Verify the database is empty.
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
   ASSERT_EQ(0U, result.size());
@@ -1160,19 +1146,19 @@
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(encountered_form),
                              /*should_PSL_matching_apply=*/true, &result));
   ASSERT_EQ(1U, result.size());
-  EXPECT_EQ(incomplete_form.url, result[0]->url);
-  EXPECT_EQ(incomplete_form.signon_realm, result[0]->signon_realm);
-  EXPECT_EQ(incomplete_form.username_value, result[0]->username_value);
-  EXPECT_EQ(incomplete_form.password_value, result[0]->password_value);
-  EXPECT_EQ(incomplete_form.date_last_used, result[0]->date_last_used);
+  EXPECT_EQ(incomplete_form.url, result[0].url);
+  EXPECT_EQ(incomplete_form.signon_realm, result[0].signon_realm);
+  EXPECT_EQ(incomplete_form.username_value, result[0].username_value);
+  EXPECT_EQ(incomplete_form.password_value, result[0].password_value);
+  EXPECT_EQ(incomplete_form.date_last_used, result[0].date_last_used);
 
   // We should return empty 'action', 'username_element', 'password_element'
   // and 'submit_element' as we can't be sure if the credentials were entered
   // in this particular form on the page.
-  EXPECT_EQ(GURL(), result[0]->action);
-  EXPECT_TRUE(result[0]->username_element.empty());
-  EXPECT_TRUE(result[0]->password_element.empty());
-  EXPECT_TRUE(result[0]->submit_element.empty());
+  EXPECT_EQ(GURL(), result[0].action);
+  EXPECT_TRUE(result[0].username_element.empty());
+  EXPECT_TRUE(result[0].password_element.empty());
+  EXPECT_TRUE(result[0].submit_element.empty());
   result.clear();
 
   // Let's say this login form worked. Now update the stored credentials with
@@ -1202,8 +1188,7 @@
   // map should be part of the default constructor.
   expected_form.password_issues =
       base::flat_map<InsecureType, InsecurityMetadata>();
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(expected_form)));
-  result.clear();
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(expected_form)));
 }
 
 TEST_F(LoginDatabaseTest, UpdateOverlappingCredentials) {
@@ -1234,7 +1219,7 @@
   EXPECT_EQ(AddChangeForForm(complete_form), db().AddLogin(complete_form));
 
   // Make sure both passwords exist.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
   ASSERT_EQ(2U, result.size());
   result.clear();
@@ -1262,10 +1247,9 @@
   EXPECT_TRUE(db().GetAutofillableLogins(&result));
   ASSERT_EQ(2U, result.size());
 
-  if (result[0]->username_element.empty())
-    std::swap(result[0], result[1]);
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(complete_form)));
-  EXPECT_THAT(result[1], Pointee(HasPrimaryKeyAndEquals(incomplete_form)));
+  EXPECT_THAT(result,
+              UnorderedElementsAre(HasPrimaryKeyAndEquals(complete_form),
+                                   HasPrimaryKeyAndEquals(incomplete_form)));
 }
 
 TEST_F(LoginDatabaseTest, DoubleAdd) {
@@ -1324,17 +1308,16 @@
   form_with_password.password_value = u"my_encrypted_password";
   EXPECT_EQ(AddChangeForForm(form_with_password), db().AddLogin(form));
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   ASSERT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
   ASSERT_EQ(1U, result.size());
-  EXPECT_EQ(form.keychain_identifier, result[0].get()->keychain_identifier);
-  EXPECT_EQ(u"my_encrypted_password", result[0].get()->password_value);
+  EXPECT_EQ(form.keychain_identifier, result[0].keychain_identifier);
+  EXPECT_EQ(u"my_encrypted_password", result[0].password_value);
 
   std::u16string decrypted;
-  EXPECT_EQ(errSecSuccess,
-            GetTextFromKeychainIdentifier(result[0].get()->keychain_identifier,
-                                          &decrypted));
+  EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
+                               result[0].keychain_identifier, &decrypted));
   EXPECT_EQ(u"my_encrypted_password", decrypted);
 }
 
@@ -1354,16 +1337,15 @@
   form.scheme = PasswordForm::Scheme::kHtml;
   EXPECT_EQ(AddChangeForForm(form), db().AddLogin(form));
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   ASSERT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
   ASSERT_EQ(1U, result.size());
-  EXPECT_NE(form.keychain_identifier, result[0].get()->keychain_identifier);
+  EXPECT_NE(form.keychain_identifier, result[0].keychain_identifier);
 
   std::u16string decrypted;
-  EXPECT_EQ(errSecSuccess,
-            GetTextFromKeychainIdentifier(result[0].get()->keychain_identifier,
-                                          &decrypted));
+  EXPECT_EQ(errSecSuccess, GetTextFromKeychainIdentifier(
+                               result[0].keychain_identifier, &decrypted));
   EXPECT_EQ(u"my_password_value", decrypted);
 }
 #endif
@@ -1407,11 +1389,10 @@
   // When we retrieve the form from the store, it should have |in_store| set.
   form.in_store = PasswordForm::Store::kProfileStore;
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, UpdateLoginWithoutPassword) {
@@ -1446,11 +1427,10 @@
   // When we retrieve the form from the store, it should have |in_store| set.
   form.in_store = PasswordForm::Store::kProfileStore;
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   ASSERT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
-  ASSERT_EQ(1U, result.size());
-  EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, RemoveWrongForm) {
@@ -1888,7 +1868,7 @@
   UpdatePasswordValueForUsername(file, u"other_username", k_plain_text_pw116);
   UpdatePasswordValueForUsername(file, u"other_username2", k_plain_text_pw216);
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   {
     LoginDatabase db(file, IsAccountStore(false));
     ASSERT_TRUE(db.Init());
@@ -1901,13 +1881,11 @@
               UnorderedElementsAre(Ne(k_obfuscated_pw), k_plain_text_pw1,
                                    k_plain_text_pw2));
   // LoginDatabase serves the original values.
-  ASSERT_THAT(forms, SizeIs(3));
-  EXPECT_THAT(
-      forms,
-      UnorderedElementsAre(
-          Pointee(Field(&PasswordForm::password_value, k_obfuscated_pw16)),
-          Pointee(Field(&PasswordForm::password_value, k_plain_text_pw116)),
-          Pointee(Field(&PasswordForm::password_value, k_plain_text_pw216))));
+  EXPECT_THAT(forms,
+              UnorderedElementsAre(
+                  Field(&PasswordForm::password_value, k_obfuscated_pw16),
+                  Field(&PasswordForm::password_value, k_plain_text_pw116),
+                  Field(&PasswordForm::password_value, k_plain_text_pw216)));
 }
 #endif  // BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS_ASH)
 
@@ -2036,11 +2014,11 @@
     ASSERT_TRUE(db.Init());
 
     // Check that the contents was preserved.
-    std::vector<std::unique_ptr<PasswordForm>> result;
+    std::vector<PasswordForm> result;
     EXPECT_TRUE(db.GetAutofillableLogins(&result));
-    EXPECT_THAT(result, UnorderedElementsAre(Pointee(IsGoogle1Account()),
-                                             Pointee(IsGoogle2Account()),
-                                             Pointee(IsBasicAuthAccount())));
+    EXPECT_THAT(result,
+                UnorderedElementsAre(IsGoogle1Account(), IsGoogle2Account(),
+                                     IsBasicAuthAccount()));
 
     // Verifies that the final version can save all the appropriate fields.
     PasswordForm form = GenerateExamplePasswordForm();
@@ -2054,8 +2032,7 @@
     result.clear();
     EXPECT_TRUE(db.GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
-    ASSERT_EQ(1U, result.size());
-    EXPECT_THAT(result[0], Pointee(HasPrimaryKeyAndEquals(form)));
+    EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
     EXPECT_TRUE(db.RemoveLogin(form, /*changes=*/nullptr));
 
     if (version() == 31) {
@@ -2228,7 +2205,7 @@
 #if BUILDFLAG(IS_MAC) || (BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CASTOS))
   // Make sure that we can't get any logins when database is corrupted.
   // Disabling the checks in chromecast because encryption is unavailable.
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_FALSE(db.GetAutofillableLogins(&result));
   EXPECT_TRUE(result.empty());
   EXPECT_FALSE(db.GetBlocklistLogins(&result));
@@ -2237,8 +2214,7 @@
   // Delete undecryptable logins and make sure we can get valid logins.
   EXPECT_EQ(DatabaseCleanupResult::kSuccess, db.DeleteUndecryptableLogins());
   EXPECT_TRUE(db.GetAutofillableLogins(&result));
-  EXPECT_THAT(result,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form1))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form1)));
 
   EXPECT_TRUE(db.GetBlocklistLogins(&result));
   EXPECT_THAT(result, IsEmpty());
@@ -2270,7 +2246,7 @@
   LoginDatabase db(database_path(), IsAccountStore(false));
   ASSERT_TRUE(db.Init());
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_FALSE(db.GetAutofillableLogins(&result));
   EXPECT_TRUE(result.empty());
 
@@ -2298,7 +2274,7 @@
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
 // Test getting auto sign in logins when there are undecryptable ones
 TEST_F(LoginDatabaseUndecryptableLoginsTest, GetAutoSignInLogins) {
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
 
   auto form1 =
       AddDummyLogin("foo1", GURL("https://foo1.com/"),
@@ -2319,9 +2295,8 @@
   feature_list.InitAndEnableFeature(features::kSkipUndecryptablePasswords);
 
   EXPECT_TRUE(db.GetAutoSignInLogins(&forms));
-  EXPECT_THAT(forms,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form1)),
-                                   Pointee(HasPrimaryKeyAndEquals(form3))));
+  EXPECT_THAT(forms, UnorderedElementsAre(HasPrimaryKeyAndEquals(form1),
+                                          HasPrimaryKeyAndEquals(form3)));
 }
 
 // Test getting logins when there are undecryptable ones
@@ -2334,7 +2309,7 @@
                     /*should_be_corrupted=*/true, /*blocklisted=*/false);
   LoginDatabase db(database_path(), IsAccountStore(false));
   ASSERT_TRUE(db.Init());
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   PasswordForm form = GenerateExamplePasswordForm();
   EXPECT_FALSE(db.GetLogins(PasswordFormDigest(form),
@@ -2346,12 +2321,12 @@
 
   EXPECT_TRUE(db.GetLogins(PasswordFormDigest(form),
                            /*should_PSL_matching_apply=*/false, &result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form1))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form1)));
 }
 
 // Test getting auto fillable logins when there are undecryptable ones
 TEST_F(LoginDatabaseUndecryptableLoginsTest, GetAutofillableLogins) {
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
 
   auto form1 =
       AddDummyLogin("foo1", GURL("https://foo1.com/"),
@@ -2372,7 +2347,7 @@
   feature_list.InitAndEnableFeature(features::kSkipUndecryptablePasswords);
 
   EXPECT_TRUE(db.GetAutofillableLogins(&result));
-  EXPECT_THAT(result, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form1))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form1)));
 }
 #endif
 
@@ -2456,15 +2431,15 @@
   ASSERT_TRUE(changes[0].form().keychain_identifier.empty());
 #endif
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/false, &forms));
 
   ASSERT_EQ(1U, forms.size());
 #if BUILDFLAG(IS_IOS)
-  ASSERT_FALSE(forms[0]->keychain_identifier.empty());
+  ASSERT_FALSE(forms[0].keychain_identifier.empty());
 #else
-  ASSERT_TRUE(forms[0]->keychain_identifier.empty());
+  ASSERT_TRUE(forms[0].keychain_identifier.empty());
 #endif
 }
 
@@ -2488,11 +2463,10 @@
       FormPrimaryKey(1), InsecureType::kPhished,
       form.password_issues[InsecureType::kPhished]);
 
-  std::vector<std::unique_ptr<PasswordForm>> result;
+  std::vector<PasswordForm> result;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /*should_PSL_matching_apply=*/true, &result));
-  EXPECT_THAT(result,
-              UnorderedElementsAre(Pointee(HasPrimaryKeyAndEquals(form))));
+  EXPECT_THAT(result, ElementsAre(HasPrimaryKeyAndEquals(form)));
 }
 
 TEST_F(LoginDatabaseTest, RetrievesNoteWithLogin) {
@@ -2504,14 +2478,13 @@
   PasswordNote note(u"example note", base::Time::Now());
   db().password_notes_table().InsertOrReplace(FormPrimaryKey(1), note);
 
-  std::vector<std::unique_ptr<PasswordForm>> results;
+  std::vector<PasswordForm> results;
   EXPECT_TRUE(db().GetLogins(PasswordFormDigest(form),
                              /* should_PSL_matching_apply */ true, &results));
 
   PasswordForm expected_form = form;
   expected_form.notes = {note};
-  EXPECT_THAT(results, UnorderedElementsAre(
-                           Pointee(HasPrimaryKeyAndEquals(expected_form))));
+  EXPECT_THAT(results, ElementsAre(HasPrimaryKeyAndEquals(expected_form)));
 }
 
 TEST_F(LoginDatabaseTest, AddLoginWithNotePersistsThem) {
@@ -2591,12 +2564,12 @@
   form2.username_value = username2;
   ASSERT_EQ(AddChangeForForm(form2), db().AddLogin(form2));
 
-  std::vector<std::unique_ptr<PasswordForm>> forms;
+  std::vector<PasswordForm> forms;
   // Check if there is exactly one form with this signon_realm & username1.
   EXPECT_EQ(
       FormRetrievalResult::kSuccess,
       db().GetLoginsBySignonRealmAndUsername(signon_realm, username1, &forms));
-  EXPECT_THAT(forms, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form1))));
+  EXPECT_THAT(forms, ElementsAre(HasPrimaryKeyAndEquals(form1)));
 
   // Insert another form with the same username as form1.
   PasswordForm form3 = GenerateExamplePasswordForm();
@@ -2609,8 +2582,8 @@
   EXPECT_EQ(
       FormRetrievalResult::kSuccess,
       db().GetLoginsBySignonRealmAndUsername(signon_realm, username1, &forms));
-  EXPECT_THAT(forms, ElementsAre(Pointee(HasPrimaryKeyAndEquals(form1)),
-                                 Pointee(HasPrimaryKeyAndEquals(form3))));
+  EXPECT_THAT(forms, ElementsAre(HasPrimaryKeyAndEquals(form1),
+                                 HasPrimaryKeyAndEquals(form3)));
 }
 
 TEST_F(LoginDatabaseTest, UpdateLoginWithAddedInsecureCredential) {
diff --git a/components/password_manager/core/browser/password_credential_filler_impl.cc b/components/password_manager/core/browser/password_credential_filler_impl.cc
index 8215cacc..2e4bfc48 100644
--- a/components/password_manager/core/browser/password_credential_filler_impl.cc
+++ b/components/password_manager/core/browser/password_credential_filler_impl.cc
@@ -73,7 +73,8 @@
     // block a form submission. Note: Don't use |check_status !=
     // kNotCheckable|, a radio button is considered a "checkable" element too,
     // but it should block a submission.
-    return field.form_control_type == "checkbox";
+    return field.form_control_type ==
+           autofill::StringToFormControlType("checkbox");
   };
 
   for (size_t i = username_index + 1; i < password_index; ++i) {
diff --git a/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc b/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
index 1546a27..2ff6236 100644
--- a/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
+++ b/components/password_manager/core/browser/password_credential_filler_impl_unittest.cc
@@ -58,8 +58,9 @@
             (type == FormFieldFocusabilityType::kFocusableInput ||
              type == FormFieldFocusabilityType::kFocusableCheckbox);
         field.form_control_type =
-            (type == FormFieldFocusabilityType::kFocusableCheckbox) ? "checkbox"
-                                                                    : "input";
+            (type == FormFieldFocusabilityType::kFocusableCheckbox)
+                ? autofill::StringToFormControlType("checkbox")
+                : autofill::StringToFormControlType("input");
         return field;
       });
   return form;
diff --git a/components/password_manager/core/browser/password_feature_manager_impl.cc b/components/password_manager/core/browser/password_feature_manager_impl.cc
index 3c03393..2960c144 100644
--- a/components/password_manager/core/browser/password_feature_manager_impl.cc
+++ b/components/password_manager/core/browser/password_feature_manager_impl.cc
@@ -8,7 +8,8 @@
 #include "build/build_config.h"
 #include "components/password_manager/core/browser/features/password_features.h"
 #include "components/password_manager/core/browser/features/password_manager_features_util.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
 #include "components/prefs/pref_service.h"
 #include "components/sync/service/sync_service.h"
@@ -24,7 +25,7 @@
       sync_service_(sync_service) {}
 
 bool PasswordFeatureManagerImpl::IsGenerationEnabled() const {
-  switch (password_manager_util::GetPasswordSyncState(sync_service_)) {
+  switch (password_manager::sync_util::GetPasswordSyncState(sync_service_)) {
     case SyncState::kNotSyncing:
       return ShouldShowAccountStorageOptIn();
     case SyncState::kSyncingWithCustomPassphrase:
diff --git a/components/password_manager/core/browser/password_feature_manager_impl_unittest.cc b/components/password_manager/core/browser/password_feature_manager_impl_unittest.cc
index 938482c..5fb0bea 100644
--- a/components/password_manager/core/browser/password_feature_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/password_feature_manager_impl_unittest.cc
@@ -9,7 +9,7 @@
 #include "components/password_manager/core/browser/features/password_features.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_client.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
 #include "components/prefs/pref_registry_simple.h"
@@ -66,7 +66,7 @@
 #endif  // !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
 
   ASSERT_EQ(
-      password_manager_util::GetPasswordSyncState(&sync_service_),
+      password_manager::sync_util::GetPasswordSyncState(&sync_service_),
       password_manager::SyncState::kAccountPasswordsActiveNormalEncryption);
 
   EXPECT_TRUE(password_feature_manager_.IsGenerationEnabled());
@@ -89,7 +89,7 @@
   sync_service_.GetUserSettings()->SetSelectedTypes(
       /*sync_everything=*/false,
       /*types=*/syncer::UserSelectableTypeSet());
-  ASSERT_EQ(password_manager_util::GetPasswordSyncState(&sync_service_),
+  ASSERT_EQ(password_manager::sync_util::GetPasswordSyncState(&sync_service_),
             password_manager::SyncState::kNotSyncing);
 
   // The user must be eligible for account storage opt in now.
@@ -99,7 +99,7 @@
   // opted-in by default.
   ASSERT_TRUE(password_feature_manager_.IsOptedInForAccountStorage());
   ASSERT_EQ(
-      password_manager_util::GetPasswordSyncState(&sync_service_),
+      password_manager::sync_util::GetPasswordSyncState(&sync_service_),
       password_manager::SyncState::kAccountPasswordsActiveNormalEncryption);
 #endif  // !BUILDFLAG(IS_IOS) && !BUILDFLAG(IS_ANDROID)
 
@@ -122,7 +122,7 @@
       /*sync_everything=*/false,
       /*types=*/syncer::UserSelectableTypeSet());
 
-  ASSERT_EQ(password_manager_util::GetPasswordSyncState(&sync_service_),
+  ASSERT_EQ(password_manager::sync_util::GetPasswordSyncState(&sync_service_),
             password_manager::SyncState::kNotSyncing);
   // The user must not be eligible for account storage opt in now.
   ASSERT_FALSE(password_feature_manager_.ShouldShowAccountStorageOptIn());
@@ -138,7 +138,7 @@
 
   ASSERT_EQ(sync_service_.GetTransportState(),
             syncer::SyncService::TransportState::PAUSED);
-  ASSERT_EQ(password_manager_util::GetPasswordSyncState(&sync_service_),
+  ASSERT_EQ(password_manager::sync_util::GetPasswordSyncState(&sync_service_),
             password_manager::SyncState::kNotSyncing);
 
   EXPECT_FALSE(password_feature_manager_.IsGenerationEnabled());
diff --git a/components/password_manager/core/browser/password_form_manager_unittest.cc b/components/password_manager/core/browser/password_form_manager_unittest.cc
index 937170d..df1bba4d 100644
--- a/components/password_manager/core/browser/password_form_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_form_manager_unittest.cc
@@ -368,14 +368,14 @@
     field.name = u"firstname";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = autofill::FieldRendererId(2);
     observed_form_.fields.push_back(field);
 
     field.name = u"username";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = autofill::FieldRendererId(3);
     observed_form_.fields.push_back(field);
 
@@ -384,7 +384,7 @@
     field.name = u"password";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = autofill::FieldRendererId(4);
     observed_form_.fields.push_back(field);
     observed_form_only_password_fields_.fields.push_back(field);
@@ -392,7 +392,7 @@
     field.name = u"password2";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = autofill::FieldRendererId(5);
     observed_form_only_password_fields_.fields.push_back(field);
 
@@ -703,7 +703,7 @@
   const autofill::FieldRendererId confirm_password_render_id(
       new_password_render_id.value() + 1);
   field.unique_renderer_id = confirm_password_render_id;
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.autocomplete_attribute = "new-password";
   observed_form_.fields.push_back(field);
 
@@ -1250,12 +1250,13 @@
        saved_match_.all_alternative_usernames) {
     FormFieldData text_field;
     text_field.name = alternative.name;
-    text_field.form_control_type = "text";
+    text_field.form_control_type = autofill::StringToFormControlType("text");
     saved_match_.form_data.fields.push_back(text_field);
   }
   FormFieldData password_field;
   password_field.name = saved_match_.password_element;
-  password_field.form_control_type = "password";
+  password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   saved_match_.form_data.fields.push_back(password_field);
   SetNonFederatedAndNotifyFetchCompleted({&saved_match_});
 
@@ -1324,12 +1325,13 @@
        saved_match_.all_alternative_usernames) {
     FormFieldData field;
     field.name = alternative.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     saved_match_.form_data.fields.push_back(field);
   }
   FormFieldData password_field;
   password_field.name = saved_match_.password_element;
-  password_field.form_control_type = "password";
+  password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   saved_match_.form_data.fields.push_back(password_field);
 
   SetNonFederatedAndNotifyFetchCompleted({&saved_match_});
@@ -2017,7 +2019,8 @@
   base::HistogramTester histogram_tester;
 
   FormData form = observed_form_;
-  form.fields[kUsernameFieldIndex].form_control_type = "password";
+  form.fields[kUsernameFieldIndex].form_control_type =
+      autofill::StringToFormControlType("password");
   EXPECT_TRUE(HasObservedFormChanged(form, *form_manager_));
   form_manager_.reset();
 
@@ -3101,7 +3104,7 @@
   FormData submitted_form = observed_form_only_password_fields_;
   FormFieldData username_field;
   username_field.name = u"username";
-  username_field.form_control_type = "text";
+  username_field.form_control_type = autofill::StringToFormControlType("text");
   username_field.value = u"oldusername";
   username_field.unique_renderer_id = autofill::FieldRendererId(2);
   submitted_form.fields.insert(std::begin(submitted_form.fields),
diff --git a/components/password_manager/core/browser/password_form_metrics_recorder.cc b/components/password_manager/core/browser/password_form_metrics_recorder.cc
index bf5ff1ca..18ddc07b 100644
--- a/components/password_manager/core/browser/password_form_metrics_recorder.cc
+++ b/components/password_manager/core/browser/password_form_metrics_recorder.cc
@@ -145,7 +145,9 @@
         is_possibly_saved_password_in_profile_store ||
         is_possibly_saved_password_in_account_store;
 
-    bool field_has_password_type = field.form_control_type == "password";
+    bool field_has_password_type =
+        field.form_control_type ==
+        autofill::StringToFormControlType("password");
 
     if (is_possibly_saved_username &&
         (!is_possibly_saved_password || !field_has_password_type)) {
diff --git a/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc b/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
index 02e63e23..8c847b98 100644
--- a/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
+++ b/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
@@ -637,7 +637,9 @@
     if (field.automatically_filled)
       form_field.properties_mask |= FieldPropertiesFlags::kAutofilledOnPageLoad;
 
-    form_field.form_control_type = field.is_password ? "password" : "text";
+    form_field.form_control_type =
+        field.is_password ? autofill::StringToFormControlType("password")
+                          : autofill::StringToFormControlType("text");
 
     form.fields.push_back(form_field);
   }
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index bcce30d..7eabcf8 100644
--- a/components/password_manager/core/browser/password_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_unittest.cc
@@ -498,7 +498,7 @@
     field.id_attribute = field.name;
     field.name_attribute = field.name;
     field.value = u"googleuser";
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = FieldRendererId(2);
     form_data.fields.push_back(field);
 
@@ -506,7 +506,7 @@
     field.id_attribute = field.name;
     field.name_attribute = field.name;
     field.value = u"p4ssword";
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = FieldRendererId(3);
     form_data.fields.push_back(field);
 
@@ -580,7 +580,7 @@
     field.name = u"Email";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = FieldRendererId(31);
     form_data.fields.push_back(field);
     return form_data;
@@ -617,7 +617,8 @@
     password_field.name = form.password_element;
     password_field.id_attribute = form.password_element;
     password_field.value = form.password_value;
-    password_field.form_control_type = "password";
+    password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     password_field.unique_renderer_id = FieldRendererId(2);
     form.form_data.fields.push_back(password_field);
 
@@ -625,7 +626,7 @@
     captcha_field.name = u"captcha_element";
     captcha_field.id_attribute = captcha_field.name;
     captcha_field.value = u"captcha_value";
-    captcha_field.form_control_type = "text";
+    captcha_field.form_control_type = autofill::StringToFormControlType("text");
     captcha_field.unique_renderer_id = FieldRendererId(3);
     form.form_data.fields.push_back(captcha_field);
 
@@ -645,7 +646,7 @@
     otp_field.name = form.username_element;
     otp_field.name_attribute = form.username_element;
     otp_field.id_attribute = form.username_element;
-    otp_field.form_control_type = "text";
+    otp_field.form_control_type = autofill::StringToFormControlType("text");
     otp_field.unique_renderer_id = FieldRendererId(61);
     form.form_data.fields.push_back(otp_field);
 
@@ -666,7 +667,7 @@
     name_field.name = u"name";
     name_field.id_attribute = name_field.name;
     name_field.value = u"Name";
-    name_field.form_control_type = "text";
+    name_field.form_control_type = autofill::StringToFormControlType("text");
     name_field.unique_renderer_id = FieldRendererId(2);
     form.form_data.fields.push_back(name_field);
 
@@ -674,7 +675,7 @@
     surname_field.name = form.username_element;
     surname_field.id_attribute = surname_field.name;
     surname_field.value = form.username_value;
-    surname_field.form_control_type = "text";
+    surname_field.form_control_type = autofill::StringToFormControlType("text");
     surname_field.unique_renderer_id = FieldRendererId(3);
     form.form_data.fields.push_back(surname_field);
 
@@ -682,7 +683,8 @@
     password_field.name = form.password_element;
     password_field.id_attribute = form.password_element;
     password_field.value = form.password_value;
-    password_field.form_control_type = "password";
+    password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     password_field.unique_renderer_id = FieldRendererId(4);
     form.form_data.fields.push_back(password_field);
 
@@ -703,7 +705,7 @@
     field.name = form.username_element;
     field.id_attribute = field.name;
     field.value = form.username_value;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = FieldRendererId(2);
     field.autocomplete_attribute = "cc-name";
     form.form_data.fields.push_back(field);
@@ -711,7 +713,7 @@
     field.name = form.password_element;
     field.id_attribute = field.name;
     field.value = form.password_value;
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = FieldRendererId(3);
     field.autocomplete_attribute = "cc-number";
     form.form_data.fields.push_back(field);
@@ -1756,7 +1758,7 @@
   field.name = u"new_password_element";
   field.id_attribute = field.name;
   field.name_attribute = field.name;
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.unique_renderer_id = FieldRendererId(4);
   form_data.fields.push_back(field);
 
@@ -1847,10 +1849,12 @@
   form_data1.fields.resize(2);
   form_data1.fields[0].name = u"Email";
   form_data1.fields[0].unique_renderer_id = FieldRendererId(1);
-  form_data1.fields[0].form_control_type = "text";
+  form_data1.fields[0].form_control_type =
+      autofill::StringToFormControlType("text");
   form_data1.fields[1].name = u"Passwd";
   form_data1.fields[1].unique_renderer_id = FieldRendererId(2);
-  form_data1.fields[1].form_control_type = "password";
+  form_data1.fields[1].form_control_type =
+      autofill::StringToFormControlType("password");
 
   FormData form_data2 = form_data1;
   form_data2.url = second_form.url;
@@ -3610,7 +3614,7 @@
   FormFieldData field;
   field.name_attribute = one_time_code_form.password_element;
   field.value = one_time_code_form.password_value;
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   one_time_code_form.form_data.fields.push_back(field);
 
   PasswordFormFillData form_data;
@@ -3826,13 +3830,13 @@
   form_data.url = GURL("http://example.com");
 
   FormFieldData username_field;
-  username_field.form_control_type = "text";
+  username_field.form_control_type = autofill::StringToFormControlType("text");
   constexpr FieldRendererId username_field_id(10);
   username_field.unique_renderer_id = username_field_id;
   form_data.fields.push_back(username_field);
 
   FormFieldData password_field;
-  password_field.form_control_type = "text";
+  password_field.form_control_type = autofill::StringToFormControlType("text");
   constexpr FieldRendererId password_field_id(11);
   password_field.unique_renderer_id = password_field_id;
   form_data.fields.push_back(password_field);
@@ -4415,7 +4419,8 @@
   form_data.url = GURL("http://www.testwebsite.com");
 
   FormFieldData old_password_field;
-  old_password_field.form_control_type = "password";
+  old_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   old_password_field.unique_renderer_id = FieldRendererId(0);
   old_password_field.name = u"oldpass";
   form_data.fields.push_back(old_password_field);
@@ -4426,13 +4431,15 @@
   // Form changes: new and confirmation password fields are added by the
   // website's scripts.
   FormFieldData new_password_field;
-  new_password_field.form_control_type = "password";
+  new_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   new_password_field.unique_renderer_id = FieldRendererId(1);
   new_password_field.name = u"newpass";
   form_data.fields.push_back(new_password_field);
 
   FormFieldData confirm_password_field;
-  confirm_password_field.form_control_type = "password";
+  confirm_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   confirm_password_field.unique_renderer_id = FieldRendererId(2);
   confirm_password_field.name = u"confpass";
   form_data.fields.push_back(confirm_password_field);
@@ -4466,21 +4473,24 @@
   form_data.url = test_form_url_;
 
   FormFieldData old_password_field;
-  old_password_field.form_control_type = "password";
+  old_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   old_password_field.unique_renderer_id = FieldRendererId(1);
   old_password_field.name = u"oldpass";
   old_password_field.value = u"oldpass";
   form_data.fields.push_back(old_password_field);
 
   FormFieldData new_password_field;
-  new_password_field.form_control_type = "password";
+  new_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   new_password_field.unique_renderer_id = FieldRendererId(2);
   new_password_field.name = u"newpass";
   new_password_field.autocomplete_attribute = "new-password";
   form_data.fields.push_back(new_password_field);
 
   FormFieldData confirm_password_field;
-  confirm_password_field.form_control_type = "password";
+  confirm_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   confirm_password_field.unique_renderer_id = FieldRendererId(3);
   confirm_password_field.name = u"confpass";
   form_data.fields.push_back(confirm_password_field);
@@ -4519,7 +4529,8 @@
   form_data.url = test_form_url_;
 
   FormFieldData password_field;
-  password_field.form_control_type = "password";
+  password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   password_field.unique_renderer_id = FieldRendererId(1);
   password_field.name = u"one-time-code";
   password_field.value = u"123456";
@@ -4550,14 +4561,16 @@
   form_data.url = test_form_url_;
 
   FormFieldData old_password_field;
-  old_password_field.form_control_type = "password";
+  old_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   old_password_field.unique_renderer_id = FieldRendererId(1);
   old_password_field.name = kEmptyName;
   old_password_field.value = u"oldpass";
   form_data.fields.push_back(old_password_field);
 
   FormFieldData new_password_field;
-  new_password_field.form_control_type = "password";
+  new_password_field.form_control_type =
+      autofill::StringToFormControlType("password");
   new_password_field.unique_renderer_id = FieldRendererId(2);
   new_password_field.name = kEmptyName;
   new_password_field.autocomplete_attribute = "new-password";
@@ -4594,21 +4607,24 @@
     form_data.url = test_form_url_;
 
     FormFieldData old_password_field;
-    old_password_field.form_control_type = "password";
+    old_password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     old_password_field.unique_renderer_id = FieldRendererId(1);
     old_password_field.name = u"oldpass";
     old_password_field.value = u"oldpass";
     form_data.fields.push_back(old_password_field);
 
     FormFieldData new_password_field;
-    new_password_field.form_control_type = "password";
+    new_password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     new_password_field.unique_renderer_id = FieldRendererId(2);
     new_password_field.name = u"newpass";
     new_password_field.autocomplete_attribute = "new-password";
     form_data.fields.push_back(new_password_field);
 
     FormFieldData confirm_password_field;
-    confirm_password_field.form_control_type = "password";
+    confirm_password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     confirm_password_field.unique_renderer_id = FieldRendererId(3);
     confirm_password_field.name = u"confpass";
     form_data.fields.push_back(confirm_password_field);
@@ -4658,14 +4674,16 @@
     form_data.url = test_form_url_;
 
     FormFieldData old_password_field;
-    old_password_field.form_control_type = "password";
+    old_password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     old_password_field.unique_renderer_id = FieldRendererId(1);
     old_password_field.name = kEmptyName;
     old_password_field.value = u"oldpass";
     form_data.fields.push_back(old_password_field);
 
     FormFieldData new_password_field;
-    new_password_field.form_control_type = "password";
+    new_password_field.form_control_type =
+        autofill::StringToFormControlType("password");
     new_password_field.unique_renderer_id = FieldRendererId(2);
     new_password_field.name = kEmptyName;
     new_password_field.autocomplete_attribute = "new-password";
@@ -5114,14 +5132,15 @@
     FormFieldData username_field;
     username_field.name = test_form_username_element_;
     username_field.value = one_time_code_form_username_value;
-    username_field.form_control_type = "text";
+    username_field.form_control_type =
+        autofill::StringToFormControlType("text");
     username_field.unique_renderer_id = FieldRendererId(1);
     one_time_code_form.form_data.fields.push_back(username_field);
   }
 
   FormFieldData otp_field;
   otp_field.value = test_form_otp_value_;
-  otp_field.form_control_type = "password";
+  otp_field.form_control_type = autofill::StringToFormControlType("password");
   otp_field.unique_renderer_id = FieldRendererId(2);
   one_time_code_form.form_data.fields.push_back(otp_field);
   switch (prediction_type) {
diff --git a/components/password_manager/core/browser/password_manager_util.cc b/components/password_manager/core/browser/password_manager_util.cc
index 88fbd053..757a781 100644
--- a/components/password_manager/core/browser/password_manager_util.cc
+++ b/components/password_manager/core/browser/password_manager_util.cc
@@ -45,8 +45,6 @@
 #include "components/prefs/pref_service.h"
 #include "components/signin/public/base/signin_metrics.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
-#include "components/sync/service/sync_service.h"
-#include "components/sync/service/sync_user_settings.h"
 #include "url/url_util.h"
 
 #if BUILDFLAG(IS_ANDROID)
@@ -87,29 +85,6 @@
   credential->all_alternative_usernames.clear();
 }
 
-password_manager::SyncState GetPasswordSyncState(
-    const syncer::SyncService* sync_service) {
-  if (!sync_service ||
-      !sync_service->GetActiveDataTypes().Has(syncer::PASSWORDS)) {
-    return password_manager::SyncState::kNotSyncing;
-  }
-
-  if (sync_service->IsSyncFeatureActive()) {
-    return sync_service->GetUserSettings()->IsUsingExplicitPassphrase()
-               ? password_manager::SyncState::kSyncingWithCustomPassphrase
-               : password_manager::SyncState::kSyncingNormalEncryption;
-  }
-
-  DCHECK(base::FeatureList::IsEnabled(
-      password_manager::features::kEnablePasswordsAccountStorage));
-
-  return sync_service->GetUserSettings()->IsUsingExplicitPassphrase()
-             ? password_manager::SyncState::
-                   kAccountPasswordsActiveWithCustomPassphrase
-             : password_manager::SyncState::
-                   kAccountPasswordsActiveNormalEncryption;
-}
-
 void TrimUsernameOnlyCredentials(
     std::vector<std::unique_ptr<PasswordForm>>* android_credentials) {
   // Remove username-only credentials which are not federated.
diff --git a/components/password_manager/core/browser/password_manager_util.h b/components/password_manager/core/browser/password_manager_util.h
index c62b0d1..120b2399 100644
--- a/components/password_manager/core/browser/password_manager_util.h
+++ b/components/password_manager/core/browser/password_manager_util.h
@@ -33,10 +33,6 @@
 class AutofillClient;
 }  // namespace autofill
 
-namespace syncer {
-class SyncService;
-}
-
 class PrefService;
 
 namespace password_manager_util {
@@ -57,11 +53,6 @@
 // Update |credential| to reflect usage.
 void UpdateMetadataForUsage(password_manager::PasswordForm* credential);
 
-// Reports whether and how passwords are currently synced. In particular, for a
-// null |sync_service| returns NOT_SYNCING.
-password_manager::SyncState GetPasswordSyncState(
-    const syncer::SyncService* sync_service);
-
 // Removes Android username-only credentials from |android_credentials|.
 // Transforms federated credentials into non zero-click ones.
 void TrimUsernameOnlyCredentials(
diff --git a/components/password_manager/core/browser/password_save_manager_impl_unittest.cc b/components/password_manager/core/browser/password_save_manager_impl_unittest.cc
index 0343d26..a53ba1b 100644
--- a/components/password_manager/core/browser/password_save_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/password_save_manager_impl_unittest.cc
@@ -217,21 +217,21 @@
     field.name = u"firstname";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = autofill::FieldRendererId(1);
     observed_form_.fields.push_back(field);
 
     field.name = u"username";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "text";
+    field.form_control_type = autofill::StringToFormControlType("text");
     field.unique_renderer_id = autofill::FieldRendererId(2);
     observed_form_.fields.push_back(field);
 
     field.name = u"password";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = autofill::FieldRendererId(3);
     observed_form_.fields.push_back(field);
     observed_form_only_password_fields_.fields.push_back(field);
@@ -239,7 +239,7 @@
     field.name = u"password2";
     field.id_attribute = field.name;
     field.name_attribute = field.name;
-    field.form_control_type = "password";
+    field.form_control_type = autofill::StringToFormControlType("password");
     field.unique_renderer_id = autofill::FieldRendererId(5);
     observed_form_only_password_fields_.fields.push_back(field);
 
@@ -1299,20 +1299,20 @@
   field.name = matched_form_username_field_name;
   field.id_attribute = field.name;
   field.name_attribute = field.name;
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   saved_match_.form_data.fields.push_back(field);
 
   field.name = u"firstname";
   field.id_attribute = field.name;
   field.name_attribute = field.name;
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   saved_match_.form_data.fields.push_back(field);
   saved_match_.username_element = field.name;
 
   field.name = u"password";
   field.id_attribute = field.name;
   field.name_attribute = field.name;
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   saved_match_.form_data.fields.push_back(field);
   saved_match_.password_element = field.name;
 
diff --git a/components/password_manager/core/browser/password_sync_util.cc b/components/password_manager/core/browser/password_sync_util.cc
index 0af49ab..bc5faf47 100644
--- a/components/password_manager/core/browser/password_sync_util.cc
+++ b/components/password_manager/core/browser/password_sync_util.cc
@@ -6,8 +6,10 @@
 
 #include "base/strings/utf_string_conversions.h"
 #include "build/build_config.h"
+#include "components/password_manager/core/browser/features/password_features.h"
 #include "components/password_manager/core/browser/features/password_manager_features_util.h"
 #include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "components/safe_browsing/core/common/safe_browsing_prefs.h"
 #include "components/signin/public/identity_manager/identity_manager.h"
@@ -132,5 +134,28 @@
   return absl::nullopt;
 }
 
+password_manager::SyncState GetPasswordSyncState(
+    const syncer::SyncService* sync_service) {
+  if (!sync_service ||
+      !sync_service->GetActiveDataTypes().Has(syncer::PASSWORDS)) {
+    return password_manager::SyncState::kNotSyncing;
+  }
+
+  if (sync_service->IsSyncFeatureActive()) {
+    return sync_service->GetUserSettings()->IsUsingExplicitPassphrase()
+               ? password_manager::SyncState::kSyncingWithCustomPassphrase
+               : password_manager::SyncState::kSyncingNormalEncryption;
+  }
+
+  DCHECK(base::FeatureList::IsEnabled(
+      password_manager::features::kEnablePasswordsAccountStorage));
+
+  return sync_service->GetUserSettings()->IsUsingExplicitPassphrase()
+             ? password_manager::SyncState::
+                   kAccountPasswordsActiveWithCustomPassphrase
+             : password_manager::SyncState::
+                   kAccountPasswordsActiveNormalEncryption;
+}
+
 }  // namespace sync_util
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/password_sync_util.h b/components/password_manager/core/browser/password_sync_util.h
index e8749b1..4a1a39b 100644
--- a/components/password_manager/core/browser/password_sync_util.h
+++ b/components/password_manager/core/browser/password_sync_util.h
@@ -17,6 +17,7 @@
 
 namespace password_manager {
 
+enum class SyncState;
 struct PasswordForm;
 
 namespace sync_util {
@@ -69,6 +70,11 @@
     const PrefService* pref_service,
     const syncer::SyncService* sync_service);
 
+// Reports whether and how passwords are currently synced. In particular, for a
+// null |sync_service| returns NOT_SYNCING.
+password_manager::SyncState GetPasswordSyncState(
+    const syncer::SyncService* sync_service);
+
 }  // namespace sync_util
 
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/store_metrics_reporter.cc b/components/password_manager/core/browser/store_metrics_reporter.cc
index cf5f705..8cddee4 100644
--- a/components/password_manager/core/browser/store_metrics_reporter.cc
+++ b/components/password_manager/core/browser/store_metrics_reporter.cc
@@ -17,11 +17,12 @@
 #include "components/password_manager/core/browser/features/password_manager_features_util.h"
 #include "components/password_manager/core/browser/password_feature_manager.h"
 #include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
 #include "components/password_manager/core/browser/password_reuse_detector.h"
 #include "components/password_manager/core/browser/password_reuse_manager.h"
 #include "components/password_manager/core/browser/password_store_consumer.h"
+#include "components/password_manager/core/browser/password_store_interface.h"
 #include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/common/password_manager_features.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
@@ -688,7 +689,7 @@
           sync_service, identity_manager);
 
   custom_passphrase_enabled_ = IsCustomPassphraseEnabled(
-      password_manager_util::GetPasswordSyncState(sync_service));
+      password_manager::sync_util::GetPasswordSyncState(sync_service));
 
   is_opted_in_account_storage_ =
       features_util::IsOptedInForAccountStorage(prefs, sync_service);
diff --git a/components/password_manager/core/browser/sync_username_test_base.cc b/components/password_manager/core/browser/sync_username_test_base.cc
index 7239122..c6c2460 100644
--- a/components/password_manager/core/browser/sync_username_test_base.cc
+++ b/components/password_manager/core/browser/sync_username_test_base.cc
@@ -21,12 +21,12 @@
   form.url = url;
   FormFieldData field;
   field.name = u"username_element";
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   field.value = ASCIIToUTF16(username);
   form.fields.push_back(field);
 
   field.name = u"password_element";
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.value = u"strong_pw";
   form.fields.push_back(field);
   return form;
diff --git a/components/password_manager/core/browser/unified_password_manager_proto_utils.cc b/components/password_manager/core/browser/unified_password_manager_proto_utils.cc
index 5d352c5..b76f764 100644
--- a/components/password_manager/core/browser/unified_password_manager_proto_utils.cc
+++ b/components/password_manager/core/browser/unified_password_manager_proto_utils.cc
@@ -45,7 +45,8 @@
     // Stored FormFieldData is used only for signature calculations, therefore
     // only members that are used for signature calculation are stored.
     serialized_field.Set(kNameKey, field.name);
-    serialized_field.Set(kFormControlTypeKey, field.form_control_type);
+    serialized_field.Set(kFormControlTypeKey, autofill::FormControlTypeToString(
+                                                  field.form_control_type));
     serialized_fields.Append(std::move(serialized_field));
   }
   serialized_data.Set(kFieldsKey, std::move(serialized_fields));
@@ -94,7 +95,7 @@
       return absl::nullopt;
     }
     field.name = base::UTF8ToUTF16(*field_name);
-    field.form_control_type = *field_type;
+    field.form_control_type = autofill::StringToFormControlType(*field_type);
     form_data.fields.push_back(field);
   }
   return form_data;
diff --git a/components/password_manager/core/browser/unified_password_manager_proto_utils_unittest.cc b/components/password_manager/core/browser/unified_password_manager_proto_utils_unittest.cc
index fe184c0..7d0af4fd 100644
--- a/components/password_manager/core/browser/unified_password_manager_proto_utils_unittest.cc
+++ b/components/password_manager/core/browser/unified_password_manager_proto_utils_unittest.cc
@@ -107,10 +107,10 @@
   ASSERT_EQ(form.form_data.fields.size(), 2u);
   EXPECT_EQ(form.form_data.fields[0].name, kTestUsernameElementName16);
   EXPECT_EQ(form.form_data.fields[0].form_control_type,
-            kTestUsernameElementType);
+            autofill::StringToFormControlType(kTestUsernameElementType));
   EXPECT_EQ(form.form_data.fields[1].name, kTestPasswordElementName16);
   EXPECT_EQ(form.form_data.fields[1].form_control_type,
-            kTestPasswordElementType);
+            autofill::StringToFormControlType(kTestPasswordElementType));
 
   PasswordWithLocalData password_data_converted_back =
       PasswordWithLocalDataFromPassword(form);
diff --git a/components/password_manager/core/common/password_manager_util_unittest.cc b/components/password_manager/core/common/password_manager_util_unittest.cc
index 28fe1f0..6be77fb 100644
--- a/components/password_manager/core/common/password_manager_util_unittest.cc
+++ b/components/password_manager/core/common/password_manager_util_unittest.cc
@@ -21,7 +21,8 @@
                               std::string autocomplete_attribute) {
   // TODO(1465839): Use autofill helpers once they are accessible to /common.
   FormFieldData field;
-  field.form_control_type = std::move(form_control_type);
+  field.form_control_type =
+      autofill::StringToFormControlType(form_control_type);
   field.autocomplete_attribute = std::move(autocomplete_attribute);
 
   return field;
diff --git a/components/password_manager/ios/shared_password_controller_unittest.mm b/components/password_manager/ios/shared_password_controller_unittest.mm
index 6e79cf6..b88a501 100644
--- a/components/password_manager/ios/shared_password_controller_unittest.mm
+++ b/components/password_manager/ios/shared_password_controller_unittest.mm
@@ -603,7 +603,7 @@
   field.id_attribute = field.name;
   field.name_attribute = field.name;
   field.value = u"googleuser";
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   field.unique_renderer_id = autofill::test::MakeFieldRendererId();
   form_data.fields.push_back(field);
 
@@ -611,7 +611,7 @@
   field.id_attribute = field.name;
   field.name_attribute = field.name;
   field.value = u"p4ssword";
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.unique_renderer_id = autofill::test::MakeFieldRendererId();
   field.max_length = max_length;
   form_data.fields.push_back(field);
diff --git a/components/password_manager/ios/test_helpers.cc b/components/password_manager/ios/test_helpers.cc
index cc59063a..df5a0ac9a 100644
--- a/components/password_manager/ios/test_helpers.cc
+++ b/components/password_manager/ios/test_helpers.cc
@@ -78,12 +78,12 @@
 
   FormFieldData field;
   field.value = base::UTF8ToUTF16(username_value);
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   field.unique_renderer_id = FieldRendererId(username_field_id);
   form_data->fields.push_back(field);
 
   field.value = base::UTF8ToUTF16(password_value);
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   field.unique_renderer_id = FieldRendererId(password_field_id);
   form_data->fields.push_back(field);
 }
@@ -99,14 +99,14 @@
   field.id_attribute = field.name;
   field.name_attribute = field.name;
   field.value = u"googleuser";
-  field.form_control_type = "text";
+  field.form_control_type = autofill::StringToFormControlType("text");
   form_data.fields.push_back(field);
 
   field.name = u"Passwd";
   field.id_attribute = field.name;
   field.name_attribute = field.name;
   field.value = u"p4ssword";
-  field.form_control_type = "password";
+  field.form_control_type = autofill::StringToFormControlType("password");
   form_data.fields.push_back(field);
 
   return form_data;
diff --git a/components/performance_manager/resource_attribution/cpu_measurement_monitor.cc b/components/performance_manager/resource_attribution/cpu_measurement_monitor.cc
index 8784475..f471091 100644
--- a/components/performance_manager/resource_attribution/cpu_measurement_monitor.cc
+++ b/components/performance_manager/resource_attribution/cpu_measurement_monitor.cc
@@ -107,17 +107,17 @@
 }
 
 // Adds the measurement in `delta` to `result`. The start time of `delta` must
-// immediately follow the end time of `result`. Used for adding successive
-// measurements of process, frame and worker contexts.
+// follow the end time of `result`. Used for adding successive measurements of
+// process, frame and worker contexts. There may be gaps between deltas, such as
+// if a process died and was restarted.
 void ApplySequentialDelta(CPUTimeResult& result, const CPUTimeResult& delta) {
   CHECK(!IsEmptyCPUTimeResult(delta));
   ValidateCPUTimeResult(delta);
   if (IsEmptyCPUTimeResult(result)) {
     result = delta;
   } else {
-    // Successive measurement periods should be back to back.
     ValidateCPUTimeResult(result);
-    CHECK_EQ(result.metadata.measurement_time, delta.start_time);
+    CHECK_LE(result.metadata.measurement_time, delta.start_time);
     result.metadata.measurement_time = delta.metadata.measurement_time;
     result.cumulative_cpu += delta.cumulative_cpu;
   }
@@ -307,7 +307,10 @@
     return;
   }
   // Only handle process start notifications (which is when the pid is
-  // assigned), not exit notifications.
+  // assigned), not exit notifications. Note the pid can be reassigned if a
+  // process dies and a new one is started for the same ProcessNode - in that
+  // case MonitorCPUUsage will reset the measurements and start monitoring the
+  // new process from scratch.
   if (!process_node->GetProcess().IsValid()) {
     return;
   }
@@ -315,11 +318,7 @@
   if (process_node->GetProcessType() != content::PROCESS_TYPE_RENDERER) {
     return;
   }
-  if (!base::Contains(cpu_measurement_map_, process_node)) {
-    // Process isn't being measured yet so it must have been created while
-    // measurements were already started.
-    MonitorCPUUsage(process_node);
-  }
+  MonitorCPUUsage(process_node);
 }
 
 void CPUMeasurementMonitor::OnBeforeProcessNodeRemoved(
@@ -400,10 +399,16 @@
   // TODO(crbug.com/1471683): Measure other process types, just don't distribute
   // the measurements to frames and workers.
   CHECK_EQ(process_node->GetProcessType(), content::PROCESS_TYPE_RENDERER);
-  const auto& [it, was_inserted] = cpu_measurement_map_.emplace(
+
+  // If a process crashes and is restarted, a new process can be assigned to the
+  // same ProcessNode (and the same ProcessContext). When that happens
+  // OnProcessLifetimeChange will call MonitorCPUUsage again for the same node,
+  // creating a new CPUMeasurement that starts measuring the new process from 0.
+  // ApplyMeasurementDeltas will add the new measurements and the old
+  // measurements in the same ProcessContext.
+  cpu_measurement_map_.insert_or_assign(
       process_node,
       CPUMeasurement(cpu_measurement_delegate_factory_.Run(process_node)));
-  CHECK(was_inserted);
 }
 
 void CPUMeasurementMonitor::UpdateAllCPUMeasurements() {
diff --git a/components/performance_manager/resource_attribution/cpu_measurement_monitor_unittest.cc b/components/performance_manager/resource_attribution/cpu_measurement_monitor_unittest.cc
index 9606b48..c3916bf9 100644
--- a/components/performance_manager/resource_attribution/cpu_measurement_monitor_unittest.cc
+++ b/components/performance_manager/resource_attribution/cpu_measurement_monitor_unittest.cc
@@ -1263,7 +1263,7 @@
   std::unique_ptr<CPUMeasurementMonitor> cpu_monitor_;
 };
 
-TEST_F(CPUMeasurementMonitorTimingTest, DISABLED_ProcessLifetime) {
+TEST_F(CPUMeasurementMonitorTimingTest, ProcessLifetime) {
   SetContents(CreateTestWebContents());
   content::NavigationSimulator::NavigateAndCommitFromBrowser(
       web_contents(), GURL("https://www.example.com/"));
@@ -1282,38 +1282,98 @@
     EXPECT_EQ(process_node->GetProcessId(), base::kNullProcessId);
 
     // Process can't be measured yet.
-    EXPECT_FALSE(base::Contains(cpu_monitor_->UpdateAndGetCPUMeasurements(),
-                                frame_context));
+    const auto measurements = cpu_monitor_->UpdateAndGetCPUMeasurements();
+    EXPECT_FALSE(
+        base::Contains(measurements, process_node->GetResourceContext()));
+    EXPECT_FALSE(base::Contains(measurements, frame_context));
   }));
 
   // Assign a real process to the ProcessNode. (Will call
-  // OnProcessLifetimeChange.)
-  LetTimePass();
-  RunOnPMSequence(base::BindLambdaForTesting([&] {
+  // OnProcessLifetimeChange and start monitoring.)
+  auto set_process_on_pm_sequence = [&process_node] {
     ASSERT_TRUE(process_node);
     ProcessNodeImpl::FromNode(process_node.get())
         ->SetProcess(base::Process::Current(), base::TimeTicks::Now());
     EXPECT_NE(process_node->GetProcessId(), base::kNullProcessId);
+  };
+  RunOnPMSequence(base::BindLambdaForTesting(set_process_on_pm_sequence));
+
+  // Let some time pass so there's CPU to measure after monitoring starts.
+  LetTimePass();
+
+  base::TimeDelta cumulative_process_cpu;
+  base::TimeDelta cumulative_frame_cpu;
+  RunOnPMSequence(base::BindLambdaForTesting([&] {
+    ASSERT_TRUE(process_node);
+    EXPECT_TRUE(process_node->GetProcess().IsValid());
 
     // Process can be measured now.
-    EXPECT_TRUE(base::Contains(cpu_monitor_->UpdateAndGetCPUMeasurements(),
-                               frame_context));
+    const auto measurements = cpu_monitor_->UpdateAndGetCPUMeasurements();
+
+    ASSERT_TRUE(
+        base::Contains(measurements, process_node->GetResourceContext()));
+    cumulative_process_cpu =
+        measurements.at(process_node->GetResourceContext()).cumulative_cpu;
+    EXPECT_FALSE(cumulative_process_cpu.is_negative());
+
+    ASSERT_TRUE(base::Contains(measurements, frame_context));
+    cumulative_frame_cpu = measurements.at(frame_context).cumulative_cpu;
+    EXPECT_FALSE(cumulative_frame_cpu.is_negative());
   }));
 
   // Simulate that the process died.
-  LetTimePass();
   process()->SimulateRenderProcessExit(
       base::TERMINATION_STATUS_NORMAL_TERMINATION, 0);
-  RunOnPMSequence(base::BindLambdaForTesting([&](Graph* graph) {
+  LetTimePass();
+  RunOnPMSequence(base::BindLambdaForTesting([&] {
     // Process is no longer running, so can't be measured.
-    // TODO(crbug.com/1410503): Capture the final CPU usage correctly.
     ASSERT_TRUE(process_node);
     EXPECT_FALSE(process_node->GetProcess().IsValid());
-    // Depending on the order that observers fire, the main frame may or may not
-    // have been deleted already. If it's deleted CPUMeasurementMonitor will
-    // return its last measured usage.
-    EXPECT_TRUE(base::Contains(cpu_monitor_->UpdateAndGetCPUMeasurements(),
-                               frame_context));
+
+    // CPUMeasurementMonitor will continue to return the last measured usage of
+    // the process and its main frame.
+    // TODO(crbug.com/1410503): Capture the final CPU usage correctly, and after
+    // the main FrameNode is deleted, only cache it for the length of one query.
+    const auto measurements = cpu_monitor_->UpdateAndGetCPUMeasurements();
+
+    ASSERT_TRUE(
+        base::Contains(measurements, process_node->GetResourceContext()));
+    const base::TimeDelta new_process_cpu =
+        measurements.at(process_node->GetResourceContext()).cumulative_cpu;
+    EXPECT_GE(new_process_cpu, cumulative_process_cpu);
+    cumulative_process_cpu = new_process_cpu;
+
+    ASSERT_TRUE(base::Contains(measurements, frame_context));
+    const base::TimeDelta new_frame_cpu =
+        measurements.at(frame_context).cumulative_cpu;
+    EXPECT_GE(new_frame_cpu, cumulative_frame_cpu);
+    cumulative_frame_cpu = new_frame_cpu;
+  }));
+
+  // Assign a new process to the same renderer. This should add the CPU usage of
+  // the new process to the existing CPU usage.
+  EXPECT_TRUE(process()->MayReuseHost());
+  RunOnPMSequence(base::BindLambdaForTesting(set_process_on_pm_sequence));
+
+  LetTimePass();
+  RunOnPMSequence(base::BindLambdaForTesting([&] {
+    ASSERT_TRUE(process_node);
+    EXPECT_TRUE(process_node->GetProcess().IsValid());
+
+    const auto measurements = cpu_monitor_->UpdateAndGetCPUMeasurements();
+
+    ASSERT_TRUE(
+        base::Contains(measurements, process_node->GetResourceContext()));
+    const base::TimeDelta new_process_cpu =
+        measurements.at(process_node->GetResourceContext()).cumulative_cpu;
+    EXPECT_GE(new_process_cpu, cumulative_process_cpu);
+    cumulative_process_cpu = new_process_cpu;
+
+    ASSERT_TRUE(base::Contains(measurements, frame_context));
+    const base::TimeDelta new_frame_cpu =
+        measurements.at(frame_context).cumulative_cpu;
+    EXPECT_GE(new_frame_cpu, cumulative_frame_cpu);
+    cumulative_frame_cpu = new_frame_cpu;
   }));
 }
 
diff --git a/components/plus_addresses/features.cc b/components/plus_addresses/features.cc
index 2600abb0..80c4498a 100644
--- a/components/plus_addresses/features.cc
+++ b/components/plus_addresses/features.cc
@@ -18,6 +18,7 @@
 const char kEnterprisePlusAddressServerUrlName[] = "server-url";
 const char kSyncWithEnterprisePlusAddressServerName[] = "sync-with-server";
 const char kEnterprisePlusAddressTimerDelayName[] = "timer-delay";
+const char kPlusAddressManagementUrlName[] = "manage-url";
 
 const base::FeatureParam<std::string> kEnterprisePlusAddressLabelOverride{
     &kFeature, kEnterprisePlusAddressLabelOverrideName, "Lorem Ipsum"};
@@ -29,4 +30,6 @@
     &kFeature, kSyncWithEnterprisePlusAddressServerName, false};
 const base::FeatureParam<base::TimeDelta> kEnterprisePlusAddressTimerDelay{
     &kFeature, kEnterprisePlusAddressTimerDelayName, base::Hours(24)};
+const base::FeatureParam<std::string> kPlusAddressManagementUrl{
+    &kFeature, kPlusAddressManagementUrlName, ""};
 }  // namespace plus_addresses
diff --git a/components/plus_addresses/features.h b/components/plus_addresses/features.h
index 9a43b9c..bd0a73d4 100644
--- a/components/plus_addresses/features.h
+++ b/components/plus_addresses/features.h
@@ -39,6 +39,10 @@
 COMPONENT_EXPORT(PLUS_ADDRESSES_FEATURES)
 extern const base::FeatureParam<base::TimeDelta>
     kEnterprisePlusAddressTimerDelay;
+
+COMPONENT_EXPORT(PLUS_ADDRESSES_FEATURES)
+extern const base::FeatureParam<std::string> kPlusAddressManagementUrl;
+
 }  // namespace plus_addresses
 
 #endif  // COMPONENTS_PLUS_ADDRESSES_FEATURES_H_
diff --git a/components/privacy_sandbox/android/BUILD.gn b/components/privacy_sandbox/android/BUILD.gn
index 928b746..6baa7a09 100644
--- a/components/privacy_sandbox/android/BUILD.gn
+++ b/components/privacy_sandbox/android/BUILD.gn
@@ -16,6 +16,7 @@
     "//base:base_java",
     "//components/browser_ui/settings/android:java",
     "//components/browser_ui/site_settings/android:java",
+    "//components/browser_ui/styles/android:java",
     "//components/content_settings/android:content_settings_enums_java",
     "//components/embedder_support/android:util_java",
     "//components/prefs/android:java",
diff --git a/components/privacy_sandbox/android/java/src/org/chromium/components/privacy_sandbox/TrackingProtectionSettings.java b/components/privacy_sandbox/android/java/src/org/chromium/components/privacy_sandbox/TrackingProtectionSettings.java
index 4108c22e..727e1d2 100644
--- a/components/privacy_sandbox/android/java/src/org/chromium/components/privacy_sandbox/TrackingProtectionSettings.java
+++ b/components/privacy_sandbox/android/java/src/org/chromium/components/privacy_sandbox/TrackingProtectionSettings.java
@@ -5,24 +5,33 @@
 package org.chromium.components.privacy_sandbox;
 
 import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
 
+import androidx.annotation.ColorInt;
+import androidx.preference.Preference;
+import androidx.preference.Preference.OnPreferenceClickListener;
 import androidx.preference.PreferenceFragmentCompat;
 
 import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
 import org.chromium.components.browser_ui.settings.CustomDividerFragment;
 import org.chromium.components.browser_ui.settings.ExpandablePreferenceGroup;
 import org.chromium.components.browser_ui.settings.SettingsUtils;
+import org.chromium.components.browser_ui.settings.TextMessagePreference;
 import org.chromium.components.browser_ui.site_settings.SiteSettingsCategory;
 import org.chromium.components.browser_ui.site_settings.Website;
 import org.chromium.components.browser_ui.site_settings.WebsitePermissionsFetcher;
+import org.chromium.components.browser_ui.styles.SemanticColorUtils;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 
 /** Fragment to manage settings for tracking protection. */
-public class TrackingProtectionSettings
-        extends PreferenceFragmentCompat implements CustomDividerFragment {
+public class TrackingProtectionSettings extends PreferenceFragmentCompat
+        implements CustomDividerFragment, OnPreferenceClickListener {
     // Must match keys in tracking_protection_preferences.xml.
     private static final String PREF_BLOCK_ALL_TOGGLE = "block_all_3pcd_toggle";
     private static final String PREF_DNT_TOGGLE = "dnt_toggle";
@@ -31,6 +40,9 @@
     // The number of sites that are on the Allowed list.
     private int mAllowedSiteCount;
 
+    // Whether the Allowed list should be shown expanded.
+    private boolean mAllowListExpanded = true;
+
     private TrackingProtectionDelegate mDelegate;
 
     @Override
@@ -57,7 +69,11 @@
             return true;
         });
 
+        mAllowListExpanded = true;
         mAllowedSiteCount = 0;
+        ExpandablePreferenceGroup allowedGroup =
+                getPreferenceScreen().findPreference(ALLOWED_GROUP);
+        allowedGroup.setOnPreferenceClickListener(this);
         getBlockingExceptions();
     }
 
@@ -67,6 +83,16 @@
         return false;
     }
 
+    // OnPreferenceClickListener:
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        if (ALLOWED_GROUP.equals(preference.getKey())) {
+            mAllowListExpanded = !mAllowListExpanded;
+        }
+        getBlockingExceptions();
+        return true;
+    }
+
     public void setTrackingProtectionDelegate(TrackingProtectionDelegate delegate) {
         mDelegate = delegate;
     }
@@ -88,17 +114,40 @@
 
         ExpandablePreferenceGroup allowedGroup =
                 getPreferenceScreen().findPreference(ALLOWED_GROUP);
+        // Add the description preference.
+        var description = new TextMessagePreference(getContext(), null);
+        description.setSummary(getString(R.string.tracking_protection_allowed_group_description));
+        allowedGroup.addPreference(description);
+        mAllowedSiteCount = 0;
         for (WebsiteExceptionRowPreference website : websites) {
             allowedGroup.addPreference(website);
             mAllowedSiteCount++;
         }
+        if (!mAllowListExpanded) allowedGroup.removeAll();
         updateExceptionsHeader();
     }
 
     private void updateExceptionsHeader() {
         ExpandablePreferenceGroup allowedGroup =
                 getPreferenceScreen().findPreference(ALLOWED_GROUP);
-        allowedGroup.setTitle(String.format(
-                getString(R.string.tracking_protection_allowed_group_title), mAllowedSiteCount));
+        SpannableStringBuilder spannable = new SpannableStringBuilder(
+                getString(R.string.tracking_protection_allowed_group_title));
+        String prefCount = String.format(Locale.getDefault(), " - %d", mAllowedSiteCount);
+        spannable.append(prefCount);
+
+        // Color the first part of the title blue.
+        ForegroundColorSpan blueSpan = new ForegroundColorSpan(
+                SemanticColorUtils.getDefaultTextColorAccent1(getContext()));
+        spannable.setSpan(blueSpan, 0, spannable.length() - prefCount.length(),
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        // Gray out the total count of items.
+        final @ColorInt int gray = SemanticColorUtils.getDefaultTextColorSecondary(getContext());
+        spannable.setSpan(new ForegroundColorSpan(gray), spannable.length() - prefCount.length(),
+                spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        // Configure the preference group.
+        allowedGroup.setTitle(spannable);
+        allowedGroup.setExpanded(mAllowListExpanded);
     }
 }
diff --git a/components/privacy_sandbox/privacy_sandbox_features.cc b/components/privacy_sandbox/privacy_sandbox_features.cc
index cc1bec9..72e891d95 100644
--- a/components/privacy_sandbox/privacy_sandbox_features.cc
+++ b/components/privacy_sandbox/privacy_sandbox_features.cc
@@ -121,4 +121,9 @@
              "TrackingProtectionOnboardingForceEligibility",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Resets the tracking protection Onboarding eligibility.
+BASE_FEATURE(kTrackingProtectionOnboardingResetEligibilityForTesting,
+             "TrackingProtectionOnboardingResetEligibilityForTesting",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 }  // namespace privacy_sandbox
diff --git a/components/privacy_sandbox/privacy_sandbox_features.h b/components/privacy_sandbox/privacy_sandbox_features.h
index 8faa20b7..f5da73b 100644
--- a/components/privacy_sandbox/privacy_sandbox_features.h
+++ b/components/privacy_sandbox/privacy_sandbox_features.h
@@ -163,6 +163,10 @@
 COMPONENT_EXPORT(PRIVACY_SANDBOX_FEATURES)
 BASE_DECLARE_FEATURE(kTrackingProtectionOnboardingForceEligibility);
 
+// Resets eligibility for Tracking Protection Onboarding.
+COMPONENT_EXPORT(PRIVACY_SANDBOX_FEATURES)
+BASE_DECLARE_FEATURE(kTrackingProtectionOnboardingResetEligibilityForTesting);
+
 }  // namespace privacy_sandbox
 
 #endif  // COMPONENTS_PRIVACY_SANDBOX_PRIVACY_SANDBOX_FEATURES_H_
diff --git a/components/privacy_sandbox/tracking_protection_onboarding.cc b/components/privacy_sandbox/tracking_protection_onboarding.cc
index 2a71b51..8dea036 100644
--- a/components/privacy_sandbox/tracking_protection_onboarding.cc
+++ b/components/privacy_sandbox/tracking_protection_onboarding.cc
@@ -51,6 +51,13 @@
   }
 }
 
+void ClearAllPrefs(PrefService* pref_service) {
+  pref_service->ClearPref(prefs::kTrackingProtectionOnboardingStatus);
+  pref_service->ClearPref(prefs::kTrackingProtectionEligibleSince);
+  pref_service->ClearPref(prefs::kTrackingProtectionOnboardedSince);
+  pref_service->ClearPref(prefs::kTrackingProtectionOnboardingAcked);
+}
+
 }  // namespace
 
 TrackingProtectionOnboarding::TrackingProtectionOnboarding(
@@ -70,6 +77,13 @@
           &TrackingProtectionOnboarding::OnOnboardingAckedChanged,
           base::Unretained(this)));
 
+  // If we're resetting eligibility, let's clear all our prefs first.
+  if (base::FeatureList::IsEnabled(
+          privacy_sandbox::
+              kTrackingProtectionOnboardingResetEligibilityForTesting)) {
+    ClearAllPrefs(pref_service_);
+  }
+
   // If we're forcing eligibility, then let' set it now.
   if (base::FeatureList::IsEnabled(
           privacy_sandbox::kTrackingProtectionOnboardingForceEligibility) &&
diff --git a/components/privacy_sandbox/tracking_protection_onboarding_unittest.cc b/components/privacy_sandbox/tracking_protection_onboarding_unittest.cc
index cafeb1f..70d799c 100644
--- a/components/privacy_sandbox/tracking_protection_onboarding_unittest.cc
+++ b/components/privacy_sandbox/tracking_protection_onboarding_unittest.cc
@@ -373,7 +373,7 @@
         std::pair(TrackingProtectionOnboardingStatus::kOnboarded,
                   TrackingProtectionOnboarding::OnboardingStatus::kOnboarded)));
 
-class TrackingProtectionOnboardingWithFeatureOverrideTest
+class TrackingProtectionOnboardingWithForceEligibilityFeatureTest
     : public TrackingProtectionOnboardingTest {
  public:
   void SetUp() override {
@@ -387,11 +387,54 @@
   base::test::ScopedFeatureList feature_list_;
 };
 
-TEST_F(TrackingProtectionOnboardingWithFeatureOverrideTest,
+TEST_F(TrackingProtectionOnboardingWithForceEligibilityFeatureTest,
        StartsUpAsEligible) {
   EXPECT_EQ(tracking_protection_onboarding()->GetOnboardingStatus(),
             TrackingProtectionOnboarding::OnboardingStatus::kEligible);
 }
 
+class TrackingProtectionOnboardingWithResetEligibilityFeatureTest
+    : public TrackingProtectionOnboardingTest {
+ public:
+  void SetUp() override {
+    feature_list_.InitAndEnableFeature(
+        privacy_sandbox::
+            kTrackingProtectionOnboardingResetEligibilityForTesting);
+
+    prefs()->SetInteger(
+        prefs::kTrackingProtectionOnboardingStatus,
+        static_cast<int>(TrackingProtectionOnboardingStatus::kOnboarded));
+    prefs()->SetBoolean(prefs::kTrackingProtectionOnboardingAcked, true);
+    prefs()->SetTime(prefs::kTrackingProtectionEligibleSince,
+                     base::Time::Now());
+    prefs()->SetTime(prefs::kTrackingProtectionOnboardedSince,
+                     base::Time::Now());
+
+    tracking_protection_onboarding_service_ =
+        std::make_unique<TrackingProtectionOnboarding>(prefs());
+  }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+};
+
+TEST_F(TrackingProtectionOnboardingWithResetEligibilityFeatureTest,
+       RequiresNoticeOnStartup) {
+  EXPECT_EQ(tracking_protection_onboarding()->GetOnboardingStatus(),
+            TrackingProtectionOnboarding::OnboardingStatus::kIneligible);
+  EXPECT_FALSE(prefs()
+                   ->FindPreference(prefs::kTrackingProtectionOnboardingStatus)
+                   ->HasUserSetting());
+  EXPECT_FALSE(prefs()
+                   ->FindPreference(prefs::kTrackingProtectionOnboardingAcked)
+                   ->HasUserSetting());
+  EXPECT_FALSE(prefs()
+                   ->FindPreference(prefs::kTrackingProtectionEligibleSince)
+                   ->HasUserSetting());
+  EXPECT_FALSE(prefs()
+                   ->FindPreference(prefs::kTrackingProtectionOnboardedSince)
+                   ->HasUserSetting());
+}
+
 }  // namespace
 }  // namespace privacy_sandbox
diff --git a/components/privacy_sandbox_strings.grdp b/components/privacy_sandbox_strings.grdp
index fc0cbe1..60a31a3 100644
--- a/components/privacy_sandbox_strings.grdp
+++ b/components/privacy_sandbox_strings.grdp
@@ -2206,7 +2206,10 @@
     Sites use their discretion when responding to this request
   </message>
   <message name="IDS_TRACKING_PROTECTION_ALLOWED_GROUP_TITLE" desc="" translateable="false" formatter_data="android_java">
-    Sites allowed to use third-party cookies - <ph name="NUM_WEBSITES">%1$s<ex>3</ex></ph>
+    Sites allowed to use third-party cookies
+  </message>
+  <message name="IDS_TRACKING_PROTECTION_ALLOWED_GROUP_DESCRIPTION" desc="" translateable="false" formatter_data="android_java">
+    Affects the sites listed here and their subdomains. For example, adding “google.com” means that third-party cookies can also be active for mail.google.com, because it’s part of google.com.
   </message>
   <message name="IDS_TRACKING_PROTECTION_NEVER_EXPIRES_LABEL" desc="" translateable="false" formatter_data="android_java">
     Does not expire
diff --git a/components/renderer_context_menu/render_view_context_menu_proxy.h b/components/renderer_context_menu/render_view_context_menu_proxy.h
index 80c06fa..7f06164 100644
--- a/components/renderer_context_menu/render_view_context_menu_proxy.h
+++ b/components/renderer_context_menu/render_view_context_menu_proxy.h
@@ -118,7 +118,7 @@
   virtual void AddAccessibilityLabelsServiceItem(bool is_checked) = 0;
 
   // Add PDF OCR item to the context menu.
-  virtual void AddPdfOcrMenuItem(bool is_checked) = 0;
+  virtual void AddPdfOcrMenuItem() = 0;
 
   // Retrieve the given associated objects with a context menu.
   virtual content::RenderViewHost* GetRenderViewHost() const = 0;
diff --git a/components/segmentation_platform/internal/database/signal_storage_config.cc b/components/segmentation_platform/internal/database/signal_storage_config.cc
index 178dd150..142e5f7 100644
--- a/components/segmentation_platform/internal/database/signal_storage_config.cc
+++ b/components/segmentation_platform/internal/database/signal_storage_config.cc
@@ -7,6 +7,7 @@
 #include "base/containers/contains.h"
 #include "base/functional/callback_helpers.h"
 #include "components/segmentation_platform/internal/metadata/metadata_utils.h"
+#include "components/segmentation_platform/public/proto/model_metadata.pb.h"
 
 namespace segmentation_platform {
 namespace {
@@ -77,6 +78,20 @@
   return nullptr;
 }
 
+void SignalStorageConfig::UpdateConfigForUMASignal(
+    int signal_storage_length,
+    bool* is_dirty,
+    const proto::UMAFeature& feature) {
+  if (metadata_utils::ValidateMetadataUmaFeature(feature) !=
+      metadata_utils::ValidationResult::kValidationSuccess) {
+    return;
+  }
+  if (UpdateConfigForSignal(signal_storage_length, feature.name_hash(), 0,
+                            feature.type())) {
+    *is_dirty = true;
+  }
+}
+
 bool SignalStorageConfig::UpdateConfigForSignal(int signal_storage_length,
                                                 uint64_t signal_hash,
                                                 uint64_t event_hash,
@@ -133,24 +148,36 @@
 
   // Loop through all the signals specified in the model, and check if they have
   // been collected long enough.
-  auto features =
-      metadata_utils::GetAllUmaFeatures(model_metadata, include_outputs);
-  for (auto const& feature : features) {
-    // Skip the signals that has bucket_count set to 0. These ones are only for
-    // collection purposes and hence don't get used in model evaluation.
-    if (feature.bucket_count() == 0)
-      continue;
+  bool meets_requirement = true;
+  auto feature_visit = base::BindRepeating(
+      [](SignalStorageConfig* config,
+         base::TimeDelta min_signal_collection_length, bool* meets_requirement,
+         const proto::UMAFeature& feature) {
+        // Skip the signals that has bucket_count set to 0. These ones are
+        // only for collection purposes and hence don't get used in model
+        // evaluation.
+        if (feature.bucket_count() == 0) {
+          return;
+        }
 
-    if (metadata_utils::ValidateMetadataUmaFeature(feature) !=
-        metadata_utils::ValidationResult::kValidationSuccess) {
-      continue;
-    }
+        if (metadata_utils::ValidateMetadataUmaFeature(feature) !=
+            metadata_utils::ValidationResult::kValidationSuccess) {
+          return;
+        }
 
-    if (!MeetsSignalCollectionRequirementForSignal(min_signal_collection_length,
-                                                   feature.name_hash(), 0,
-                                                   feature.type())) {
-      return false;
-    };
+        if (!config->MeetsSignalCollectionRequirementForSignal(
+                min_signal_collection_length, feature.name_hash(), 0,
+                feature.type())) {
+          *meets_requirement = false;
+          return;
+        };
+      },
+      base::Unretained(this), min_signal_collection_length,
+      base::Unretained(&meets_requirement));
+  metadata_utils::VisitAllUmaFeatures(model_metadata, include_outputs,
+                                      std::move(feature_visit));
+  if (!meets_requirement) {
+    return false;
   }
 
   // Loop through sql features.
@@ -188,18 +215,11 @@
 
   // Run through the model and calculate for each signal.
   bool is_dirty = false;
-  auto features = metadata_utils::GetAllUmaFeatures(model_metadata,
-                                                    /*include_outputs=*/true);
-  for (auto const& feature : features) {
-    if (metadata_utils::ValidateMetadataUmaFeature(feature) !=
-        metadata_utils::ValidationResult::kValidationSuccess) {
-      continue;
-    }
-    if (UpdateConfigForSignal(signal_storage_length, feature.name_hash(), 0,
-                              feature.type())) {
-      is_dirty = true;
-    }
-  }
+  metadata_utils::VisitAllUmaFeatures(
+      model_metadata, /*include_outputs=*/true,
+      base::BindRepeating(&SignalStorageConfig::UpdateConfigForUMASignal,
+                          base::Unretained(this), signal_storage_length,
+                          base::Unretained(&is_dirty)));
 
   // Add signals for sql features.
   for (auto const& feature : model_metadata.input_features()) {
diff --git a/components/segmentation_platform/internal/database/signal_storage_config.h b/components/segmentation_platform/internal/database/signal_storage_config.h
index 8fbddcd..a6f62a4b 100644
--- a/components/segmentation_platform/internal/database/signal_storage_config.h
+++ b/components/segmentation_platform/internal/database/signal_storage_config.h
@@ -88,6 +88,10 @@
                                          uint64_t event_hash,
                                          proto::SignalType signal_type);
 
+  void UpdateConfigForUMASignal(int signal_storage_length,
+                                bool* is_dirty,
+                                const proto::UMAFeature& feature);
+
   bool UpdateConfigForSignal(int signal_storage_length,
                              uint64_t signal_hash,
                              uint64_t event_hash,
diff --git a/components/segmentation_platform/internal/execution/model_manager.h b/components/segmentation_platform/internal/execution/model_manager.h
index 8a909f6..ead71f76 100644
--- a/components/segmentation_platform/internal/execution/model_manager.h
+++ b/components/segmentation_platform/internal/execution/model_manager.h
@@ -6,7 +6,9 @@
 #define COMPONENTS_SEGMENTATION_PLATFORM_INTERNAL_EXECUTION_MODEL_MANAGER_H_
 
 #include "base/functional/callback_forward.h"
+#include "components/segmentation_platform/public/proto/model_metadata.pb.h"
 #include "components/segmentation_platform/public/proto/segmentation_platform.pb.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace segmentation_platform {
 namespace proto {
@@ -28,7 +30,8 @@
   // Invoked whenever there are changes to the state of a segmentation model.
   // Will not be invoked unless the proto::SegmentInfo is valid.
   using SegmentationModelUpdatedCallback =
-      base::RepeatingCallback<void(proto::SegmentInfo)>;
+      base::RepeatingCallback<void(proto::SegmentInfo,
+                                   /*old_version*/ absl::optional<int64_t>)>;
 
   virtual void Initialize() = 0;
 
diff --git a/components/segmentation_platform/internal/execution/model_manager_impl.cc b/components/segmentation_platform/internal/execution/model_manager_impl.cc
index da7fdcc..ae44604 100644
--- a/components/segmentation_platform/internal/execution/model_manager_impl.cc
+++ b/components/segmentation_platform/internal/execution/model_manager_impl.cc
@@ -4,15 +4,11 @@
 
 #include "components/segmentation_platform/internal/execution/model_manager_impl.h"
 
-#include <deque>
 #include <map>
 #include <memory>
-#include <vector>
 
 #include "base/functional/bind.h"
 #include "base/functional/callback_helpers.h"
-#include "base/location.h"
-#include "base/logging.h"
 #include "base/time/clock.h"
 #include "base/time/time.h"
 #include "base/trace_event/typed_macros.h"
@@ -105,15 +101,17 @@
   }
 
   if (!metadata.has_value()) {
-    if (!segment_database_->GetCachedSegmentInfo(segment_id, model_source)) {
+    const auto* deleted_segment =
+        segment_database_->GetCachedSegmentInfo(segment_id, model_source);
+    if (!deleted_segment) {
       return;
     }
 
     segment_database_->UpdateSegment(
         segment_id, model_source, absl::nullopt,
         base::BindOnce(&ModelManagerImpl::OnSegmentInfoDeleted,
-                       weak_ptr_factory_.GetWeakPtr(), segment_id,
-                       model_source));
+                       weak_ptr_factory_.GetWeakPtr(), segment_id, model_source,
+                       deleted_segment->model_version()));
     return;
   }
 
@@ -197,11 +195,12 @@
   stats::RecordModelDeliveryMetadataFeatureCount(
       segment_id, model_source,
       new_segment_info.model_metadata().input_features_size());
+
   // Now that we've merged the old and the new SegmentInfo, we want to store
   // the new version in the database.
-  auto update_callback =
-      base::BindOnce(&ModelManagerImpl::OnUpdatedSegmentInfoStored,
-                     weak_ptr_factory_.GetWeakPtr(), new_segment_info);
+  auto update_callback = base::BindOnce(
+      &ModelManagerImpl::OnUpdatedSegmentInfoStored,
+      weak_ptr_factory_.GetWeakPtr(), new_segment_info, old_model_version);
   segment_database_->UpdateSegment(
       segment_id, model_source,
       absl::make_optional(std::move(new_segment_info)),
@@ -210,6 +209,7 @@
 
 void ModelManagerImpl::OnSegmentInfoDeleted(SegmentId segment_id,
                                             proto::ModelSource model_source,
+                                            int64_t deleted_version,
                                             bool success) {
   stats::RecordModelDeliveryDeleteResult(segment_id, model_source, success);
 
@@ -221,11 +221,12 @@
   proto::SegmentInfo deleted_segment_info;
   deleted_segment_info.set_segment_id(segment_id);
   deleted_segment_info.set_model_source(model_source);
-  model_updated_callback_.Run(std::move(deleted_segment_info));
+  model_updated_callback_.Run(std::move(deleted_segment_info), deleted_version);
 }
 
 void ModelManagerImpl::OnUpdatedSegmentInfoStored(
     proto::SegmentInfo segment_info,
+    absl::optional<int64_t> old_model_version,
     bool success) {
   TRACE_EVENT("segmentation_platform",
               "ModelManagerImpl::OnUpdatedSegmentInfoStored");
@@ -243,7 +244,7 @@
   if (segment_info.model_source() == proto::ModelSource::DEFAULT_MODEL_SOURCE) {
     return;
   }
-  model_updated_callback_.Run(std::move(segment_info));
+  model_updated_callback_.Run(std::move(segment_info), old_model_version);
 }
 
 }  // namespace segmentation_platform
diff --git a/components/segmentation_platform/internal/execution/model_manager_impl.h b/components/segmentation_platform/internal/execution/model_manager_impl.h
index a71507d..430d1b0 100644
--- a/components/segmentation_platform/internal/execution/model_manager_impl.h
+++ b/components/segmentation_platform/internal/execution/model_manager_impl.h
@@ -88,11 +88,13 @@
   // Callback after storing the updated version of the SegmentInfo.
   // Responsible for invoking the SegmentationModelUpdatedCallback.
   void OnUpdatedSegmentInfoStored(proto::SegmentInfo segment_info,
+                                  absl::optional<int64_t> old_model_version,
                                   bool success);
 
   // Callback after deleting the previous version of the SegmentInfo.
   void OnSegmentInfoDeleted(SegmentId segment_id,
                             proto::ModelSource model_source,
+                            int64_t deleted_version,
                             bool success);
 
   const base::flat_set<SegmentId>& segment_ids_;
diff --git a/components/segmentation_platform/internal/execution/model_manager_impl_unittest.cc b/components/segmentation_platform/internal/execution/model_manager_impl_unittest.cc
index dc24e36..244bd26f 100644
--- a/components/segmentation_platform/internal/execution/model_manager_impl_unittest.cc
+++ b/components/segmentation_platform/internal/execution/model_manager_impl_unittest.cc
@@ -44,6 +44,7 @@
 namespace segmentation_platform {
 namespace {
 
+const int64_t kOldModelVersion = 100;
 const int64_t kModelVersion = 123;
 
 constexpr SegmentId kSearchUserSegmentId =
@@ -154,7 +155,7 @@
   // Verify that the ModelManager never invokes its
   // SegmentInfoDatabase, nor invokes the callback.
   EXPECT_CALL(*mock_segment_database_ptr, GetSegmentInfo(_, _, _)).Times(0);
-  EXPECT_CALL(callback, Run(_)).Times(0);
+  EXPECT_CALL(callback, Run(_, _)).Times(0);
   model_provider_data_.model_providers_callbacks[segment_id].Run(
       segment_id, metadata, kModelVersion);
 }
@@ -169,7 +170,8 @@
   proto::SegmentationModelMetadata metadata;
   metadata.set_bucket_duration(42u);
   metadata.set_time_unit(proto::TimeUnit::DAY);
-  EXPECT_CALL(callback, Run(_)).WillOnce(SaveArg<0>(&segment_info));
+  EXPECT_CALL(callback, Run(_, absl::optional<int64_t>()))
+      .WillOnce(SaveArg<0>(&segment_info));
   model_provider_data_.model_providers_callbacks[segment_id].Run(
       segment_id, metadata, kModelVersion);
 
@@ -209,6 +211,8 @@
   segment_database_->AddUserActionFeature(segment_id, "hello", 2, 2,
                                           proto::Aggregation::BUCKETED_COUNT);
   segment_database_->AddPredictionResult(segment_id, 2, clock_.Now());
+  segment_database_->FindOrCreateSegment(segment_id)
+      ->set_model_version(kOldModelVersion);
 
   base::MockCallback<SegmentInfoDatabase::SegmentInfoCallback> db_callback_1;
   absl::optional<proto::SegmentInfo> segment_info_from_db_1;
@@ -259,7 +263,8 @@
 
   // Invoke the callback and store the resulting invocation of the outer
   // callback for verification.
-  EXPECT_CALL(callback, Run(_)).WillOnce(SaveArg<0>(&segment_info));
+  EXPECT_CALL(callback, Run(_, absl::optional<int64_t>(kOldModelVersion)))
+      .WillOnce(SaveArg<0>(&segment_info));
   model_provider_data_.model_providers_callbacks[segment_id].Run(
       segment_id, metadata, kModelVersion);
 
@@ -310,7 +315,8 @@
   base::MockCallback<ModelManager::SegmentationModelUpdatedCallback>
       model_updated_callback;
   proto::SegmentInfo updated_segment_info;
-  EXPECT_CALL(model_updated_callback, Run(_))
+  EXPECT_CALL(model_updated_callback,
+              Run(_, absl::optional<int64_t>(kOldModelVersion)))
       .WillOnce(SaveArg<0>(&updated_segment_info));
 
   // Fill in old data for a server model in the SegmentInfo database.
@@ -322,13 +328,16 @@
       proto::ModelSource::SERVER_MODEL_SOURCE);
   segment_database_->AddPredictionResult(
       segment_id, 2, clock_.Now(), proto::ModelSource::SERVER_MODEL_SOURCE);
+  segment_database_->FindOrCreateSegment(segment_id)
+      ->set_model_version(kOldModelVersion);
 
   CreateModelManager({segment_id}, model_updated_callback.Get());
 
   // If the server stops serving a model then we'll receive a callback with null
   // metadata.
   model_provider_data_.model_providers_callbacks[segment_id].Run(
-      segment_id, /* metadata = */ absl::nullopt, /* model_version = */ 0);
+      segment_id, /* metadata = */ absl::nullopt,
+      /* model_version = */ kModelVersion);
 
   base::MockCallback<SegmentInfoDatabase::SegmentInfoCallback> db_callback;
   absl::optional<proto::SegmentInfo> segment_info_from_db;
diff --git a/components/segmentation_platform/internal/metadata/metadata_utils.cc b/components/segmentation_platform/internal/metadata/metadata_utils.cc
index dd7253e..212ba6b 100644
--- a/components/segmentation_platform/internal/metadata/metadata_utils.cc
+++ b/components/segmentation_platform/internal/metadata/metadata_utils.cc
@@ -513,15 +513,31 @@
     const proto::SegmentationModelMetadata& model_metadata,
     bool include_outputs) {
   std::vector<proto::UMAFeature> features;
+  VisitAllUmaFeatures(model_metadata, include_outputs,
+                      base::BindRepeating(
+                          [](std::vector<proto::UMAFeature>* features,
+                             const proto::UMAFeature& feature) {
+                            features->push_back(feature);
+                          },
+                          base::Unretained(&features)));
+  return features;
+}
+
+using VisitUmaFeature =
+    base::RepeatingCallback<void(const proto::UMAFeature& feature)>;
+void VisitAllUmaFeatures(const proto::SegmentationModelMetadata& model_metadata,
+                         bool include_outputs,
+                         VisitUmaFeature visit) {
+  std::vector<proto::UMAFeature> features;
   for (int i = 0; i < model_metadata.features_size(); ++i) {
-    features.push_back(model_metadata.features(i));
+    visit.Run(model_metadata.features(i));
   }
 
   // Add training/inference inputs.
   for (int i = 0; i < model_metadata.input_features_size(); ++i) {
     auto feature = model_metadata.input_features(i);
     if (feature.has_uma_feature())
-      features.push_back(feature.uma_feature());
+      visit.Run(feature.uma_feature());
   }
 
   // Add training/inference outputs.
@@ -529,7 +545,7 @@
     for (const auto& output : model_metadata.training_outputs().outputs()) {
       DCHECK(output.has_uma_output()) << "Currently only support UMA output.";
       if (output.has_uma_output())
-        features.push_back(output.uma_output().uma_feature());
+        visit.Run(output.uma_output().uma_feature());
     }
   }
 
@@ -541,12 +557,10 @@
       const auto& trigger = training_config.observation_trigger(i);
       if (trigger.has_uma_trigger() &&
           trigger.uma_trigger().has_uma_feature()) {
-        features.push_back(trigger.uma_trigger().uma_feature());
+        visit.Run(trigger.uma_trigger().uma_feature());
       }
     }
   }
-
-  return features;
 }
 
 proto::PredictionResult CreatePredictionResult(
diff --git a/components/segmentation_platform/internal/metadata/metadata_utils.h b/components/segmentation_platform/internal/metadata/metadata_utils.h
index 411320ed..11d1052 100644
--- a/components/segmentation_platform/internal/metadata/metadata_utils.h
+++ b/components/segmentation_platform/internal/metadata/metadata_utils.h
@@ -134,9 +134,17 @@
 std::string SegmetationModelMetadataToString(
     const proto::SegmentationModelMetadata& model_metadata);
 
-// Helper method to get all UMAFeatures from a segmentation model's metadata.
+// Helper method to visit all UMAFeatures from a segmentation model's metadata.
 // When |include_outputs| is true, the UMA features for training outputs will be
 // included. Otherwise only input UMA features are included.
+using VisitUmaFeature =
+    base::RepeatingCallback<void(const proto::UMAFeature& feature)>;
+void VisitAllUmaFeatures(const proto::SegmentationModelMetadata& model_metadata,
+                         bool include_outputs,
+                         VisitUmaFeature visit);
+
+// Same as VisitAllUmaFeatures(), but copies the features and returns a vector.
+// Prefer VisitAllUmaFeatures() unless copies are required.
 std::vector<proto::UMAFeature> GetAllUmaFeatures(
     const proto::SegmentationModelMetadata& model_metadata,
     bool include_outputs);
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_impl.cc b/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
index c4100cc..4ea0bfe 100644
--- a/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
+++ b/components/segmentation_platform/internal/segmentation_platform_service_impl.cc
@@ -260,7 +260,8 @@
 }
 
 void SegmentationPlatformServiceImpl::OnSegmentationModelUpdated(
-    proto::SegmentInfo segment_info) {
+    proto::SegmentInfo segment_info,
+    absl::optional<int64_t> old_model_version) {
   CHECK(IsPlatformInitialized());
   if (!segment_info.has_model_metadata()) {
     signal_handler_.OnSignalListUpdated();
@@ -271,7 +272,13 @@
   DCHECK(metadata_utils::ValidateSegmentInfoMetadataAndFeatures(segment_info) ==
          metadata_utils::ValidationResult::kValidationSuccess);
 
-  signal_handler_.OnSignalListUpdated();
+  // This method is called when model is available for execution at startup. The
+  // segment info would not have changed for most cases.
+  const bool version_updated =
+      !old_model_version || *old_model_version != segment_info.model_version();
+  if (version_updated) {
+    signal_handler_.OnSignalListUpdated();
+  }
 
   if (!metadata_utils::SegmentUsesLegacyOutput(segment_info.segment_id())) {
     result_refresh_manager_->OnModelUpdated(&segment_info, &execution_service_);
diff --git a/components/segmentation_platform/internal/segmentation_platform_service_impl.h b/components/segmentation_platform/internal/segmentation_platform_service_impl.h
index 42724d0e..37cab873 100644
--- a/components/segmentation_platform/internal/segmentation_platform_service_impl.h
+++ b/components/segmentation_platform/internal/segmentation_platform_service_impl.h
@@ -10,7 +10,6 @@
 
 #include "base/containers/circular_deque.h"
 #include "base/containers/flat_map.h"
-#include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/memory/weak_ptr.h"
@@ -132,7 +131,8 @@
   void OnDatabaseInitialized(bool success);
 
   // Must only be invoked with a valid SegmentInfo.
-  void OnSegmentationModelUpdated(proto::SegmentInfo segment_info);
+  void OnSegmentationModelUpdated(proto::SegmentInfo segment_info,
+                                  absl::optional<int64_t> old_model_version);
 
   // Callback sent to child classes to notify when model results need to be
   // refreshed. For example, when history is cleared.
diff --git a/components/segmentation_platform/internal/signals/signal_filter_processor.cc b/components/segmentation_platform/internal/signals/signal_filter_processor.cc
index 8bc1bb1..a76dc36 100644
--- a/components/segmentation_platform/internal/signals/signal_filter_processor.cc
+++ b/components/segmentation_platform/internal/signals/signal_filter_processor.cc
@@ -18,6 +18,7 @@
 #include "components/segmentation_platform/internal/signals/user_action_signal_handler.h"
 #include "components/segmentation_platform/internal/stats.h"
 #include "components/segmentation_platform/internal/ukm_data_manager.h"
+#include "components/segmentation_platform/public/proto/model_metadata.pb.h"
 #include "components/segmentation_platform/public/proto/types.pb.h"
 
 namespace segmentation_platform {
@@ -30,7 +31,10 @@
     for (auto& info : segment_infos) {
       const proto::SegmentInfo& segment_info = *info.second;
       const auto& metadata = segment_info.model_metadata();
-      AddUmaFeatures(metadata);
+      metadata_utils::VisitAllUmaFeatures(
+          metadata, /*include_outputs=*/true,
+          base::BindRepeating(&FilterExtractor::AddUmaFeature,
+                              base::Unretained(this)));
       if (AddUkmFeatures(metadata)) {
         history_based_segments.insert(segment_info.segment_id());
       }
@@ -43,30 +47,23 @@
   base::flat_set<SegmentId> history_based_segments;
 
  private:
-  void AddUmaFeatures(const proto::SegmentationModelMetadata& metadata) {
-    auto features =
-        metadata_utils::GetAllUmaFeatures(metadata, /*include_outputs=*/true);
-    for (auto const& feature : features) {
-      if (feature.type() == proto::SignalType::USER_ACTION &&
-          feature.name_hash() != 0) {
-        user_actions.insert(feature.name_hash());
-        VLOG(1) << "Segmentation platform started observing " << feature.name();
-        continue;
-      }
-
-      if ((feature.type() == proto::SignalType::HISTOGRAM_VALUE ||
-           feature.type() == proto::SignalType::HISTOGRAM_ENUM) &&
-          !feature.name().empty()) {
-        VLOG(1) << "Segmentation platform started observing " << feature.name();
-        histograms.insert(std::make_pair(feature.name(), feature.type()));
-        continue;
-      }
-
-      NOTREACHED() << "Unexpected feature type";
-
-      // TODO(shaktisahu): We can filter out enum values as an optimization
-      // before storing in DB.
+  void AddUmaFeature(const proto::UMAFeature& feature) {
+    if (feature.type() == proto::SignalType::USER_ACTION &&
+        feature.name_hash() != 0) {
+      user_actions.insert(feature.name_hash());
+      VLOG(1) << "Segmentation platform started observing " << feature.name();
+      return;
     }
+
+    if ((feature.type() == proto::SignalType::HISTOGRAM_VALUE ||
+         feature.type() == proto::SignalType::HISTOGRAM_ENUM) &&
+        !feature.name().empty()) {
+      VLOG(1) << "Segmentation platform started observing " << feature.name();
+      histograms.insert(std::make_pair(feature.name(), feature.type()));
+      return;
+    }
+
+    NOTREACHED() << "Unexpected feature type";
   }
 
   bool AddUkmFeatures(const proto::SegmentationModelMetadata& metadata) {
diff --git a/components/send_tab_to_self/features.cc b/components/send_tab_to_self/features.cc
index 75b591468..d052ac5 100644
--- a/components/send_tab_to_self/features.cc
+++ b/components/send_tab_to_self/features.cc
@@ -11,7 +11,7 @@
 
 BASE_FEATURE(kSendTabToSelfSigninPromo,
              "SendTabToSelfSigninPromo",
-             base::FEATURE_DISABLED_BY_DEFAULT);
+             base::FEATURE_ENABLED_BY_DEFAULT);
 
 BASE_FEATURE(kSendTabToSelfEnableNotificationTimeOut,
              "SendTabToSelfEnableNotificationTimeOut",
diff --git a/components/session_proto_db/session_proto_db.h b/components/session_proto_db/session_proto_db.h
index 348f972..2a0acf65 100644
--- a/components/session_proto_db/session_proto_db.h
+++ b/components/session_proto_db/session_proto_db.h
@@ -98,9 +98,9 @@
   void DeleteOneEntry(const std::string& key,
                       OperationCallback callback) override;
 
-  void UpdateEntries(
-      const std::vector<std::pair<std::string, T>>& entries_to_update,
-      OperationCallback callback) override;
+  void UpdateEntries(std::unique_ptr<ContentEntry> entries_to_update,
+                     std::unique_ptr<std::vector<std::string>> keys_to_remove,
+                     OperationCallback callback) override;
 
   void DeleteContentWithPrefix(const std::string& key_prefix,
                                OperationCallback callback) override;
@@ -328,23 +328,20 @@
 
 template <typename T>
 void SessionProtoDB<T>::UpdateEntries(
-    const std::vector<std::pair<std::string, T>>& entries_to_update,
+    std::unique_ptr<ContentEntry> entries_to_update,
+    std::unique_ptr<std::vector<std::string>> keys_to_remove,
     OperationCallback callback) {
   if (InitStatusUnknown()) {
     deferred_operations_.push_back(base::BindOnce(
         &SessionProtoDB::UpdateEntries, weak_ptr_factory_.GetWeakPtr(),
-        entries_to_update, std::move(callback)));
+        std::move(entries_to_update), std::move(keys_to_remove),
+        std::move(callback)));
   } else if (FailedToInit()) {
     ui_thread_task_runner_->PostTask(
         FROM_HERE, base::BindOnce(std::move(callback), false));
   } else {
-    auto contents_to_save = std::make_unique<ContentEntry>();
-    for (auto& entry : entries_to_update) {
-      contents_to_save->emplace_back(entry.first, entry.second);
-    }
     storage_database_->UpdateEntries(
-        std::move(contents_to_save),
-        std::make_unique<std::vector<std::string>>(),
+        std::move(entries_to_update), std::move(keys_to_remove),
         base::BindOnce(&SessionProtoDB::OnOperationCommitted,
                        weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
   }
diff --git a/components/session_proto_db/session_proto_db_unittest.cc b/components/session_proto_db/session_proto_db_unittest.cc
index e334747..409dfe6 100644
--- a/components/session_proto_db/session_proto_db_unittest.cc
+++ b/components/session_proto_db/session_proto_db_unittest.cc
@@ -20,6 +20,8 @@
 #include "testing/gtest/include/gtest/gtest.h"
 
 using testing::_;
+using KeyValuePair =
+    std::pair<std::string, persisted_state_db::PersistedStateContentProto>;
 
 namespace {
 persisted_state_db::PersistedStateContentProto BuildProto(
@@ -642,7 +644,7 @@
 
 TEST_F(SessionProtoDBTest, TestUpdateEntries) {
   InitPersistedStateDB();
-  base::RunLoop run_loop[4];
+  base::RunLoop run_loop[6];
   persisted_state_db()->InsertContent(
       kMockKeyA, kMockValueA,
       base::BindOnce(&SessionProtoDBTest::OperationEvaluation,
@@ -651,13 +653,12 @@
   run_loop[0].Run();
 
   // Do one update and one insert for the UpdateEntries call.
-  std::vector<
-      std::pair<std::string, persisted_state_db::PersistedStateContentProto>>
-      entries_to_update;
-  entries_to_update.emplace_back(kMockKeyA, kMockValueB);
-  entries_to_update.emplace_back(kMockKeyB, kMockValueA);
+  auto entries_to_update = std::make_unique<std::vector<KeyValuePair>>();
+  entries_to_update->emplace_back(kMockKeyA, kMockValueB);
+  entries_to_update->emplace_back(kMockKeyB, kMockValueA);
   persisted_state_db()->UpdateEntries(
       std::move(entries_to_update),
+      std::make_unique<std::vector<std::string>>(),
       base::BindOnce(&SessionProtoDBTest::OperationEvaluation,
                      base::Unretained(this), run_loop[1].QuitClosure(), true));
   MockInsertCallbackPersistedStateDB(content_db(), true);
@@ -677,6 +678,23 @@
                      kExpectedA));
   content_db()->GetCallback(true);
   run_loop[3].Run();
+
+  // Reverts the update and insertion earlier.
+  entries_to_update = std::make_unique<std::vector<KeyValuePair>>();
+  auto keys_to_remove = std::make_unique<std::vector<std::string>>();
+  entries_to_update->emplace_back(kMockKeyA, kMockValueA);
+  keys_to_remove->emplace_back(kMockKeyB);
+  persisted_state_db()->UpdateEntries(
+      std::move(entries_to_update), std::move(keys_to_remove),
+      base::BindOnce(&SessionProtoDBTest::OperationEvaluation,
+                     base::Unretained(this), run_loop[4].QuitClosure(), true));
+  MockInsertCallbackPersistedStateDB(content_db(), true);
+  run_loop[4].Run();
+
+  persisted_state_db()->LoadAllEntries(base::BindOnce(
+      &SessionProtoDBTest::GetEvaluationPersistedStateDB,
+      base::Unretained(this), run_loop[5].QuitClosure(), kExpectedA));
+  MockLoadCallbackPersistedStateDB(content_db(), true);
 }
 
 TEST_F(SessionProtoDBTest, TestMaintenanceKeepSomeKeys) {
diff --git a/components/session_proto_db/session_proto_storage.h b/components/session_proto_db/session_proto_storage.h
index df7b1bc..de37c5a 100644
--- a/components/session_proto_db/session_proto_storage.h
+++ b/components/session_proto_db/session_proto_storage.h
@@ -70,7 +70,8 @@
 
   // Updates the value of multiple entries in the database.
   virtual void UpdateEntries(
-      const std::vector<std::pair<std::string, T>>& entries_to_update,
+      std::unique_ptr<std::vector<KeyAndValue>> entries_to_update,
+      std::unique_ptr<std::vector<std::string>> keys_to_remove,
       OperationCallback callback) = 0;
 
   // Deletes content in the database, matching all keys which have a prefix
diff --git a/components/sync/android/java/src/org/chromium/components/sync/SyncServiceImpl.java b/components/sync/android/java/src/org/chromium/components/sync/SyncServiceImpl.java
index 3624e89..6cbf663 100644
--- a/components/sync/android/java/src/org/chromium/components/sync/SyncServiceImpl.java
+++ b/components/sync/android/java/src/org/chromium/components/sync/SyncServiceImpl.java
@@ -346,9 +346,7 @@
 
     @Override
     public boolean isSyncingUnencryptedUrls() {
-        return isEngineInitialized()
-                && (getActiveDataTypes().contains(ModelType.TYPED_URLS)
-                        || getActiveDataTypes().contains(ModelType.HISTORY))
+        return isEngineInitialized() && getActiveDataTypes().contains(ModelType.HISTORY)
                 && (getPassphraseType() == PassphraseType.KEYSTORE_PASSPHRASE
                         || getPassphraseType() == PassphraseType.TRUSTED_VAULT_PASSPHRASE);
     }
diff --git a/components/sync/base/model_type.cc b/components/sync/base/model_type.cc
index d4bac14..2a7742f 100644
--- a/components/sync/base/model_type.cc
+++ b/components/sync/base/model_type.cc
@@ -81,9 +81,6 @@
     {THEMES, "THEME", "themes", "Themes",
      sync_pb::EntitySpecifics::kThemeFieldNumber,
      ModelTypeForHistograms::kThemes},
-    {TYPED_URLS, "TYPED_URL", "typed_urls", "Typed URLs",
-     sync_pb::EntitySpecifics::kTypedUrlFieldNumber,
-     ModelTypeForHistograms::kTypedUrls},
     {EXTENSIONS, "EXTENSION", "extensions", "Extensions",
      sync_pb::EntitySpecifics::kExtensionFieldNumber,
      ModelTypeForHistograms::kExtensions},
@@ -210,7 +207,7 @@
 static_assert(std::size(kModelTypeInfoMap) == GetNumModelTypes(),
               "kModelTypeInfoMap should have GetNumModelTypes() elements");
 
-static_assert(49 == syncer::GetNumModelTypes(),
+static_assert(48 == syncer::GetNumModelTypes(),
               "When adding a new type, update enum SyncModelTypes in enums.xml "
               "and suffix SyncModelType in histograms.xml.");
 
@@ -242,7 +239,6 @@
         {sync_pb::EntitySpecifics::kAutofillWalletUsageFieldNumber,
          AUTOFILL_WALLET_USAGE},
         {sync_pb::EntitySpecifics::kThemeFieldNumber, THEMES},
-        {sync_pb::EntitySpecifics::kTypedUrlFieldNumber, TYPED_URLS},
         {sync_pb::EntitySpecifics::kExtensionFieldNumber, EXTENSIONS},
         {sync_pb::EntitySpecifics::kSearchEngineFieldNumber, SEARCH_ENGINES},
         {sync_pb::EntitySpecifics::kSessionFieldNumber, SESSIONS},
@@ -334,9 +330,6 @@
     case THEMES:
       specifics->mutable_theme();
       break;
-    case TYPED_URLS:
-      specifics->mutable_typed_url();
-      break;
     case EXTENSIONS:
       specifics->mutable_extension();
       break;
@@ -474,7 +467,7 @@
 }
 
 ModelType GetModelTypeFromSpecifics(const sync_pb::EntitySpecifics& specifics) {
-  static_assert(49 == syncer::GetNumModelTypes(),
+  static_assert(48 == syncer::GetNumModelTypes(),
                 "When adding new protocol types, the following type lookup "
                 "logic must be updated.");
   if (specifics.has_bookmark())
@@ -493,8 +486,6 @@
     return AUTOFILL_WALLET_METADATA;
   if (specifics.has_theme())
     return THEMES;
-  if (specifics.has_typed_url())
-    return TYPED_URLS;
   if (specifics.has_extension())
     return EXTENSIONS;
   if (specifics.has_search_engine())
@@ -582,7 +573,7 @@
 }
 
 ModelTypeSet EncryptableUserTypes() {
-  static_assert(49 == syncer::GetNumModelTypes(),
+  static_assert(48 == syncer::GetNumModelTypes(),
                 "If adding an unencryptable type, remove from "
                 "encryptable_user_types below.");
   ModelTypeSet encryptable_user_types = UserTypes();
diff --git a/components/sync/base/model_type.h b/components/sync/base/model_type.h
index 7ccfce9..822f69e 100644
--- a/components/sync/base/model_type.h
+++ b/components/sync/base/model_type.h
@@ -74,15 +74,12 @@
   AUTOFILL_WALLET_USAGE,
   // A theme object.
   THEMES,
-  // A typed_url object, i.e. a URL the user has typed into the Omnibox.
-  TYPED_URLS,
   // An extension object.
   EXTENSIONS,
   // An object representing a custom search engine.
   SEARCH_ENGINES,
   // An object representing a browser session, e.g. an open tab. This is used
-  // for both "History" (together with TYPED_URLS) and "Tabs" (depending on
-  // PROXY_TABS).
+  // for "Tabs" (depending on PROXY_TABS).
   SESSIONS,
   // An app object.
   APPS,
@@ -158,8 +155,10 @@
   // real user types. By convention, we prefix them with 'PROXY_' to distinguish
   // them from normal protocol types.
   //
-  // Tab sync. This is a placeholder type, so that Sessions can be implicitly
-  // enabled for history sync and tabs sync.
+  // Tab sync. This is a placeholder type, which implicitly enables Sessions
+  // for tabs sync.
+  // TODO(crbug.com/1365291): Now that TYPED_URLS is gone, it should be possible
+  // to remove this type, and the whole concept of "proxy types".
   PROXY_TABS,
   LAST_USER_MODEL_TYPE = PROXY_TABS,
 
@@ -197,7 +196,7 @@
   kAutofillProfile = 5,
   kAutofill = 6,
   kThemes = 7,
-  kTypedUrls = 8,
+  // kDeprecatedTypedUrls = 8,
   kExtensions = 9,
   kSearchEngines = 10,
   kSessions = 11,
@@ -276,7 +275,6 @@
           AUTOFILL_WALLET_OFFER,
           AUTOFILL_WALLET_USAGE,
           THEMES,
-          TYPED_URLS,
           EXTENSIONS,
           SEARCH_ENGINES,
           SESSIONS,
diff --git a/components/sync/base/model_type_unittest.cc b/components/sync/base/model_type_unittest.cc
index db8d502..cd87462 100644
--- a/components/sync/base/model_type_unittest.cc
+++ b/components/sync/base/model_type_unittest.cc
@@ -71,7 +71,7 @@
   // identifiers are stable.
   EXPECT_EQ(3, ModelTypeToStableIdentifier(BOOKMARKS));
   EXPECT_EQ(7, ModelTypeToStableIdentifier(AUTOFILL));
-  EXPECT_EQ(9, ModelTypeToStableIdentifier(TYPED_URLS));
+  EXPECT_EQ(52, ModelTypeToStableIdentifier(HISTORY));
 }
 
 TEST_F(ModelTypeTest, DefaultFieldValues) {
diff --git a/components/sync/base/pref_names.h b/components/sync/base/pref_names.h
index 7a318001..db778ec 100644
--- a/components/sync/base/pref_names.h
+++ b/components/sync/base/pref_names.h
@@ -85,6 +85,7 @@
 inline constexpr char kSyncReadingList[] = "sync.reading_list";
 inline constexpr char kSyncTabs[] = "sync.tabs";
 inline constexpr char kSyncThemes[] = "sync.themes";
+// TODO(crbug.com/1365291): Rename kSyncTypedUrls to kSyncHistory.
 inline constexpr char kSyncTypedUrls[] = "sync.typed_urls";
 inline constexpr char kSyncSavedTabGroups[] = "sync.saved_tab_groups";
 
diff --git a/components/sync/base/user_selectable_type.cc b/components/sync/base/user_selectable_type.cc
index bb47d0c..a2710ed 100644
--- a/components/sync/base/user_selectable_type.cc
+++ b/components/sync/base/user_selectable_type.cc
@@ -39,7 +39,7 @@
 constexpr char kPaymentsTypeName[] = "payments";
 
 UserSelectableTypeInfo GetUserSelectableTypeInfo(UserSelectableType type) {
-  static_assert(49 == syncer::GetNumModelTypes(),
+  static_assert(48 == syncer::GetNumModelTypes(),
                 "Almost always when adding a new ModelType, you must tie it to "
                 "a UserSelectableType below (new or existing) so the user can "
                 "disable syncing of that data. Today you must also update the "
diff --git a/components/sync/engine/cycle/data_type_tracker.cc b/components/sync/engine/cycle/data_type_tracker.cc
index ddd5789e..56eccac 100644
--- a/components/sync/engine/cycle/data_type_tracker.cc
+++ b/components/sync/engine/cycle/data_type_tracker.cc
@@ -83,7 +83,6 @@
     case AUTOFILL_WALLET_USAGE:
     case CONTACT_INFO:
     case THEMES:
-    case TYPED_URLS:
     case EXTENSIONS:
     case SEARCH_ENGINES:
     case APPS:
@@ -146,7 +145,6 @@
     case AUTOFILL_WALLET_OFFER:
     case AUTOFILL_WALLET_USAGE:
     case THEMES:
-    case TYPED_URLS:
     case EXTENSIONS:
     case SEARCH_ENGINES:
     case APPS:
diff --git a/components/sync/engine/cycle/nudge_tracker_unittest.cc b/components/sync/engine/cycle/nudge_tracker_unittest.cc
index 36db8ed6..864cadd 100644
--- a/components/sync/engine/cycle/nudge_tracker_unittest.cc
+++ b/components/sync/engine/cycle/nudge_tracker_unittest.cc
@@ -6,14 +6,11 @@
 
 #include <stdint.h>
 
+#include <memory>
 #include <string>
-#include <utility>
-#include <vector>
 
-#include "base/run_loop.h"
 #include "components/sync/protocol/data_type_progress_marker.pb.h"
 #include "components/sync/test/mock_invalidation.h"
-#include "components/sync/test/model_type_test_util.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace syncer {
@@ -116,7 +113,7 @@
   EXPECT_EQ(sync_pb::SyncEnums::GU_TRIGGER, nudge_tracker_.GetOrigin());
 
   // A refresh request will override it.
-  nudge_tracker_.RecordLocalRefreshRequest({TYPED_URLS});
+  nudge_tracker_.RecordLocalRefreshRequest({PASSWORDS});
   EXPECT_EQ(sync_pb::SyncEnums::GU_TRIGGER, nudge_tracker_.GetOrigin());
 
   // Another local nudge will not be enough to change it.
@@ -130,7 +127,7 @@
   // Neither local nudges nor refresh requests will override it.
   nudge_tracker_.RecordLocalChange(BOOKMARKS);
   EXPECT_EQ(sync_pb::SyncEnums::GU_TRIGGER, nudge_tracker_.GetOrigin());
-  nudge_tracker_.RecordLocalRefreshRequest({TYPED_URLS});
+  nudge_tracker_.RecordLocalRefreshRequest({PASSWORDS});
   EXPECT_EQ(sync_pb::SyncEnums::GU_TRIGGER, nudge_tracker_.GetOrigin());
 }
 
@@ -670,21 +667,23 @@
 // Test the default nudge delays for various types.
 TEST_F(NudgeTrackerTest, NudgeDelayTest) {
   // Most data types have a medium delay.
-  EXPECT_EQ(nudge_tracker_.RecordLocalChange(TYPED_URLS),
+  EXPECT_EQ(nudge_tracker_.RecordLocalChange(CONTACT_INFO),
             nudge_tracker_.RecordLocalChange(PASSWORDS));
-  EXPECT_EQ(nudge_tracker_.RecordLocalChange(TYPED_URLS),
+  EXPECT_EQ(nudge_tracker_.RecordLocalChange(CONTACT_INFO),
             nudge_tracker_.RecordLocalChange(EXTENSIONS));
 
   // Bookmarks and preferences sometimes have automatic changes (not directly
   // caused by a user actions), so they have bigger delays.
   EXPECT_GT(nudge_tracker_.RecordLocalChange(BOOKMARKS),
-            nudge_tracker_.RecordLocalChange(TYPED_URLS));
+            nudge_tracker_.RecordLocalChange(CONTACT_INFO));
   EXPECT_EQ(nudge_tracker_.RecordLocalChange(BOOKMARKS),
             nudge_tracker_.RecordLocalChange(PREFERENCES));
 
-  // Sessions has an even bigger delay.
+  // Sessions and history have an even bigger delay.
   EXPECT_GT(nudge_tracker_.RecordLocalChange(SESSIONS),
             nudge_tracker_.RecordLocalChange(BOOKMARKS));
+  EXPECT_GT(nudge_tracker_.RecordLocalChange(HISTORY),
+            nudge_tracker_.RecordLocalChange(BOOKMARKS));
 
   // Autofill and UserEvents are "accompany types" that rely on nudges from
   // other types. They have the longest delay of all, which really only acts as
diff --git a/components/sync/engine/loopback_server/loopback_server.cc b/components/sync/engine/loopback_server/loopback_server.cc
index 413361d3..9afa3bb 100644
--- a/components/sync/engine/loopback_server/loopback_server.cc
+++ b/components/sync/engine/loopback_server/loopback_server.cc
@@ -644,9 +644,6 @@
   string guid = commit.cache_guid();
   ModelTypeSet committed_model_types;
 
-  ModelTypeSet enabled_types = GetModelTypeSetFromSpecificsFieldNumberList(
-      commit.config_params().enabled_type_ids());
-
   // TODO(pvalenzuela): Add validation of CommitMessage.entries.
   for (const sync_pb::SyncEntity& client_entity : commit.entries()) {
     sync_pb::CommitResponse_EntryResponse* entry_response =
@@ -680,30 +677,9 @@
     DCHECK(iter != entities_.end());
     committed_model_types.Put(iter->second->GetModelType());
 
-    // Notify observers about history having been synced. There are two
-    // iterations of "History sync" both guarded by the user's selection in the
-    // settings page:
-    // 1) The "old" one based on SESSIONS data, only enabled if TYPED_URLS and
-    //    HISTORY_DELETE_DIRECTIVES are also enabled. Note that for custom
-    //    passphrase users, HISTORY_DELETE_DIRECTIVES will not be enabled (and
-    //    since they commit encrypted specifics, the server couldn't inspect the
-    //    data anyway).
-    // 2) The "new" one based on a dedicated HISTORY data type. This data type
-    //    is itself disabled for custom passphrase users.
-    // In practice, at most one of TYPED_URLS or HISTORY can be enabled at the
-    // same time, so OnHistoryCommit() gets called at most once per URL.
-    DCHECK(!(enabled_types.Has(TYPED_URLS) && enabled_types.Has(HISTORY)));
+    // Notify observers about history having been synced.
     if (observer_for_tests_) {
-      if (iter->second->GetModelType() == SESSIONS &&
-          enabled_types.Has(HISTORY_DELETE_DIRECTIVES) &&
-          enabled_types.Has(TYPED_URLS)) {
-        // "Old" history sync.
-        for (const sync_pb::TabNavigation& navigation :
-             client_entity.specifics().session().tab().navigation()) {
-          observer_for_tests_->OnHistoryCommit(navigation.virtual_url());
-        }
-      } else if (iter->second->GetModelType() == HISTORY) {
-        // "New" history sync.
+      if (iter->second->GetModelType() == HISTORY) {
         const sync_pb::HistorySpecifics& specifics =
             client_entity.specifics().history();
         // The last entry of the redirect chain is the "actual" URL. In the case
diff --git a/components/sync/engine/sync_scheduler_impl_unittest.cc b/components/sync/engine/sync_scheduler_impl_unittest.cc
index 5ec3e7c..141ff3c 100644
--- a/components/sync/engine/sync_scheduler_impl_unittest.cc
+++ b/components/sync/engine/sync_scheduler_impl_unittest.cc
@@ -256,8 +256,8 @@
                                           MakeFakeActivationResponse(NIGORI));
     model_type_registry_->ConnectDataType(THEMES,
                                           MakeFakeActivationResponse(THEMES));
-    model_type_registry_->ConnectDataType(
-        TYPED_URLS, MakeFakeActivationResponse(TYPED_URLS));
+    model_type_registry_->ConnectDataType(HISTORY,
+                                          MakeFakeActivationResponse(HISTORY));
 
     context_ = std::make_unique<SyncCycleContext>(
         connection_.get(), extensions_activity_.get(),
@@ -518,7 +518,7 @@
   EXPECT_CALL(*syncer(), NormalSyncShare)
       .WillOnce(
           DoAll(Invoke(SimulateNormalSuccess), RecordSyncShare(&times2, true)));
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
   RunLoop();
 }
 
@@ -702,7 +702,7 @@
           DoAll(Invoke(SimulateNormalSuccess), RecordSyncShare(&times, true)));
   TimeTicks optimal_time = TimeTicks::Now() + default_delay();
   scheduler()->ScheduleLocalNudge(THEMES);
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
   RunLoop();
 
   ASSERT_EQ(1U, times.size());
@@ -734,7 +734,7 @@
   delay_map[THEMES] = delay;
   scheduler()->OnReceivedCustomNudgeDelays(delay_map);
   scheduler()->ScheduleLocalNudge(THEMES);
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
 
   TimeTicks min_time = TimeTicks::Now();
   TimeTicks max_time = TimeTicks::Now() + delay;
@@ -768,8 +768,8 @@
   EXPECT_CALL(*syncer(), NormalSyncShare)
       .WillOnce(
           DoAll(Invoke(SimulateNormalSuccess), RecordSyncShare(&times2, true)));
-  scheduler()->SetHasPendingInvalidations(TYPED_URLS, true);
-  scheduler()->ScheduleInvalidationNudge(TYPED_URLS);
+  scheduler()->SetHasPendingInvalidations(HISTORY, true);
+  scheduler()->ScheduleInvalidationNudge(HISTORY);
   RunLoop();
 }
 
@@ -1134,7 +1134,7 @@
       .RetiresOnSaturation();
 
   // Sync still can throttle.
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
   PumpLoop();  // TO get TypesUnblock called.
   PumpLoop();  // To get TrySyncCycleJob called.
 
@@ -1177,7 +1177,7 @@
   PumpLoop();  // To get PerformDelayedNudge called.
   PumpLoop();  // To get TrySyncCycleJob called
 
-  const ModelType backed_off_type = TYPED_URLS;
+  const ModelType backed_off_type = HISTORY;
 
   EXPECT_CALL(*syncer(), NormalSyncShare)
       .WillOnce(DoAll(WithArg<2>(SimulatePartialFailure(backed_off_type)),
@@ -1303,8 +1303,8 @@
 
   StartSyncConfiguration();
 
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
+  scheduler()->ScheduleLocalNudge(HISTORY);
 
   SyncShareTimes times;
   EXPECT_CALL(*syncer(), ConfigureSyncShare)
@@ -1873,7 +1873,7 @@
       .RetiresOnSaturation();
 
   // Do a successful Sync.
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
   PumpLoop();  // TO get PerformDelayedNudge called.
   PumpLoop();  // To get TrySyncCycleJob called.
 
@@ -1914,7 +1914,7 @@
   EXPECT_FALSE(scheduler()->IsGlobalThrottle());
 
   // Set anther backoff datatype.
-  const ModelType backed_off_type2 = TYPED_URLS;
+  const ModelType backed_off_type2 = HISTORY;
   EXPECT_CALL(*syncer(), NormalSyncShare)
       .WillOnce(DoAll(WithArg<2>(SimulatePartialFailure(backed_off_type2)),
                       Return(true)))
@@ -1968,7 +1968,7 @@
 
   // This is the tricky piece. We have a gap while the sync job is bouncing to
   // get onto the |pending_wakeup_timer_|, should be scheduled with no delay.
-  scheduler()->ScheduleLocalNudge(TYPED_URLS);
+  scheduler()->ScheduleLocalNudge(HISTORY);
   EXPECT_TRUE(BlockTimerIsRunning());
   EXPECT_EQ(base::TimeDelta(), GetPendingWakeupTimerDelay());
   EXPECT_FALSE(scheduler()->IsGlobalBackoff());
@@ -1979,14 +1979,14 @@
       .WillOnce(
           DoAll(Invoke(SimulateCommitFailed), RecordSyncShare(&times, false)));
   // Triggers the THEMES TrySyncCycleJobImpl(), which we've setup to fail. Its
-  // RestartWaiting won't schedule a delayed retry, as the TYPED_URLS nudge has
+  // RestartWaiting won't schedule a delayed retry, as the HISTORY nudge has
   // a smaller delay. We verify this by making sure the delay is still zero.
   PumpLoop();
   EXPECT_TRUE(BlockTimerIsRunning());
   EXPECT_EQ(base::TimeDelta(), GetPendingWakeupTimerDelay());
   EXPECT_TRUE(scheduler()->IsGlobalBackoff());
 
-  // Triggers TYPED_URLS PerformDelayedNudge(), which should no-op, because
+  // Triggers HISTORY PerformDelayedNudge(), which should no-op, because
   // we're no long healthy, and normal priorities shouldn't go through, but it
   // does need to setup the |pending_wakeup_timer_|. The delay should be ~60
   // seconds, so verifying it's greater than 50 should be safe.
diff --git a/components/sync/protocol/proto_value_conversions_unittest.cc b/components/sync/protocol/proto_value_conversions_unittest.cc
index be168f0..b455c20 100644
--- a/components/sync/protocol/proto_value_conversions_unittest.cc
+++ b/components/sync/protocol/proto_value_conversions_unittest.cc
@@ -65,7 +65,7 @@
 
 DEFINE_SPECIFICS_TO_VALUE_TEST(encrypted)
 
-static_assert(49 == syncer::GetNumModelTypes(),
+static_assert(48 == syncer::GetNumModelTypes(),
               "When adding a new field, add a DEFINE_SPECIFICS_TO_VALUE_TEST "
               "for your field below, and optionally a test for the specific "
               "conversions.");
diff --git a/components/sync/protocol/proto_visitors.h b/components/sync/protocol/proto_visitors.h
index 55e5c5f5..66e694d 100644
--- a/components/sync/protocol/proto_visitors.h
+++ b/components/sync/protocol/proto_visitors.h
@@ -577,7 +577,7 @@
 }
 
 VISIT_PROTO_FIELDS(const sync_pb::EntitySpecifics& proto) {
-  static_assert(49 == GetNumModelTypes(),
+  static_assert(48 == GetNumModelTypes(),
                 "When adding a new protocol type, you will likely need to add "
                 "it here as well.");
   VISIT(encrypted);
diff --git a/components/sync/service/model_type_controller.cc b/components/sync/service/model_type_controller.cc
index 174c76a..6e72b15c 100644
--- a/components/sync/service/model_type_controller.cc
+++ b/components/sync/service/model_type_controller.cc
@@ -89,8 +89,8 @@
     // * PASSWORDS: Already supported on desktop; mobile is WIP.
     // * INCOMING_PASSWORD_SHARING_INVITATION: Depends on PASSWORDS support.
     // * PREFERENCES in all variants: Support is WIP.
-    // * History-related types (HISTORY, HISTORY_DELETE_DIRECTIVES, TYPED_URLS,
-    //   SESSIONS) are okay to *not* support transport mode.
+    // * History-related types (HISTORY, HISTORY_DELETE_DIRECTIVES, SESSIONS)
+    //   are okay to *not* support transport mode.
     // * APPS/APP_SETTINGS: Deprecated and will eventually be removed.
     // * AUTOFILL/AUTOFILL_PROFILE: Semi-deprecated; will eventually be removed
     //   or replaced by CONTACT_INFO.
@@ -116,7 +116,6 @@
         AUTOFILL_WALLET_OFFER,
         AUTOFILL_WALLET_USAGE,
         THEMES,
-        TYPED_URLS,
         EXTENSIONS,
         SEARCH_ENGINES,
         SESSIONS,
diff --git a/components/sync/service/sync_service_impl_unittest.cc b/components/sync/service/sync_service_impl_unittest.cc
index f6d5ac5..a4a4158c 100644
--- a/components/sync/service/sync_service_impl_unittest.cc
+++ b/components/sync/service/sync_service_impl_unittest.cc
@@ -1475,7 +1475,7 @@
 TEST_F(SyncServiceImplTest, ShouldEnableAndDisableInvalidationsForSessions) {
   PopulatePrefsForInitialSyncFeatureSetupComplete();
   SignInWithSyncConsent();
-  InitializeService({{SESSIONS, false}, {TYPED_URLS, false}});
+  InitializeService({{SESSIONS, false}, {BOOKMARKS, false}});
   base::RunLoop().RunUntilIdle();
 
   EXPECT_CALL(*sync_invalidations_service(),
diff --git a/components/sync/service/sync_user_settings_impl.cc b/components/sync/service/sync_user_settings_impl.cc
index c7fbb0c..c30012b 100644
--- a/components/sync/service/sync_user_settings_impl.cc
+++ b/components/sync/service/sync_user_settings_impl.cc
@@ -340,7 +340,7 @@
   // though they're technically not registered.
   types.PutAll(ControlTypes());
 
-  static_assert(49 == GetNumModelTypes(),
+  static_assert(48 == GetNumModelTypes(),
                 "If adding a new sync data type, update the list below below if"
                 " you want to disable the new data type for local sync.");
   if (prefs_->IsLocalSyncEnabled()) {
diff --git a/components/sync/service/sync_user_settings_impl_unittest.cc b/components/sync/service/sync_user_settings_impl_unittest.cc
index 486e4b9..f2b4b51 100644
--- a/components/sync/service/sync_user_settings_impl_unittest.cc
+++ b/components/sync/service/sync_user_settings_impl_unittest.cc
@@ -104,10 +104,6 @@
       MakeSyncUserSettings(GetUserTypes());
 
   ModelTypeSet expected_types = GetUserTypes();
-  // TODO(crbug.com/1365291): TYPED_URLS is not used anymore, and in particular
-  // is not part of any UserSelectableType. Eventually TYPED_URLS should be
-  // fully removed.
-  expected_types.Remove(TYPED_URLS);
   EXPECT_TRUE(sync_user_settings->IsSyncEverythingEnabled());
   EXPECT_EQ(expected_types, GetPreferredUserTypes(*sync_user_settings));
 
@@ -218,10 +214,6 @@
       MakeSyncUserSettings(GetUserTypes());
 
   ModelTypeSet expected_types = GetUserTypes();
-  // TODO(crbug.com/1365291): TYPED_URLS is not used anymore, and in particular
-  // is not part of any UserSelectableType. Eventually TYPED_URLS should be
-  // fully removed.
-  expected_types.Remove(TYPED_URLS);
   EXPECT_TRUE(sync_user_settings->IsSyncAllOsTypesEnabled());
   EXPECT_EQ(expected_types, GetPreferredUserTypes(*sync_user_settings));
 
diff --git a/components/ui_devtools/views/dom_agent_mac.mm b/components/ui_devtools/views/dom_agent_mac.mm
index c7986c5..17d0e637 100644
--- a/components/ui_devtools/views/dom_agent_mac.mm
+++ b/components/ui_devtools/views/dom_agent_mac.mm
@@ -72,8 +72,16 @@
   for (NSWindow* window in NSApp.windows) {
     if (views::Widget* widget =
             views::Widget::GetWidgetForNativeWindow(window)) {
-      widget->AddObserver(this);
-      roots_.push_back(widget);
+      // When in immersive fullscreen mode, an overlay widget has two associated
+      // NSWindows:
+      // 1. An invisible one created by Chrome, which serves as an anchor
+      //    for child widgets.
+      // 2. A visible AppKit-owned NSToolbarFullScreenWindow.
+      // We ensures here that a widget is only observed once.
+      if (!widget->HasObserver(this)) {
+        widget->AddObserver(this);
+        roots_.push_back(widget);
+      }
     }
   }
 }
diff --git a/components/ui_devtools/views/overlay_agent_mac.mm b/components/ui_devtools/views/overlay_agent_mac.mm
index cc85470..1f5370e2d 100644
--- a/components/ui_devtools/views/overlay_agent_mac.mm
+++ b/components/ui_devtools/views/overlay_agent_mac.mm
@@ -80,7 +80,9 @@
     return;
   widget->GetRootView()->AddPreTargetHandler(
       this, ui::EventTarget::Priority::kSystem);
-  widget->AddObserver(this);
+  if (!widget->HasObserver(this)) {
+    widget->AddObserver(this);
+  }
 }
 void OverlayAgentMac::RemovePreTargetHandlerOnWidget(views::Widget* widget) {
   if (!widget)
diff --git a/components/viz/service/display/dc_layer_overlay.cc b/components/viz/service/display/dc_layer_overlay.cc
index 3b392211c..b17f1714 100644
--- a/components/viz/service/display/dc_layer_overlay.cc
+++ b/components/viz/service/display/dc_layer_overlay.cc
@@ -368,14 +368,12 @@
 bool HasOccludingDamageRect(
     const SharedQuadState* shared_quad_state,
     const SurfaceDamageRectList& surface_damage_rect_list,
-    const gfx::Rect& quad_rect_in_root_space) {
+    const gfx::Rect& quad_rect_in_target_space) {
   if (!shared_quad_state->overlay_damage_index.has_value())
-    return !quad_rect_in_root_space.IsEmpty();
+    return !quad_rect_in_target_space.IsEmpty();
 
   size_t overlay_damage_index = shared_quad_state->overlay_damage_index.value();
-  if (overlay_damage_index >= surface_damage_rect_list.size()) {
-    DCHECK(false);
-  }
+  CHECK_LT(overlay_damage_index, surface_damage_rect_list.size());
 
   // Damage rects in surface_damage_rect_list are arranged from top to bottom.
   // surface_damage_rect_list[0] is the one on the very top.
@@ -385,14 +383,14 @@
   for (size_t i = 0; i < overlay_damage_index; ++i) {
     occluding_damage_rect.Union(surface_damage_rect_list[i]);
   }
-  occluding_damage_rect.Intersect(quad_rect_in_root_space);
+  occluding_damage_rect.Intersect(quad_rect_in_target_space);
 
   return !occluding_damage_rect.IsEmpty();
 }
 
 bool IsPossibleFullScreenLetterboxing(const QuadList::Iterator& it,
                                       QuadList::ConstIterator quad_list_end,
-                                      const gfx::RectF& display_rect) {
+                                      const gfx::Rect& display_rect) {
   // Two cases are considered as possible fullscreen letterboxing:
   // 1. If the quad beneath the overlay quad is DrawQuad::Material::kSolidColor
   // with black, and it touches two sides of the screen, while starting at
@@ -412,7 +410,7 @@
          SolidColorDrawQuad::MaterialCast(*beneath_overlay_it)->color ==
              SkColors::kBlack)) {
       gfx::RectF beneath_rect = ClippedQuadRectangleF(*beneath_overlay_it);
-      return (beneath_rect.origin() == display_rect.origin() &&
+      return (beneath_rect.origin() == gfx::PointF(display_rect.origin()) &&
               (beneath_rect.width() == display_rect.width() ||
                beneath_rect.height() == display_rect.height()));
     }
@@ -470,21 +468,28 @@
 }
 
 // This function records the damage rect rect of the current frame.
-void RecordOverlayHistograms(OverlayCandidateList* dc_layer_overlays,
-                             bool has_occluding_surface_damage,
-                             const gfx::Rect* damage_rect) {
+void RecordOverlayHistograms(
+    const DCLayerOverlayProcessor::RenderPassOverlayDataMap&
+        render_pass_overlay_data_map,
+    bool has_occluding_surface_damage) {
   // If an underlay is found, we record the damage rect of this frame as an
   // underlay.
   bool is_overlay = true;
-  for (const auto& dc_layer : *dc_layer_overlays) {
-    if (dc_layer.plane_z_order != 1) {
-      is_overlay = false;
+  for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+    is_overlay = base::ranges::all_of(
+        overlay_data.promoted_overlays,
+        [](const auto& dc_layer) { return dc_layer.plane_z_order > 0; });
+    if (!is_overlay) {
       break;
     }
   }
 
+  bool damage_rects_empty = base::ranges::all_of(
+      render_pass_overlay_data_map,
+      [](const auto& data) { return data.second.damage_rect.IsEmpty(); });
+
   OverlayProcessorInterface::RecordOverlayDamageRectHistograms(
-      is_overlay, has_occluding_surface_damage, damage_rect->IsEmpty());
+      is_overlay, has_occluding_surface_damage, damage_rects_empty);
 }
 
 QuadList::Iterator FindAnOverlayCandidate(QuadList& quad_list) {
@@ -548,6 +553,38 @@
   return IsVideoQuad(it) && !IsOverlayRequiredForQuad(it);
 }
 
+// This is the damage contribution due to previous frame's overlays which can
+// be empty.
+gfx::Rect PreviousFrameOverlayDamageContribution(
+    const std::vector<DCLayerOverlayProcessor::OverlayRect>&
+        previous_frame_overlay_rects) {
+  gfx::Rect rects_union;
+  for (const auto& overlay : previous_frame_overlay_rects) {
+    rects_union.Union(overlay.rect);
+  }
+  return rects_union;
+}
+
+bool IsPreviousFrameUnderlayRect(
+    const std::vector<DCLayerOverlayProcessor::OverlayRect>&
+        previous_frame_overlay_rects,
+    const gfx::Rect& quad_rect,
+    size_t index) {
+  if (index >= previous_frame_overlay_rects.size()) {
+    return false;
+  } else {
+    // Although we can loop through the list to find out if there is an
+    // underlay with the same size from the previous frame, checking
+    // previous_frame_overlay_rects[index] is the quickest way to do it. If we
+    // cannot find a match with the same index, there is probably a change in
+    // the number of overlays or layout. Then we won't be able to get a zero
+    // damage rect in this case. Looping through the list won't give better
+    // power.
+    return (previous_frame_overlay_rects[index].rect == quad_rect) &&
+           (previous_frame_overlay_rects[index].is_overlay == false);
+  }
+}
+
 }  // namespace
 
 DCLayerOverlayProcessor::DCLayerOverlayProcessor(
@@ -595,73 +632,65 @@
   UpdateP010VideoProcessorSupport();
 }
 
-void DCLayerOverlayProcessor::ClearOverlayState() {
-  previous_frame_overlay_rects_.clear();
-  previous_frame_underlay_is_opaque_ = true;
-}
-
-gfx::Rect DCLayerOverlayProcessor::PreviousFrameOverlayDamageContribution() {
-  gfx::Rect rects_union;
-  for (const auto& overlay : previous_frame_overlay_rects_)
-    rects_union.Union(overlay.rect);
-  return rects_union;
-}
-
 void DCLayerOverlayProcessor::RemoveOverlayDamageRect(
-    const QuadList::Iterator& it) {
+    const QuadList::Iterator& it,
+    RenderPassCurrentFrameState& render_pass_state) const {
   // This is done by setting the overlay surface damage rect in the
-  // |surface_damage_rect_list_| to zero.
+  // |surface_damage_rect_list| to zero.
   if (it->shared_quad_state->overlay_damage_index.has_value()) {
     size_t overlay_damage_index =
         it->shared_quad_state->overlay_damage_index.value();
-    if (overlay_damage_index >= surface_damage_rect_list_.size())
-      DCHECK(false);
-    else
-      damages_to_be_removed_.push_back(overlay_damage_index);
+    CHECK_LT(overlay_damage_index,
+             render_pass_state.surface_damage_rect_list.size());
+    render_pass_state.damages_to_be_removed.push_back(overlay_damage_index);
   }
 }
 
-// This is called at the end of Process(). The goal is to get an empty root
-// damage rect if the overlays are the only damages in the frame.
-void DCLayerOverlayProcessor::UpdateRootDamageRect(
-    const gfx::RectF& display_rect,
-    gfx::Rect* damage_rect) {
+// This is called at the end of Process(). The goal is to get an empty damage
+// rect if the overlays are the only damages in the frame.
+void DCLayerOverlayProcessor::UpdateDamageRect(
+    AggregatedRenderPass* render_pass,
+    const RenderPassPreviousFrameState& previous_frame_state,
+    RenderPassOverlayData& overlay_data,
+    RenderPassCurrentFrameState& current_frame_state) const {
   // Check whether the overlay rect union from the previous frame should be
   // added to the current frame and whether the overlay damages can be removed
   // from the current damage rect.
 
+  const std::vector<OverlayRect>& previous_frame_overlay_rects =
+      previous_frame_state.overlay_rects;
+
   bool should_add_previous_frame_overlay_damage = true;
-  size_t current_frame_overlay_count = current_frame_overlay_rects_.size();
-  if (current_frame_overlay_count > 0 &&
-      current_frame_overlay_count == previous_frame_overlay_rects_.size() &&
-      display_rect == previous_display_rect_) {
-    bool same_overlays = true;
-    for (size_t i = 0; i < current_frame_overlay_count; ++i) {
-      if (previous_frame_overlay_rects_[i] != current_frame_overlay_rects_[i]) {
-        same_overlays = false;
-        break;
-      }
-    }
+  if (!current_frame_state.overlay_rects.empty() &&
+      current_frame_state.overlay_rects == previous_frame_overlay_rects &&
+      render_pass->output_rect == previous_frame_state.display_rect) {
+    // No need to add back the overlay rect union from the previous frame
+    // if no changes in overlays.
+    should_add_previous_frame_overlay_damage = false;
 
-    if (same_overlays) {
-      // No need to add back the overlay rect union from the previous frame
-      // if no changes in overlays.
-      should_add_previous_frame_overlay_damage = false;
-
-      // The final root damage rect is computed by add up all surface damages
-      // except for the overlay surface damages and the damages right below
-      // the overlays.
-      gfx::Rect root_damage_rect;
+    // Only perform this optimization if the transform is axis aligned.
+    // Transforms that are not axis aligned make the original rect larger when
+    // the transformation is applied. Since we transform the damage rects
+    // between root space and render pass space (SurfaceAggregator converts
+    // to root space and Process() converts to render pass space), the damage
+    // rects will be larger than the original rects. This would result in
+    // subtracting a larger damage than the overlay itself.
+    if (render_pass->transform_to_root_target.Preserves2dAxisAlignment()) {
+      // The final damage rect is computed by add up all surface damages except
+      // for the overlay surface damages and the damages right below the
+      // overlays.
+      gfx::Rect final_damage_rect;
       size_t surface_index = 0;
-      for (auto surface_damage_rect : surface_damage_rect_list_) {
+      for (auto surface_damage_rect :
+           current_frame_state.surface_damage_rect_list) {
         // We only support at most two overlays. The size of
-        // damages_to_be_removed_ will not be bigger than 2. We should
-        // revisit this damages_to_be_removed_ for-loop if we try to support
-        // many overlays.
-        // See capabilities.supports_two_yuv_hardware_overlays.
-        for (const auto index_to_be_removed : damages_to_be_removed_) {
+        // damages_to_be_removed will not be bigger than 2. We should revisit
+        // this damages_to_be_removed for-loop if we try to support many
+        // overlays. See capabilities.supports_two_yuv_hardware_overlays.
+        for (const auto index_to_be_removed :
+             current_frame_state.damages_to_be_removed) {
           // The overlay damages and the damages right below them will not be
-          // added to the root damage rect.
+          // added to the damage rect.
           if (surface_index == index_to_be_removed) {
             // This is the overlay surface.
             surface_damage_rect = gfx::Rect();
@@ -669,66 +698,58 @@
           } else if (surface_index > index_to_be_removed) {
             // This is the surface below the overlays.
             surface_damage_rect.Subtract(
-                surface_damage_rect_list_[index_to_be_removed]);
+                current_frame_state
+                    .surface_damage_rect_list[index_to_be_removed]);
           }
         }
-        root_damage_rect.Union(surface_damage_rect);
+        final_damage_rect.Union(surface_damage_rect);
         ++surface_index;
       }
 
-      *damage_rect = root_damage_rect;
+      overlay_data.damage_rect = final_damage_rect;
     }
   }
 
   if (should_add_previous_frame_overlay_damage) {
-    damage_rect->Union(PreviousFrameOverlayDamageContribution());
+    overlay_data.damage_rect.Union(
+        PreviousFrameOverlayDamageContribution(previous_frame_overlay_rects));
   }
-  damage_rect->Intersect(gfx::ToEnclosingRect(display_rect));
-  damages_to_be_removed_.clear();
-}
-
-bool DCLayerOverlayProcessor::IsPreviousFrameUnderlayRect(
-    const gfx::Rect& quad_rectangle,
-    size_t index) {
-  if (index >= previous_frame_overlay_rects_.size()) {
-    return false;
-  } else {
-    // Although we can loop through the list to find out if there is an
-    // underlay with the same size from the previous frame, checking
-    // _rectx_[index] is the quickest way to do it. If we cannot find a match
-    // with the same index, there is probably a change in the number of
-    // overlays or layout. Then we won't be able to get a zero root damage
-    // rect in this case. Looping through the list won't give better power.
-    return (previous_frame_overlay_rects_[index].rect == quad_rectangle) &&
-           (previous_frame_overlay_rects_[index].is_overlay == false);
-  }
+  overlay_data.damage_rect.Intersect(render_pass->output_rect);
+  current_frame_state.damages_to_be_removed.clear();
 }
 
 void DCLayerOverlayProcessor::RemoveClearVideoQuadCandidatesIfMoving(
-    const QuadList* quad_list,
-    std::vector<QuadList::Iterator>& candidates) {
+    RenderPassOverlayDataMap& render_pass_overlay_data_map,
+    RenderPassCurrentFrameStateMap& render_pass_state_map) {
   // The number of frames all overlay candidates need to be stable before we
   // allow overlays again. This number was chosen experimentally.
   constexpr int kFramesOfStabilityForOverlayPromotion = 5;
 
-  std::vector<gfx::Rect> current_frame_overlay_candidate_rects;
-  current_frame_overlay_candidate_rects.reserve(candidates.size());
+  std::vector<gfx::Rect> current_overlay_candidate_rects;
 
-  for (const auto& candidate_it : candidates) {
-    if (IsClearVideoQuad(candidate_it)) {
-      gfx::Rect quad_rectangle_in_target_space =
-          ClippedQuadRectangle(*candidate_it);
-      current_frame_overlay_candidate_rects.push_back(
-          quad_rectangle_in_target_space);
+  for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+    std::vector<QuadList::Iterator>& candidates =
+        render_pass_state_map[render_pass].candidates;
+    current_overlay_candidate_rects.reserve(
+        current_overlay_candidate_rects.size() + candidates.size());
+    for (auto candidate_it : candidates) {
+      if (IsClearVideoQuad(candidate_it)) {
+        gfx::Rect quad_rect_in_target_space =
+            ClippedQuadRectangle(*candidate_it);
+        gfx::Rect quad_rect_in_root_space =
+            cc::MathUtil::MapEnclosingClippedRect(
+                render_pass->transform_to_root_target,
+                quad_rect_in_target_space);
+        current_overlay_candidate_rects.push_back(quad_rect_in_root_space);
+      }
     }
   }
 
   if (previous_frame_overlay_candidate_rects_ !=
-      current_frame_overlay_candidate_rects) {
+      current_overlay_candidate_rects) {
     frames_since_last_overlay_candidate_rects_change_ = 0;
-
     std::swap(previous_frame_overlay_candidate_rects_,
-              current_frame_overlay_candidate_rects);
+              current_overlay_candidate_rects);
   } else {
     frames_since_last_overlay_candidate_rects_change_++;
   }
@@ -736,34 +757,32 @@
   if (frames_since_last_overlay_candidate_rects_change_ <=
       kFramesOfStabilityForOverlayPromotion) {
     // Remove all video quad candidates if any of them moved recently
-    auto candidate_it = candidates.begin();
-    while (candidate_it != candidates.end()) {
-      if (IsClearVideoQuad(*candidate_it)) {
-        RecordDCLayerResult(DC_LAYER_FAILED_YUV_VIDEO_QUAD_MOVED,
-                            *candidate_it);
-        candidate_it = candidates.erase(candidate_it);
-      } else {
-        candidate_it++;
+    for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+      std::vector<QuadList::Iterator>& candidates =
+          render_pass_state_map[render_pass].candidates;
+
+      auto candidate_it = candidates.begin();
+      while (candidate_it != candidates.end()) {
+        if (IsClearVideoQuad(*candidate_it)) {
+          RecordDCLayerResult(DC_LAYER_FAILED_YUV_VIDEO_QUAD_MOVED,
+                              *candidate_it);
+          candidate_it = candidates.erase(candidate_it);
+        } else {
+          candidate_it++;
+        }
       }
     }
   }
 }
 
-void DCLayerOverlayProcessor::Process(
+void DCLayerOverlayProcessor::CollectCandidates(
     DisplayResourceProvider* resource_provider,
-    const gfx::RectF& display_rect,
-    const FilterOperationsMap& render_pass_filters,
-    const FilterOperationsMap& render_pass_backdrop_filters,
     AggregatedRenderPass* render_pass,
-    gfx::Rect* damage_rect,
-    SurfaceDamageRectList surface_damage_rect_list,
-    OverlayCandidateList* dc_layer_overlays,
+    const FilterOperationsMap& render_pass_backdrop_filters,
     bool is_video_capture_enabled,
-    bool is_page_fullscreen_mode) {
-  bool this_frame_has_occluding_damage_rect = false;
-  processed_yuv_overlay_count_ = 0;
-  surface_damage_rect_list_ = std::move(surface_damage_rect_list);
-
+    RenderPassOverlayData& overlay_data,
+    RenderPassCurrentFrameState& render_pass_state,
+    GlobalOverlayState& global_overlay_state) {
   // Output rects of child render passes that have backdrop filters in target
   // space. These rects are used to determine if the overlay rect could be read
   // by backdrop filters.
@@ -771,24 +790,20 @@
 
   // Skip overlay for copy request, video capture or HDR P010 format.
   if (ShouldSkipOverlay(render_pass, is_video_capture_enabled)) {
-    // Update damage rect before calling ClearOverlayState, otherwise
-    // previous_frame_overlay_rect_union will be empty.
-    damage_rect->Union(PreviousFrameOverlayDamageContribution());
-    ClearOverlayState();
-
+    auto it = previous_frame_render_pass_states_.find(render_pass->id);
+    if (it != previous_frame_render_pass_states_.end()) {
+      // Add any overlay damage from the previous frame. Since we're not
+      // promoting overlays this frame, damages that may have been removed in
+      // the previous frame's UpdateDamageRect() now needs to be accounted
+      // for.
+      overlay_data.damage_rect.Union(
+          PreviousFrameOverlayDamageContribution(it->second.overlay_rects));
+      previous_frame_render_pass_states_.erase(it);
+    }
     return;
   }
 
-  std::vector<QuadList::Iterator> candidates;
   QuadList* quad_list = &render_pass->quad_list;
-
-  // Used for whether overlay should be skipped
-  int yuv_quads_in_quad_list = 0;
-  int damaged_yuv_quads_in_quad_list = 0;
-  // Tracks whether we have anything other than clear video overlays e.g. low
-  // latency canvas or protected video which are allowed for multiple overlays.
-  bool has_non_clear_video_overlays = false;
-
   for (auto it = quad_list->begin(); it != quad_list->end(); ++it) {
     if (it->material == DrawQuad::Material::kAggregatedRenderPass) {
       const auto* rpdq = AggregatedRenderPassDrawQuad::MaterialCast(*it);
@@ -808,7 +823,8 @@
         result = ValidateYUVQuad(
             YUVVideoDrawQuad::MaterialCast(*it), backdrop_filter_rects,
             has_overlay_support_, has_p010_video_processor_support_,
-            allowed_yuv_overlay_count_, processed_yuv_overlay_count_,
+            allowed_yuv_overlay_count_,
+            global_overlay_state.processed_yuv_overlay_count,
             resource_provider);
         is_yuv_overlay = true;
         break;
@@ -823,7 +839,8 @@
           result = ValidateTextureQuad(
               tex_quad, backdrop_filter_rects, has_overlay_support_,
               has_p010_video_processor_support_, allowed_yuv_overlay_count_,
-              processed_yuv_overlay_count_, resource_provider);
+              global_overlay_state.processed_yuv_overlay_count,
+              resource_provider);
         }
 
         is_yuv_overlay = tex_quad->is_video_frame;
@@ -842,20 +859,21 @@
     }
 
     if (is_yuv_overlay) {
-      yuv_quads_in_quad_list++;
+      global_overlay_state.yuv_quads++;
       if (no_undamaged_overlay_promotion_) {
         if (it->shared_quad_state->overlay_damage_index.has_value() &&
-            !surface_damage_rect_list_[it->shared_quad_state
-                                           ->overlay_damage_index.value()]
+            !render_pass_state
+                 .surface_damage_rect_list[it->shared_quad_state
+                                               ->overlay_damage_index.value()]
                  .IsEmpty()) {
-          damaged_yuv_quads_in_quad_list++;
+          global_overlay_state.damaged_yuv_quads++;
           if (result == DC_LAYER_SUCCESS) {
-            processed_yuv_overlay_count_++;
+            global_overlay_state.processed_yuv_overlay_count++;
           }
         }
       } else {
         if (result == DC_LAYER_SUCCESS) {
-          processed_yuv_overlay_count_++;
+          global_overlay_state.processed_yuv_overlay_count++;
         }
       }
     }
@@ -877,51 +895,28 @@
     }
 
     if (!IsClearVideoQuad(it)) {
-      has_non_clear_video_overlays = true;
+      global_overlay_state.has_non_clear_video_overlays = true;
     }
 
-    candidates.push_back(it);
+    render_pass_state.candidates.push_back(it);
   }
+}
 
-  // We might not save power if there are more than one videos and only part of
-  // them are promoted to overlay. Skip overlays for this frame unless there are
-  // protected video or texture overlays.
-  // In case of videos being paused or not started yet, we will allow multiple
-  // overlays if the number of damaged overlays doesn't exceed
-  // |allowed_yuv_overlay_count|. However, videos are not always damaged in
-  // every frame during video playback. To prevent overlay promotion from being
-  // switched between on and off, we wait for
-  // |kDCLayerFramesDelayedBeforeOverlay| frames before allowing multiple
-  // overlays
-  bool reject_overlays = false;
-  if (yuv_quads_in_quad_list > 1 && !has_non_clear_video_overlays) {
-    if (no_undamaged_overlay_promotion_) {
-      if (damaged_yuv_quads_in_quad_list == processed_yuv_overlay_count_) {
-        frames_since_last_qualified_multi_overlays_++;
-      } else {
-        frames_since_last_qualified_multi_overlays_ = 0;
-      }
-      reject_overlays = frames_since_last_qualified_multi_overlays_ <=
-                        kDCLayerFramesDelayedBeforeOverlay;
-    } else {
-      if (yuv_quads_in_quad_list != processed_yuv_overlay_count_)
-        reject_overlays = true;
-    }
-  }
-
-  // A YUV quad might be rejected later due to not allowed as an underlay.
-  // Recount the YUV overlays when they are added to the overlay list
-  // successfully.
-  processed_yuv_overlay_count_ = 0;
-
-  if (base::FeatureList::IsEnabled(features::kDisableVideoOverlayIfMoving)) {
-    RemoveClearVideoQuadCandidatesIfMoving(quad_list, candidates);
-  }
+void DCLayerOverlayProcessor::PromoteCandidates(
+    DisplayResourceProvider* resource_provider,
+    AggregatedRenderPass* render_pass,
+    const FilterOperationsMap& render_pass_filters,
+    const RenderPassPreviousFrameState& previous_frame_state,
+    bool is_page_fullscreen_mode,
+    RenderPassOverlayData& overlay_data,
+    RenderPassCurrentFrameState& current_frame_state,
+    GlobalOverlayState& global_overlay_state) {
+  QuadList* quad_list = &render_pass->quad_list;
 
   // Copy the overlay quad info to dc_layer_overlays and replace/delete overlay
   // quads in quad_list.
-  for (auto& it : candidates) {
-    if (reject_overlays) {
+  for (auto& it : current_frame_state.candidates) {
+    if (global_overlay_state.reject_overlays) {
       RecordDCLayerResult(DC_LAYER_FAILED_TOO_MANY_OVERLAYS, it);
       continue;
     }
@@ -929,21 +924,22 @@
     // Do not promote undamaged video to overlays.
     bool undamaged =
         it->shared_quad_state->overlay_damage_index.has_value() &&
-        surface_damage_rect_list_[it->shared_quad_state->overlay_damage_index
-                                      .value()]
+        current_frame_state
+            .surface_damage_rect_list[it->shared_quad_state
+                                          ->overlay_damage_index.value()]
             .IsEmpty();
 
-    if (yuv_quads_in_quad_list > allowed_yuv_overlay_count_ &&
-        !has_non_clear_video_overlays && undamaged &&
+    if (global_overlay_state.yuv_quads > allowed_yuv_overlay_count_ &&
+        !global_overlay_state.has_non_clear_video_overlays && undamaged &&
         no_undamaged_overlay_promotion_ && IsVideoQuad(it)) {
       RecordDCLayerResult(DC_LAYER_FAILED_NOT_DAMAGED, it);
       continue;
     }
 
-    gfx::Rect quad_rectangle_in_target_space = ClippedQuadRectangle(*it);
+    gfx::Rect quad_rect_in_target_space = ClippedQuadRectangle(*it);
 
     // Quad is considered an "overlay" if it has no occluders.
-    bool is_overlay = !IsOccluded(gfx::RectF(quad_rectangle_in_target_space),
+    bool is_overlay = !IsOccluded(gfx::RectF(quad_rect_in_target_space),
                                   quad_list->begin(), it, render_pass_filters);
 
     // Protected video is always put in an overlay, but texture quads can be
@@ -966,48 +962,149 @@
       }
     }
 
-    gfx::Rect quad_rectangle_in_root_space =
-        cc::MathUtil::MapEnclosingClippedRect(
-            render_pass->transform_to_root_target,
-            quad_rectangle_in_target_space);
-
     // Used by a histogram.
-    this_frame_has_occluding_damage_rect =
-        !is_overlay &&
-        HasOccludingDamageRect(it->shared_quad_state, surface_damage_rect_list_,
-                               quad_rectangle_in_root_space);
+    global_overlay_state.has_occluding_damage_rect =
+        global_overlay_state.has_occluding_damage_rect ||
+        (!is_overlay &&
+         HasOccludingDamageRect(it->shared_quad_state,
+                                current_frame_state.surface_damage_rect_list,
+                                quad_rect_in_target_space));
 
-    UpdateDCLayerOverlays(resource_provider, display_rect, render_pass, it,
-                          quad_rectangle_in_root_space, is_overlay, damage_rect,
-                          dc_layer_overlays, is_page_fullscreen_mode);
+    UpdateDCLayerOverlays(resource_provider, render_pass, it,
+                          quad_rect_in_target_space, previous_frame_state,
+                          is_overlay, is_page_fullscreen_mode, overlay_data,
+                          current_frame_state, global_overlay_state);
   }
 
-  // Update previous frame state after processing root pass. If there is no
+  // Update previous frame state after processing render pass. If there is no
   // overlay in this frame, previous_frame_overlay_rect_union will be added
   // to the damage_rect here for GL composition because the overlay image from
   // the previous frame is missing in the GL composition path. If any overlay is
   // found in this frame, the previous overlay rects would have been handled
   // above and previous_frame_overlay_rect_union becomes empty.
-  UpdateRootDamageRect(display_rect, damage_rect);
+  UpdateDamageRect(render_pass, previous_frame_state, overlay_data,
+                   current_frame_state);
 
-  std::swap(previous_frame_overlay_rects_, current_frame_overlay_rects_);
-  current_frame_overlay_rects_.clear();
-  previous_display_rect_ = display_rect;
+  RenderPassPreviousFrameState& previous_frame_render_pass_state =
+      previous_frame_render_pass_states_.at(render_pass->id);
+  std::swap(previous_frame_render_pass_state.overlay_rects,
+            current_frame_state.overlay_rects);
+  previous_frame_render_pass_state.display_rect = render_pass->output_rect;
+}
 
-  if (processed_yuv_overlay_count_ > 0) {
+void DCLayerOverlayProcessor::Process(
+    DisplayResourceProvider* resource_provider,
+    const FilterOperationsMap& render_pass_filters,
+    const FilterOperationsMap& render_pass_backdrop_filters,
+    const SurfaceDamageRectList& surface_damage_rect_list_in_root_space,
+    bool is_video_capture_enabled,
+    bool is_page_fullscreen_mode,
+    RenderPassOverlayDataMap& render_pass_overlay_data_map) {
+  GlobalOverlayState global_overlay_state;
+  RenderPassCurrentFrameStateMap render_pass_state_map;
+  render_pass_state_map.reserve(render_pass_overlay_data_map.size());
+
+  for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+    if (!render_pass->transform_to_root_target.IsInvertible()) {
+      // We can skip render passes that do not have an invertible transform
+      // since it isn't visible.
+      continue;
+    }
+
+    RenderPassCurrentFrameState& current_frame_state =
+        render_pass_state_map[render_pass];
+    // Convert the surface damage rects from root space to render pass space.
+    // |surface_damage_rect_list_in_root_space| contains surface damages for all
+    // surfaces in the frame across all render passes. We only need the surface
+    // damage rects for the current render pass, but since we don't expect this
+    // list to be large, this keeps the entire list for the simplicity.
+    current_frame_state.surface_damage_rect_list =
+        surface_damage_rect_list_in_root_space;
+    for (auto& rect : current_frame_state.surface_damage_rect_list) {
+      rect = render_pass->transform_to_root_target.InverseMapRect(rect).value();
+    }
+
+    CollectCandidates(resource_provider, render_pass,
+                      render_pass_backdrop_filters, is_video_capture_enabled,
+                      overlay_data, current_frame_state, global_overlay_state);
+  }
+
+  // We might not save power if there are more than one videos and only part of
+  // them are promoted to overlay. Skip overlays for this frame unless there are
+  // protected video or texture overlays.
+  // In case of videos being paused or not started yet, we will allow multiple
+  // overlays if the number of damaged overlays doesn't exceed
+  // |allowed_yuv_overlay_count|. However, videos are not always damaged in
+  // every frame during video playback. To prevent overlay promotion from being
+  // switched between on and off, we wait for
+  // |kDCLayerFramesDelayedBeforeOverlay| frames before allowing multiple
+  // overlays
+  if (global_overlay_state.yuv_quads > 1 &&
+      !global_overlay_state.has_non_clear_video_overlays) {
+    if (no_undamaged_overlay_promotion_) {
+      if (global_overlay_state.damaged_yuv_quads ==
+          global_overlay_state.processed_yuv_overlay_count) {
+        frames_since_last_qualified_multi_overlays_++;
+      } else {
+        frames_since_last_qualified_multi_overlays_ = 0;
+      }
+      global_overlay_state.reject_overlays =
+          frames_since_last_qualified_multi_overlays_ <=
+          kDCLayerFramesDelayedBeforeOverlay;
+    } else {
+      if (global_overlay_state.yuv_quads !=
+          global_overlay_state.processed_yuv_overlay_count) {
+        global_overlay_state.reject_overlays = true;
+      }
+    }
+  }
+
+  // A YUV quad might be rejected later due to not allowed as an underlay.
+  // Recount the YUV overlays when they are added to the overlay list
+  // successfully.
+  global_overlay_state.processed_yuv_overlay_count = 0;
+
+  if (base::FeatureList::IsEnabled(features::kDisableVideoOverlayIfMoving)) {
+    RemoveClearVideoQuadCandidatesIfMoving(render_pass_overlay_data_map,
+                                           render_pass_state_map);
+  }
+
+  // Swap the entire map into a local variable. For the rest of this function,
+  // information about the current frame is populated into the member variable,
+  // while the local variable is used to access information about the previous
+  // frame. Clearing the member variable allows us to remove render passes that
+  // don't exist in the current frame.
+  base::flat_map<AggregatedRenderPassId, RenderPassPreviousFrameState>
+      previous_frame_render_pass_states;
+  std::swap(previous_frame_render_pass_states_,
+            previous_frame_render_pass_states);
+
+  for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+    // Create an entry for the current frame. This is the only place where an
+    // entry is created in this map.
+    previous_frame_render_pass_states_.emplace(render_pass->id,
+                                               RenderPassPreviousFrameState());
+
+    PromoteCandidates(resource_provider, render_pass, render_pass_filters,
+                      previous_frame_render_pass_states[render_pass->id],
+                      is_page_fullscreen_mode, overlay_data,
+                      render_pass_state_map[render_pass], global_overlay_state);
+  }
+
+  if (global_overlay_state.processed_yuv_overlay_count > 0) {
     base::UmaHistogramExactLinear(
         "GPU.DirectComposition.DCLayer.YUVOverlayCount",
-        /*sample=*/processed_yuv_overlay_count_,
-        /*value_max=*/10);
+        /*sample=*/global_overlay_state.processed_yuv_overlay_count,
+        /*exclusive_max=*/10);
 
-    RecordOverlayHistograms(dc_layer_overlays,
-                            this_frame_has_occluding_damage_rect, damage_rect);
+    RecordOverlayHistograms(render_pass_overlay_data_map,
+                            global_overlay_state.has_occluding_damage_rect);
   }
 }
 
 bool DCLayerOverlayProcessor::ShouldSkipOverlay(
     AggregatedRenderPass* render_pass,
-    bool is_video_capture_enabled) {
+    bool is_video_capture_enabled) const {
   QuadList* quad_list = &render_pass->quad_list;
 
   // Skip overlay processing if we have copy request or video capture is
@@ -1058,14 +1155,15 @@
 
 void DCLayerOverlayProcessor::UpdateDCLayerOverlays(
     DisplayResourceProvider* resource_provider,
-    const gfx::RectF& display_rect,
     AggregatedRenderPass* render_pass,
     const QuadList::Iterator& it,
-    const gfx::Rect& quad_rectangle_in_root_space,
+    const gfx::Rect& quad_rect_in_target_space,
+    const RenderPassPreviousFrameState& previous_frame_state,
     bool is_overlay,
-    gfx::Rect* damage_rect,
-    OverlayCandidateList* dc_layer_overlays,
-    bool is_page_fullscreen_mode) {
+    bool is_page_fullscreen_mode,
+    RenderPassOverlayData& overlay_data,
+    RenderPassCurrentFrameState& current_frame_state,
+    GlobalOverlayState& global_overlay_state) {
   // Record the result first before ProcessForOverlay().
   RecordDCLayerResult(DC_LAYER_SUCCESS, it);
 
@@ -1073,20 +1171,20 @@
   dc_layer.possible_video_fullscreen_letterboxing =
       is_page_fullscreen_mode
           ? IsPossibleFullScreenLetterboxing(it, render_pass->quad_list.end(),
-                                             display_rect)
+                                             render_pass->output_rect)
           : false;
   switch (it->material) {
     case DrawQuad::Material::kYuvVideoContent:
       FromYUVQuad(YUVVideoDrawQuad::MaterialCast(*it),
                   render_pass->transform_to_root_target, &dc_layer);
-      processed_yuv_overlay_count_++;
+      global_overlay_state.processed_yuv_overlay_count++;
       break;
     case DrawQuad::Material::kTextureContent: {
       const TextureDrawQuad* tex_quad = TextureDrawQuad::MaterialCast(*it);
       FromTextureQuad(tex_quad, render_pass->transform_to_root_target,
                       resource_provider, &dc_layer);
       if (tex_quad->is_video_frame) {
-        processed_yuv_overlay_count_++;
+        global_overlay_state.processed_yuv_overlay_count++;
       }
     } break;
     default:
@@ -1096,33 +1194,38 @@
   // Underlays are less efficient, so attempt regular overlays first. We can
   // only check for occlusion within a render pass.
   if (is_overlay) {
-    ProcessForOverlay(display_rect, render_pass, it);
+    ProcessForOverlay(render_pass, it, previous_frame_state,
+                      current_frame_state);
   } else {
-    ProcessForUnderlay(display_rect, render_pass, quad_rectangle_in_root_space,
-                       it, dc_layer_overlays->size(), damage_rect, &dc_layer);
+    ProcessForUnderlay(render_pass, it, quad_rect_in_target_space,
+                       previous_frame_state, global_overlay_state, overlay_data,
+                       current_frame_state, dc_layer);
   }
 
-  current_frame_overlay_rects_.push_back(
-      {quad_rectangle_in_root_space, is_overlay});
-  dc_layer_overlays->push_back(dc_layer);
+  current_frame_state.overlay_rects.push_back(
+      {quad_rect_in_target_space, is_overlay});
+
+  overlay_data.promoted_overlays.push_back(dc_layer);
 
   // Recorded for each overlay.
   UMA_HISTOGRAM_BOOLEAN("GPU.DirectComposition.IsUnderlay", !is_overlay);
 }
 
 void DCLayerOverlayProcessor::ProcessForOverlay(
-    const gfx::RectF& display_rect,
     AggregatedRenderPass* render_pass,
-    const QuadList::Iterator& it) {
+    const QuadList::Iterator& it,
+    const RenderPassPreviousFrameState& previous_frame_state,
+    RenderPassCurrentFrameState& current_frame_state) const {
   // The quad is on top, so promote it to an overlay and remove all damage
   // underneath it.
-  const bool display_rect_changed = (display_rect != previous_display_rect_);
+  const bool display_rect_changed =
+      render_pass->output_rect != previous_frame_state.display_rect;
   const bool is_axis_aligned = it->shared_quad_state->quad_to_target_transform
                                    .Preserves2dAxisAlignment();
   const bool needs_blending = it->ShouldDrawWithBlending();
 
   if (is_axis_aligned && !display_rect_changed && !needs_blending) {
-    RemoveOverlayDamageRect(it);
+    RemoveOverlayDamageRect(it, current_frame_state);
   }
 
   // Overlay quads should not be drawn. Removing the quads from the quad list
@@ -1133,17 +1236,20 @@
 }
 
 void DCLayerOverlayProcessor::ProcessForUnderlay(
-    const gfx::RectF& display_rect,
     AggregatedRenderPass* render_pass,
-    const gfx::Rect& quad_rectangle,
     const QuadList::Iterator& it,
-    size_t processed_overlay_count,
-    gfx::Rect* damage_rect,
-    OverlayCandidate* dc_layer) {
+    const gfx::Rect& quad_rect_in_target_space,
+    const RenderPassPreviousFrameState& previous_frame_state,
+    const GlobalOverlayState& global_overlay_state,
+    RenderPassOverlayData& overlay_data,
+    RenderPassCurrentFrameState& current_frame_state,
+    OverlayCandidate& dc_layer) {
   // Assign decreasing z-order so that underlays processed earlier, and hence
   // which are above the subsequent underlays, are placed above in the direct
-  // composition visual tree.
-  dc_layer->plane_z_order = -1 - processed_overlay_count;
+  // composition visual tree. The z-orders are assigned relative to other
+  // underlays in its render pass, not relative to the total number of underlays
+  // across all render passes.
+  dc_layer.plane_z_order = -1 - overlay_data.promoted_overlays.size();
 
   // If the video is translucent and uses SrcOver blend mode, we can achieve the
   // same result as compositing with video on top if we replace video quad with
@@ -1173,13 +1279,17 @@
     is_opaque = true;
   }
 
-  const bool display_rect_unchanged = (display_rect == previous_display_rect_);
-  const bool underlay_rect_unchanged =
-      IsPreviousFrameUnderlayRect(quad_rectangle, processed_overlay_count);
+  const bool display_rect_unchanged =
+      render_pass->output_rect == previous_frame_state.display_rect;
+  const bool underlay_rect_unchanged = IsPreviousFrameUnderlayRect(
+      previous_frame_state.overlay_rects, quad_rect_in_target_space,
+      overlay_data.promoted_overlays.size());
   const bool is_axis_aligned = it->shared_quad_state->quad_to_target_transform
                                    .Preserves2dAxisAlignment();
-  bool opacity_unchanged = (is_opaque == previous_frame_underlay_is_opaque_);
-  previous_frame_underlay_is_opaque_ = is_opaque;
+  bool opacity_unchanged =
+      (is_opaque == previous_frame_state.underlay_is_opaque);
+  previous_frame_render_pass_states_.at(render_pass->id).underlay_is_opaque =
+      is_opaque;
 
   if (is_axis_aligned && opacity_unchanged && underlay_rect_unchanged &&
       display_rect_unchanged) {
@@ -1188,15 +1298,45 @@
     // cleared last frame.
 
     // If none of the quads on top give any damage, we can skip compositing
-    // these quads. The output root damage rect might be empty after we remove
-    // the damage from the video quad. We can save power if the root damage
-    // rect is empty.
-    RemoveOverlayDamageRect(it);
+    // these quads. The output damage rect might be empty after we remove the
+    // the damage from the video quad. We can save power if the damage rect is
+    // empty.
+    RemoveOverlayDamageRect(it, current_frame_state);
   } else {
     // Entire replacement quad must be redrawn.
-    damage_rect->Union(quad_rectangle);
-    surface_damage_rect_list_.push_back(quad_rectangle);
+    overlay_data.damage_rect.Union(quad_rect_in_target_space);
+    current_frame_state.surface_damage_rect_list.push_back(
+        quad_rect_in_target_space);
   }
 }
 
+DCLayerOverlayProcessor::RenderPassOverlayData::RenderPassOverlayData() =
+    default;
+DCLayerOverlayProcessor::RenderPassOverlayData::~RenderPassOverlayData() =
+    default;
+DCLayerOverlayProcessor::RenderPassOverlayData::RenderPassOverlayData(
+    RenderPassOverlayData&&) = default;
+DCLayerOverlayProcessor::RenderPassOverlayData&
+DCLayerOverlayProcessor::RenderPassOverlayData::operator=(
+    RenderPassOverlayData&&) = default;
+
+DCLayerOverlayProcessor::RenderPassPreviousFrameState::
+    RenderPassPreviousFrameState() = default;
+DCLayerOverlayProcessor::RenderPassPreviousFrameState::
+    ~RenderPassPreviousFrameState() = default;
+DCLayerOverlayProcessor::RenderPassPreviousFrameState::
+    RenderPassPreviousFrameState(RenderPassPreviousFrameState&&) = default;
+DCLayerOverlayProcessor::RenderPassPreviousFrameState&
+DCLayerOverlayProcessor::RenderPassPreviousFrameState::operator=(
+    RenderPassPreviousFrameState&&) = default;
+
+DCLayerOverlayProcessor::RenderPassCurrentFrameState::
+    RenderPassCurrentFrameState() = default;
+DCLayerOverlayProcessor::RenderPassCurrentFrameState::
+    ~RenderPassCurrentFrameState() = default;
+DCLayerOverlayProcessor::RenderPassCurrentFrameState::
+    RenderPassCurrentFrameState(RenderPassCurrentFrameState&&) = default;
+DCLayerOverlayProcessor::RenderPassCurrentFrameState&
+DCLayerOverlayProcessor::RenderPassCurrentFrameState::operator=(
+    RenderPassCurrentFrameState&&) = default;
 }  // namespace viz
diff --git a/components/viz/service/display/dc_layer_overlay.h b/components/viz/service/display/dc_layer_overlay.h
index 66f6d3e3..f57a250 100644
--- a/components/viz/service/display/dc_layer_overlay.h
+++ b/components/viz/service/display/dc_layer_overlay.h
@@ -7,6 +7,7 @@
 
 #include <vector>
 
+#include "base/check_is_test.h"
 #include "base/containers/flat_map.h"
 #include "base/memory/raw_ptr.h"
 #include "base/threading/thread_checker.h"
@@ -40,21 +41,44 @@
 
   ~DCLayerOverlayProcessor() override;
 
-  // Virtual for testing.
-  virtual void Process(DisplayResourceProvider* resource_provider,
-                       const gfx::RectF& display_rect,
-                       const FilterOperationsMap& render_pass_filters,
-                       const FilterOperationsMap& render_pass_backdrop_filters,
-                       AggregatedRenderPass* render_pass,
-                       gfx::Rect* damage_rect,
-                       SurfaceDamageRectList surface_damage_rect_list,
-                       OverlayCandidateList* dc_layer_overlays,
-                       bool is_video_capture_enabled,
-                       bool is_page_fullscreen_mode);
-  void ClearOverlayState();
-  // This is the damage contribution due to previous frame's overlays which can
-  // be empty.
-  gfx::Rect PreviousFrameOverlayDamageContribution();
+  // Encapsulates all of the information about a render pass's overlays that
+  // are returned back to OverlayProcessorWin. This is passed to Process() as an
+  // in/out parameter.
+  struct VIZ_SERVICE_EXPORT RenderPassOverlayData {
+    RenderPassOverlayData();
+    ~RenderPassOverlayData();
+
+    RenderPassOverlayData(RenderPassOverlayData&&);
+    RenderPassOverlayData& operator=(RenderPassOverlayData&&);
+
+    // Damage rect of the render pass. Set by OverlayProcessorWin and may be
+    // optimized in UpdateDamageRect() if overlays are promoted.
+    gfx::Rect damage_rect;
+
+    // List of overlays that are actually promoted. Only used for output back to
+    // OverlayProcessorWin. Contains all the information necessary to draw the
+    // overlay quads in SkiaRenderer.
+    OverlayCandidateList promoted_overlays;
+  };
+  using RenderPassOverlayDataMap =
+      base::flat_map<raw_ptr<AggregatedRenderPass>, RenderPassOverlayData>;
+
+  // Virtual for testing. All render passes that should be considered for
+  // overlays in this frame should be in |render_pass_overlay_data_map|. After
+  // this function executes, |render_pass_overlay_data_map[render_pass]| will
+  // contain the all of the overlays promoted for |render_pass|. The z-order
+  // of the overlays are assigned relative to other overlays within the render
+  // pass, with positive z-orders being overlays and negative z-orders being
+  // underlays. The caller must aggregate overlays from all render passes into
+  // a global overlay list, taking into account the render pass's z-order.
+  virtual void Process(
+      DisplayResourceProvider* resource_provider,
+      const FilterOperationsMap& render_pass_filters,
+      const FilterOperationsMap& render_pass_backdrop_filters,
+      const SurfaceDamageRectList& surface_damage_rect_list_in_root_space,
+      bool is_video_capture_enabled,
+      bool is_page_fullscreen_mode,
+      RenderPassOverlayDataMap& render_pass_overlay_data_map);
 
   // DirectCompositionOverlayCapsObserver implementation.
   void OnOverlayCapsChanged() override;
@@ -65,46 +89,217 @@
   void set_frames_since_last_qualified_multi_overlays_for_testing(int value) {
     frames_since_last_qualified_multi_overlays_ = value;
   }
+  size_t get_previous_frame_render_pass_count() const {
+    CHECK_IS_TEST();
+    return previous_frame_render_pass_states_.size();
+  }
+  std::vector<AggregatedRenderPassId> get_previous_frame_render_pass_ids()
+      const {
+    std::vector<AggregatedRenderPassId> ids;
+    for (const auto& [id, _] : previous_frame_render_pass_states_) {
+      ids.push_back(id);
+    }
+    return ids;
+  }
+
+  // This struct only contains minimal information about the overlays, enough to
+  // perform damage optimizations across frames.
+  struct OverlayRect {
+    gfx::Rect rect;
+    bool is_overlay = true;  // If false, it's an underlay.
+    bool operator==(const OverlayRect& b) const {
+      return rect == b.rect && is_overlay == b.is_overlay;
+    }
+    bool operator!=(const OverlayRect& b) const { return !(*this == b); }
+  };
 
  private:
+  // Information about a render pass's overlays from the previous frame. The
+  // previous frame's overlays are used for optimizations, which are done
+  // independently for each render pass. These optimizations try to remove
+  // render pass packing damage if the overlays are not changed between frames,
+  // which potentially allows us to skip drawing the render pass. We also add
+  // damage from overlays in the previous frame in the scenarios where we skip
+  // overlays in the current frame or if the overlays have changed. This damage
+  // needs to be re-added because the content under the overlays from the
+  // previous frame are likely out of date if they were optimized out.
+  struct RenderPassPreviousFrameState {
+    RenderPassPreviousFrameState();
+    ~RenderPassPreviousFrameState();
+
+    RenderPassPreviousFrameState(RenderPassPreviousFrameState&&);
+    RenderPassPreviousFrameState& operator=(
+        RenderPassPreviousFrameState&& other);
+
+    // Whether the render pass had any promoted underlay quads that were opaque
+    // in the previous frame.
+    bool underlay_is_opaque = true;
+
+    // The output rect of the render pass in the previous frame.
+    gfx::Rect display_rect;
+
+    // Rects of all overlay and underlay quads that were promoted in the
+    // previous frame.
+    std::vector<OverlayRect> overlay_rects;
+  };
+
+  // Information about a render pass's overlays in the current frame being
+  // processed. This struct primarily serves to encapsulate all parameters
+  // relating to a render pass into one object that can be passed between
+  // multiple functions. These objects do not persist after this current frame
+  // is processed. While RenderPassOverlayData stores information that are
+  // exposed and returned to OverlayProcessorWin, this struct contains data used
+  // only internally to this class.
+  struct RenderPassCurrentFrameState {
+    RenderPassCurrentFrameState();
+    ~RenderPassCurrentFrameState();
+
+    RenderPassCurrentFrameState(RenderPassCurrentFrameState&&);
+    RenderPassCurrentFrameState& operator=(RenderPassCurrentFrameState&& other);
+
+    // The surface damage rect list for the frame, in *render pass space*.
+    SurfaceDamageRectList surface_damage_rect_list;
+
+    // Overlay quad candidates in the render pass's quad list. These are
+    // overlays that have been identified as potential candidates for promotion
+    // and are collected in the initial stage of processing. Some or all of
+    // these candidates may or may not be actually promoted. We're storing
+    // iterators instead of the actual quad because some functions such as
+    // IsPossiblefullScreenLetterboxing and ProcessForUnderlay require knowing
+    // the position of the quad in the quad list.
+    std::vector<QuadList::Iterator> candidates;
+
+    // Rects of overlays that have been processed and successfully promoted and
+    // added to |RenderPassOverlayData::promoted_overlays|.
+    std::vector<OverlayRect> overlay_rects;
+
+    // Overlay damages that can be removed from the render pass's damage rect
+    // at the end of processing overlays. This vector stores indices of damages
+    // in |surface_damage_rect_list| that can be removed.
+    std::vector<size_t> damages_to_be_removed;
+  };
+  using RenderPassCurrentFrameStateMap =
+      base::flat_map<raw_ptr<AggregatedRenderPass>,
+                     RenderPassCurrentFrameState>;
+
+  // Information about overlays in the current frame being processed. Unlike
+  // fields in RenderPassCurrentFrameState, these fields are not specific to any
+  // render pass. They are global to the entire frame. Similarly, this struct
+  // exists primarily to encapsulate variables into one object to pass between
+  // functions.
+  struct GlobalOverlayState {
+    // Actual number of yuv quads that are successfully processed and added as
+    // an overlay. Used to determine whether overlay should be skipped.
+    int processed_yuv_overlay_count = 0;
+
+    // Total number of yuv quads.
+    int yuv_quads = 0;
+
+    // Number of yuv quads that were considered for overlay promotion and have a
+    // non-empty surface damage.
+    int damaged_yuv_quads = 0;
+
+    // Tracks whether we have anything other than clear video overlays e.g. low
+    // latency canvas or protected video which are allowed for multiple
+    // overlays.
+    bool has_non_clear_video_overlays = false;
+
+    // Used for recording overlay histograms.
+    bool has_occluding_damage_rect = false;
+
+    // Whether to reject all overlays for this frame. This can be true if we
+    // have more than one overlay quad and not all of them are promoted to
+    // overlays.
+    bool reject_overlays = false;
+  };
+
+  // Collects the overlay candidates for a render pass. Coordinate systems for
+  // all parameters should be in render pass space.
+  //
+  // If video capture is enabled, overlays are not processed. In this case, the
+  // render pass's previous frame data is erased since there will be no overlays
+  // in the current frame.
+  //
+  // This function adds overlay candidates for |render_pass| into
+  // |render_pass_state| and accumulates information about |render_pass|
+  // into |global_overlay_state|. If overlays should be skipped for this
+  // render pass, the damage rect in |overlay_data| is unioned with the previous
+  // frame's overlay damages, and the previous frame state is cleared.
+  void CollectCandidates(
+      DisplayResourceProvider* resource_provider,
+      AggregatedRenderPass* render_pass,
+      const FilterOperationsMap& render_pass_backdrop_filters,
+      bool is_video_capture_enabled,
+      RenderPassOverlayData& overlay_data,
+      RenderPassCurrentFrameState& render_pass_state,
+      GlobalOverlayState& global_overlay_state);
+
+  // Promotes overlay candidates for a render pass. Coordinate systems for all
+  // parameters should be in in render pass space.
+  //
+  // The render pass's corresponding RenderPassPreviousFrameState object in
+  // |previous_frame_overlay_candidate_rects_| is updated to contain this
+  // frame's data.
+  //
+  // This function adds overlays that have been promoted into |overlay_data|
+  // and accumulates their rects into the damage rect. It also updates all of
+  // |current_frame_state|'s fields and |processed_yuv_overlay_count| to reflect
+  // the actual number of overlays promoted.
+  void PromoteCandidates(
+      DisplayResourceProvider* resource_provider,
+      AggregatedRenderPass* render_pass,
+      const FilterOperationsMap& render_pass_filters,
+      const RenderPassPreviousFrameState& previous_frame_state,
+      bool is_page_fullscreen_mode,
+      RenderPassOverlayData& overlay_data,
+      RenderPassCurrentFrameState& current_frame_state,
+      GlobalOverlayState& global_overlay_state);
+
   // Detects overlay processing skip inside |render_pass|.
   bool ShouldSkipOverlay(AggregatedRenderPass* render_pass,
-                         bool is_video_capture_enabled);
+                         bool is_video_capture_enabled) const;
 
-  // UpdateDCLayerOverlays() adds the quad at |it| to the overlay list
-  // |dc_layer_overlays|.
-  void UpdateDCLayerOverlays(DisplayResourceProvider* resource_provider,
-                             const gfx::RectF& display_rect,
-                             AggregatedRenderPass* render_pass,
-                             const QuadList::Iterator& it,
-                             const gfx::Rect& quad_rectangle_in_root_space,
-                             bool is_overlay,
-                             gfx::Rect* damage_rect,
-                             OverlayCandidateList* dc_layer_overlays,
-                             bool is_page_fullscreen_mode);
+  // Creates an OverlayCandidate for a quad candidate and updates the states
+  // for the render pass.
+  void UpdateDCLayerOverlays(
+      DisplayResourceProvider* resource_provider,
+      AggregatedRenderPass* render_pass,
+      const QuadList::Iterator& it,
+      const gfx::Rect& quad_rectangle_in_target_space,
+      const RenderPassPreviousFrameState& previous_frame_state,
+      bool is_overlay,
+      bool is_page_fullscreen_mode,
+      RenderPassOverlayData& overlay_data,
+      RenderPassCurrentFrameState& current_frame_state,
+      GlobalOverlayState& global_overlay_state);
 
-  // Returns an iterator to the element after |it|.
-  void ProcessForOverlay(const gfx::RectF& display_rect,
-                         AggregatedRenderPass* render_pass,
-                         const QuadList::Iterator& it);
-  void ProcessForUnderlay(const gfx::RectF& display_rect,
-                          AggregatedRenderPass* render_pass,
-                          const gfx::Rect& quad_rectangle,
-                          const QuadList::Iterator& it,
-                          size_t processed_overlay_count,
-                          gfx::Rect* damage_rect,
-                          OverlayCandidate* dc_layer);
+  void ProcessForOverlay(
+      AggregatedRenderPass* render_pass,
+      const QuadList::Iterator& it,
+      const RenderPassPreviousFrameState& previous_frame_state,
+      RenderPassCurrentFrameState& current_frame_state) const;
+  void ProcessForUnderlay(
+      AggregatedRenderPass* render_pass,
+      const QuadList::Iterator& it,
+      const gfx::Rect& quad_rectangle_in_target_space,
+      const RenderPassPreviousFrameState& previous_frame_state,
+      const GlobalOverlayState& global_overlay_state,
+      RenderPassOverlayData& overlay_data,
+      RenderPassCurrentFrameState& current_frame_state,
+      OverlayCandidate& dc_layer);
 
-  void UpdateRootDamageRect(const gfx::RectF& display_rect,
-                            gfx::Rect* damage_rect);
+  void UpdateDamageRect(
+      AggregatedRenderPass* render_pass,
+      const RenderPassPreviousFrameState& previous_frame_state,
+      RenderPassOverlayData& overlay_data,
+      RenderPassCurrentFrameState& current_frame_state) const;
 
-  void RemoveOverlayDamageRect(const QuadList::Iterator& it);
+  void RemoveOverlayDamageRect(
+      const QuadList::Iterator& it,
+      RenderPassCurrentFrameState& render_pass_state) const;
 
-  bool IsPreviousFrameUnderlayRect(const gfx::Rect& quad_rectangle,
-                                   size_t index);
-
-  // Remove all video overlay candidates from `candidate_index_list` if any of
-  // them have moved in the last several frames.
+  // Remove all video overlay candidates if any overlays in any render passes
+  // have moved in the last several frames.
   //
   // We do this because it could cause visible stuttering of playback on certain
   // older hardware. The stuttering does not occur if other overlay quads move
@@ -115,45 +310,28 @@
   // even if they could cause stutter. Software-protected video aren't required
   // to be in overlay, but we also exclude them from de-promotion to keep the
   // protection benefits of being in an overlay.
-  //
-  // `transform_to_root_target` is needed to track the quad positions in a
-  // uniform space. It should be in screen space (to handle the case when the
-  // window itself moves), but we don't easily know of the position of the
-  // window in screen space.
-  //
-  // `candidate_index_list` contains the indexes in `quad_list` of overlay
-  // candidates.
   void RemoveClearVideoQuadCandidatesIfMoving(
-      const QuadList* quad_list,
-      std::vector<QuadList::Iterator>& candidates);
+      RenderPassOverlayDataMap& render_pass_overlay_data_map,
+      RenderPassCurrentFrameStateMap& render_pass_current_state_map);
 
   bool has_overlay_support_;
   bool has_p010_video_processor_support_ = false;
   bool system_hdr_enabled_ = false;
   const int allowed_yuv_overlay_count_;
-  int processed_yuv_overlay_count_ = 0;
   uint64_t frames_since_last_qualified_multi_overlays_ = 0;
 
-  bool previous_frame_underlay_is_opaque_ = true;
   bool allow_promotion_hinting_ = false;
-  gfx::RectF previous_display_rect_;
-  std::vector<size_t> damages_to_be_removed_;
 
-  struct OverlayRect {
-    gfx::Rect rect;
-    bool is_overlay;  // If false, it's an underlay.
-    bool operator==(const OverlayRect& b) const {
-      return rect == b.rect && is_overlay == b.is_overlay;
-    }
-    bool operator!=(const OverlayRect& b) const { return !(*this == b); }
-  };
-  std::vector<OverlayRect> previous_frame_overlay_rects_;
-  std::vector<OverlayRect> current_frame_overlay_rects_;
-  SurfaceDamageRectList surface_damage_rect_list_;
+  // Information about overlays from the previous frame.
+  base::flat_map<AggregatedRenderPassId, RenderPassPreviousFrameState>
+      previous_frame_render_pass_states_;
 
-  // Used in `RemoveClearVideoQuadCandidatesIfMoving`:
-  // List of clear video content candidate bounds.
-  std::vector<gfx::Rect> previous_frame_overlay_candidate_rects_{};
+  // Used in `RemoveClearVideoQuadCandidatesIfMoving`
+  // List of clear video content candidate bounds. These rects are in root space
+  // and contains the candidate rects for all render passes.
+  // TODO(crbug.com/1454329): Compute these values using
+  // |previous_frame_render_pass_states_| and remove this field.
+  std::vector<gfx::Rect> previous_frame_overlay_candidate_rects_;
   int frames_since_last_overlay_candidate_rects_change_ = 0;
   bool no_undamaged_overlay_promotion_;
 
diff --git a/components/viz/service/display/overlay_dc_unittest.cc b/components/viz/service/display/overlay_dc_unittest.cc
index 82a9867..6464c175 100644
--- a/components/viz/service/display/overlay_dc_unittest.cc
+++ b/components/viz/service/display/overlay_dc_unittest.cc
@@ -81,8 +81,8 @@
   DebugRendererSettings debug_settings_;
 };
 
-std::unique_ptr<AggregatedRenderPass> CreateRenderPass() {
-  AggregatedRenderPassId render_pass_id{1};
+std::unique_ptr<AggregatedRenderPass> CreateRenderPass(
+    AggregatedRenderPassId render_pass_id = AggregatedRenderPassId{1}) {
   gfx::Rect output_rect(0, 0, 256, 256);
 
   auto pass = std::make_unique<AggregatedRenderPass>();
@@ -1374,11 +1374,17 @@
   const gfx::Rect kVideoRect = gfx::Rect(0, 0, 100, 100);
   const gfx::Rect kOpaqueRect = gfx::Rect(90, 80, 15, 30);
   const gfx::Transform kRenderPassToRootTransform =
-      gfx::Transform::MakeTranslation(27, 45);
+      gfx::Transform::MakeTranslation(20, 45);
+  // Surface damages in root space.
   const SurfaceDamageRectList kSurfaceDamageRectList = {
-      gfx::Rect(25, 20, 10, 10),    // above overlay
-      gfx::Rect(27, 45, 100, 100),  // damage rect of video overlay
-      gfx::Rect(30, 25, 50, 50)};   // below overlay
+      // On top and does not intersect overlay. Translates to (110,5 20x10) in
+      // render pass space.
+      gfx::Rect(130, 50, 20, 10),
+      // The video overlay damage rect. (0,0 100x100) in render pass space.
+      gfx::Rect(20, 45, 100, 100),
+      // Under and intersects the overlay. Translates to (95,25 20x10) in
+      // render pass space.
+      gfx::Rect(115, 70, 20, 10)};
   const size_t kOverlayDamageIndex = 1;
 
   for (size_t frame = 0; frame < 3; frame++) {
@@ -1413,7 +1419,8 @@
         render_pass_filters, render_pass_backdrop_filters,
         std::move(surface_damage_rect_list), GetOutputSurfacePlane(),
         &dc_layer_list, &damage_rect_, &content_bounds_);
-    LOG(INFO) << damage_rect_.ToString();
+    LOG(INFO) << "frame " << frame
+              << " damage rect: " << damage_rect_.ToString();
 
     EXPECT_EQ(dc_layer_list.size(), 1u);
     EXPECT_TRUE(
@@ -1431,13 +1438,203 @@
       // overlays are being processed for the first time.
       EXPECT_EQ(gfx::Rect(0, 0, 256, 256), damage_rect_);
     } else {
-      // With the render pass to root transform, the video overlay should have
-      // been translated to (27,45 100x100). The final damage rect should
-      // include (25,20 10x10), which doesn't intersect the overlay. The
-      // (30,25 50x50) surface damage is partially under the overlay, so the
-      // overlay damage can be subtracted to become (30,25 50x15). The final
-      // damage rect is (25,20 10x10) union (30,25 50x15).
-      EXPECT_EQ(damage_rect_, gfx::Rect(25, 20, 55, 25));
+      // To calculate the damage rect in root space, we first subtract the video
+      // damage from (115,70 20x10) since this damage is under the video. This
+      // results in (120,70 20x10). This then gets unioned with (130,50 20x10),
+      // which doesn't intersect the video. This results in (120,50 30x30).
+      // The damage rect returned from the DCLayerOverlayProcessor is in
+      // render pass space, so we apply the (20, 45) inverse transform,
+      // resulting in (100,5 30x30).
+      EXPECT_EQ(damage_rect_, gfx::Rect(100, 5, 30, 30));
+    }
+  }
+}
+
+// Tests processing overlays/underlays on multiple render passes per frame,
+// where only one render pass has an overlay.
+TEST_P(DCLayerOverlayTest, MultipleRenderPassesOneOverlay) {
+  InitializeOverlayProcessor(/*allowed_yuv_overlay_count*/ 1);
+  const gfx::Rect output_rect = {0, 0, 256, 256};
+  const size_t num_render_passes = 3;
+  for (size_t frame = 0; frame < 3; frame++) {
+    AggregatedRenderPassList render_passes;  // Used to keep render passes alive
+    DCLayerOverlayProcessor::RenderPassOverlayDataMap
+        render_pass_overlay_data_map;
+
+    OverlayProcessorInterface::FilterOperationsMap render_pass_filters;
+    OverlayProcessorInterface::FilterOperationsMap render_pass_backdrop_filters;
+    SurfaceDamageRectList surface_damage_rect_list;
+
+    // Create 3 render passes, with only one containing an overlay candidate.
+    for (size_t id = 1; id <= num_render_passes; id++) {
+      auto pass = CreateRenderPass(AggregatedRenderPassId{id});
+      pass->transform_to_root_target = gfx::Transform::MakeTranslation(id, 0);
+
+      gfx::Rect quad_rect_in_root_space =
+          gfx::Rect(0, 0, id * 16, pass->output_rect.height());
+
+      if (id == 1) {
+        // Create an overlay quad in the first render pass.
+        auto* video_quad = CreateFullscreenCandidateYUVVideoQuad(
+            resource_provider_.get(), child_resource_provider_.get(),
+            child_provider_.get(), pass->shared_quad_state_list.back(),
+            pass.get());
+        gfx::Rect quad_rect_in_quad_space =
+            pass->transform_to_root_target
+                .InverseMapRect(quad_rect_in_root_space)
+                .value();
+        video_quad->rect = quad_rect_in_quad_space;
+        video_quad->visible_rect = quad_rect_in_quad_space;
+        pass->shared_quad_state_list.back()->overlay_damage_index = id - 1;
+      } else {
+        // Create a quad that's not an overlay.
+        CreateSolidColorQuadAt(pass->shared_quad_state_list.back(),
+                               SkColors::kBlue, pass.get(),
+                               pass->transform_to_root_target
+                                   .InverseMapRect(quad_rect_in_root_space)
+                                   .value());
+      }
+
+      surface_damage_rect_list.emplace_back(quad_rect_in_root_space);
+      render_pass_overlay_data_map[pass.get()].damage_rect = output_rect;
+      render_passes.emplace_back(std::move(pass));
+    }
+
+    surface_damage_rect_list.emplace_back(0, 0, 256, 256);
+
+    overlay_processor_->ProcessOnDCLayerOverlayProcessorForTesting(
+        resource_provider_.get(), render_pass_filters,
+        render_pass_backdrop_filters, std::move(surface_damage_rect_list),
+        false /*is_video_capture_enabled*/,
+        false
+        /*is_page_fullscreen_mode*/,
+        render_pass_overlay_data_map);
+
+    for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+      LOG(INFO) << "frame " << frame << " render pass " << render_pass->id
+                << " damage rect : " << overlay_data.damage_rect.ToString();
+      LOG(INFO) << "frame " << frame << " render pass " << render_pass->id
+                << " number of overlays: "
+                << overlay_data.promoted_overlays.size();
+
+      if (render_pass->id == AggregatedRenderPassId(1)) {
+        // The render pass that contains an overlay.
+        EXPECT_EQ(overlay_data.promoted_overlays.size(), 1u);
+        EXPECT_EQ(absl::get<gfx::Transform>(
+                      overlay_data.promoted_overlays[0].transform),
+                  gfx::Transform::MakeTranslation(1, 0));
+        EXPECT_GT(overlay_data.promoted_overlays[0].plane_z_order, 0);
+
+        // The rect of the candidate should be in render pass space, which is an
+        // arbitrary space. Combining with the render pass to root transform
+        // results in a rect in root space. The transform is defined above as an
+        // x translation of -1.
+        EXPECT_EQ(overlay_data.promoted_overlays[0].display_rect,
+                  gfx::RectF(-1, 0, 16, 256));
+
+        if (frame == 0) {
+          // On the first frame, the damage rect should be unchanged since the
+          // overlays are being processed for the first time.
+          EXPECT_EQ(overlay_data.damage_rect, output_rect);
+        } else {
+          // On subsequent frames, the video rect should be subtracted from
+          // the damage rect. The x coordinate is 15 instead of 16 because of
+          // the root_to_transform_target. The surface_damage_rect_list damages
+          // are in root space, while the damage_rect output is in render pass
+          // space.
+          EXPECT_EQ(overlay_data.damage_rect, gfx::Rect(15, 0, 240, 256));
+        }
+      } else {
+        // All other render passes do not have overlays.
+        EXPECT_TRUE(overlay_data.promoted_overlays.empty());
+
+        // With no overlays, the damage should be unchanged since there are no
+        // overlays to subtract.
+        EXPECT_EQ(overlay_data.damage_rect, output_rect);
+      }
+    }
+  }
+}
+
+// Tests processing overlays/underlays on multiple render passes per frame, with
+// each render pass having an overlay. This exceeds the maximum allowed number
+// of overlays, so all overlays should be rejected.
+TEST_P(DCLayerOverlayTest, MultipleRenderPassesExceedsOverlayAllowance) {
+  const gfx::Rect output_rect = {0, 0, 256, 256};
+  const size_t num_render_passes = 3;
+  InitializeOverlayProcessor(num_render_passes - 1);
+  for (size_t frame = 1; frame <= 3; frame++) {
+    AggregatedRenderPassList
+        render_passes;  // Used to keep render passes alive.
+    DCLayerOverlayProcessor::RenderPassOverlayDataMap
+        render_pass_overlay_data_map;
+
+    OverlayProcessorInterface::FilterOperationsMap render_pass_filters;
+    OverlayProcessorInterface::FilterOperationsMap render_pass_backdrop_filters;
+    SurfaceDamageRectList surface_damage_rect_list;
+
+    // Create 3 render passes that all have a video overlay candidate. Start
+    // at the frame number so that we switch up the render pass IDs to verify
+    // that render passes that do not exist are not kept in
+    // |DCLayerOverlayProcessor::previous_frame_render_pass_states_|.
+    for (size_t id = frame; id < num_render_passes + frame; id++) {
+      auto pass = CreateRenderPass(AggregatedRenderPassId{id});
+      pass->transform_to_root_target = gfx::Transform::MakeTranslation(id, 0);
+      pass->shared_quad_state_list.back()->overlay_damage_index = id - 1;
+
+      gfx::Rect video_rect_in_root_space =
+          gfx::Rect(0, 0, id * 16, pass->output_rect.height());
+
+      gfx::Rect video_rect_in_render_pass_space =
+          pass->transform_to_root_target
+              .InverseMapRect(video_rect_in_root_space)
+              .value();
+      auto* video_quad = CreateFullscreenCandidateYUVVideoQuad(
+          resource_provider_.get(), child_resource_provider_.get(),
+          child_provider_.get(), pass->shared_quad_state_list.back(),
+          pass.get());
+      video_quad->rect = video_rect_in_render_pass_space;
+      video_quad->visible_rect = video_rect_in_render_pass_space;
+
+      surface_damage_rect_list.emplace_back(video_rect_in_root_space);
+      render_pass_overlay_data_map[pass.get()].damage_rect = output_rect;
+      render_passes.emplace_back(std::move(pass));
+    }
+
+    surface_damage_rect_list.emplace_back(0, 0, 256, 256);
+
+    overlay_processor_->ProcessOnDCLayerOverlayProcessorForTesting(
+        resource_provider_.get(), render_pass_filters,
+        render_pass_backdrop_filters, std::move(surface_damage_rect_list),
+        false /*is_video_capture_enabled*/,
+        false
+        /*is_page_fullscreen_mode*/,
+        render_pass_overlay_data_map);
+
+    // Verify that the previous frame states contain only 3 render passes and
+    // that they have the IDs that we set them to.
+    EXPECT_EQ(3U, overlay_processor_->get_previous_frame_render_pass_count());
+    std::vector<AggregatedRenderPassId> previous_frame_render_pass_ids =
+        overlay_processor_->get_previous_frame_render_pass_ids();
+    std::sort(previous_frame_render_pass_ids.begin(),
+              previous_frame_render_pass_ids.end());
+    for (size_t id = frame; id < num_render_passes + frame; id++) {
+      EXPECT_EQ(id, previous_frame_render_pass_ids[id - frame].value());
+    }
+
+    for (auto& [render_pass, overlay_data] : render_pass_overlay_data_map) {
+      LOG(INFO) << "frame " << frame << " render pass " << render_pass->id
+                << " damage rect : " << overlay_data.damage_rect.ToString();
+      LOG(INFO) << "frame " << frame << " render pass " << render_pass->id
+                << " number of overlays: "
+                << overlay_data.promoted_overlays.size();
+
+      // Since there is more than one overlay, all overlays should be rejected.
+      EXPECT_EQ(overlay_data.promoted_overlays.size(), 0u);
+
+      // With no overlays, the damage should be unchanged since there are no
+      // overlays to subtract.
+      EXPECT_EQ(overlay_data.damage_rect, output_rect);
     }
   }
 }
diff --git a/components/viz/service/display/overlay_processor_win.cc b/components/viz/service/display/overlay_processor_win.cc
index cc0c252b..c187544 100644
--- a/components/viz/service/display/overlay_processor_win.cc
+++ b/components/viz/service/display/overlay_processor_win.cc
@@ -65,10 +65,10 @@
     const OverlayProcessorInterface::FilterOperationsMap& render_pass_filters,
     const OverlayProcessorInterface::FilterOperationsMap&
         render_pass_backdrop_filters,
-    SurfaceDamageRectList surface_damage_rect_list,
+    SurfaceDamageRectList surface_damage_rect_list_in_root_space,
     OutputSurfaceOverlayPlane* output_surface_plane,
     CandidateList* candidates,
-    gfx::Rect* damage_rect,
+    gfx::Rect* root_damage_rect,
     std::vector<gfx::Rect>* content_bounds) {
   TRACE_EVENT0("viz", "OverlayProcessorWin::ProcessForOverlays");
 
@@ -78,11 +78,21 @@
     root_render_pass = (*render_passes)[render_passes->size() - 2].get();
   }
 
+  DCLayerOverlayProcessor::RenderPassOverlayDataMap
+      render_pass_overlay_data_map;
+  auto emplace_pair = render_pass_overlay_data_map.emplace(
+      root_render_pass, DCLayerOverlayProcessor::RenderPassOverlayData());
+  DCHECK(emplace_pair.second);  // Verify insertion occurred.
+  DCHECK_EQ(emplace_pair.first->first, root_render_pass);
+  DCLayerOverlayProcessor::RenderPassOverlayData&
+      root_render_pass_overlay_data = emplace_pair.first->second;
+  root_render_pass_overlay_data.damage_rect = *root_damage_rect;
   dc_layer_overlay_processor_->Process(
-      resource_provider, gfx::RectF(root_render_pass->output_rect),
-      render_pass_filters, render_pass_backdrop_filters, root_render_pass,
-      damage_rect, std::move(surface_damage_rect_list), candidates,
-      is_video_capture_enabled_, is_page_fullscreen_mode_);
+      resource_provider, render_pass_filters, render_pass_backdrop_filters,
+      surface_damage_rect_list_in_root_space, is_video_capture_enabled_,
+      is_page_fullscreen_mode_, render_pass_overlay_data_map);
+  *root_damage_rect = root_render_pass_overlay_data.damage_rect;
+  *candidates = root_render_pass_overlay_data.promoted_overlays;
 
   // Force a swap chain when there is a copy request, since read back is
   // impossible with a DComp surface.
@@ -110,7 +120,7 @@
     // The entire surface has to be redrawn if switching from or to direct
     // composition layers, because the previous contents are discarded and some
     // contents would otherwise be undefined.
-    *damage_rect = root_render_pass->output_rect;
+    *root_damage_rect = root_render_pass->output_rect;
   }
 
   if (base::FeatureList::IsEnabled(features::kDCompPresenter)) {
@@ -145,13 +155,13 @@
 
   if (debug_settings_->show_dc_layer_debug_borders) {
     InsertDebugBorderDrawQuadsForOverlayCandidates(
-        *candidates, root_render_pass, *damage_rect);
+        *candidates, root_render_pass, *root_damage_rect);
 
     // Mark the entire output as damaged because the border quads might not be
     // inside the current damage rect.  It's far simpler to mark the entire
     // output as damaged instead of accounting for individual border quads which
     // can change positions across frames.
-    *damage_rect = root_render_pass->output_rect;
+    *root_damage_rect = root_render_pass->output_rect;
   }
 }
 
@@ -210,4 +220,20 @@
   is_page_fullscreen_mode_ = enabled;
 }
 
+void OverlayProcessorWin::ProcessOnDCLayerOverlayProcessorForTesting(
+    DisplayResourceProvider* resource_provider,
+    const FilterOperationsMap& render_pass_filters,
+    const FilterOperationsMap& render_pass_backdrop_filters,
+    SurfaceDamageRectList surface_damage_rect_list,
+    bool is_video_capture_enabled,
+    bool is_page_fullscreen_mode,
+    DCLayerOverlayProcessor::RenderPassOverlayDataMap&
+        render_pass_overlay_data_map) {
+  CHECK_IS_TEST();
+  dc_layer_overlay_processor_->Process(
+      resource_provider, render_pass_filters, render_pass_backdrop_filters,
+      surface_damage_rect_list, is_video_capture_enabled,
+      is_page_fullscreen_mode, render_pass_overlay_data_map);
+}
+
 }  // namespace viz
diff --git a/components/viz/service/display/overlay_processor_win.h b/components/viz/service/display/overlay_processor_win.h
index 662088b..d1dd5e5 100644
--- a/components/viz/service/display/overlay_processor_win.h
+++ b/components/viz/service/display/overlay_processor_win.h
@@ -8,6 +8,7 @@
 #include <memory>
 #include <vector>
 
+#include "base/check_is_test.h"
 #include "base/memory/raw_ptr.h"
 #include "build/build_config.h"
 #include "components/viz/common/quads/aggregated_render_pass.h"
@@ -61,10 +62,10 @@
       const SkM44& output_color_matrix,
       const FilterOperationsMap& render_pass_filters,
       const FilterOperationsMap& render_pass_backdrop_filters,
-      SurfaceDamageRectList surface_damage_rect_list,
+      SurfaceDamageRectList surface_damage_rect_list_in_root_space,
       OutputSurfaceOverlayPlane* output_surface_plane,
       OverlayCandidateList* overlay_candidates,
-      gfx::Rect* damage_rect,
+      gfx::Rect* root_damage_rect,
       std::vector<gfx::Rect>* content_bounds) override;
 
   void set_using_dc_layers_for_testing(bool value) { using_dc_layers_ = value; }
@@ -72,6 +73,24 @@
     GetOverlayProcessor()
         ->set_frames_since_last_qualified_multi_overlays_for_testing(value);
   }
+  size_t get_previous_frame_render_pass_count() {
+    CHECK_IS_TEST();
+    return GetOverlayProcessor()->get_previous_frame_render_pass_count();
+  }
+  std::vector<AggregatedRenderPassId> get_previous_frame_render_pass_ids() {
+    CHECK_IS_TEST();
+    return GetOverlayProcessor()->get_previous_frame_render_pass_ids();
+  }
+
+  void ProcessOnDCLayerOverlayProcessorForTesting(
+      DisplayResourceProvider* resource_provider,
+      const FilterOperationsMap& render_pass_filters,
+      const FilterOperationsMap& render_pass_backdrop_filters,
+      SurfaceDamageRectList surface_damage_rect_list,
+      bool is_video_capture_enabled,
+      bool is_page_fullscreen_mode,
+      DCLayerOverlayProcessor::RenderPassOverlayDataMap&
+          render_pass_overlay_data_map);
 
  protected:
   // For testing.
diff --git a/components/viz/test/test_context_provider.cc b/components/viz/test/test_context_provider.cc
index ae6fa76a..2901aebf 100644
--- a/components/viz/test/test_context_provider.cc
+++ b/components/viz/test/test_context_provider.cc
@@ -339,9 +339,9 @@
   CHECK(it != mailbox_to_gmb_handle_info_map_.end());
 
   auto handle_info = it->second;
-  return SharedImageInterface::ScopedMapping::Create(
-      handle_info.handle.Clone(), handle_info.format, handle_info.size,
-      handle_info.buffer_usage);
+  // NOTE: We pass `handle_info` by copy here to ensure that the handle is
+  // cloned and hence can be reused by subsequent calls to MapSharedImage().
+  return SharedImageInterface::ScopedMapping::Create(handle_info);
 }
 
 bool TestSharedImageInterface::CheckSharedImageExists(
diff --git a/components/webauthn/android/BUILD.gn b/components/webauthn/android/BUILD.gn
index 6916b22b..830f3aa 100644
--- a/components/webauthn/android/BUILD.gn
+++ b/components/webauthn/android/BUILD.gn
@@ -20,6 +20,7 @@
     "java/src/org/chromium/components/webauthn/AuthenticatorFactory.java",
     "java/src/org/chromium/components/webauthn/AuthenticatorImpl.java",
     "java/src/org/chromium/components/webauthn/AuthenticatorIncognitoConfirmationBottomsheet.java",
+    "java/src/org/chromium/components/webauthn/Barrier.java",
     "java/src/org/chromium/components/webauthn/CredManHelper.java",
     "java/src/org/chromium/components/webauthn/CredManMetricsHelper.java",
     "java/src/org/chromium/components/webauthn/Fido2Api.java",
diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/Barrier.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/Barrier.java
new file mode 100644
index 0000000..ba992074
--- /dev/null
+++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/Barrier.java
@@ -0,0 +1,148 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.components.webauthn;
+
+import org.chromium.base.Callback;
+import org.chromium.blink.mojom.AuthenticatorStatus;
+
+/**
+ * Barrier class is responsible for waiting the completion of Fido2 and/or Android Credential
+ * Manager APIs. It supports either single mode (just one of the calls) or parallel mode (both
+ * calls).
+ *
+ * In parallel mode, Fido2 API call results are privileged. For example, if Fido2 API call fails
+ * with error code 1 and Android Credential Manager fails with error code 2, the resulting error
+ * code is 1.
+ */
+public class Barrier {
+    private enum Status {
+        NONE,
+        WAITING,
+        SUCCESS,
+        FAILURE,
+    }
+
+    public enum Mode {
+        // Barrier will complete once the Fido2Api calls are complete.
+        ONLY_FIDO_2_API,
+        // Barrier will complete once the CredMan calls are complete.
+        ONLY_CRED_MAN,
+        // Barrier will complete when both calls are complete.
+        BOTH,
+    }
+
+    private Callback<Integer> mErrorCallback;
+    private Runnable mFido2ApiRunnable;
+    private Runnable mCredManRunnable;
+    private Status mFido2ApiStatus;
+    private Status mCredManStatus;
+    private int mFido2ApiError;
+    private boolean mFido2ApiCancelled;
+    private boolean mCredManCancelled;
+
+    public Barrier(Callback<Integer> errorCallback) {
+        mErrorCallback = errorCallback;
+        mFido2ApiStatus = Status.NONE;
+        mCredManStatus = Status.NONE;
+    }
+
+    public void resetAndSetWaitStatus(Mode mode) {
+        reset();
+        switch (mode) {
+            case ONLY_FIDO_2_API:
+                mFido2ApiStatus = Status.WAITING;
+                break;
+            case ONLY_CRED_MAN:
+                mCredManStatus = Status.WAITING;
+                break;
+            case BOTH:
+                mFido2ApiStatus = Status.WAITING;
+                mCredManStatus = Status.WAITING;
+                break;
+            default:
+                assert false : "Unhandled Barrier mode: " + mode;
+        }
+    }
+
+    public void onCredManSuccessful(Runnable onBarrierComplete) {
+        if (mFido2ApiStatus == Status.FAILURE) {
+            onBarrierComplete.run();
+        } else if (mFido2ApiStatus == Status.SUCCESS) {
+            onBarrierComplete.run();
+            mFido2ApiRunnable.run();
+        } else if (mFido2ApiStatus == Status.WAITING) {
+            mCredManRunnable = onBarrierComplete;
+            mCredManStatus = Status.SUCCESS;
+        } else {
+            onBarrierComplete.run();
+        }
+    }
+
+    public void onCredManFailed(int error) {
+        if (mFido2ApiStatus == Status.FAILURE) {
+            mErrorCallback.onResult(mFido2ApiError);
+        } else if (mFido2ApiStatus == Status.SUCCESS) {
+            mFido2ApiRunnable.run();
+        } else if (mFido2ApiStatus == Status.WAITING) {
+            mCredManStatus = Status.FAILURE;
+        } else {
+            mErrorCallback.onResult(error);
+        }
+    }
+
+    public void onFido2ApiSuccessful(Runnable onBarrierComplete) {
+        if (mCredManStatus == Status.FAILURE) {
+            onBarrierComplete.run();
+        } else if (mCredManStatus == Status.SUCCESS) {
+            mCredManRunnable.run();
+            onBarrierComplete.run();
+        } else if (mCredManStatus == Status.WAITING) {
+            mFido2ApiRunnable = onBarrierComplete;
+            mFido2ApiStatus = Status.SUCCESS;
+        } else {
+            onBarrierComplete.run();
+        }
+    }
+
+    public void onFido2ApiFailed(int error) {
+        if (mCredManStatus == Status.FAILURE) {
+            mErrorCallback.onResult(error);
+        } else if (mCredManStatus == Status.SUCCESS) {
+            mCredManRunnable.run();
+        } else if (mCredManStatus == Status.WAITING) {
+            mFido2ApiError = error;
+            mFido2ApiStatus = Status.FAILURE;
+        } else {
+            mErrorCallback.onResult(error);
+        }
+    }
+
+    public void onCredManCancelled() {
+        if (mFido2ApiStatus == Status.NONE || mFido2ApiCancelled) {
+            mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+            mFido2ApiCancelled = false;
+            return;
+        }
+        mCredManCancelled = true;
+    }
+
+    public void onFido2ApiCancelled() {
+        if (mCredManStatus == Status.NONE || mCredManCancelled) {
+            mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+            mCredManCancelled = false;
+            return;
+        }
+        mFido2ApiCancelled = true;
+    }
+
+    private void reset() {
+        mFido2ApiRunnable = null;
+        mCredManRunnable = null;
+        mFido2ApiStatus = Status.NONE;
+        mCredManStatus = Status.NONE;
+        mCredManCancelled = false;
+        mFido2ApiCancelled = false;
+    }
+}
diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/CredManHelper.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/CredManHelper.java
index e3fd1a0..217f4d77 100644
--- a/components/webauthn/android/java/src/org/chromium/components/webauthn/CredManHelper.java
+++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/CredManHelper.java
@@ -60,10 +60,12 @@
             "com.android.chrome.PASSWORDS_ONLY_FOR_THE_CHANNEL";
     private static final String PASSWORDS_WITH_NO_USERNAME_INCLUDED =
             "com.android.chrome.PASSWORDS_WITH_NO_USERNAME_INCLUDED";
+    private static final String IGNORE_GPM = "com.android.chrome.GPM_IGNORE";
     private static final String TAG = "CredManHelper";
     private static final String TYPE_PASSKEY = CRED_MAN_PREFIX + "TYPE_PUBLIC_KEY_CREDENTIAL";
 
     private Callback<Integer> mErrorCallback;
+    private Barrier mBarrier;
     private boolean mIsCrossOrigin;
     private boolean mPlayServicesAvailable;
     private boolean mRequestPasswords;
@@ -231,18 +233,18 @@
 
     /**
      * Queries credential availability using the Android 14 CredMan API.
-     * Returns `AuthenticatorStatus.SUCCESS` if the request is successfully sent to the platform.
      */
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public int startPrefetchRequest(Context context, RenderFrameHost frameHost,
+    public void startPrefetchRequest(Context context, RenderFrameHost frameHost,
             PublicKeyCredentialRequestOptions options, String originString, boolean isCrossOrigin,
             byte[] maybeClientDataHash, GetAssertionResponseCallback getCallback,
-            Callback<Integer> errorCallback) {
+            Callback<Integer> errorCallback, Barrier barrier, boolean ignoreGpm) {
         long startTimeMs = SystemClock.elapsedRealtime();
         mContext = context;
         mFrameHost = frameHost;
         mErrorCallback = errorCallback;
         mIsCrossOrigin = isCrossOrigin;
+        mBarrier = barrier;
 
         // The Android 14 APIs have to be called via reflection until Chromium
         // builds with the Android 14 SDK by default.
@@ -257,7 +259,7 @@
                 Log.e(TAG, "CredMan prepareGetCredential call failed: %s",
                         getCredManExceptionType(e) + " (" + e.getMessage() + ")");
                 mConditionalUiState = ConditionalUiState.NONE;
-                mErrorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
+                mBarrier.onCredManFailed(AuthenticatorStatus.UNKNOWN_ERROR);
                 mMetricsHelper.recordCredmanPrepareRequestHistogram(
                         CredManPrepareRequestEnum.FAILURE);
             }
@@ -282,19 +284,22 @@
                 } catch (ReflectiveOperationException e) {
                     Log.e(TAG, "Reflection failed; are you running on Android 14?", e);
                     mConditionalUiState = ConditionalUiState.NONE;
-                    mErrorCallback.onResult(AuthenticatorStatus.UNKNOWN_ERROR);
+                    mBarrier.onCredManFailed(AuthenticatorStatus.UNKNOWN_ERROR);
                     mMetricsHelper.recordCredmanPrepareRequestHistogram(
                             CredManPrepareRequestEnum.FAILURE);
                     return;
                 }
 
                 mConditionalUiState = ConditionalUiState.WAITING_FOR_SELECTION;
-                mBridgeProvider.getBridge().onCredManConditionalRequestPending(
-                        mFrameHost, hasPublicKeyCredentials, (requestPasswords) -> {
-                            setRequestPasswords(requestPasswords);
-                            startGetRequest(mContext, mFrameHost, options, originString,
-                                    isCrossOrigin, maybeClientDataHash, getCallback, errorCallback);
-                        });
+                mBarrier.onCredManSuccessful(() -> {
+                    mBridgeProvider.getBridge().onCredManConditionalRequestPending(
+                            mFrameHost, hasPublicKeyCredentials, (requestPasswords) -> {
+                                setRequestPasswords(requestPasswords);
+                                startGetRequest(mContext, mFrameHost, options, originString,
+                                        isCrossOrigin, maybeClientDataHash, getCallback,
+                                        errorCallback, ignoreGpm);
+                            });
+                });
                 mMetricsHelper.recordCredmanPrepareRequestHistogram(hasPublicKeyCredentials
                                 ? CredManPrepareRequestEnum.SUCCESS_HAS_RESULTS
                                 : CredManPrepareRequestEnum.SUCCESS_NO_RESULTS);
@@ -307,12 +312,14 @@
             mConditionalUiState = ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST;
             final Object getCredentialRequest =
                     buildGetCredentialRequest(options, originString, maybeClientDataHash,
-                            /*requestPasswords=*/false, /*preferImmediatelyAvailable=*/false);
+                            /*requestPasswords=*/false, /*preferImmediatelyAvailable=*/false,
+                            /*ignoreGpm=*/ignoreGpm);
             if (getCredentialRequest == null) {
                 mConditionalUiState = ConditionalUiState.NONE;
                 mMetricsHelper.recordCredmanPrepareRequestHistogram(
                         CredManPrepareRequestEnum.COULD_NOT_SEND_REQUEST);
-                return AuthenticatorStatus.NOT_ALLOWED_ERROR;
+                mBarrier.onCredManFailed(AuthenticatorStatus.NOT_ALLOWED_ERROR);
+                return;
             }
 
             final Object manager = credentialManagerService(mContext);
@@ -329,9 +336,8 @@
             mConditionalUiState = ConditionalUiState.NONE;
             mMetricsHelper.recordCredmanPrepareRequestHistogram(
                     CredManPrepareRequestEnum.COULD_NOT_SEND_REQUEST);
-            return AuthenticatorStatus.UNKNOWN_ERROR;
+            mBarrier.onCredManFailed(AuthenticatorStatus.UNKNOWN_ERROR);
         }
-        return AuthenticatorStatus.SUCCESS;
     }
 
     public void setNoCredentialsFallback(Runnable noCredentialsFallback) {
@@ -345,7 +351,7 @@
     public int startGetRequest(Context context, RenderFrameHost frameHost,
             PublicKeyCredentialRequestOptions options, String originString, boolean isCrossOrigin,
             byte[] maybeClientDataHash, GetAssertionResponseCallback getCallback,
-            Callback<Integer> errorCallback) {
+            Callback<Integer> errorCallback, boolean ignoreGpm) {
         mContext = context;
         mFrameHost = frameHost;
         mErrorCallback = errorCallback;
@@ -363,7 +369,7 @@
                 if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                     mConditionalUiState = ConditionalUiState.NONE;
                     mBridgeProvider.getBridge().cleanupCredManRequest(mFrameHost);
-                    mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+                    mBarrier.onCredManCancelled();
                     return;
                 }
                 if (errorType.equals(CRED_MAN_EXCEPTION_GET_CREDENTIAL_TYPE_USER_CANCEL)) {
@@ -403,7 +409,7 @@
                     notifyBrowserOnCredManClosed(false);
                     mConditionalUiState = ConditionalUiState.NONE;
                     mBridgeProvider.getBridge().cleanupCredManRequest(mFrameHost);
-                    mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+                    mBarrier.onCredManCancelled();
                     return;
                 }
                 Bundle data;
@@ -492,7 +498,7 @@
         try {
             final Object getCredentialRequest = buildGetCredentialRequest(options, originString,
                     maybeClientDataHash, mRequestPasswords,
-                    /*preferImmediatelyAvailable=*/!options.isConditional);
+                    /*preferImmediatelyAvailable=*/!options.isConditional, ignoreGpm);
             if (getCredentialRequest == null) {
                 mMetricsHelper.reportGetCredentialMetrics(
                         CredManGetRequestEnum.COULD_NOT_SEND_REQUEST, mConditionalUiState);
@@ -525,12 +531,12 @@
         switch (mConditionalUiState) {
             case WAITING_FOR_CREDENTIAL_LIST:
                 mConditionalUiState = ConditionalUiState.CANCEL_PENDING;
-                mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+                mBarrier.onCredManCancelled();
                 break;
             case WAITING_FOR_SELECTION:
                 mBridgeProvider.getBridge().cleanupCredManRequest(frameHost);
                 mConditionalUiState = ConditionalUiState.NONE;
-                mErrorCallback.onResult(AuthenticatorStatus.ABORT_ERROR);
+                mBarrier.onCredManCancelled();
                 break;
             case REQUEST_SENT_TO_PLATFORM:
                 // If the platform successfully completes the getAssertion then cancelation is
@@ -629,10 +635,12 @@
      * @param requestPasswords True if password credentials should also be requested.
      * @param preferImmediatelyAvailable True to make the eventual request fail with a
      *         `NO_CREDENTIAL` error if there are no credentials found.
+     * @param ignoreGpm True if Google Password Manager should ignore CredMan requests.
      */
     private Object buildGetCredentialRequest(PublicKeyCredentialRequestOptions options,
             String originString, byte[] maybeClientDataHash, boolean requestPasswords,
-            boolean preferImmediatelyAvailable) throws ReflectiveOperationException {
+            boolean preferImmediatelyAvailable, boolean ignoreGpm)
+            throws ReflectiveOperationException {
         final String requestAsJson =
                 Fido2CredentialRequestJni.get().getOptionsToJson(options.serialize());
         final byte[] clientDataHash = maybeClientDataHash != null
@@ -646,7 +654,7 @@
         }
 
         Bundle publicKeyCredentialOptionBundle =
-                buildPublicKeyCredentialOptionBundle(requestAsJson, clientDataHash);
+                buildPublicKeyCredentialOptionBundle(requestAsJson, clientDataHash, ignoreGpm);
 
         // Build the CredentialOption for passkeys:
         Object credentialOption;
@@ -662,9 +670,11 @@
         // Build the GetCredentialRequest:
         final Class<?> getCredentialRequestBuilderClass = credManGetRequestBuilderClass();
         Bundle getCredentialRequestBundle = new Bundle();
-        getCredentialRequestBundle.putParcelable(
-                CRED_MAN_PREFIX + "BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME",
-                GPM_COMPONENT_NAME);
+        if (!ignoreGpm) {
+            getCredentialRequestBundle.putParcelable(
+                    CRED_MAN_PREFIX + "BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME",
+                    GPM_COMPONENT_NAME);
+        }
         // The CredMan UI for the case where there aren't any credentials isn't
         // suitable for the modal case. This bundle key requests that the
         // request fail immediately if there aren't any credentials. It'll fail
@@ -680,7 +690,7 @@
                 .getMethod("addCredentialOption", credentialOption.getClass())
                 .invoke(getCredentialRequestBuilderObject, credentialOption);
         if (requestPasswords) {
-            Object passwordCredentialOption = buildPasswordOption();
+            Object passwordCredentialOption = buildPasswordOption(ignoreGpm);
             if (passwordCredentialOption != null) {
                 getCredentialRequestBuilderClass
                         .getMethod("addCredentialOption", passwordCredentialOption.getClass())
@@ -694,7 +704,7 @@
     }
 
     private Bundle buildPublicKeyCredentialOptionBundle(
-            String requestAsJson, byte[] clientDataHash) {
+            String requestAsJson, byte[] clientDataHash, boolean ignoreGpm) {
         final Bundle publicKeyCredentialOptionBundle = new Bundle();
         publicKeyCredentialOptionBundle.putString(CRED_MAN_PREFIX + "BUNDLE_KEY_SUBTYPE",
                 CRED_MAN_PREFIX + "BUNDLE_VALUE_SUBTYPE_GET_PUBLIC_KEY_CREDENTIAL_OPTION");
@@ -703,15 +713,17 @@
         publicKeyCredentialOptionBundle.putByteArray(
                 CRED_MAN_PREFIX + "BUNDLE_KEY_CLIENT_DATA_HASH", clientDataHash);
         publicKeyCredentialOptionBundle.putString(CHANNEL_KEY, getChannel());
+        publicKeyCredentialOptionBundle.putBoolean(IGNORE_GPM, ignoreGpm);
         return publicKeyCredentialOptionBundle;
     }
 
-    private Object buildPasswordOption() throws ReflectiveOperationException {
+    private Object buildPasswordOption(boolean ignoreGpm) throws ReflectiveOperationException {
         Object passwordCredentialOption;
         Bundle passwordOptionBundle = new Bundle();
         passwordOptionBundle.putString(CHANNEL_KEY, getChannel());
         passwordOptionBundle.putBoolean(PASSWORDS_ONLY_FOR_THE_CHANNEL, true);
         passwordOptionBundle.putBoolean(PASSWORDS_WITH_NO_USERNAME_INCLUDED, true);
+        passwordOptionBundle.putBoolean(IGNORE_GPM, ignoreGpm);
 
         final Class<?> credentialOptionBuilderClass = credManCredentialOptionBuilderClass();
         final Object credentialOptionBuilder =
diff --git a/components/webauthn/android/java/src/org/chromium/components/webauthn/Fido2CredentialRequest.java b/components/webauthn/android/java/src/org/chromium/components/webauthn/Fido2CredentialRequest.java
index da80a22..456eff2 100644
--- a/components/webauthn/android/java/src/org/chromium/components/webauthn/Fido2CredentialRequest.java
+++ b/components/webauthn/android/java/src/org/chromium/components/webauthn/Fido2CredentialRequest.java
@@ -15,10 +15,8 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
-import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.os.BuildCompat;
 
 import com.google.android.gms.tasks.Task;
 
@@ -105,6 +103,7 @@
     private MakeCredentialResponseCallback mMakeCredentialCallback;
     private FidoErrorResponseCallback mErrorCallback;
     private CredManHelper mCredManHelper;
+    private Barrier mBarrier;
     // mFrameHost is null in makeCredential requests. For getAssertion requests
     // it's non-null for conditional requests and may be non-null in other
     // requests.
@@ -146,6 +145,7 @@
         mIntentSender = intentSender;
         mPlayServicesAvailable = Fido2ApiCallHelper.getInstance().arePlayServicesAvailable();
         mCredManHelper = new CredManHelper(this, mPlayServicesAvailable);
+        mBarrier = new Barrier(this::returnErrorAndResetCallback);
     }
 
     private void returnErrorAndResetCallback(int error) {
@@ -157,11 +157,10 @@
         mMakeCredentialCallback = null;
     }
 
-    @OptIn(markerClass = androidx.core.os.BuildCompat.PrereleaseSdkCheck.class)
     private boolean isCredManEnabled() {
         if (sCredManSupport == CredManSupport.NOT_EVALUATED) {
             if (!mOverrideVersionCheckForTesting) {
-                if (!BuildCompat.isAtLeastU()) {
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
                     sCredManSupport = CredManSupport.DISABLED;
                 } else {
                     int packageVersion = PackageUtils.getPackageVersion("com.google.android.gms");
@@ -192,6 +191,15 @@
                         DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN_FOR_HYBRID);
     }
 
+    private Barrier.Mode getBarrierMode() {
+        if (!isCredManEnabled()) return Barrier.Mode.ONLY_FIDO_2_API;
+        if (mIsHybridRequest) return Barrier.Mode.ONLY_CRED_MAN;
+        return DeviceFeatureMap.getInstance().getFieldTrialParamByFeatureAsBoolean(
+                       DeviceFeatureList.WEBAUTHN_ANDROID_CRED_MAN, "gpm_in_cred_man", false)
+                ? Barrier.Mode.ONLY_CRED_MAN
+                : Barrier.Mode.BOTH;
+    }
+
     /**
      * Process a WebAuthn create() request.
      *
@@ -340,12 +348,14 @@
         // Payments should still go through Google Play Services. Also, if the request has
         // pre-hashed PRF inputs then we cannot represent that in JSON and so can only forward to
         // Play Services.
-        if (payment == null && !options.extensions.prfInputsHashed && isCredManEnabled()) {
+        if (payment == null && !options.extensions.prfInputsHashed
+                && getBarrierMode() == Barrier.Mode.ONLY_CRED_MAN) {
             if (options.isConditional) {
-                int response = mCredManHelper.startPrefetchRequest(mContext, mFrameHost, options,
+                mBarrier.resetAndSetWaitStatus(Barrier.Mode.ONLY_CRED_MAN);
+                mCredManHelper.startPrefetchRequest(mContext, mFrameHost, options,
                         convertOriginToString(origin), mIsCrossOrigin,
-                        /*maybeClientDataHash=*/null, callback, this::returnErrorAndResetCallback);
-                if (response != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(response);
+                        /*maybeClientDataHash=*/null, callback, this::returnErrorAndResetCallback,
+                        mBarrier, /*ignoreGpm=*/false);
             } else if (hasAllowCredentials && mPlayServicesAvailable) {
                 // If the allowlist contains non-discoverable credentials then
                 // the request needs to be routed directly to Play Services.
@@ -358,7 +368,7 @@
                                         /*credentialId=*/null));
                 int response = mCredManHelper.startGetRequest(mContext, mFrameHost, options,
                         convertOriginToString(origin), mIsCrossOrigin, maybeClientDataHash,
-                        callback, this::returnErrorAndResetCallback);
+                        callback, this::returnErrorAndResetCallback, /*ignoreGpm=*/false);
                 if (response != AuthenticatorStatus.SUCCESS) returnErrorAndResetCallback(response);
             }
             return;
@@ -389,12 +399,24 @@
             // Enumerate credentials from Play Services so that we can show the
             // picker in Chrome UI.
             final byte[] finalClientDataHash = clientDataHash;
+
+            if (getBarrierMode() == Barrier.Mode.BOTH) {
+                mBarrier.resetAndSetWaitStatus(Barrier.Mode.BOTH);
+                mCredManHelper.startPrefetchRequest(context, frameHost, options, callerOriginString,
+                        hasAllowCredentials, maybeClientDataHash, callback,
+                        this::returnErrorAndResetCallback, mBarrier, /*ignoreGpm=*/true);
+            } else {
+                mBarrier.resetAndSetWaitStatus(Barrier.Mode.ONLY_FIDO_2_API);
+            }
             mConditionalUiState = ConditionalUiState.WAITING_FOR_CREDENTIAL_LIST;
             Fido2ApiCallHelper.getInstance().invokeFido2GetCredentials(options.relyingPartyId,
                     (credentials)
-                            -> onWebAuthnCredentialDetailsListReceived(
-                                    options, callerOriginString, finalClientDataHash, credentials),
-                    this::onBinderCallException);
+                            -> mBarrier.onFido2ApiSuccessful(
+                                    ()
+                                            -> onWebAuthnCredentialDetailsListReceived(options,
+                                                    callerOriginString, finalClientDataHash,
+                                                    credentials)),
+                    (e) -> mBarrier.onFido2ApiFailed(AuthenticatorStatus.NOT_ALLOWED_ERROR));
             return;
         }
 
@@ -406,12 +428,12 @@
         switch (mConditionalUiState) {
             case WAITING_FOR_CREDENTIAL_LIST:
                 mConditionalUiState = ConditionalUiState.CANCEL_PENDING;
-                returnErrorAndResetCallback(AuthenticatorStatus.ABORT_ERROR);
+                mBarrier.onFido2ApiCancelled();
                 break;
             case WAITING_FOR_SELECTION:
                 mBrowserBridge.cleanupRequest(frameHost);
                 mConditionalUiState = ConditionalUiState.NONE;
-                returnErrorAndResetCallback(AuthenticatorStatus.ABORT_ERROR);
+                mBarrier.onFido2ApiCancelled();
                 break;
             case REQUEST_SENT_TO_PLATFORM:
                 // If the platform successfully completes the getAssertion then cancelation is
@@ -426,7 +448,7 @@
 
     public void handleIsUserVerifyingPlatformAuthenticatorAvailableRequest(
             Context context, IsUvpaaResponseCallback callback) {
-        if (isCredManEnabled()) {
+        if (getBarrierMode() == Barrier.Mode.ONLY_CRED_MAN) {
             callback.onIsUserVerifyingPlatformAuthenticatorAvailableResponse(true);
             return;
         }
@@ -510,6 +532,10 @@
         mCredManHelper = helper;
     }
 
+    public void setBarrierForTesting(Barrier barrier) {
+        mBarrier = barrier;
+    }
+
     private void onWebAuthnCredentialDetailsListReceived(PublicKeyCredentialRequestOptions options,
             String callerOriginString, byte[] clientDataHash,
             List<WebAuthnCredentialDetails> credentials) {
@@ -603,7 +629,7 @@
                     mCredManHelper.startGetRequest(mContext, mFrameHost, options,
                             convertOriginToString(callerOrigin), mIsCrossOrigin,
                             maybeClientDataHash, mGetAssertionCallback,
-                            this::returnErrorAndResetCallback);
+                            this::returnErrorAndResetCallback, /*ignoreGpm=*/false);
                 });
     }
 
@@ -639,7 +665,7 @@
         // so route to CredMan.
         mCredManHelper.startGetRequest(mContext, mFrameHost, options,
                 convertOriginToString(callerOrigin), mIsCrossOrigin, maybeClientDataHash,
-                mGetAssertionCallback, this::returnErrorAndResetCallback);
+                mGetAssertionCallback, this::returnErrorAndResetCallback, /*ignoreGpm=*/false);
     }
 
     private void maybeDispatchGetAssertionRequest(PublicKeyCredentialRequestOptions options,
@@ -774,7 +800,7 @@
                 if (mConditionalUiState == ConditionalUiState.CANCEL_PENDING) {
                     mConditionalUiState = ConditionalUiState.NONE;
                     mBrowserBridge.cleanupRequest(mFrameHost);
-                    returnErrorAndResetCallback(AuthenticatorStatus.ABORT_ERROR);
+                    mBarrier.onFido2ApiCancelled();
                 } else {
                     // The user can try again by selecting another conditional UI credential.
                     mConditionalUiState = ConditionalUiState.WAITING_FOR_SELECTION;
diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn
index 3084e11..72e86e01 100644
--- a/content/browser/BUILD.gn
+++ b/content/browser/BUILD.gn
@@ -1767,8 +1767,6 @@
     "renderer_host/input/touch_timeout_handler.h",
     "renderer_host/input/touchpad_pinch_event_queue.cc",
     "renderer_host/input/touchpad_pinch_event_queue.h",
-    "renderer_host/input/touchscreen_tap_suppression_controller.cc",
-    "renderer_host/input/touchscreen_tap_suppression_controller.h",
     "renderer_host/ipc_utils.cc",
     "renderer_host/ipc_utils.h",
     "renderer_host/isolated_context_util.cc",
diff --git a/content/browser/devtools/protocol/preload_handler.cc b/content/browser/devtools/protocol/preload_handler.cc
index 1656501..557d69e0 100644
--- a/content/browser/devtools/protocol/preload_handler.cc
+++ b/content/browser/devtools/protocol/preload_handler.cc
@@ -39,8 +39,6 @@
       return Preload::PrerenderFinalStatusEnum::DidFailLoad;
     case PrerenderFinalStatus::kDownload:
       return Preload::PrerenderFinalStatusEnum::Download;
-    case PrerenderFinalStatus::kInProgressNavigation:
-      return Preload::PrerenderFinalStatusEnum::InProgressNavigation;
     case PrerenderFinalStatus::kInvalidSchemeNavigation:
       return Preload::PrerenderFinalStatusEnum::InvalidSchemeNavigation;
     case PrerenderFinalStatus::kInvalidSchemeRedirect:
diff --git a/content/browser/devtools/protocol/storage_handler.cc b/content/browser/devtools/protocol/storage_handler.cc
index 158460d..d352975 100644
--- a/content/browser/devtools/protocol/storage_handler.cc
+++ b/content/browser/devtools/protocol/storage_handler.cc
@@ -1066,7 +1066,7 @@
   if (group.ads) {
     for (const auto& ad : *group.ads) {
       auto protocol_ad = protocol::Storage::InterestGroupAd::Create()
-                             .SetRenderUrl(ad.render_url.spec())
+                             .SetRenderURL(ad.render_url.spec())
                              .Build();
       if (ad.metadata) {
         protocol_ad->SetMetadata(*ad.metadata);
@@ -1079,7 +1079,7 @@
   if (group.ad_components) {
     for (const auto& ad : *group.ad_components) {
       auto protocol_ad = protocol::Storage::InterestGroupAd::Create()
-                             .SetRenderUrl(ad.render_url.spec())
+                             .SetRenderURL(ad.render_url.spec())
                              .Build();
       if (ad.metadata) {
         protocol_ad->SetMetadata(*ad.metadata);
@@ -1098,17 +1098,17 @@
           .SetAdComponents(std::move(ad_components))
           .Build();
   if (group.bidding_url) {
-    protocol_group->SetBiddingUrl(group.bidding_url->spec());
+    protocol_group->SetBiddingLogicURL(group.bidding_url->spec());
   }
   if (group.bidding_wasm_helper_url) {
-    protocol_group->SetBiddingWasmHelperUrl(
+    protocol_group->SetBiddingWasmHelperURL(
         group.bidding_wasm_helper_url->spec());
   }
   if (group.update_url) {
-    protocol_group->SetUpdateUrl(group.update_url->spec());
+    protocol_group->SetUpdateURL(group.update_url->spec());
   }
   if (group.trusted_bidding_signals_url) {
-    protocol_group->SetTrustedBiddingSignalsUrl(
+    protocol_group->SetTrustedBiddingSignalsURL(
         group.trusted_bidding_signals_url->spec());
   }
   if (group.user_bidding_signals) {
diff --git a/content/browser/gpu/gpu_process_host.cc b/content/browser/gpu/gpu_process_host.cc
index 640e1a9..3cc40e4b 100644
--- a/content/browser/gpu/gpu_process_host.cc
+++ b/content/browser/gpu/gpu_process_host.cc
@@ -262,6 +262,7 @@
     switches::kProfilingFile,
     switches::kProfilingFlush,
     switches::kRunAllCompositorStagesBeforeDraw,
+    switches::kShaderCachePath,
     switches::kSkiaFontCacheLimitMb,
     switches::kSkiaGraphiteBackend,
     switches::kSkiaResourceCacheLimitMb,
diff --git a/content/browser/media/cdm_storage_manager.cc b/content/browser/media/cdm_storage_manager.cc
index bd48424f..dec45f9 100644
--- a/content/browser/media/cdm_storage_manager.cc
+++ b/content/browser/media/cdm_storage_manager.cc
@@ -26,6 +26,8 @@
 const char kDeleteFileError[] = "DeleteFileError.";
 const char kWriteFileError[] = "WriteFileError.";
 const char kReadFileError[] = "ReadFileError.";
+const char kDatabaseOpenErrorNoPeriod[] = "DatabaseOpenError";
+const char kDatabaseOpenError[] = "DatabaseOpenError.";
 
 // Creates a task runner suitable for running SQLite database operations.
 scoped_refptr<base::SequencedTaskRunner> CreateDatabaseTaskRunner() {
@@ -236,7 +238,14 @@
   base::UmaHistogramBoolean(GetHistogramName(kDeleteDatabaseError), !success);
 }
 
-void CdmStorageManager::ReportDatabaseOpenError(CdmStorageOpenError error) {}
+void CdmStorageManager::ReportDatabaseOpenError(CdmStorageOpenError error) {
+  // General Errors without distinguishing incognito or not.
+  base::UmaHistogramEnumeration(
+      std::string{kUmaPrefix} + std::string{kDatabaseOpenErrorNoPeriod}, error);
+
+  // Histogram split by incognito and non-incognito.
+  base::UmaHistogramEnumeration(GetHistogramName(kDatabaseOpenError), error);
+}
 
 std::string CdmStorageManager::GetHistogramName(const char operation[]) {
   return std::string{kUmaPrefix} + std::string{operation} +
diff --git a/content/browser/media/key_system_support_android.cc b/content/browser/media/key_system_support_android.cc
index 434f75ad..8ca260d 100644
--- a/content/browser/media/key_system_support_android.cc
+++ b/content/browser/media/key_system_support_android.cc
@@ -20,6 +20,7 @@
 #include "media/base/media_switches.h"
 #include "media/base/media_util.h"
 #include "media/base/video_codecs.h"
+#include "media/media_buildflags.h"
 
 using media::MediaCodecUtil;
 using media::MediaDrmBridge;
@@ -83,10 +84,9 @@
     return;
   }
 
+  // Rendering of hardware secure codecs is only supported when AndroidOverlay
+  // is enabled.
   if (is_secure) {
-    // Rendering of hardware secure codecs is only supported when
-    // AndroidOverlay is enabled.
-    // TODO(crbug.com/853336): Allow Cast override.
     bool are_overlay_supported =
         content::AndroidOverlayProvider::GetInstance()->AreOverlaysSupported();
     bool overlay_fullscreen_video =
diff --git a/content/browser/media/session/media_session_impl.cc b/content/browser/media/session/media_session_impl.cc
index a9a1203..fb69e18 100644
--- a/content/browser/media/session/media_session_impl.cc
+++ b/content/browser/media/session/media_session_impl.cc
@@ -1039,7 +1039,13 @@
   info->is_controllable = IsControllable();
 
   // If the browser context is off the record then it should be sensitive.
-  info->is_sensitive = web_contents()->GetBrowserContext()->IsOffTheRecord();
+  // This is used as a proxy to hide the metadata from sensitive surfaces such
+  // as the lock screen.
+  // TODO(1484490): Remove this field once the new feature to hide metadata from
+  // sensitive profiles is launched.
+  info->is_sensitive =
+      web_contents()->GetBrowserContext()->IsOffTheRecord() &&
+      !base::FeatureList::IsEnabled(media::kHideIncognitoMediaMetadata);
 
   info->picture_in_picture_state =
       web_contents()->HasPictureInPictureVideo() ||
diff --git a/content/browser/media/session/media_session_impl_unittest.cc b/content/browser/media/session/media_session_impl_unittest.cc
index ed264cb6..03cd9b2b 100644
--- a/content/browser/media/session/media_session_impl_unittest.cc
+++ b/content/browser/media/session/media_session_impl_unittest.cc
@@ -21,6 +21,7 @@
 #include "content/public/test/test_renderer_host.h"
 #include "content/test/test_web_contents.h"
 #include "media/base/media_content_type.h"
+#include "media/base/media_switches.h"
 #include "services/media_session/public/cpp/features.h"
 #include "services/media_session/public/cpp/test/audio_focus_test_util.h"
 #include "services/media_session/public/cpp/test/mock_media_session.h"
@@ -680,22 +681,6 @@
   EXPECT_NE(GetMediaSession()->GetSourceId(), other_session->GetSourceId());
 }
 
-TEST_F(MediaSessionImplTest, SessionInfoSensitive) {
-  EXPECT_FALSE(browser_context()->IsOffTheRecord());
-  EXPECT_FALSE(media_session::test::GetMediaSessionInfoSync(GetMediaSession())
-                   ->is_sensitive);
-}
-
-TEST_F(MediaSessionImplTest, SessionInfoSensitive_OffTheRecord) {
-  auto other_context = std::make_unique<TestBrowserContext>();
-  other_context->set_is_off_the_record(true);
-  auto other_contents = TestWebContents::Create(other_context.get(), nullptr);
-  MediaSessionImpl* other_session = MediaSessionImpl::Get(other_contents.get());
-
-  EXPECT_TRUE(other_context->IsOffTheRecord());
-  EXPECT_TRUE(media_session::test::GetMediaSessionInfoSync(other_session)
-                  ->is_sensitive);
-}
 
 TEST_F(MediaSessionImplTest, SessionInfoPictureInPicture) {
   WebContentsImpl* web_contents_impl =
@@ -833,6 +818,58 @@
                   ->hide_metadata);
 }
 
+class MediaSessionImplIsSensitive : public MediaSessionImplTest,
+                                    public ::testing::WithParamInterface<bool> {
+ public:
+  MediaSessionImplIsSensitive()
+      : is_incognito_media_feature_enabled_(GetParam()) {}
+
+  void SetUp() override {
+    MediaSessionImplTest::SetUp();
+
+    if (is_incognito_media_feature_enabled_) {
+      scoped_feature_list_.InitAndEnableFeature(
+          media::kHideIncognitoMediaMetadata);
+    } else {
+      scoped_feature_list_.InitAndDisableFeature(
+          media::kHideIncognitoMediaMetadata);
+    }
+  }
+
+  void TearDown() override {
+    scoped_feature_list_.Reset();
+    MediaSessionImplTest::TearDown();
+  }
+
+  bool is_incognito_media_feature_enabled_;
+
+ private:
+  base::test::ScopedFeatureList scoped_feature_list_;
+};
+
+INSTANTIATE_TEST_SUITE_P(MediaSessionImplIsSensitive,
+                         MediaSessionImplIsSensitive,
+                         testing::Bool());
+
+TEST_P(MediaSessionImplIsSensitive, SessionInfo_IsSensitive_NormalSession) {
+  EXPECT_FALSE(browser_context()->IsOffTheRecord());
+
+  EXPECT_FALSE(media_session::test::GetMediaSessionInfoSync(GetMediaSession())
+                   ->is_sensitive);
+}
+
+TEST_P(MediaSessionImplIsSensitive, SessionInfo_IsSensitive_OffTheRecord) {
+  auto other_context = std::make_unique<TestBrowserContext>();
+  other_context->set_is_off_the_record(true);
+  auto other_contents = TestWebContents::Create(other_context.get(), nullptr);
+  MediaSessionImpl* other_session = MediaSessionImpl::Get(other_contents.get());
+
+  EXPECT_TRUE(other_context->IsOffTheRecord());
+  EXPECT_EQ(!is_incognito_media_feature_enabled_,
+            media_session::test::GetMediaSessionInfoSync(other_session)
+                ->is_sensitive);
+}
+
 // Tests for throttling duration updates.
 // TODO (jazzhsu): Remove these tests once media session supports livestream.
 class MediaSessionImplDurationThrottleTest : public MediaSessionImplTest {
diff --git a/content/browser/network/dns_config_android_browsertest.cc b/content/browser/network/dns_config_android_browsertest.cc
deleted file mode 100644
index 568dc419..0000000
--- a/content/browser/network/dns_config_android_browsertest.cc
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include <memory>
-#include <string>
-
-#include "base/logging.h"
-#include "content/public/browser/network_service_util.h"
-#include "content/public/test/browser_test.h"
-#include "content/public/test/content_browser_test.h"
-#include "content/public/test/network_service_test_helper.h"
-#include "mojo/public/cpp/bindings/remote.h"
-#include "mojo/public/cpp/bindings/sync_call_restrictions.h"
-#include "net/base/network_change_notifier.h"
-#include "net/dns/dns_config_service_android.h"
-#include "net/dns/system_dns_config_change_notifier.h"
-#include "services/network/public/mojom/network_service_test.mojom.h"
-#include "services/network/public/mojom/system_dns_config_observer.mojom.h"
-
-namespace content {
-namespace {
-
-class MockSystemDnsConfigObserver
-    : public network::mojom::SystemDnsConfigObserver {
- public:
-  ~MockSystemDnsConfigObserver() override = default;
-
-  net::DnsConfig WaitForDnsConfig(
-      const std::string& wait_dns_over_tls_hostname) {
-    wait_dns_over_tls_hostname_ = wait_dns_over_tls_hostname;
-    run_loop_ = std::make_unique<base::RunLoop>();
-    run_loop_->Run();
-    EXPECT_TRUE(run_loop_->AnyQuitCalled()) << "Timed out waiting DnsConfig."
-                                               "wait_dns_over_tls_hostname="
-                                            << wait_dns_over_tls_hostname
-                                            << ", the last "
-                                               "config instead is "
-                                            << dns_config_.ToDict();
-    return dns_config_;
-  }
-
- private:
-  void OnConfigChanged(const net::DnsConfig& config) override {
-    dns_config_ = config;
-    if (config.dns_over_tls_hostname == wait_dns_over_tls_hostname_) {
-      run_loop_->Quit();
-    }
-  }
-
-  std::unique_ptr<base::RunLoop> run_loop_;
-  std::string wait_dns_over_tls_hostname_;
-  net::DnsConfig dns_config_;
-};
-
-class DnsConfigAndroidInProcessBrowserTest : public ContentBrowserTest {
- public:
-  DnsConfigAndroidInProcessBrowserTest() { ForceInProcessNetworkService(); }
-};
-
-IN_PROC_BROWSER_TEST_F(DnsConfigAndroidInProcessBrowserTest,
-                       DnsConfigListenAllowed) {
-  mojo::ScopedAllowSyncCallForTesting allow_sync_call;
-
-  auto helper = NetworkServiceTestHelper::CreateInProcessReceiver(
-      network_service_test().BindNewPipeAndPassReceiver());
-
-  MockSystemDnsConfigObserver mock_observer;
-  mojo::Receiver<network::mojom::SystemDnsConfigObserver> receiver(
-      &mock_observer);
-  network_service_test()->AddSystemDnsConfigObserver(
-      receiver.BindNewPipeAndPassRemote());
-
-  net::DnsConfig expect_config;
-  expect_config.nameservers = {net::IPEndPoint(net::IPAddress(1, 2, 3, 4), 80)};
-  expect_config.dns_over_tls_active = true;
-  expect_config.dns_over_tls_hostname = "https://example.com/";
-  expect_config.search = {"foo"};
-  auto* system_dns_config_notifier =
-      net::NetworkChangeNotifier::GetSystemDnsConfigNotifier();
-  CHECK(system_dns_config_notifier);
-  system_dns_config_notifier->OnConfigChangedForTesting(expect_config);
-
-  // `mock_observer` should observe DNS config change over mojo
-  const net::DnsConfig return_config =
-      mock_observer.WaitForDnsConfig(expect_config.dns_over_tls_hostname);
-  EXPECT_EQ(expect_config.nameservers, return_config.nameservers);
-  EXPECT_EQ(expect_config.dns_over_tls_active,
-            return_config.dns_over_tls_active);
-  EXPECT_EQ(expect_config.dns_over_tls_hostname,
-            return_config.dns_over_tls_hostname);
-  EXPECT_EQ(expect_config.search, return_config.search);
-}
-
-// TODO(yoichio): Add DnsConfigAndroidOutOfProcessSandboxedBrowserTest.
-
-}  // namespace
-}  // namespace content
diff --git a/content/browser/picture_in_picture/document_picture_in_picture_window_controller_impl.cc b/content/browser/picture_in_picture/document_picture_in_picture_window_controller_impl.cc
index 834d256..811c35f 100644
--- a/content/browser/picture_in_picture/document_picture_in_picture_window_controller_impl.cc
+++ b/content/browser/picture_in_picture/document_picture_in_picture_window_controller_impl.cc
@@ -107,8 +107,8 @@
 }
 
 void DocumentPictureInPictureWindowControllerImpl::CloseAndFocusInitiator() {
-  Close(false /* should_pause_video */);
   FocusInitiator();
+  Close(false /* should_pause_video */);
 }
 
 void DocumentPictureInPictureWindowControllerImpl::OnWindowDestroyed(
diff --git a/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc b/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc
index f6ef9683..a5cc6f7 100644
--- a/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc
+++ b/content/browser/picture_in_picture/picture_in_picture_service_impl_unittest.cc
@@ -162,6 +162,7 @@
   }
 
   void TearDown() override {
+    service_impl_ = nullptr;
     RenderViewHostImplTestHarness::TearDown();
   }
 
@@ -180,7 +181,7 @@
   PictureInPictureTestBrowserClient browser_client_;
   PictureInPictureDelegate delegate_;
   // Will be deleted when the frame is destroyed.
-  raw_ptr<PictureInPictureServiceImpl, DanglingUntriaged> service_impl_;
+  raw_ptr<PictureInPictureServiceImpl> service_impl_;
   // Required to pass a valid PendingRemote to StartSession() in the tests.
   PictureInPictureMediaPlayerReceiver media_player_receiver_;
 };
diff --git a/content/browser/preloading/prerender/prerender_final_status.h b/content/browser/preloading/prerender/prerender_final_status.h
index b95fd397..2a8870d 100644
--- a/content/browser/preloading/prerender/prerender_final_status.h
+++ b/content/browser/preloading/prerender/prerender_final_status.h
@@ -34,7 +34,7 @@
   // kCrossOriginNavigation = 4,
   kInvalidSchemeRedirect = 5,
   kInvalidSchemeNavigation = 6,
-  kInProgressNavigation = 7,
+  // kInProgressNavigation = 7,  // No longer used.
   // kNavigationRequestFailure = 8,  // No longer used.
   kNavigationRequestBlockedByCsp = 9,
   kMainFrameNavigation = 10,
diff --git a/content/browser/preloading/prerender/prerender_host.cc b/content/browser/preloading/prerender/prerender_host.cc
index 7565abc9..5e96f65 100644
--- a/content/browser/preloading/prerender/prerender_host.cc
+++ b/content/browser/preloading/prerender/prerender_host.cc
@@ -1019,7 +1019,6 @@
     case PrerenderFinalStatus::kLowEndDevice:
     case PrerenderFinalStatus::kInvalidSchemeRedirect:
     case PrerenderFinalStatus::kInvalidSchemeNavigation:
-    case PrerenderFinalStatus::kInProgressNavigation:
     case PrerenderFinalStatus::kNavigationRequestBlockedByCsp:
     case PrerenderFinalStatus::kMainFrameNavigation:
     case PrerenderFinalStatus::kMojoBinderPolicy:
diff --git a/content/browser/preloading/prerender/prerender_host_registry.cc b/content/browser/preloading/prerender/prerender_host_registry.cc
index 78f369e6..6add2e4f 100644
--- a/content/browser/preloading/prerender/prerender_host_registry.cc
+++ b/content/browser/preloading/prerender/prerender_host_registry.cc
@@ -220,7 +220,6 @@
       NOTREACHED_NORETURN();
     case PrerenderFinalStatus::kInvalidSchemeNavigation:
       return PreloadingEligibility::kHttpOrHttpsOnly;
-    case PrerenderFinalStatus::kInProgressNavigation:
     case PrerenderFinalStatus::kNavigationRequestBlockedByCsp:
     case PrerenderFinalStatus::kMainFrameNavigation:
     case PrerenderFinalStatus::kMojoBinderPolicy:
diff --git a/content/browser/renderer_host/input/fling_controller.h b/content/browser/renderer_host/input/fling_controller.h
index 7805c15b..cc53bb3 100644
--- a/content/browser/renderer_host/input/fling_controller.h
+++ b/content/browser/renderer_host/input/fling_controller.h
@@ -7,9 +7,9 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/time/time.h"
-#include "content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h"
 #include "content/common/content_export.h"
 #include "content/common/input/touchpad_tap_suppression_controller.h"
+#include "content/common/input/touchscreen_tap_suppression_controller.h"
 #include "third_party/blink/public/mojom/input/input_event_result.mojom-shared.h"
 #include "ui/events/blink/fling_booster.h"
 
diff --git a/content/browser/renderer_host/input/gesture_event_queue.cc b/content/browser/renderer_host/input/gesture_event_queue.cc
index d7e830a8..33062fa7 100644
--- a/content/browser/renderer_host/input/gesture_event_queue.cc
+++ b/content/browser/renderer_host/input/gesture_event_queue.cc
@@ -6,8 +6,8 @@
 
 #include "base/auto_reset.h"
 #include "base/trace_event/trace_event.h"
-#include "content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h"
 #include "content/common/input/touchpad_tap_suppression_controller.h"
+#include "content/common/input/touchscreen_tap_suppression_controller.h"
 #include "third_party/blink/public/mojom/input/input_handler.mojom.h"
 #include "ui/events/blink/blink_features.h"
 #include "ui/events/blink/web_input_event_traits.h"
diff --git a/content/browser/renderer_host/media/media_stream_manager_unittest.cc b/content/browser/renderer_host/media/media_stream_manager_unittest.cc
index 072ab991..f2fdfef 100644
--- a/content/browser/renderer_host/media/media_stream_manager_unittest.cc
+++ b/content/browser/renderer_host/media/media_stream_manager_unittest.cc
@@ -326,8 +326,6 @@
                              GetDeviceFormatsInUseCallback callback) override {}
   void OnLog(const base::UnguessableToken& device_id,
              const std::string& reason) override {}
-  void OnFrameDropped(const base::UnguessableToken& device_id,
-                      media::VideoCaptureFrameDropReason reason) override {}
 };
 
 }  // namespace
diff --git a/content/browser/renderer_host/media/video_capture_browsertest.cc b/content/browser/renderer_host/media/video_capture_browsertest.cc
index 238d966..bcd29a4 100644
--- a/content/browser/renderer_host/media/video_capture_browsertest.cc
+++ b/content/browser/renderer_host/media/video_capture_browsertest.cc
@@ -61,7 +61,7 @@
                void(const VideoCaptureControllerID& id,
                     const ReadyBuffer& fullsized_buffer,
                     const std::vector<ReadyBuffer>& downscaled_buffers));
-  MOCK_METHOD2(OnFrameDroppedEarly,
+  MOCK_METHOD2(OnFrameDropped,
                void(const VideoCaptureControllerID& id,
                     media::VideoCaptureFrameDropReason reason));
   MOCK_METHOD1(OnFrameWithEmptyRegionCapture,
diff --git a/content/browser/renderer_host/media/video_capture_controller.cc b/content/browser/renderer_host/media/video_capture_controller.cc
index 2fcd7f5..2f29558b0 100644
--- a/content/browser/renderer_host/media/video_capture_controller.cc
+++ b/content/browser/renderer_host/media/video_capture_controller.cc
@@ -569,27 +569,17 @@
 
 void VideoCaptureController::OnFrameDropped(
     media::VideoCaptureFrameDropReason reason) {
-  OnFrameDroppedLogging(reason);
   // This method implements media::VideoFrameReceiver, which implements signals
-  // between the capture process and browser process. Therefore we'll want to
-  // forward the frame drop signal to the renderer process.
+  // between the capture process and browser process. We forward this call to
+  // the renderer process where it eventually reached the MediaStreamVideoTrack.
   for (const auto& client : controller_clients_) {
     if (client->session_closed) {
       continue;
     }
-    client->event_handler->OnFrameDroppedEarly(client->controller_id, reason);
+    client->event_handler->OnFrameDropped(client->controller_id, reason);
   }
 }
 
-void VideoCaptureController::OnFrameDroppedByRenderer(
-    media::VideoCaptureFrameDropReason reason) {
-  OnFrameDroppedLogging(reason);
-}
-
-// TODO(https://crbug.com/1481448): Delete this, it's no longer used.
-void VideoCaptureController::OnFrameDroppedLogging(
-    media::VideoCaptureFrameDropReason reason) {}
-
 void VideoCaptureController::OnNewCropVersion(uint32_t crop_version) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
   EmitLogMessage(base::StringPrintf("%s(%u)", __func__, crop_version), 3);
diff --git a/content/browser/renderer_host/media/video_capture_controller.h b/content/browser/renderer_host/media/video_capture_controller.h
index bed3e605..8820930 100644
--- a/content/browser/renderer_host/media/video_capture_controller.h
+++ b/content/browser/renderer_host/media/video_capture_controller.h
@@ -124,12 +124,6 @@
   void OnStartedUsingGpuDecode() override;
   void OnStopped() override;
 
-  // Frames can be dropped early (e.g. capture process) or late (e.g. renderer
-  // process). Only if the frame drop happened early do we want to forward that
-  // signal to the renderer process for the purpose of the MediaStreamTrack
-  // Statistics API counting frame drops.
-  void OnFrameDroppedByRenderer(media::VideoCaptureFrameDropReason reason);
-
   // Implementation of VideoCaptureDeviceLauncher::Callbacks interface:
   void OnDeviceLaunched(
       std::unique_ptr<LaunchedVideoCaptureDevice> device) override;
@@ -254,8 +248,6 @@
                                    const VideoCaptureControllerID& id)>;
   void PerformForClientsWithOpenSession(EventHandlerAction action);
 
-  void OnFrameDroppedLogging(media::VideoCaptureFrameDropReason reason);
-
   void EmitLogMessage(const std::string& message, int verbose_log_level);
 
   void MaybeEmitFrameDropLogMessage(media::VideoCaptureFrameDropReason reason);
diff --git a/content/browser/renderer_host/media/video_capture_controller_event_handler.h b/content/browser/renderer_host/media/video_capture_controller_event_handler.h
index dfd6376f..07cfca8b 100644
--- a/content/browser/renderer_host/media/video_capture_controller_event_handler.h
+++ b/content/browser/renderer_host/media/video_capture_controller_event_handler.h
@@ -66,18 +66,10 @@
       const ReadyBuffer& buffer,
       const std::vector<ReadyBuffer>& scaled_buffers) = 0;
 
-  // A frame was dropped early, such as by the capture process. This refers to
-  // a frame where we never called OnBufferReady().
-  //
-  // This callback is used to signal frame drops from browser process to
-  // renderer process and is named OnFrameDroppedEarly() to avoid naming clashes
-  // with VideoCaptureImpl::OnFrameDropped() which signals about frame drops in
-  // the other direction (renderer -> browser).
-  // TODO(https://crbug.com/1481448): When the UMAs are moved and the other
-  // OnFrameDropped() callback is deleted, rename this method to OnFrameDropped.
-  virtual void OnFrameDroppedEarly(
-      const VideoCaptureControllerID& id,
-      media::VideoCaptureFrameDropReason reason) = 0;
+  // A frame was dropped - OnBufferReady() was never called for this frame. In
+  // other words the frame was dropped before it reached the renderer process.
+  virtual void OnFrameDropped(const VideoCaptureControllerID& id,
+                              media::VideoCaptureFrameDropReason reason) = 0;
 
   // All subsequent buffers are guaranteed to have a crop version whose value
   // is at least |crop_version|.
diff --git a/content/browser/renderer_host/media/video_capture_controller_unittest.cc b/content/browser/renderer_host/media/video_capture_controller_unittest.cc
index ea6c5f8..e615c5af 100644
--- a/content/browser/renderer_host/media/video_capture_controller_unittest.cc
+++ b/content/browser/renderer_host/media/video_capture_controller_unittest.cc
@@ -98,7 +98,7 @@
   MOCK_METHOD1(OnStartedUsingGpuDecode, void(const VideoCaptureControllerID&));
   MOCK_METHOD2(OnNewCropVersion,
                void(const VideoCaptureControllerID&, uint32_t));
-  MOCK_METHOD2(OnFrameDroppedEarly,
+  MOCK_METHOD2(OnFrameDropped,
                void(const VideoCaptureControllerID&,
                     media::VideoCaptureFrameDropReason));
 
@@ -984,23 +984,16 @@
   }
 }
 
-TEST_F(VideoCaptureControllerTest, EarlyDroppedFramesAreSignalled) {
+TEST_F(VideoCaptureControllerTest, OnFrameDroppedIsForwarded) {
   media::VideoCaptureParams requested_params;
   requested_params.requested_format = arbitrary_format_;
   controller_->AddClient(arbitrary_route_id_, client_a_.get(),
                          arbitrary_session_id_, requested_params);
 
-  // When OnFrameDropped() is called, the signal is forwarded.
-  EXPECT_CALL(*client_a_, OnFrameDroppedEarly(_, _)).Times(1);
+  EXPECT_CALL(*client_a_, OnFrameDropped(_, _)).Times(1);
   controller_->OnFrameDropped(
       media::VideoCaptureFrameDropReason::kBufferPoolMaxBufferCountExceeded);
   Mock::VerifyAndClearExpectations(client_a_.get());
-
-  // When OnFrameDroppedByRenderer() is called, the signal is NOT forwarded.
-  EXPECT_CALL(*client_a_, OnFrameDroppedEarly(_, _)).Times(0);
-  controller_->OnFrameDroppedByRenderer(
-      media::VideoCaptureFrameDropReason::kBufferPoolMaxBufferCountExceeded);
-  Mock::VerifyAndClearExpectations(client_a_.get());
 }
 
 TEST_F(VideoCaptureControllerTest, DeviceClientWithColorSpace) {
diff --git a/content/browser/renderer_host/media/video_capture_host.cc b/content/browser/renderer_host/media/video_capture_host.cc
index e4870f9..368c3b4 100644
--- a/content/browser/renderer_host/media/video_capture_host.cc
+++ b/content/browser/renderer_host/media/video_capture_host.cc
@@ -191,7 +191,7 @@
       std::move(mojom_buffer), std::move(mojom_scaled_buffers));
 }
 
-void VideoCaptureHost::OnFrameDroppedEarly(
+void VideoCaptureHost::OnFrameDropped(
     const VideoCaptureControllerID& controller_id,
     media::VideoCaptureFrameDropReason reason) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
@@ -199,7 +199,7 @@
       !base::Contains(device_id_to_observer_map_, controller_id)) {
     return;
   }
-  device_id_to_observer_map_[controller_id]->OnFrameDroppedEarly(reason);
+  device_id_to_observer_map_[controller_id]->OnFrameDropped(reason);
 }
 
 void VideoCaptureHost::OnFrameWithEmptyRegionCapture(
@@ -405,24 +405,6 @@
   std::move(callback).Run(formats_in_use);
 }
 
-void VideoCaptureHost::OnFrameDropped(
-    const base::UnguessableToken& device_id,
-    media::VideoCaptureFrameDropReason reason) {
-  DCHECK_CURRENTLY_ON(BrowserThread::IO);
-
-  VideoCaptureControllerID controller_id(device_id);
-  auto it = controllers_.find(controller_id);
-  if (it == controllers_.end())
-    return;
-
-  const base::WeakPtr<VideoCaptureController>& controller = it->second;
-  if (controller) {
-    // TODO(https://crbug.com/1481448): Delete this callback when
-    // MediaStreamTrackImpl handles the logging and UMAs related to frame drops.
-    controller->OnFrameDroppedByRenderer(reason);
-  }
-}
-
 void VideoCaptureHost::OnNewCropVersion(const base::UnguessableToken& device_id,
                                         uint32_t crop_version) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
diff --git a/content/browser/renderer_host/media/video_capture_host.h b/content/browser/renderer_host/media/video_capture_host.h
index 56437114..6b660af1 100644
--- a/content/browser/renderer_host/media/video_capture_host.h
+++ b/content/browser/renderer_host/media/video_capture_host.h
@@ -74,8 +74,8 @@
   void OnBufferReady(const VideoCaptureControllerID& controller_id,
                      const ReadyBuffer& buffer,
                      const std::vector<ReadyBuffer>& scaled_buffers) override;
-  void OnFrameDroppedEarly(const VideoCaptureControllerID& controller_id,
-                           media::VideoCaptureFrameDropReason reason) override;
+  void OnFrameDropped(const VideoCaptureControllerID& controller_id,
+                      media::VideoCaptureFrameDropReason reason) override;
   void OnFrameWithEmptyRegionCapture(
       const VideoCaptureControllerID& controller_id) override;
   void OnEnded(const VideoCaptureControllerID& id) override;
@@ -105,8 +105,6 @@
                              const base::UnguessableToken& session_id,
                              GetDeviceFormatsInUseCallback callback) override;
   // This refers to a late frame drop, originating from the renderer process.
-  void OnFrameDropped(const base::UnguessableToken& device_id,
-                      media::VideoCaptureFrameDropReason reason) override;
   void OnNewCropVersion(const base::UnguessableToken& device_id,
                         uint32_t crop_version) override;
   void OnLog(const base::UnguessableToken& device_id,
diff --git a/content/browser/renderer_host/media/video_capture_manager_unittest.cc b/content/browser/renderer_host/media/video_capture_manager_unittest.cc
index 461723a4..8e27ecc 100644
--- a/content/browser/renderer_host/media/video_capture_manager_unittest.cc
+++ b/content/browser/renderer_host/media/video_capture_manager_unittest.cc
@@ -189,9 +189,8 @@
   void OnBufferReady(const VideoCaptureControllerID& id,
                      const ReadyBuffer& buffer,
                      const std::vector<ReadyBuffer>& scaled_buffers) override {}
-  void OnFrameDroppedEarly(const VideoCaptureControllerID& id,
-                           media::VideoCaptureFrameDropReason reason) override {
-  }
+  void OnFrameDropped(const VideoCaptureControllerID& id,
+                      media::VideoCaptureFrameDropReason reason) override {}
   void OnNewCropVersion(const VideoCaptureControllerID& id,
                         uint32_t crop_version) override {}
   void OnFrameWithEmptyRegionCapture(const VideoCaptureControllerID&) override {
diff --git a/content/browser/renderer_host/media/video_capture_unittest.cc b/content/browser/renderer_host/media/video_capture_unittest.cc
index 00646045..299ef9fb 100644
--- a/content/browser/renderer_host/media/video_capture_unittest.cc
+++ b/content/browser/renderer_host/media/video_capture_unittest.cc
@@ -213,7 +213,7 @@
   }
   MOCK_METHOD1(DoOnBufferReady, void(int32_t));
   MOCK_METHOD1(OnBufferDestroyed, void(int32_t));
-  MOCK_METHOD1(OnFrameDroppedEarly, void(media::VideoCaptureFrameDropReason));
+  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason));
   MOCK_METHOD1(OnNewCropVersion, void(uint32_t));
 
   void StartCapture() {
diff --git a/content/browser/resources/webxr_internals/session_info_table.ts b/content/browser/resources/webxr_internals/session_info_table.ts
index 55f9a85..291d52c2 100644
--- a/content/browser/resources/webxr_internals/session_info_table.ts
+++ b/content/browser/resources/webxr_internals/session_info_table.ts
@@ -6,22 +6,18 @@
 import {Time} from 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-webui.js';
 
 import {getTemplate} from './session_info_table.html.js';
-import {SessionRequestRecord} from './webxr_internals.mojom-webui.js';
-import {depthFormatToString, depthUsageToString, sessionFeatureToString, sessionModeToString} from './xr_session_util.js';
+import {SessionRejectedRecord, SessionRequestedRecord, SessionStartedRecord, SessionStoppedRecord} from './webxr_internals.mojom-webui.js';
+import * as XRSessionUtil from './xr_session_util.js';
 
-// TODO(https://crbug.com/1458662): Consider converting this to an object that
-// can also handle parsing the value out of the request object
 const COLUMN_NAMES = [
   'Trace ID',
-  'Mode',
-  'Required Features',
-  'Optional Features',
-  'Depth Usage Preferences',
-  'Depth Data Format Preferences',
-  'Requested Time',
+  'Session State',
+  'Attributes',
 ];
 
 export class SessionInfoTableElement extends CustomElement {
+  private traceIdToTableCell: {[key: string]: HTMLTableCellElement} = {};
+
   static override get template() {
     return getTemplate();
   }
@@ -40,34 +36,130 @@
     });
   }
 
-  addRow(sessionRequestRecord: SessionRequestRecord) {
+  addSessionRequestedRow(sessionRequestedRecord: SessionRequestedRecord) {
+    const {options, requestedTime} = sessionRequestedRecord;
     const {traceId, mode, requiredFeatures, optionalFeatures, depthOptions} =
-        sessionRequestRecord.options;
+        options;
 
-    const cellValues = [
-      traceId.toString(),
-      sessionModeToString(mode),
-      requiredFeatures.map(sessionFeatureToString).join(', '),
-      optionalFeatures.map(sessionFeatureToString).join(', '),
-      depthOptions?.usagePreferences.map(depthUsageToString).join(', '),
-      depthOptions?.dataFormatPreferences.map(depthFormatToString).join(', '),
-      formatMojoTime(sessionRequestRecord.requestedTime),
+    const attributes = [
+      `Mode: ${XRSessionUtil.sessionModeToString(mode)}`,
+      `Required Features: ${
+          requiredFeatures.map(XRSessionUtil.sessionFeatureToString)
+              .join(', ')}`,
+      `Optional Features: ${
+          optionalFeatures.map(XRSessionUtil.sessionFeatureToString)
+              .join(', ')}`,
+      `Requested Time: ${formatMojoTime(requestedTime)}`,
     ];
 
+    const depthUsagePreferences = depthOptions?.usagePreferences || [];
+    if (depthUsagePreferences.length) {
+      attributes.push(`Depth Usage Preferences: ${
+          depthUsagePreferences.map(XRSessionUtil.depthUsageToString)
+              .join(', ')}`);
+    }
+
+    const depthDataFormatPreferences =
+        depthOptions?.dataFormatPreferences || [];
+    if (depthDataFormatPreferences.length) {
+      attributes.push(`Depth Data Format Preferences: ${
+          depthDataFormatPreferences.map(XRSessionUtil.depthFormatToString)
+              .join(', ')}`);
+    }
+
+    this.addSessionRow(traceId.toString(), 'Requested', attributes);
+  }
+
+  addSessionRejectedRow(sessionRejectedRecord: SessionRejectedRecord) {
+    const {
+      traceId,
+      failureReason,
+      failureReasonDescription,
+      rejectedFeatures,
+      rejectedTime,
+    } = sessionRejectedRecord;
+
+    const rejectedFeaturesDescription = rejectedFeatures.length ?
+        ` rejectedFeatures=${
+            rejectedFeatures.map(XRSessionUtil.sessionFeatureToString)
+                .join(', ')}` :
+        '';
+    const attributes = [
+      `Failure Reason: ${
+          XRSessionUtil.requestSessionErrorToString(failureReason)}`,
+      `Failure Reason Description: ${failureReasonDescription} ${
+          rejectedFeaturesDescription}`,
+      `Rejected Time: ${formatMojoTime(rejectedTime)}`,
+    ];
+
+    this.addSessionRow(traceId.toString(), 'Rejected', attributes);
+  }
+
+  addSessionStartedRow(sessionStartedRecord: SessionStartedRecord) {
+    const {traceId, startedTime} = sessionStartedRecord;
+    const attributes = [
+      `Started Time: ${formatMojoTime(startedTime)}`,
+    ];
+
+    this.addSessionRow(traceId.toString(), 'Started', attributes);
+  }
+
+  addSessionStoppedRow(sessionStoppedRecord: SessionStoppedRecord) {
+    const {traceId, stoppedTime} = sessionStoppedRecord;
+    const attributes = [
+      `Stopped Time: ${formatMojoTime(stoppedTime)}`,
+    ];
+
+    this.addSessionRow(traceId.toString(), 'Stopped', attributes);
+  }
+
+  private createTableCell(textContent: string = ''): HTMLTableCellElement {
+    const cell = document.createElement('td');
+    cell.textContent = textContent;
+    return cell;
+  }
+
+  private createAttributesList(attributes: string[]): HTMLUListElement {
+    const ul = document.createElement('ul');
+    ul.style.padding = '0';
+
+    attributes.forEach((attribute) => {
+      const li = document.createElement('li');
+      li.textContent = attribute;
+      ul.appendChild(li);
+    });
+
+    return ul;
+  }
+
+  private updateTraceIdCell(traceId: string, newRow: HTMLTableRowElement) {
+    let traceIdCell = this.traceIdToTableCell[traceId];
+
+    if (traceIdCell === undefined) {
+      traceIdCell = this.createTableCell(traceId);
+      newRow.appendChild(traceIdCell);
+      traceIdCell.rowSpan = 1;
+      this.traceIdToTableCell[traceId] = traceIdCell;
+    } else {
+      traceIdCell.rowSpan++;
+    }
+  }
+
+  private addSessionRow(
+      traceId: string, sessionType: string, attributes: string[]) {
     const table =
         this.getRequiredElement<HTMLTableElement>('#session-info-table');
     const newRow = table.insertRow();
 
-    cellValues.forEach((value) => {
-      const cell = newRow.insertCell();
-      if (typeof value === 'object') {
-        cell.textContent = JSON.stringify(value);
-      } else if (value !== undefined) {
-        cell.textContent = value;
-      } else {
-        cell.textContent = '[]';
-      }
-    });
+    this.updateTraceIdCell(traceId, newRow);
+
+    const sessionTypeCell = this.createTableCell(sessionType);
+    newRow.appendChild(sessionTypeCell);
+
+    const attributesCell = this.createTableCell();
+    const ul = this.createAttributesList(attributes);
+    attributesCell.appendChild(ul);
+    newRow.appendChild(attributesCell);
   }
 }
 
diff --git a/content/browser/resources/webxr_internals/webxr_internals.ts b/content/browser/resources/webxr_internals/webxr_internals.ts
index ff9da6d..e350cb1 100644
--- a/content/browser/resources/webxr_internals/webxr_internals.ts
+++ b/content/browser/resources/webxr_internals/webxr_internals.ts
@@ -9,8 +9,7 @@
 import {getRequiredElement} from 'chrome://resources/js/util_ts.js';
 
 import {BrowserProxy} from './browser_proxy.js';
-import {SessionRequestRecord} from './webxr_internals.mojom-webui.js';
-
+import {SessionRejectedRecord, SessionRequestedRecord, SessionStartedRecord, SessionStoppedRecord} from './webxr_internals.mojom-webui.js';
 
 let browserProxy: BrowserProxy;
 
@@ -69,9 +68,24 @@
 
   const table = document.createElement('session-info-table');
 
-  browserProxy.getBrowserCallback().addXrSessionRequest.addListener(
-      (sessionRequestRecord: SessionRequestRecord) => {
-        table.addRow(sessionRequestRecord);
+  browserProxy.getBrowserCallback().logXrSessionRequested.addListener(
+      (sessionRequestedRecord: SessionRequestedRecord) => {
+        table.addSessionRequestedRow(sessionRequestedRecord);
+      });
+
+  browserProxy.getBrowserCallback().logXrSessionRejected.addListener(
+      (sessionRejectedRecord: SessionRejectedRecord) => {
+        table.addSessionRejectedRow(sessionRejectedRecord);
+      });
+
+  browserProxy.getBrowserCallback().logXrSessionStarted.addListener(
+      (sessionStartedRecord: SessionStartedRecord) => {
+        table.addSessionStartedRow(sessionStartedRecord);
+      });
+
+  browserProxy.getBrowserCallback().logXrSessionStopped.addListener(
+      (sessionStoppedRecord: SessionStoppedRecord) => {
+        table.addSessionStoppedRow(sessionStoppedRecord);
       });
 
   sessionInfoConent.appendChild(table);
diff --git a/content/browser/resources/webxr_internals/xr_session_util.ts b/content/browser/resources/webxr_internals/xr_session_util.ts
index 9e985c4c..b6bcfd2 100644
--- a/content/browser/resources/webxr_internals/xr_session_util.ts
+++ b/content/browser/resources/webxr_internals/xr_session_util.ts
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {XRDepthDataFormat, XRDepthUsage, XRSessionFeature, XRSessionMode} from './xr_session.mojom-webui.js';
+import {RequestSessionError, XRDepthDataFormat, XRDepthUsage, XRSessionFeature, XRSessionMode} from './xr_session.mojom-webui.js';
 
 export function depthFormatToString(format: XRDepthDataFormat): string {
   switch (format) {
@@ -26,6 +26,32 @@
   }
 }
 
+export function requestSessionErrorToString(
+    requestSessionError: RequestSessionError): string {
+  switch (requestSessionError) {
+    case RequestSessionError.EXISTING_IMMERSIVE_SESSION:
+      return 'EXISTING_IMMERSIVE_SESSION';
+    case RequestSessionError.INVALID_CLIENT:
+      return 'INVALID_CLIENT';
+    case RequestSessionError.USER_DENIED_CONSENT:
+      return 'USER_DENIED_CONSENT';
+    case RequestSessionError.NO_RUNTIME_FOUND:
+      return 'NO_RUNTIME_FOUND';
+    case RequestSessionError.UNKNOWN_RUNTIME_ERROR:
+      return 'UNKNOWN_RUNTIME_ERROR';
+    case RequestSessionError.RUNTIME_INSTALL_FAILURE:
+      return 'RUNTIMES_CHANGED';
+    case RequestSessionError.RUNTIMES_CHANGED:
+      return 'EXISTING_IMMERSIVE_SESSION';
+    case RequestSessionError.FULLSCREEN_ERROR:
+      return 'FULLSCREEN_ERROR';
+    case RequestSessionError.UNKNOWN_FAILURE:
+      return 'UNKNOWN_FAILURE';
+    default:
+      return '';
+  }
+}
+
 export function sessionFeatureToString(feature: XRSessionFeature): string {
   switch (feature) {
     case XRSessionFeature.REF_SPACE_VIEWER:
diff --git a/content/browser/service_worker/service_worker_version.cc b/content/browser/service_worker/service_worker_version.cc
index 06b6b92..644feb9 100644
--- a/content/browser/service_worker/service_worker_version.cc
+++ b/content/browser/service_worker/service_worker_version.cc
@@ -1198,6 +1198,15 @@
 
   is_endpoint_ready_ = true;
   associated_registry_ = std::make_unique<blink::AssociatedInterfaceRegistry>();
+
+  // If we have allocated the process we can tell the client to register
+  // services.
+  if (embedded_worker()->process_id() != ChildProcessHost::kInvalidUniqueID) {
+    GetContentClient()
+        ->browser()
+        ->RegisterAssociatedInterfaceBindersForServiceWorker(
+            GetInfo(), *associated_registry_);
+  }
 }
 
 bool ServiceWorkerVersion::IsControlleeProcessID(int process_id) const {
@@ -1381,6 +1390,10 @@
 }
 
 void ServiceWorkerVersion::OnProcessAllocated() {
+  // If we have not initialized the global scope yet, return early.
+  if (!is_endpoint_ready_) {
+    return;
+  }
   GetContentClient()
       ->browser()
       ->RegisterAssociatedInterfaceBindersForServiceWorker(
diff --git a/content/browser/speech/speech_recognizer_impl.cc b/content/browser/speech/speech_recognizer_impl.cc
index 5f12109..25c1d76c4 100644
--- a/content/browser/speech/speech_recognizer_impl.cc
+++ b/content/browser/speech/speech_recognizer_impl.cc
@@ -578,10 +578,13 @@
   int chunk_duration_ms = recognition_engine_->GetDesiredAudioChunkDurationMs();
 
   if (!device_params_.IsValid()) {
-    DLOG(ERROR) << "Audio input device not found";
-    return Abort(blink::mojom::SpeechRecognitionError(
-        blink::mojom::SpeechRecognitionErrorCode::kAudioCapture,
-        blink::mojom::SpeechAudioErrorDetails::kNoMic));
+    DLOG(WARNING) << "Audio input device not found, but one should exist -- "
+                     "using fake audio input parameters.";
+
+    // It's okay to try with fake parameters since we've already been given
+    // permission from SpeechRecognitionManagerImpl. If no device exists, this
+    // will just result in an OnCaptureError().
+    device_params_ = media::AudioParameters::UnavailableDeviceParams();
   }
 
   // Audio converter shall provide audio based on these parameters as output.
diff --git a/content/browser/speech/speech_recognizer_impl_unittest.cc b/content/browser/speech/speech_recognizer_impl_unittest.cc
index 72ff260ff..28f87d96 100644
--- a/content/browser/speech/speech_recognizer_impl_unittest.cc
+++ b/content/browser/speech/speech_recognizer_impl_unittest.cc
@@ -315,10 +315,32 @@
   EXPECT_TRUE(recognition_started_);
   EXPECT_FALSE(audio_started_);
   EXPECT_FALSE(result_received_);
+  OnCaptureError();
+  base::RunLoop().RunUntilIdle();
   EXPECT_EQ(blink::mojom::SpeechRecognitionErrorCode::kAudioCapture, error_);
   CheckFinalEventsConsistency();
 }
 
+TEST_F(SpeechRecognizerImplTest, StartFakeInputDevice) {
+  // Check for callbacks when stopping record before any audio gets recorded.
+  audio_manager_->SetHasInputDevices(false);
+  recognizer_->StartRecognition(
+      media::AudioDeviceDescription::kDefaultDeviceId);
+  base::RunLoop().RunUntilIdle();  // EVENT_PREPARE processing.
+  WaitForAudioThreadToPostDeviceInfo();
+  base::RunLoop().RunUntilIdle();  // EVENT_START processing.
+  Capture(audio_bus_.get());
+  recognizer_->StopAudioCapture();
+  base::RunLoop().RunUntilIdle();
+  EXPECT_TRUE(recognition_started_);
+  EXPECT_TRUE(audio_started_);
+  EXPECT_FALSE(result_received_);
+  EXPECT_EQ(blink::mojom::SpeechRecognitionErrorCode::kNone, error_);
+  recognizer_->AbortRecognition();
+  base::RunLoop().RunUntilIdle();
+  CheckFinalEventsConsistency();
+}
+
 TEST_F(SpeechRecognizerImplTest, StopBeforeDeviceInfoReceived) {
   // Check for callbacks when stopping record before reply is received from
   // AudioSystem.
diff --git a/content/browser/web_contents/web_contents_impl_unittest.cc b/content/browser/web_contents/web_contents_impl_unittest.cc
index cc62c1d3..2a01f88 100644
--- a/content/browser/web_contents/web_contents_impl_unittest.cc
+++ b/content/browser/web_contents/web_contents_impl_unittest.cc
@@ -250,7 +250,7 @@
   }
 
  private:
-  raw_ptr<WebContents, DanglingUntriaged> fullscreened_contents_;
+  raw_ptr<WebContents> fullscreened_contents_;
 };
 
 class FakeWebContentsDelegate : public WebContentsDelegate {
diff --git a/content/browser/webid/federated_auth_request_impl.cc b/content/browser/webid/federated_auth_request_impl.cc
index 6b3dd4b..04363ef6 100644
--- a/content/browser/webid/federated_auth_request_impl.cc
+++ b/content/browser/webid/federated_auth_request_impl.cc
@@ -1055,8 +1055,8 @@
     fetch_data_.for_idp_signin = for_idp_signin;
   }
 
-  provider_fetcher_ =
-      std::make_unique<FederatedProviderFetcher>(network_manager_.get());
+  provider_fetcher_ = std::make_unique<FederatedProviderFetcher>(
+      render_frame_host(), network_manager_.get());
   provider_fetcher_->Start(
       fetch_data_.pending_idps, icon_ideal_size, icon_minimum_size,
       base::BindOnce(&FederatedAuthRequestImpl::OnAllConfigAndWellKnownFetched,
diff --git a/content/browser/webid/federated_auth_request_impl_unittest.cc b/content/browser/webid/federated_auth_request_impl_unittest.cc
index 9c94224..f3e26b7 100644
--- a/content/browser/webid/federated_auth_request_impl_unittest.cc
+++ b/content/browser/webid/federated_auth_request_impl_unittest.cc
@@ -1458,6 +1458,10 @@
 // Test that request fails if IDP signin URL is different origin from IDP config
 // URL.
 TEST_F(FederatedAuthRequestImplTest, SigninUrlDifferentOriginIdp) {
+  // We only validate the signin_url if IdpSigninStatus is enabled.
+  base::test::ScopedFeatureList list;
+  list.InitAndEnableFeature(features::kFedCmIdpSigninStatusEnabled);
+
   MockConfiguration configuration = kConfigurationValid;
   configuration.idp_info[kProviderUrlFull].config.idp_signin_url =
       "https://idp2.example/signin_url";
diff --git a/content/browser/webid/federated_auth_user_info_request.cc b/content/browser/webid/federated_auth_user_info_request.cc
index b38d2aa..35f0fb9c 100644
--- a/content/browser/webid/federated_auth_user_info_request.cc
+++ b/content/browser/webid/federated_auth_user_info_request.cc
@@ -165,8 +165,8 @@
   // FederatedProviderFetcher is stored as a member so that
   // FederatedProviderFetcher is destroyed when FederatedAuthRequestImpl is
   // destroyed.
-  provider_fetcher_ =
-      std::make_unique<FederatedProviderFetcher>(network_manager_.get());
+  provider_fetcher_ = std::make_unique<FederatedProviderFetcher>(
+      *render_frame_host_, network_manager_.get());
   provider_fetcher_->Start(
       {idp_config_url_}, /*icon_ideal_size=*/0, /*icon_minimum_size=*/0,
       base::BindOnce(
diff --git a/content/browser/webid/federated_provider_fetcher.cc b/content/browser/webid/federated_provider_fetcher.cc
index 2368452..5b66086 100644
--- a/content/browser/webid/federated_provider_fetcher.cc
+++ b/content/browser/webid/federated_provider_fetcher.cc
@@ -39,8 +39,10 @@
 FederatedProviderFetcher::FetchResult::~FetchResult() = default;
 
 FederatedProviderFetcher::FederatedProviderFetcher(
+    RenderFrameHost& render_frame_host,
     IdpNetworkRequestManager* network_manager)
-    : network_manager_(network_manager) {}
+    : render_frame_host_(render_frame_host),
+      network_manager_(network_manager) {}
 
 FederatedProviderFetcher::~FederatedProviderFetcher() = default;
 
@@ -232,8 +234,12 @@
   bool is_accounts_valid =
       webid::IsEndpointSameOrigin(fetch_result.identity_provider_config_url,
                                   fetch_result.endpoints.accounts);
+  url::Origin idp_origin =
+      url::Origin::Create(fetch_result.identity_provider_config_url);
+  // We only need a signin URL if the IDP signin status API is enabled.
   bool is_signin_url_valid =
-      idp_metadata.idp_signin_url.is_empty() ||
+      webid::GetIdpSigninStatusMode(render_frame_host_.get(), idp_origin) !=
+          FedCmIdpSigninStatusMode::ENABLED ||
       webid::IsEndpointSameOrigin(fetch_result.identity_provider_config_url,
                                   idp_metadata.idp_signin_url);
   if (!is_token_valid || !is_accounts_valid || !is_signin_url_valid) {
diff --git a/content/browser/webid/federated_provider_fetcher.h b/content/browser/webid/federated_provider_fetcher.h
index a25ea51e..e9e8a7d 100644
--- a/content/browser/webid/federated_provider_fetcher.h
+++ b/content/browser/webid/federated_provider_fetcher.h
@@ -19,6 +19,7 @@
 #include "third_party/blink/public/mojom/devtools/inspector_issue.mojom.h"
 
 namespace content {
+class RenderFrameHost;
 
 // Fetches the config and well-known files for a list of identity providers.
 // Validates returned information and calls callback when done.
@@ -48,7 +49,10 @@
 
   using RequesterCallback = base::OnceCallback<void(std::vector<FetchResult>)>;
 
-  explicit FederatedProviderFetcher(IdpNetworkRequestManager* network_manager);
+  // TODO(crbug.com/1487668): Remove |render_frame_host| when the IDP signin
+  // status API is enabled by default.
+  FederatedProviderFetcher(RenderFrameHost& render_frame_host,
+                           IdpNetworkRequestManager* network_manager);
   ~FederatedProviderFetcher();
 
   FederatedProviderFetcher(const FederatedProviderFetcher&) = delete;
@@ -79,6 +83,8 @@
 
   void RunCallbackIfDone();
 
+  raw_ref<RenderFrameHost> render_frame_host_;
+
   RequesterCallback callback_;
 
   // Config endpoints which has not yet been fetched.
diff --git a/content/browser/xr/metrics/session_metrics_helper.cc b/content/browser/xr/metrics/session_metrics_helper.cc
index 3975ab2..8b2f95f 100644
--- a/content/browser/xr/metrics/session_metrics_helper.cc
+++ b/content/browser/xr/metrics/session_metrics_helper.cc
@@ -12,6 +12,8 @@
 #include "base/metrics/histogram_macros.h"
 #include "content/browser/xr/metrics/session_timer.h"
 #include "content/browser/xr/metrics/webxr_session_tracker.h"
+#include "content/browser/xr/service/xr_runtime_manager_impl.h"
+#include "content/browser/xr/webxr_internals/mojom/webxr_internals.mojom.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/web_contents.h"
@@ -149,9 +151,17 @@
   DVLOG(1) << __func__;
   DCHECK(!webxr_immersive_session_tracker_);
 
-  session_timer_ = std::make_unique<SessionTimer>();
+  session_timer_ = std::make_unique<SessionTimer>(session_options.trace_id);
   session_timer_->StartSession();
 
+  webxr::mojom::SessionStartedRecordPtr session_started_record =
+      webxr::mojom::SessionStartedRecord::New();
+  session_started_record->trace_id = session_timer_->GetTraceId();
+  session_started_record->started_time = session_timer_->GetStartTime();
+  XRRuntimeManagerImpl::GetOrCreateInstance()
+      ->GetLoggerManager()
+      .RecordSessionStarted(std::move(session_started_record));
+
   // TODO(crbug.com/1061899): The code here assumes that it's called on
   // behalf of the active frame, which is not always true.
   // Plumb explicit RenderFrameHost reference from VRSessionImpl.
@@ -175,7 +185,17 @@
     return;
   }
 
-  webxr_immersive_session_tracker_->SetSessionEnd(base::Time::Now());
+  base::Time stop_time = base::Time::Now();
+
+  webxr::mojom::SessionStoppedRecordPtr session_stopped_record =
+      webxr::mojom::SessionStoppedRecord::New();
+  session_stopped_record->trace_id = session_timer_->GetTraceId();
+  session_stopped_record->stopped_time = stop_time;
+  XRRuntimeManagerImpl::GetOrCreateInstance()
+      ->GetLoggerManager()
+      .RecordSessionStopped(std::move(session_stopped_record));
+
+  webxr_immersive_session_tracker_->SetSessionEnd(stop_time);
   webxr_immersive_session_tracker_->ukm_entry()->SetDuration(
       webxr_immersive_session_tracker_->GetRoundedDurationInSeconds());
   webxr_immersive_session_tracker_->RecordEntry();
diff --git a/content/browser/xr/metrics/session_timer.cc b/content/browser/xr/metrics/session_timer.cc
index 8179ba8..f686d11 100644
--- a/content/browser/xr/metrics/session_timer.cc
+++ b/content/browser/xr/metrics/session_timer.cc
@@ -8,12 +8,20 @@
 
 namespace content {
 
-SessionTimer::SessionTimer() = default;
+SessionTimer::SessionTimer(size_t trace_id) : trace_id_(trace_id) {}
 
 SessionTimer::~SessionTimer() {
   StopSession();
 }
 
+size_t SessionTimer::GetTraceId() {
+  return trace_id_;
+}
+
+base::Time SessionTimer::GetStartTime() {
+  return start_time_;
+}
+
 void SessionTimer::StartSession() {
   DCHECK(start_time_.is_null())
       << "Must stop existing session before starting a new one";
@@ -35,8 +43,8 @@
                                   base::TimeDelta(), base::Hours(5), 100);
   }
 
-  // Clear out start time.
   start_time_ = base::Time();
+  trace_id_ = 0;
 }
 
 }  // namespace content
diff --git a/content/browser/xr/metrics/session_timer.h b/content/browser/xr/metrics/session_timer.h
index df96d99d..1164da7c 100644
--- a/content/browser/xr/metrics/session_timer.h
+++ b/content/browser/xr/metrics/session_timer.h
@@ -14,18 +14,21 @@
 // destruction.
 class SessionTimer {
  public:
-  explicit SessionTimer();
+  explicit SessionTimer(size_t trace_id);
 
   virtual ~SessionTimer();
 
   SessionTimer(const SessionTimer&) = delete;
   SessionTimer& operator=(const SessionTimer&) = delete;
 
+  base::Time GetStartTime();
+  size_t GetTraceId();
   void StartSession();
   void StopSession();
 
  private:
   base::Time start_time_;
+  size_t trace_id_;
 };
 
 }  // namespace content
diff --git a/content/browser/xr/service/vr_service_impl.cc b/content/browser/xr/service/vr_service_impl.cc
index 7f408b7..3350216 100644
--- a/content/browser/xr/service/vr_service_impl.cc
+++ b/content/browser/xr/service/vr_service_impl.cc
@@ -101,26 +101,56 @@
   return permissions;
 }
 
-bool AreAllRequiredFeaturesEnabled(
+// TODO(https://crbug.com/1480022): Replace with base::ranges::set_difference
+std::unordered_set<device::mojom::XRSessionFeature> GetMissingRequiredFeatures(
     const std::unordered_set<device::mojom::XRSessionFeature>& enabled_features,
     const std::unordered_set<device::mojom::XRSessionFeature>&
         required_features) {
   DVLOG(3) << __func__
            << ": enabled_features.size()=" << enabled_features.size();
-  return base::ranges::all_of(required_features, [&enabled_features](
-                                                     const auto&
-                                                         required_feature) {
-    if (!base::Contains(enabled_features, required_feature)) {
-      DVLOG(2)
-          << __func__
-          << ": one of the required features was not enabled on the created "
-             "session, feature: "
-          << required_feature;
-      return false;
-    }
 
-    return true;
-  });
+  std::unordered_set<device::mojom::XRSessionFeature> missing_required_features;
+
+  for (const auto& required_feature : required_features) {
+    if (!base::Contains(enabled_features, required_feature)) {
+      DVLOG(2) << __func__
+               << ": one of the required features was not enabled on the "
+                  "created session, feature: "
+               << required_feature;
+      missing_required_features.insert(required_feature);
+    }
+  }
+
+  return missing_required_features;
+}
+
+void RejectSession(device::mojom::VRService::RequestSessionCallback callback,
+                   size_t trace_id,
+                   device::mojom::RequestSessionError error,
+                   const std::string& failure_reason_description,
+                   std::unordered_set<device::mojom::XRSessionFeature>*
+                       rejected_features = nullptr) {
+  DVLOG(2) << __func__
+           << ": failure reason description=" << failure_reason_description;
+
+  webxr::mojom::SessionRejectedRecordPtr session_rejected_record =
+      webxr::mojom::SessionRejectedRecord::New();
+  session_rejected_record->trace_id = trace_id;
+  session_rejected_record->failure_reason = error;
+  session_rejected_record->rejected_time = base::Time::Now();
+  session_rejected_record->failure_reason_description =
+      failure_reason_description;
+  if (rejected_features) {
+    session_rejected_record->rejected_features.assign(
+        rejected_features->begin(), rejected_features->end());
+  }
+
+  content::XRRuntimeManagerImpl::GetOrCreateInstance()
+      ->GetLoggerManager()
+      .RecordSessionRejected(std::move(session_rejected_record));
+
+  std::move(callback).Run(
+      device::mojom::RequestSessionResult::NewFailureReason(error));
 }
 
 }  // namespace
@@ -145,9 +175,9 @@
   // hit DCHECKs for dropping the callback without closing the pipe.
   // This most often occurs when the Permissions prompt is dismissed.
   if (callback) {
-    std::move(callback).Run(
-        device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_FAILURE));
+    RejectSession(std::move(callback), options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
+                  "SessionRequestData destroyed without running callback.");
   }
 }
 
@@ -295,9 +325,9 @@
                 "VRServiceImpl::OnInlineSessionCreated: no session_result",
                 perfetto::Flow::Global(request.options->trace_id));
 
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
+                  "Runtime did not provide a session.");
     return;
   }
 
@@ -314,8 +344,9 @@
   std::unordered_set<device::mojom::XRSessionFeature> enabled_features(
       session->enabled_features.begin(), session->enabled_features.end());
 
-  if (!AreAllRequiredFeaturesEnabled(enabled_features,
-                                     request.required_features)) {
+  auto missing_required_features =
+      GetMissingRequiredFeatures(enabled_features, request.required_features);
+  if (!missing_required_features.empty()) {
     // UNKNOWN_FAILURE since a runtime should not return a session if there
     // exists a required feature that was not enabled - this would signify a bug
     // in the runtime.
@@ -325,9 +356,9 @@
         "VRServiceImpl::OnInlineSessionCreated: required feature not granted",
         perfetto::Flow::Global(request.options->trace_id));
 
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_FAILURE));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
+                  "Required feature not granted.", &missing_required_features);
     return;
   }
 
@@ -348,9 +379,9 @@
                 "VRServiceImpl::OnImmersiveSessionCreated: no session_result",
                 perfetto::Flow::Global(request.options->trace_id));
 
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
+                  "Runtime did not provide a session.");
     return;
   }
 
@@ -358,8 +389,9 @@
   std::unordered_set<device::mojom::XRSessionFeature> enabled_features(
       session->enabled_features.begin(), session->enabled_features.end());
 
-  if (!AreAllRequiredFeaturesEnabled(enabled_features,
-                                     request.required_features)) {
+  auto missing_required_features =
+      GetMissingRequiredFeatures(enabled_features, request.required_features);
+  if (!missing_required_features.empty()) {
     // UNKNOWN_FAILURE since a runtime should not return a session if there
     // exists a required feature that was not enabled - this would signify a bug
     // in the runtime.
@@ -369,9 +401,9 @@
                 "not granted",
                 perfetto::Flow::Global(request.options->trace_id));
 
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_FAILURE));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
+                  "Required feature not granted.", &missing_required_features);
     return;
   }
 
@@ -453,12 +485,12 @@
   DVLOG(2) << __func__;
   DCHECK(options);
 
-  webxr::mojom::SessionRequestRecordPtr session_request_record =
-      webxr::mojom::SessionRequestRecord::New();
-  session_request_record->options = options->Clone();
-  session_request_record->requested_time = base::Time::Now();
-  runtime_manager_->GetLoggerManager().RecordSessionRequest(
-      std::move(session_request_record));
+  webxr::mojom::SessionRequestedRecordPtr session_requested_record =
+      webxr::mojom::SessionRequestedRecord::New();
+  session_requested_record->options = options->Clone();
+  session_requested_record->requested_time = base::Time::Now();
+  runtime_manager_->GetLoggerManager().RecordSessionRequested(
+      std::move(session_requested_record));
 
   // Queue the request to get to when initialization has completed.
   if (!initialization_complete_) {
@@ -473,18 +505,20 @@
       runtime_manager_->HasPendingImmersiveRequest()) {
     DVLOG(2) << __func__
              << ": can't create sessions while an immersive session exists";
+
     // Can't create sessions while an immersive session exists.
-    std::move(callback).Run(
-        device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION));
+    RejectSession(
+        std::move(callback), options->trace_id,
+        device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION,
+        "There is an existing immersive session.");
     return;
   }
 
   auto* runtime = runtime_manager_->GetRuntimeForOptions(options.get());
   if (!runtime) {
-    std::move(callback).Run(
-        device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::NO_RUNTIME_FOUND));
+    RejectSession(std::move(callback), options->trace_id,
+                  device::mojom::RequestSessionError::NO_RUNTIME_FOUND,
+                  "No runtime found for the given session options.");
     return;
   }
 
@@ -495,9 +529,9 @@
     // (everything that happens up to this point should not take enough time for
     // the user activation to expire). Treat lack of user activation as unknown
     // failure:
-    std::move(callback).Run(
-        device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_FAILURE));
+    RejectSession(std::move(callback), options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_FAILURE,
+                  "Missing user activation.");
     return;
   }
 
@@ -573,9 +607,9 @@
   DVLOG(2) << __func__ << ": is_consent_granted=" << is_consent_granted;
 
   if (!is_consent_granted) {
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::USER_DENIED_CONSENT));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::USER_DENIED_CONSENT,
+                  "Consent was not granted for the requested mode.");
     return;
   }
 
@@ -603,17 +637,24 @@
   const XrPermissionResults permission_results(permissions,
                                                permission_statuses);
 
+  std::unordered_set<device::mojom::XRSessionFeature> rejected_features;
   for (auto& required_feature : request.required_features) {
     if (!permission_results.HasPermissionsFor(required_feature)) {
       DVLOG(1) << __func__ << ": required_feature=" << required_feature
                << " lacks neccessary permissions";
-      std::move(request.callback)
-          .Run(device::mojom::RequestSessionResult::NewFailureReason(
-              device::mojom::RequestSessionError::USER_DENIED_CONSENT));
-      return;
+
+      rejected_features.insert(required_feature);
     }
   }
 
+  if (!rejected_features.empty()) {
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::USER_DENIED_CONSENT,
+                  "Lacks necessary permissions for the required feature.",
+                  &rejected_features);
+    return;
+  }
+
   std::unordered_set<device::mojom::XRSessionFeature> granted_optional_features;
 
   for (auto& optional_feature : request.optional_features) {
@@ -632,9 +673,10 @@
   // Re-check for another client instance after a potential user consent.
   if (runtime_manager_->IsOtherClientPresenting(this)) {
     // Can't create sessions while an immersive session exists.
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION));
+    RejectSession(
+        std::move(request.callback), request.options->trace_id,
+        device::mojom::RequestSessionError::EXISTING_IMMERSIVE_SESSION,
+        "Another client started presenting while waiting for permissions.");
     return;
   }
 
@@ -655,9 +697,11 @@
              << ": failed to obtain the runtime or the runtime id does not "
                 "match the expected ID, request.runtime_id="
              << request.runtime_id;
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::RUNTIMES_CHANGED));
+
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::RUNTIMES_CHANGED,
+                  "failed to obtain the runtime or the runtime id does not "
+                  "match the expected ID.");
     return;
   }
 
@@ -673,9 +717,9 @@
   DVLOG(2) << __func__ << ": install_succeeded=" << install_succeeded;
 
   if (!install_succeeded) {
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::RUNTIME_INSTALL_FAILURE,
+                  "Runtime installation failed.");
     return;
   }
 
@@ -702,9 +746,9 @@
     TRACE_EVENT("xr", "VRServiceImpl::DoRequestSession: mismatching runtime",
                 perfetto::Flow::Global(request.options->trace_id));
 
-    std::move(request.callback)
-        .Run(device::mojom::RequestSessionResult::NewFailureReason(
-            device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR));
+    RejectSession(std::move(request.callback), request.options->trace_id,
+                  device::mojom::RequestSessionError::UNKNOWN_RUNTIME_ERROR,
+                  "Mismatching runtime or invalid runtime.");
     return;
   }
 
diff --git a/content/browser/xr/webxr_internals/mojom/webxr_internals.mojom b/content/browser/xr/webxr_internals/mojom/webxr_internals.mojom
index f16a16c..e9fc92a 100644
--- a/content/browser/xr/webxr_internals/mojom/webxr_internals.mojom
+++ b/content/browser/xr/webxr_internals/mojom/webxr_internals.mojom
@@ -15,12 +15,36 @@
     string gpu_gl_renderer;
 };
 
-// Human readable information about the session request
-struct SessionRequestRecord {
+// Human readable information about the session requested record.
+struct SessionRequestedRecord {
     device.mojom.XRSessionOptions options;
     mojo_base.mojom.Time requested_time;
 };
 
+// Human readable information about the session rejected record.
+struct SessionRejectedRecord {
+    // ID of this request, used for tracing.
+    uint64 trace_id;
+    device.mojom.RequestSessionError failure_reason;
+    mojo_base.mojom.Time rejected_time;
+    string failure_reason_description;
+    array<device.mojom.XRSessionFeature> rejected_features;
+};
+
+// Human readable information about the session started record.
+struct SessionStartedRecord {
+    // ID of this request, used for tracing.
+    uint64 trace_id;
+    mojo_base.mojom.Time started_time;
+};
+
+// Human readable information about the session stopped record.
+struct SessionStoppedRecord {
+    // ID of this request, used for tracing.
+    uint64 trace_id;
+    mojo_base.mojom.Time stopped_time;
+};
+
 // Interface for controlling WebXR Internals.
 // Handles requests from "chrome://webxr-internals"
 // This is expected to be hosted in the browser process and is used from the
@@ -37,6 +61,15 @@
 // Used by webxr internals WebUI to query browser process about session
 // events.
 interface XRInternalsSessionListener {
-    // Begin listening for updates to add session request.
-    AddXrSessionRequest(SessionRequestRecord session_request_record);
+    // Begin listening for updates to log session requested record.
+    LogXrSessionRequested(SessionRequestedRecord session_requested_record);
+
+    // Begin listening for updates to log session rejected record.
+    LogXrSessionRejected(SessionRejectedRecord session_rejected_record);
+
+    // Begin listening for updates to log session started record.
+    LogXrSessionStarted(SessionStartedRecord session_started_record);
+
+    // Begin listening for updates to log session stopped record.
+    LogXrSessionStopped(SessionStoppedRecord session_stopped_record);
 };
\ No newline at end of file
diff --git a/content/browser/xr/webxr_internals/webxr_logger_manager.cc b/content/browser/xr/webxr_internals/webxr_logger_manager.cc
index 790d745..0af1fde 100644
--- a/content/browser/xr/webxr_internals/webxr_logger_manager.cc
+++ b/content/browser/xr/webxr_internals/webxr_logger_manager.cc
@@ -16,13 +16,40 @@
 
 WebXrLoggerManager::~WebXrLoggerManager() = default;
 
-void WebXrLoggerManager::RecordSessionRequest(
-    webxr::mojom::SessionRequestRecordPtr session_request_record) {
+void WebXrLoggerManager::RecordSessionRequested(
+    webxr::mojom::SessionRequestedRecordPtr session_requested_record) {
   for (const auto& remote : remote_set_) {
-    remote->AddXrSessionRequest(session_request_record->Clone());
+    remote->LogXrSessionRequested(session_requested_record->Clone());
   }
 
-  session_request_records_.push_back(std::move(session_request_record));
+  session_requested_records_.push_back(std::move(session_requested_record));
+}
+
+void WebXrLoggerManager::RecordSessionRejected(
+    webxr::mojom::SessionRejectedRecordPtr session_rejected_record) {
+  for (const auto& remote : remote_set_) {
+    remote->LogXrSessionRejected(session_rejected_record->Clone());
+  }
+
+  session_rejected_records_.push_back(std::move(session_rejected_record));
+}
+
+void WebXrLoggerManager::RecordSessionStarted(
+    webxr::mojom::SessionStartedRecordPtr session_started_record) {
+  for (const auto& remote : remote_set_) {
+    remote->LogXrSessionStarted(session_started_record->Clone());
+  }
+
+  session_started_records_.push_back(std::move(session_started_record));
+}
+
+void WebXrLoggerManager::RecordSessionStopped(
+    webxr::mojom::SessionStoppedRecordPtr session_stopped_record) {
+  for (const auto& remote : remote_set_) {
+    remote->LogXrSessionStopped(session_stopped_record->Clone());
+  }
+
+  session_stopped_records_.push_back(std::move(session_stopped_record));
 }
 
 void WebXrLoggerManager::SubscribeToEvents(
@@ -33,8 +60,20 @@
 
   // Send all previously received options to the remote before adding it to the
   // set.
-  for (const auto& request_record : session_request_records_) {
-    remote->AddXrSessionRequest(request_record->Clone());
+  for (const auto& requested_record : session_requested_records_) {
+    remote->LogXrSessionRequested(requested_record->Clone());
+  }
+
+  for (const auto& rejected_record : session_rejected_records_) {
+    remote->LogXrSessionRejected(rejected_record->Clone());
+  }
+
+  for (const auto& started_record : session_started_records_) {
+    remote->LogXrSessionStarted(started_record->Clone());
+  }
+
+  for (const auto& stopped_record : session_stopped_records_) {
+    remote->LogXrSessionStopped(stopped_record->Clone());
   }
 
   remote_set_.Add(std::move(remote));
diff --git a/content/browser/xr/webxr_internals/webxr_logger_manager.h b/content/browser/xr/webxr_internals/webxr_logger_manager.h
index d5cb9158..61d2a2cb 100644
--- a/content/browser/xr/webxr_internals/webxr_logger_manager.h
+++ b/content/browser/xr/webxr_internals/webxr_logger_manager.h
@@ -24,14 +24,26 @@
   WebXrLoggerManager(const WebXrLoggerManager&) = delete;
   WebXrLoggerManager& operator=(const WebXrLoggerManager&) = delete;
 
-  void RecordSessionRequest(
-      webxr::mojom::SessionRequestRecordPtr session_request_record);
+  void RecordSessionRequested(
+      webxr::mojom::SessionRequestedRecordPtr session_requested_record);
+  void RecordSessionRejected(
+      webxr::mojom::SessionRejectedRecordPtr session_rejected_record);
+  void RecordSessionStarted(
+      webxr::mojom::SessionStartedRecordPtr session_started_record);
+  void RecordSessionStopped(
+      webxr::mojom::SessionStoppedRecordPtr session_stopped_record);
+
   void SubscribeToEvents(
       mojo::PendingRemote<webxr::mojom::XRInternalsSessionListener>
           pending_remote);
 
  private:
-  std::vector<webxr::mojom::SessionRequestRecordPtr> session_request_records_;
+  std::vector<webxr::mojom::SessionRequestedRecordPtr>
+      session_requested_records_;
+  std::vector<webxr::mojom::SessionRejectedRecordPtr> session_rejected_records_;
+  std::vector<webxr::mojom::SessionStartedRecordPtr> session_started_records_;
+  std::vector<webxr::mojom::SessionStoppedRecordPtr> session_stopped_records_;
+
   mojo::RemoteSet<webxr::mojom::XRInternalsSessionListener> remote_set_;
 };
 
diff --git a/content/common/BUILD.gn b/content/common/BUILD.gn
index ec4235a..0c72e7f2 100644
--- a/content/common/BUILD.gn
+++ b/content/common/BUILD.gn
@@ -137,6 +137,8 @@
     "input/touch_event_stream_validator.h",
     "input/touchpad_tap_suppression_controller.cc",
     "input/touchpad_tap_suppression_controller.h",
+    "input/touchscreen_tap_suppression_controller.cc",
+    "input/touchscreen_tap_suppression_controller.h",
     "input/web_touch_event_traits.cc",
     "input/web_touch_event_traits.h",
     "media/cdm_info.cc",
diff --git a/content/browser/renderer_host/input/touchscreen_tap_suppression_controller.cc b/content/common/input/touchscreen_tap_suppression_controller.cc
similarity index 88%
rename from content/browser/renderer_host/input/touchscreen_tap_suppression_controller.cc
rename to content/common/input/touchscreen_tap_suppression_controller.cc
index 8131a02..1ef82e8 100644
--- a/content/browser/renderer_host/input/touchscreen_tap_suppression_controller.cc
+++ b/content/common/input/touchscreen_tap_suppression_controller.cc
@@ -2,12 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h"
+#include "content/common/input/touchscreen_tap_suppression_controller.h"
 
 #include <utility>
 
-#include "content/browser/renderer_host/input/gesture_event_queue.h"
-
 using blink::WebInputEvent;
 
 namespace content {
diff --git a/content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h b/content/common/input/touchscreen_tap_suppression_controller.h
similarity index 80%
rename from content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h
rename to content/common/input/touchscreen_tap_suppression_controller.h
index 87b7093..d8e0cb01 100644
--- a/content/browser/renderer_host/input/touchscreen_tap_suppression_controller.h
+++ b/content/common/input/touchscreen_tap_suppression_controller.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef CONTENT_BROWSER_RENDERER_HOST_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
-#define CONTENT_BROWSER_RENDERER_HOST_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
+#ifndef CONTENT_COMMON_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
+#define CONTENT_COMMON_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
 
 #include "content/common/input/event_with_latency_info.h"
 #include "content/common/input/tap_suppression_controller.h"
@@ -31,4 +31,4 @@
 
 }  // namespace content
 
-#endif  // CONTENT_BROWSER_RENDERER_HOST_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
+#endif  // CONTENT_COMMON_INPUT_TOUCHSCREEN_TAP_SUPPRESSION_CONTROLLER_H_
diff --git a/content/public/test/network_service_test_helper.cc b/content/public/test/network_service_test_helper.cc
index 9c659b0..f371ea9a 100644
--- a/content/public/test/network_service_test_helper.cc
+++ b/content/public/test/network_service_test_helper.cc
@@ -21,7 +21,6 @@
 #include "base/task/thread_pool.h"
 #include "base/threading/thread_restrictions.h"
 #include "build/build_config.h"
-#include "content/public/browser/network_service_util.h"
 #include "content/public/common/content_features.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/test_host_resolver.h"
@@ -67,8 +66,6 @@
 
 #if BUILDFLAG(IS_ANDROID)
 #include "base/test/android/url_utils.h"
-#include "net/dns/system_dns_config_change_notifier.h"
-#include "services/network/public/mojom/system_dns_config_observer.mojom.h"
 #endif
 
 namespace content {
@@ -420,27 +417,6 @@
   base::WeakPtrFactory<SimpleCache> weak_factory_{this};
 };
 
-#if BUILDFLAG(IS_ANDROID)
-class ProxySystemDnsConfigChangeObserver
-    : public net::SystemDnsConfigChangeNotifier::Observer {
- public:
-  explicit ProxySystemDnsConfigChangeObserver(
-      mojo::PendingRemote<network::mojom::SystemDnsConfigObserver>
-          mojo_observer)
-      : mojo_observer_(std::move(mojo_observer)) {}
-  virtual ~ProxySystemDnsConfigChangeObserver() = default;
-
-  void OnSystemDnsConfigChanged(
-      absl::optional<net::DnsConfig> config) override {
-    net::DnsConfig invalid_config;
-    mojo_observer_->OnConfigChanged(config ? *config : invalid_config);
-  }
-
- private:
-  mojo::Remote<network::mojom::SystemDnsConfigObserver> mojo_observer_;
-};
-#endif
-
 }  // namespace
 
 class NetworkServiceTestHelper::NetworkServiceTestImpl
@@ -613,25 +589,6 @@
         ->ReplaceSystemDnsConfigForTesting(std::move(callback));
   }
 
-#if BUILDFLAG(IS_ANDROID)
-  void AddSystemDnsConfigObserver(
-      mojo::PendingRemote<network::mojom::SystemDnsConfigObserver>
-          remote_observer,
-      AddSystemDnsConfigObserverCallback callback) override {
-    CHECK(!proxy_dns_config_observer_);
-
-    proxy_dns_config_observer_ =
-        std::make_unique<ProxySystemDnsConfigChangeObserver>(
-            std::move(remote_observer));
-    auto* system_dns_config_notifier =
-        net::NetworkChangeNotifier::GetSystemDnsConfigNotifier();
-    CHECK(system_dns_config_notifier);
-    system_dns_config_notifier->AddObserver(proxy_dns_config_observer_.get());
-
-    std::move(callback).Run();
-  }
-#endif
-
   void SetTestDohConfig(net::SecureDnsMode secure_dns_mode,
                         const net::DnsOverHttpsConfig& doh_config,
                         SetTestDohConfigCallback callback) override {
@@ -695,10 +652,7 @@
   void BindReceiver(
       mojo::PendingReceiver<network::mojom::NetworkServiceTest> receiver) {
     receivers_.Add(this, std::move(receiver));
-    if (base::CurrentIOThread::IsSet() &&
-        !registered_as_destruction_observer_) {
-      // Needs to be called on the IO thread.
-      // TODO(https://crbug.846445): Check this is still needed.
+    if (!registered_as_destruction_observer_) {
       base::CurrentIOThread::Get()->AddDestructionObserver(this);
       registered_as_destruction_observer_ = true;
     }
@@ -851,10 +805,6 @@
           base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_NONE;
   int write_result_;
   std::unique_ptr<disk_cache::Backend> disk_cache_backend_;
-#if BUILDFLAG(IS_ANDROID)
-  std::unique_ptr<ProxySystemDnsConfigChangeObserver>
-      proxy_dns_config_observer_;
-#endif
 
   base::WeakPtrFactory<NetworkServiceTestImpl> weak_factory_{this};
 };
@@ -891,15 +841,4 @@
       base::Unretained(this)));
 }
 
-std::unique_ptr<NetworkServiceTestHelper>
-NetworkServiceTestHelper::CreateInProcessReceiver(
-    mojo::PendingReceiver<network::mojom::NetworkServiceTest> receiver) {
-  CHECK(!base::CommandLine::ForCurrentProcess()->HasSwitch(
-            switches::kProcessType) &&
-        IsInProcessNetworkService());
-  std::unique_ptr<NetworkServiceTestHelper> helper(
-      new NetworkServiceTestHelper());
-  helper->network_service_test_impl_->BindReceiver(std::move(receiver));
-  return helper;
-}
 }  // namespace content
diff --git a/content/public/test/network_service_test_helper.h b/content/public/test/network_service_test_helper.h
index a4febeca..d6e6abf 100644
--- a/content/public/test/network_service_test_helper.h
+++ b/content/public/test/network_service_test_helper.h
@@ -7,8 +7,6 @@
 
 #include <memory>
 
-#include "mojo/public/cpp/bindings/pending_receiver.h"
-#include "services/network/public/mojom/network_service_test.mojom.h"
 #include "services/service_manager/public/cpp/binder_registry.h"
 
 namespace content {
@@ -23,17 +21,11 @@
   // If this returns an instance, it is ready to fulfill NetworkServiceTest
   // mojo interface requests within the network service.
   static std::unique_ptr<NetworkServiceTestHelper> Create();
-
   NetworkServiceTestHelper(const NetworkServiceTestHelper&) = delete;
   NetworkServiceTestHelper& operator=(const NetworkServiceTestHelper&) = delete;
 
   ~NetworkServiceTestHelper();
 
-  // Binds and returns the instance in the browser process. This assumes
-  // in-process network service.
-  static std::unique_ptr<NetworkServiceTestHelper> CreateInProcessReceiver(
-      mojo::PendingReceiver<network::mojom::NetworkServiceTest> receiver);
-
  private:
   class NetworkServiceTestImpl;
 
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index e467c15..ca03124 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1979,7 +1979,6 @@
       "../browser/android/synchronous_compositor_browsertest.cc",
       "../browser/date_time_chooser/android/date_time_chooser_browsertest.cc",
       "../browser/media/session/audio_focus_delegate_android_browsertest.cc",
-      "../browser/network/dns_config_android_browsertest.cc",
       "../browser/renderer_host/compositor_impl_android_browsertest.cc",
       "../shell/android/browsertests_apk/content_browser_tests_jni_onload.cc",
     ]
diff --git a/content/test/data/gpu/pixel_webgl_float.html b/content/test/data/gpu/pixel_webgl_float.html
index 012ee00..e834231f 100644
--- a/content/test/data/gpu/pixel_webgl_float.html
+++ b/content/test/data/gpu/pixel_webgl_float.html
@@ -35,7 +35,7 @@
 function drawQuad(gl, color) {
   let ext = gl.getExtension('EXT_color_buffer_half_float');
   if (!ext) {
-    send("FAILURE", "EXT_color_buffer_half_float not supported");
+    sendResult("FAILURE", "EXT_color_buffer_half_float not supported");
     return;
   }
 
diff --git a/content/test/gpu/gpu_tests/test_expectations/context_lost_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/context_lost_expectations.txt
index 7a40047e..5a37b74 100644
--- a/content/test/gpu/gpu_tests/test_expectations/context_lost_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/context_lost_expectations.txt
@@ -198,7 +198,6 @@
 
 crbug.com/1469875 [ mac graphite-enabled intel release asan ] GpuCrash_InfoForHardwareGpu [ Failure ]
 
-crbug.com/1484919 [ ventura intel-0x3e9b angle-metal graphite-enabled ] ContextLost_WebGLContextLostFromQuantity [ RetryOnFailure ]
 crbug.com/1484919 [ ventura intel-0x3e9b angle-metal graphite-enabled ] ContextLost_WebGLContextRestoredInHiddenTab [ RetryOnFailure ]
 
 #######################################################################
diff --git a/content/test/gpu/gpu_tests/test_expectations/gpu_process_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/gpu_process_expectations.txt
index 1bf5657..5b671dc 100644
--- a/content/test/gpu/gpu_tests/test_expectations/gpu_process_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/gpu_process_expectations.txt
@@ -137,7 +137,7 @@
 ###################
 # Non-"Skip" expectations go here to suppress regular flakes/failures.
 
-
+crbug.com/1487800 [ android android-sm-a135m ] GpuProcess_visibility [ RetryOnFailure ]
 
 
 
diff --git a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
index b5702f6..ed9d961 100644
--- a/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/pixel_expectations.txt
@@ -339,8 +339,6 @@
 
 crbug.com/1345777 [ android ] Pixel_VideoStreamFromWebGLCanvas [ Skip ]
 
-# Nexus 5x failure while using DMSAA for Tiles.
-crbug.com/1479154 [ android android-nexus-5x no-clang-coverage no-passthrough ] Pixel_GpuRasterization_ConcavePaths [ Failure ]
 
 # WebGPU interop fails -- pixel color diff
 crbug.com/1395227 [ amd-0x7340 no-clang-coverage release-x64 target-cpu-64 win ] Pixel_WebGPUImportVideoFrameUnaccelerated [ Failure ]
@@ -368,10 +366,11 @@
 crbug.com/1392806 [ chromeos lacros-chrome ] Pixel_WebGLContextRestored [ Skip ]
 crbug.com/1392806 [ chromeos lacros-chrome ] Pixel_WebGLPreservedAfterTabSwitch [ Skip ]
 
-# Lacros on jacuzzi produces the wrong aspect ratio image
-crbug.com/1392806 [ chromeos lacros-chrome chromeos-board-jacuzzi ] Pixel_Video_MP4_FourColors_Aspect_4x3 [ Failure ]
+# jacuzzi produces the wrong aspect ratio image
+crbug.com/1392806 [ chromeos chromeos-board-jacuzzi ] Pixel_Video_MP4_FourColors_Aspect_4x3 [ Failure ]
 
-
+# ChromeOS jacuzzi.
+crbug.com/1286915 [ chromeos cros-chrome chromeos-board-jacuzzi ] Pixel_CanvasLowLatencyWebGLAlphaFalse [ Failure ]
 
 crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLCopyImage [ Skip ]
 crbug.com/1456360 [ chromeos chromeos-board-amd64-generic ] Pixel_WebGLSadCanvas [ Skip ]
@@ -384,7 +383,7 @@
 crbug.com/1469875 [ mac graphite-enabled ] Pixel_BackgroundImage [ Failure ]
 crbug.com/1469875 [ mac graphite-enabled ] Pixel_CSSFilterEffects [ Failure ]
 crbug.com/1469875 [ mac graphite-enabled ] Pixel_CSSFilterEffects_NoOverlays [ Failure ]
-crbug.com/1469875 [ mac graphite-enabled ] Pixel_OffscreenCanvasTransferAfterStyleResize [ Failure ]
+crbug.com/1469875 [ amd-0x679e graphite-enabled mac-x86_64 monterey no-asan ] Pixel_OffscreenCanvasTransferAfterStyleResize [ Failure ]
 crbug.com/1469875 [ mac graphite-enabled amd release no-asan ] Pixel_SVGHuge [ Failure ]
 crbug.com/1469875 [ mac graphite-enabled apple ] Pixel_SVGHuge [ Failure ]
 crbug.com/1469875 [ mac graphite-enabled intel release no-asan ] Pixel_SVGHuge [ Failure ]
@@ -394,7 +393,6 @@
 crbug.com/1469875 [ apple graphite-enabled mac-arm64 no-asan ventura ] Pixel_WebGPUImportVideoFrameUnacceleratedOffscreenCanvas [ Failure ]
 
 crbug.com/1474611 [ mac nvidia angle-opengl graphite-disabled ] Pixel_WebGLGreenTriangle_NonChromiumImage_NoAA_NoAlpha [ Failure ]
-crbug.com/1476029 [ mac asan graphite-enabled ] Pixel_Video_BackdropFilter [ Failure ]
 crbug.com/1477318 [ mac ] Pixel_WebGLPreservedAfterTabSwitch [ RetryOnFailure ]
 
 #######################################################################
diff --git a/content/test/gpu/gpu_tests/test_expectations/trace_test_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/trace_test_expectations.txt
index 6ccbb3db..178cdb0 100644
--- a/content/test/gpu/gpu_tests/test_expectations/trace_test_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/trace_test_expectations.txt
@@ -243,6 +243,9 @@
 # Win AMD failure since test added
 crbug.com/1446057 [ win10 amd-0x7340 angle-d3d11 ] VideoPathTraceTest_DirectComposition_Video_SW_Decode [ Failure ]
 
+# Android devices
+crbug.com/1487723 [ android android-shield-android-tv ] TraceTest_CSS3DBlueBox [ RetryOnFailure ]
+crbug.com/1487723 [ android android-shield-android-tv ] TraceTest_SolidColorBackground [ RetryOnFailure ]
 
 #######################################################################
 # Automated Entries After This Point - Do Not Manually Add Below Here #
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
index 04a3fcd..743bbb5 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl2_conformance_expectations.txt
@@ -540,7 +540,6 @@
 crbug.com/1421437 [ mac amd-0x679e angle-metal ] conformance2/extensions/webgl-shader-pixel-local-storage.html [ Failure ]
 crbug.com/1487266 [ mac passthrough angle-metal amd release ] deqp/functional/gles3/texturefiltering/2d_array_combinations_05.html [ Failure ]
 
-crbug.com/641209 [ mac amd-0x679e angle-metal ] deqp/functional/gles3/fbomultisample* [ Failure ]
 crbug.com/1474883 [ mac amd angle-metal ] deqp/functional/gles3/texturespecification/teximage3d_depth.html [ Failure ]
 
 ## Metal Intel ##
diff --git a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
index 609440c..f8c1b83f9 100644
--- a/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
+++ b/content/test/gpu/gpu_tests/test_expectations/webgl_conformance_expectations.txt
@@ -416,7 +416,6 @@
 crbug.com/1175371 [ angle-opengl display-server-x linux passthrough renderer-skia-gl ] conformance/extensions/khr-parallel-shader-compile.html [ Failure ]
 crbug.com/1175371 [ angle-opengles display-server-wayland linux passthrough ] conformance/extensions/khr-parallel-shader-compile.html [ Failure ]
 crbug.com/1175371 [ mac angle-metal ] conformance/extensions/khr-parallel-shader-compile.html [ Failure ]
-crbug.com/1175371 [ mac angle-opengl ] conformance/extensions/khr-parallel-shader-compile.html [ Failure ]
 crbug.com/1175371 [ win ] conformance/extensions/khr-parallel-shader-compile.html [ Failure ]
 
 # Won't investigate failure on validating command decoder. Remove once it's unshipped.
@@ -482,7 +481,6 @@
 # all tests for the moment so that the test suite can remain enabled on this bot.
 crbug.com/1446091 [ win amd-0x7340 angle-d3d9 passthrough ] conformance/* [ RetryOnFailure ]
 
-crbug.com/1159126 [ win amd-0x7340 angle-d3d9 passthrough ] conformance/ogles/GL/* [ RetryOnFailure ]
 crbug.com/1159126 [ win amd-0x7340 angle-d3d9 passthrough ] conformance/uniforms/* [ RetryOnFailure ]
 crbug.com/1361088 [ win amd-0x7340 angle-d3d9 passthrough ] conformance/textures/webgl_canvas/tex-2d-rgb-rgb-unsigned_byte.html [ Skip ]
 crbug.com/1361088 [ win amd-0x7340 angle-d3d9 passthrough ] conformance/textures/webgl_canvas/tex-2d-rgba-rgba-unsigned_byte.html [ Skip ]
@@ -660,7 +658,7 @@
 # TODO(crbug.com/1276153) uncomment after fix for updated part of test applies
 # crbug.com/957807 [ chromeos ] conformance/context/context-attributes-alpha-depth-stencil-antialias.html [ Failure ]
 
-crbug.com/957807 [ chromeos chromeos-board-amd64-generic mesa_ge_21.0 target-cpu-64 ] conformance/uniforms/uniform-samplers-test.html [ Failure ]
+crbug.com/957807 [ angle-opengles chromeos chromeos-board-amd64-generic mesa_ge_21.0 passthrough target-cpu-64 ] conformance/uniforms/uniform-samplers-test.html [ Failure ]
 
 # finder:disable-unused Reported by Intel but not currently testable in Chromium
 crbug.com/1237319 [ chromeos chromeos-board-eve passthrough intel ] conformance/textures/misc/texture-size-limit.html [ Failure ]
diff --git a/content/test/gpu/gpu_tests/webgpu_cts_integration_test_base.py b/content/test/gpu/gpu_tests/webgpu_cts_integration_test_base.py
index 5c7ae04e..47ec762d 100644
--- a/content/test/gpu/gpu_tests/webgpu_cts_integration_test_base.py
+++ b/content/test/gpu/gpu_tests/webgpu_cts_integration_test_base.py
@@ -55,6 +55,7 @@
 JAVASCRIPT_DURATION = 'javascript_duration'
 MAY_EXONERATE = 'may_exonerate'
 MESSAGE_TYPE_CONNECTION_ACK = 'CONNECTION_ACK'
+MESSAGE_TYPE_INFRA_FAILURE = 'INFRA_FAILURE'
 MESSAGE_TYPE_TEST_STARTED = 'TEST_STARTED'
 MESSAGE_TYPE_TEST_HEARTBEAT = 'TEST_HEARTBEAT'
 MESSAGE_TYPE_TEST_STATUS = 'TEST_STATUS'
@@ -387,6 +388,7 @@
       return SLOW_MULTIPLIER
     return 1
 
+  #pylint: disable=too-many-branches
   def HandleMessageLoop(self, first_load) -> WebGpuTestResult:
     """Helper function to handle the loop for the message protocol.
 
@@ -448,6 +450,9 @@
               'Hit %.3f second global timeout. Message state: %s' %
               (global_timeout, message_state))
 
+        if response_type == MESSAGE_TYPE_INFRA_FAILURE:
+          self.fail(response['message'])
+
         if response_type == MESSAGE_TYPE_TEST_STARTED:
           # If we ever want the adapter information from WebGPU, we would
           # retrieve it from the message here. However, to avoid pylint
@@ -489,6 +494,7 @@
       finally:
         self._test_duration = time.time() - start_time
     return result
+  # pylint: enable=too-many-branches
 
   def HandleDurationTagOnFailure(self, message_state: Dict[str, bool],
                                  test_timeout: float) -> None:
diff --git a/content/utility/BUILD.gn b/content/utility/BUILD.gn
index 27bd16a..6930232 100644
--- a/content/utility/BUILD.gn
+++ b/content/utility/BUILD.gn
@@ -63,6 +63,9 @@
     "//services/data_decoder:lib",
     "//services/data_decoder/public/cpp",
     "//services/network:network_service",
+    "//services/on_device_model:on_device_model_service",
+    "//services/on_device_model/public/cpp",
+    "//services/on_device_model/public/mojom",
     "//services/service_manager/public/cpp",
     "//services/service_manager/public/mojom",
     "//services/shape_detection:lib",
diff --git a/content/utility/DEPS b/content/utility/DEPS
index 23a44487..aeb8b1b 100644
--- a/content/utility/DEPS
+++ b/content/utility/DEPS
@@ -14,6 +14,7 @@
   "+services/audio",
   "+services/data_decoder",
   "+services/network",
+  "+services/on_device_model",
   "+services/proxy_resolver",
   "+services/service_manager",
   "+services/shape_detection",
diff --git a/content/utility/services.cc b/content/utility/services.cc
index 64501098..bbaf0f0a 100644
--- a/content/utility/services.cc
+++ b/content/utility/services.cc
@@ -7,6 +7,7 @@
 #include <utility>
 
 #include "base/command_line.h"
+#include "base/feature_list.h"
 #include "base/lazy_instance.h"
 #include "base/task/single_thread_task_runner.h"
 #include "build/branding_buildflags.h"
@@ -29,6 +30,8 @@
 #include "services/audio/service_factory.h"
 #include "services/data_decoder/data_decoder_service.h"
 #include "services/network/network_service.h"
+#include "services/on_device_model/on_device_model_service.h"
+#include "services/on_device_model/public/cpp/features.h"
 #include "services/tracing/public/mojom/tracing_service.mojom.h"
 #include "services/tracing/tracing_service.h"
 #include "services/video_capture/public/mojom/video_capture_service.mojom.h"
@@ -322,6 +325,13 @@
   return service;
 }
 
+auto RunOnDeviceModel(
+    mojo::PendingReceiver<on_device_model::mojom::OnDeviceModelService>
+        receiver) {
+  return std::make_unique<on_device_model::OnDeviceModelService>(
+      std::move(receiver));
+}
+
 #if BUILDFLAG(ENABLE_VR) && !BUILDFLAG(IS_ANDROID)
 auto RunXrDeviceService(
     mojo::PendingReceiver<device::mojom::XRDeviceService> receiver) {
@@ -393,6 +403,11 @@
   services.Add(RunTracing);
   services.Add(RunVideoCapture);
 
+  if (base::FeatureList::IsEnabled(
+          on_device_model::features::kOnDeviceModelService)) {
+    services.Add(RunOnDeviceModel);
+  }
+
 #if BUILDFLAG(GOOGLE_CHROME_BRANDING) && BUILDFLAG(IS_CHROMEOS)
   services.Add(RunShapeDetectionService);
 #endif
diff --git a/device/fido/enclave/enclave_authenticator.cc b/device/fido/enclave/enclave_authenticator.cc
index cc08df32..af2aa6d 100644
--- a/device/fido/enclave/enclave_authenticator.cc
+++ b/device/fido/enclave/enclave_authenticator.cc
@@ -9,6 +9,7 @@
 #include <utility>
 
 #include "base/functional/bind.h"
+#include "base/functional/callback.h"
 #include "base/json/json_writer.h"
 #include "base/ranges/algorithm.h"
 #include "components/device_event_log/device_event_log.h"
@@ -38,16 +39,31 @@
 
 }  // namespace
 
+EnclaveAuthenticator::PendingGetAssertionRequest::PendingGetAssertionRequest(
+    const CtapGetAssertionRequest& in_request,
+    const CtapGetAssertionOptions& in_options,
+    GetAssertionCallback in_callback)
+    : request(in_request),
+      options(in_options),
+      callback(std::move(in_callback)) {}
+EnclaveAuthenticator::PendingGetAssertionRequest::
+    ~PendingGetAssertionRequest() = default;
+
 EnclaveAuthenticator::EnclaveAuthenticator(
     const GURL& service_url,
     base::span<const uint8_t, kP256X962Length> peer_identity,
-    std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys)
+    std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys,
+    std::vector<uint8_t> device_id,
+    const std::string& username,
+    RequestSigningCallback request_signing_callback)
     : peer_identity_(fido_parsing_utils::Materialize(peer_identity)),
-      available_passkeys_(std::move(passkeys)) {
+      available_passkeys_(std::move(passkeys)),
+      device_id_(std::move(device_id)),
+      request_signing_callback_(request_signing_callback) {
   // base::Unretained is safe because this class owns http_client_, which
   // holds this callback.
   http_client_ = std::make_unique<EnclaveHttpClient>(
-      service_url,
+      service_url, username,
       base::BindRepeating(&EnclaveAuthenticator::OnResponseReceived,
                           base::Unretained(this)));
 }
@@ -67,20 +83,11 @@
 void EnclaveAuthenticator::GetAssertion(CtapGetAssertionRequest request,
                                         CtapGetAssertionOptions options,
                                         GetAssertionCallback callback) {
-  CHECK(!pending_get_assertion_callback_);
+  CHECK(!pending_get_assertion_request_);
   CHECK(request.allow_list.size() == 1);
-  const std::string selected_credential_id(request.allow_list[0].id.begin(),
-                                           request.allow_list[0].id.end());
-  auto found_passkey_it =
-      std::find_if(available_passkeys_.begin(), available_passkeys_.end(),
-                   [&](const auto& passkey) {
-                     return selected_credential_id == passkey.credential_id();
-                   });
-  CHECK(found_passkey_it != available_passkeys_.end());
 
-  BuildGetAssertionRequestBody(*found_passkey_it, std::move(options.json),
-                               &pending_request_body_);
-  pending_get_assertion_callback_ = std::move(callback);
+  pending_get_assertion_request_ = std::make_unique<PendingGetAssertionRequest>(
+      request, options, std::move(callback));
 
   if (state_ == State::kInitialized) {
     // Connect to the enclave service now.
@@ -103,27 +110,25 @@
     absl::optional<std::vector<uint8_t>> data) {
   if (status != net::OK) {
     FIDO_LOG(ERROR) << "Message to enclave service failed: [" << status << "]";
-    if (pending_get_assertion_callback_) {
-      std::move(pending_get_assertion_callback_)
-          .Run(CtapDeviceResponseCode::kCtap2ErrOther, {});
+    if (pending_get_assertion_request_) {
+      CompleteGetAssertionRequest(CtapDeviceResponseCode::kCtap2ErrOther, {});
     }
     return;
   }
   CHECK(data.has_value());
 
   if (state_ == State::kWaitingForHandshakeResponse) {
-    CHECK(pending_get_assertion_callback_);
     cablev2::HandshakeResult result = handshake_->ProcessResponse(*data);
     handshake_.reset();
 
     if (!result) {
       FIDO_LOG(ERROR) << "Enclave connection handshake failed.";
-      std::move(pending_get_assertion_callback_)
-          .Run(CtapDeviceResponseCode::kCtap2ErrOther, {});
+      CompleteGetAssertionRequest(CtapDeviceResponseCode::kCtap2ErrOther, {});
       return;
     }
     crypter_ = std::move(result->first);
     handshake_hash_ = result->second;
+
     state_ = State::kConnected;
 
     SendCommand();
@@ -132,45 +137,63 @@
     std::vector<uint8_t> plaintext;
     if (!crypter_->Decrypt(*data, &plaintext)) {
       FIDO_LOG(ERROR) << "Response from enclave failed to decrypt.";
-      std::move(pending_get_assertion_callback_)
-          .Run(CtapDeviceResponseCode::kCtap2ErrOther, {});
+      CompleteGetAssertionRequest(CtapDeviceResponseCode::kCtap2ErrOther, {});
       return;
     }
-    std::string plaintext_json(plaintext.begin(), plaintext.end());
-    auto decode_result =
-        AuthenticatorGetAssertionResponseFromJson(plaintext_json);
-    if (!decode_result.first) {
-      FIDO_LOG(ERROR) << "Failed to parse decrypted JSON: "
-                      << decode_result.second;
-      std::move(pending_get_assertion_callback_)
-          .Run(CtapDeviceResponseCode::kCtap2ErrOther, {});
+
+    if (pending_get_assertion_request_) {
+      // TODO(kenrb): Add handling of MakeCredential responses.
+      auto decode_result = ParseGetAssertionResponse(plaintext);
+      if (!decode_result.first) {
+        FIDO_LOG(ERROR) << "Error in response received from server: "
+                        << decode_result.second;
+        CompleteGetAssertionRequest(CtapDeviceResponseCode::kCtap2ErrOther, {});
+      }
+      std::vector<AuthenticatorGetAssertionResponse> responses;
+      responses.emplace_back(*std::move(decode_result.first));
+      CompleteGetAssertionRequest(CtapDeviceResponseCode::kSuccess,
+                                  std::move(responses));
     }
-    std::vector<AuthenticatorGetAssertionResponse> responses;
-    responses.emplace_back(*std::move(decode_result.first));
-    std::move(pending_get_assertion_callback_)
-        .Run(CtapDeviceResponseCode::kSuccess, std::move(responses));
     return;
   }
   NOTREACHED() << "State is " << static_cast<int>(state_);
 }
 
 void EnclaveAuthenticator::SendCommand() {
-  std::vector<uint8_t> request_bytes(pending_request_body_.begin(),
-                                     pending_request_body_.end());
-  if (!crypter_->Encrypt(&request_bytes)) {
+  CHECK(pending_get_assertion_request_);
+  CHECK(handshake_hash_);
+
+  const auto& request = pending_get_assertion_request_->request;
+  const std::string selected_credential_id(request.allow_list[0].id.begin(),
+                                           request.allow_list[0].id.end());
+  auto found_passkey_it =
+      std::find_if(available_passkeys_.begin(), available_passkeys_.end(),
+                   [&](const auto& passkey) {
+                     return selected_credential_id == passkey.credential_id();
+                   });
+  CHECK(found_passkey_it != available_passkeys_.end());
+  std::vector<uint8_t> request_body = BuildCommandRequestBody(
+      base::BindOnce(&BuildGetAssertionCommand, *found_passkey_it,
+                     std::move(pending_get_assertion_request_->options.json),
+                     request.client_data_json, request.rp_id),
+      request_signing_callback_, *handshake_hash_, device_id_);
+
+  if (!crypter_->Encrypt(&request_body)) {
     FIDO_LOG(ERROR) << "Failed to encrypt command to enclave service.";
-    std::move(pending_get_assertion_callback_)
-        .Run(CtapDeviceResponseCode::kCtap2ErrOther, {});
+    CompleteGetAssertionRequest(CtapDeviceResponseCode::kCtap2ErrOther, {});
     return;
   }
 
-  // TODO(kenrb): |request_bytes| has to be signed by the registered key in
-  // secure storage, so the enclave service knows this came from an authorized
-  // device. Alternatively there could be a WebAuthn challenge to the
-  // platform authenticator. This is TBD.
-
   http_client_->SendHttpRequest(EnclaveHttpClient::RequestType::kCommand,
-                                request_bytes);
+                                request_body);
+}
+
+void EnclaveAuthenticator::CompleteGetAssertionRequest(
+    CtapDeviceResponseCode status,
+    std::vector<AuthenticatorGetAssertionResponse> responses) {
+  std::move(pending_get_assertion_request_->callback)
+      .Run(status, std::move(responses));
+  pending_get_assertion_request_.reset();
 }
 
 void EnclaveAuthenticator::Cancel() {
diff --git a/device/fido/enclave/enclave_authenticator.h b/device/fido/enclave/enclave_authenticator.h
index 8383a06..e9405a9f 100644
--- a/device/fido/enclave/enclave_authenticator.h
+++ b/device/fido/enclave/enclave_authenticator.h
@@ -36,10 +36,19 @@
 class COMPONENT_EXPORT(DEVICE_FIDO) EnclaveAuthenticator
     : public FidoAuthenticator {
  public:
+  // The first argument is the handshake_hash, the second is the data that will
+  // be signed.
+  using RequestSigningCallback =
+      base::RepeatingCallback<std::vector<uint8_t>(base::span<const uint8_t>,
+                                                   base::span<const uint8_t>)>;
+
   EnclaveAuthenticator(
       const GURL& service_url,
       base::span<const uint8_t, device::kP256X962Length> peer_identity,
-      std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys);
+      std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys,
+      std::vector<uint8_t> device_id,
+      const std::string& username,
+      RequestSigningCallback request_signing_callback);
   ~EnclaveAuthenticator() override;
 
   EnclaveAuthenticator(const EnclaveAuthenticator&) = delete;
@@ -58,6 +67,20 @@
     kError,
   };
 
+  struct PendingGetAssertionRequest {
+    PendingGetAssertionRequest(const CtapGetAssertionRequest&,
+                               const CtapGetAssertionOptions&,
+                               GetAssertionCallback);
+    ~PendingGetAssertionRequest();
+    PendingGetAssertionRequest(const PendingGetAssertionRequest&) = delete;
+    PendingGetAssertionRequest& operator=(const PendingGetAssertionRequest&) =
+        delete;
+
+    CtapGetAssertionRequest request;
+    CtapGetAssertionOptions options;
+    GetAssertionCallback callback;
+  };
+
   // FidoAuthenticator:
   void InitializeAuthenticator(base::OnceClosure callback) override;
   void MakeCredential(CtapMakeCredentialRequest request,
@@ -73,6 +96,9 @@
   void OnResponseReceived(int status,
                           absl::optional<std::vector<uint8_t>> data);
   void SendCommand();
+  void CompleteGetAssertionRequest(
+      CtapDeviceResponseCode status,
+      std::vector<AuthenticatorGetAssertionResponse> responses);
 
   State state_ = State::kInitialized;
 
@@ -81,15 +107,23 @@
   // The peer's public key.
   const std::array<uint8_t, device::kP256X962Length> peer_identity_;
 
+  // Synced passkeys available for this account. Calls to |GetAssertion| must
+  // identify one from this list in the request's allowCredentials.
+  std::vector<sync_pb::WebauthnCredentialSpecifics> available_passkeys_;
+
+  // Identifier for this device, previously registered to the enclave.
+  std::vector<uint8_t> device_id_;
+
+  // Callback for signing requests with the device-bound key.
+  RequestSigningCallback request_signing_callback_;
+
+  // Fields for establishing and using the encrypted channel.
   std::unique_ptr<cablev2::HandshakeInitiator> handshake_;
   absl::optional<std::array<uint8_t, 32>> handshake_hash_;
   std::unique_ptr<cablev2::Crypter> crypter_;
 
-  // GetAssertion arguments while waiting for the connection to be established.
-  std::string pending_request_body_;
-  GetAssertionCallback pending_get_assertion_callback_;
-
-  std::vector<sync_pb::WebauthnCredentialSpecifics> available_passkeys_;
+  // Caches the request while waiting for the connection to be established.
+  std::unique_ptr<PendingGetAssertionRequest> pending_get_assertion_request_;
 
   base::WeakPtrFactory<EnclaveAuthenticator> weak_factory_{this};
 };
diff --git a/device/fido/enclave/enclave_discovery.cc b/device/fido/enclave/enclave_discovery.cc
index ca901d3..f9fb2f8c 100644
--- a/device/fido/enclave/enclave_discovery.cc
+++ b/device/fido/enclave/enclave_discovery.cc
@@ -4,7 +4,11 @@
 
 #include "device/fido/enclave/enclave_discovery.h"
 
+#include <vector>
+
+#include "base/containers/span.h"
 #include "base/functional/bind.h"
+#include "base/functional/callback.h"
 #include "base/task/sequenced_task_runner.h"
 #include "device/fido/enclave/enclave_authenticator.h"
 #include "url/gurl.h"
@@ -37,14 +41,17 @@
   // TODO(kenrb): These temporary hard-coded values will be replaced by real
   // values, plumbed from chrome layer.
   static GURL localUrl = GURL("http://127.0.0.1:8880");
+  static char testUsername[] = "testuser";
   static uint8_t peerPublicKey[kP256X962Length] = {
       4,   244, 60,  222, 80,  52,  238, 134, 185, 2,   84,  48,  248,
       87,  211, 219, 145, 204, 130, 45,  180, 44,  134, 205, 239, 90,
       127, 34,  229, 225, 93,  163, 51,  206, 28,  47,  134, 238, 116,
       86,  252, 239, 210, 98,  147, 46,  198, 87,  75,  254, 37,  114,
       179, 110, 145, 23,  34,  208, 25,  171, 184, 129, 14,  84,  80};
+  std::vector<uint8_t> device_id = {1, 2, 3, 4};
   authenticator_ = std::make_unique<EnclaveAuthenticator>(
-      localUrl, peerPublicKey, std::move(passkeys_));
+      localUrl, peerPublicKey, std::move(passkeys_), std::move(device_id),
+      testUsername, EnclaveAuthenticator::RequestSigningCallback());
   observer()->DiscoveryStarted(this, /*success=*/true, {authenticator_.get()});
 }
 
diff --git a/device/fido/enclave/enclave_http_client.cc b/device/fido/enclave/enclave_http_client.cc
index 8331a78..0c8d2c4c 100644
--- a/device/fido/enclave/enclave_http_client.cc
+++ b/device/fido/enclave/enclave_http_client.cc
@@ -82,8 +82,11 @@
 }  // namespace
 
 EnclaveHttpClient::EnclaveHttpClient(const GURL& service_url,
+                                     const std::string& username,
                                      RequestCallback on_request_done)
-    : service_url_(service_url), on_request_done_(std::move(on_request_done)) {
+    : service_url_(service_url),
+      username_(username),
+      on_request_done_(std::move(on_request_done)) {
   net::URLRequestContextBuilder builder;
   builder.DisableHttpCache();
   builder.set_proxy_config_service(
@@ -110,16 +113,20 @@
   GURL::Replacements replacement;
 
   switch (type) {
-    case RequestType::kInit:
-      replacement.SetPathStr(kInitPath);
+    case RequestType::kInit: {
+      std::string path = base::StrCat({username_, "/", kInitPath});
+      replacement.SetPathStr(path);
       request_url = service_url_.ReplaceComponents(replacement);
       BuildInitBody(data);
       break;
-    case RequestType::kCommand:
-      replacement.SetPathStr(kCommandPath);
+    }
+    case RequestType::kCommand: {
+      std::string path = base::StrCat({username_, "/", kCommandPath});
+      replacement.SetPathStr(path);
       request_url = service_url_.ReplaceComponents(replacement);
       BuildCommandBody(data);
       break;
+    }
     case RequestType::kNone:
       NOTREACHED();
   }
diff --git a/device/fido/enclave/enclave_http_client.h b/device/fido/enclave/enclave_http_client.h
index 5d1109a..b26d93b 100644
--- a/device/fido/enclave/enclave_http_client.h
+++ b/device/fido/enclave/enclave_http_client.h
@@ -32,7 +32,9 @@
     kCommand,
   };
 
-  EnclaveHttpClient(const GURL& service_url, RequestCallback on_request_done);
+  EnclaveHttpClient(const GURL& service_url,
+                    const std::string& username,
+                    RequestCallback on_request_done);
   ~EnclaveHttpClient() override;
 
   EnclaveHttpClient(const EnclaveHttpClient&) = delete;
@@ -67,6 +69,7 @@
   RequestType request_in_progress_ = RequestType::kNone;
 
   GURL service_url_;
+  std::string username_;
   RequestCallback on_request_done_;
 
   // url_request_ has to be before url_request_context_ for destruction
diff --git a/device/fido/enclave/enclave_protocol_utils.cc b/device/fido/enclave/enclave_protocol_utils.cc
index e1e75c2..40764e7 100644
--- a/device/fido/enclave/enclave_protocol_utils.cc
+++ b/device/fido/enclave/enclave_protocol_utils.cc
@@ -7,6 +7,7 @@
 #include <array>
 
 #include "base/base64url.h"
+#include "base/functional/callback.h"
 #include "base/json/json_reader.h"
 #include "base/json/json_writer.h"
 #include "base/numerics/safe_conversions.h"
@@ -22,23 +23,40 @@
 #include "device/fido/fido_transport_protocol.h"
 #include "device/fido/json_request.h"
 #include "device/fido/public_key_credential_user_entity.h"
+#include "device/fido/value_response_conversions.h"
 
 namespace device::enclave {
 
 namespace {
 
 // JSON keys for front-end service HTTP request bodies.
-const char kCommandRequestCommandTag[] = "command";
+const char kCommandRequestCommandKey[] = "command";
 
-// JSON keys for command.
-const char kCommandEncodedRequestsTag[] = "encoded_requests";
+// JSON keys for command issuance.
+const char kCommandEncodedRequestsKey[] = "encoded_requests";
+const char kCommandDeviceIdKey[] = "device_id";
+const char kCommandSigKey[] = "sig";
+const char kCommandAuthLevelKey[] = "auth_level";
 
 // JSON keys for GetAssertion request fields.
-const char kGetAssertionRequestCommandTag[] = "cmd";
-const char kGetAssertionRequestDataTag[] = "data";
-const char kGetAssertionRequestEntityTag[] = "entity";
+const char kGetAssertionRequestCommandKey[] = "cmd";
+const char kGetAssertionRequestDataKey[] = "request";
+const char kGetAssertionRequestProtobufKey[] = "protobuf";
+const char kGetAssertionRequestClientDataJSONKey[] = "client_data_json";
+const char kGetAssertionRequestUvKey[] = "uv";
 
-// JSON value keys
+// JSON keys for GetAssertion response fields.
+const char kGetAssertionResponseKey[] = "response";
+
+// JSON keys for successful responses and error codes.
+const char kCommandResponseElementSuccessKey[] = "ok";
+const char kCommandResponseElementErrorKey[] = "err";
+
+// Specific command names recognizable by the enclave processor.
+const char kGetAssertionCommandName[] = "passkeys/assert";
+
+// JSON value keys (obsolete, but still referenced by the out-of-date service
+// implementation).
 const char kUserDisplayNameKey[] = "user-display-name";
 const char kUserEntityKey[] = "user-entity";
 const char kUserIdKey[] = "user-id";
@@ -119,25 +137,6 @@
   }
 }
 
-cbor::Value BuildCommandListEntry(
-    const sync_pb::WebauthnCredentialSpecifics& passkey,
-    scoped_refptr<JSONRequest> request) {
-  cbor::Value::MapValue entry_map;
-
-  entry_map.emplace(cbor::Value(kGetAssertionRequestCommandTag),
-                    cbor::Value("navigator.credentials.get"));
-  entry_map.emplace(cbor::Value(kGetAssertionRequestDataTag),
-                    toCbor(*request->value));
-
-  int passkey_byte_size = passkey.ByteSize();
-  std::vector<uint8_t> serialized_passkey;
-  serialized_passkey.resize(passkey_byte_size);
-  CHECK(passkey.SerializeToArray(serialized_passkey.data(), passkey_byte_size));
-  entry_map.emplace(cbor::Value(kGetAssertionRequestEntityTag),
-                    cbor::Value(serialized_passkey));
-  return cbor::Value(entry_map);
-}
-
 bool ParseCommandListEntry(const cbor::Value& entry,
                            sync_pb::WebauthnCredentialSpecifics* out_passkey,
                            base::Value* out_request) {
@@ -147,10 +146,10 @@
   }
 
   const auto& tag_it =
-      entry.GetMap().find(cbor::Value(kGetAssertionRequestCommandTag));
+      entry.GetMap().find(cbor::Value(kGetAssertionRequestCommandKey));
   if (tag_it == entry.GetMap().end() || !tag_it->second.is_string()) {
     FIDO_LOG(ERROR) << base::StrCat(
-        {"Invalid command list entry field: ", kGetAssertionRequestCommandTag});
+        {"Invalid command list entry field: ", kGetAssertionRequestCommandKey});
     return false;
   }
   if (tag_it->second.GetString() != std::string("navigator.credentials.get")) {
@@ -159,19 +158,19 @@
   }
 
   const auto& data_it =
-      entry.GetMap().find(cbor::Value(kGetAssertionRequestDataTag));
+      entry.GetMap().find(cbor::Value(kGetAssertionRequestDataKey));
   if (data_it == entry.GetMap().end()) {
     FIDO_LOG(ERROR) << base::StrCat(
-        {"Invalid command list entry field: ", kGetAssertionRequestDataTag});
+        {"Invalid command list entry field: ", kGetAssertionRequestDataKey});
     return false;
   }
   *out_request = toJson(data_it->second);
 
   const auto& entity_it =
-      entry.GetMap().find(cbor::Value(kGetAssertionRequestEntityTag));
+      entry.GetMap().find(cbor::Value(kGetAssertionRequestProtobufKey));
   if (entity_it == entry.GetMap().end() || !entity_it->second.is_bytestring()) {
-    FIDO_LOG(ERROR) << base::StrCat(
-        {"Invalid command list entry field: ", kGetAssertionRequestEntityTag});
+    FIDO_LOG(ERROR) << base::StrCat({"Invalid command list entry field: ",
+                                     kGetAssertionRequestProtobufKey});
     return false;
   }
   if (!out_passkey->ParseFromArray(entity_it->second.GetBytestring().data(),
@@ -212,76 +211,109 @@
 }
 
 std::pair<absl::optional<AuthenticatorGetAssertionResponse>, std::string>
-AuthenticatorGetAssertionResponseFromJson(const std::string& json) {
-  absl::optional<base::Value> parsed_json = base::JSONReader::Read(json);
-  if (!parsed_json || !parsed_json->is_dict()) {
-    return {absl::nullopt, "Invalid JSON in response."};
+ParseGetAssertionResponse(const std::vector<uint8_t>& response_cbor) {
+  absl::optional<cbor::Value> response_value =
+      cbor::Reader::Read(response_cbor);
+  if (!response_value || !response_value->is_array() ||
+      response_value->GetArray().empty()) {
+    return {absl::nullopt, "Command response was not a valid CBOR array."};
   }
 
-  // TODO(kenrb): Pull authenticator data from the response JSON and decode it,
-  // once the server can provide it. This is an empty stand-in for now.
-  AuthenticatorData authenticator_data(
-      std::array<const uint8_t, kRpIdHashLength>{},
-      static_cast<uint8_t>(AuthenticatorData::Flag::kTestOfUserVerification),
-      std::array<const uint8_t, kSignCounterLength>{}, absl::nullopt);
+  base::Value response_element = toJson(response_value->GetArray()[0]);
 
-  AuthenticatorGetAssertionResponse response(std::move(authenticator_data), {},
-                                             FidoTransportProtocol::kInternal);
-
-  const base::Value::Dict* user_entity =
-      parsed_json->GetDict().FindDict(kUserEntityKey);
-  if (user_entity) {
-    PublicKeyCredentialUserEntity user;
-    const std::string* id_string = user_entity->FindString(kUserIdKey);
-    if (!id_string) {
-      return {absl::nullopt, "User entity missing ID."};
-    }
-    absl::optional<std::vector<uint8_t>> decoded_id = base::Base64UrlDecode(
-        *id_string, base::Base64UrlDecodePolicy::DISALLOW_PADDING);
-    if (!decoded_id) {
-      return {absl::nullopt, "User ID failed to decode."};
-    }
-    user.id = std::move(*decoded_id);
-    const std::string* user_name = user_entity->FindString(kUserNameKey);
-    if (user_name) {
-      user.name = std::move(*user_name);
-    }
-    const std::string* display_name =
-        user_entity->FindString(kUserDisplayNameKey);
-    if (display_name) {
-      user.display_name = std::move(*display_name);
-    }
-    response.user_entity = std::move(user);
+  if (!response_element.is_dict()) {
+    return {absl::nullopt, "Command response element is not a map."};
   }
+
+  if (const std::string* error = response_element.GetDict().FindString(
+          kCommandResponseElementErrorKey)) {
+    return {absl::nullopt,
+            base::StrCat({"Error received from enclave: ", *error})};
+  }
+
+  base::Value::Dict* success_response =
+      response_element.GetDict().FindDict(kCommandResponseElementSuccessKey);
+  if (!success_response) {
+    return {
+        absl::nullopt,
+        "Command response did not contain a successful response or an error."};
+  }
+
+  base::Value* assertion_response =
+      success_response->Find(kGetAssertionResponseKey);
+  if (!assertion_response) {
+    return {absl::nullopt,
+            "Command response did not contain a response field."};
+  }
+
+  absl::optional<AuthenticatorGetAssertionResponse> response =
+      AuthenticatorGetAssertionResponseFromValue(*assertion_response);
+  if (!response) {
+    return {absl::nullopt, "Assertion response failed to parse."};
+  }
+
   return {std::move(response), std::string()};
 }
 
-void BuildGetAssertionRequestBody(
+cbor::Value BuildGetAssertionCommand(
     const sync_pb::WebauthnCredentialSpecifics& passkey,
     scoped_refptr<JSONRequest> request,
-    std::string* out_request_body) {
-  base::Value::Dict request_json;
+    std::string client_data_json,
+    std::string rp_id) {
+  cbor::Value::MapValue entry_map;
+
+  entry_map.emplace(cbor::Value(kGetAssertionRequestCommandKey),
+                    cbor::Value(kGetAssertionCommandName));
+  entry_map.emplace(cbor::Value(kGetAssertionRequestDataKey),
+                    toCbor(*request->value));
+
+  int passkey_byte_size = passkey.ByteSize();
+  std::vector<uint8_t> serialized_passkey;
+  serialized_passkey.resize(passkey_byte_size);
+  CHECK(passkey.SerializeToArray(serialized_passkey.data(), passkey_byte_size));
+  entry_map.emplace(cbor::Value(kGetAssertionRequestProtobufKey),
+                    cbor::Value(serialized_passkey));
+
+  entry_map.emplace(cbor::Value(kGetAssertionRequestClientDataJSONKey),
+                    cbor::Value(client_data_json));
+
+  entry_map.emplace(cbor::Value(kGetAssertionRequestUvKey), cbor::Value(true));
+
+  return cbor::Value(entry_map);
+}
+
+std::vector<uint8_t> BuildCommandRequestBody(
+    base::OnceCallback<cbor::Value()> command_callback,
+    base::RepeatingCallback<std::vector<uint8_t>(base::span<const uint8_t>,
+                                                 base::span<const uint8_t>)>
+        signing_callback,
+    base::span<uint8_t> handshake_hash_,
+    const std::vector<uint8_t>& device_id) {
   cbor::Value::MapValue request_body_map;
 
+  request_body_map.emplace(cbor::Value(kCommandDeviceIdKey),
+                           cbor::Value(device_id));
+
   cbor::Value::ArrayValue command_list;
-  command_list.emplace_back(BuildCommandListEntry(passkey, request));
+  command_list.emplace_back(std::move(command_callback).Run());
   absl::optional<std::vector<uint8_t>> serialized_command_list =
       cbor::Writer::Write(cbor::Value(command_list));
 
-  // TODO(kenrb): This needs public key hash and signature when those are
-  // plumbed. The signature is over |serialized_command_list|.
-  request_body_map.emplace(cbor::Value(kCommandEncodedRequestsTag),
+  // TODO(kenrb): The |signing_callback| invocation probably needs to be
+  // asynchronous, which would require a small refactor here.
+  request_body_map.emplace(cbor::Value(kCommandSigKey),
+                           cbor::Value(signing_callback.Run(
+                               handshake_hash_, *serialized_command_list)));
+
+  request_body_map.emplace(cbor::Value(kCommandAuthLevelKey),
+                           cbor::Value("hw"));
+
+  request_body_map.emplace(cbor::Value(kCommandEncodedRequestsKey),
                            cbor::Value(*serialized_command_list));
 
   absl::optional<std::vector<uint8_t>> serialized_request =
       cbor::Writer::Write(cbor::Value(request_body_map));
-  std::string encoded_request_command;
-  base::Base64UrlEncode(*serialized_request,
-                        base::Base64UrlEncodePolicy::OMIT_PADDING,
-                        &encoded_request_command);
-  request_json.Set(kCommandRequestCommandTag, encoded_request_command);
-
-  base::JSONWriter::Write(request_json, out_request_body);
+  return std::move(*serialized_request);
 }
 
 bool ParseGetAssertionRequestBody(
@@ -296,7 +328,7 @@
   }
 
   std::string* encoded_request_command =
-      request_json->GetDict().FindString(kCommandRequestCommandTag);
+      request_json->GetDict().FindString(kCommandRequestCommandKey);
   if (!encoded_request_command) {
     FIDO_LOG(ERROR) << "Command not found in request JSON.";
     return false;
@@ -318,7 +350,7 @@
   }
 
   const auto& it =
-      request_cbor->GetMap().find(cbor::Value(kCommandEncodedRequestsTag));
+      request_cbor->GetMap().find(cbor::Value(kCommandEncodedRequestsKey));
   if (it == request_cbor->GetMap().end() || !it->second.is_bytestring()) {
     FIDO_LOG(ERROR) << "Invalid command array found in the decoded CBOR.";
     return false;
diff --git a/device/fido/enclave/enclave_protocol_utils.h b/device/fido/enclave/enclave_protocol_utils.h
index 84901d0..de16c7f 100644
--- a/device/fido/enclave/enclave_protocol_utils.h
+++ b/device/fido/enclave/enclave_protocol_utils.h
@@ -7,8 +7,11 @@
 
 #include <string>
 #include <utility>
+#include <vector>
 
 #include "base/component_export.h"
+#include "base/containers/span.h"
+#include "base/functional/callback_forward.h"
 #include "base/memory/scoped_refptr.h"
 #include "base/values.h"
 #include "device/fido/authenticator_get_assertion_response.h"
@@ -31,24 +34,47 @@
 // Keys for the RPC param names to the HTTP front end:
 const char kInitSessionRequestData[] = "request";
 const char kInitSessionResponseData[] = "response";
-const char kSessionId[] = "session_id";
-const char kSendCommandRequestData[] = "command";
+const char kSessionId[] = "session_name";
+const char kSendCommandRequestData[] = "request";
 const char kSendCommandResponseData[] = "response";
 
-// For testing only.
+// Parses a decrypted command response from the enclave.
+std::pair<absl::optional<AuthenticatorGetAssertionResponse>, std::string>
+ParseGetAssertionResponse(const std::vector<uint8_t>& response_cbor);
+
+// Returns a CBOR value with the provided GetAssertion request and associated
+// passkey. The return value can be serialized into a Command request according
+// to the enclave protocol.
+cbor::Value BuildGetAssertionCommand(
+    const sync_pb::WebauthnCredentialSpecifics& passkey,
+    scoped_refptr<JSONRequest> request,
+    std::string client_data_hash,
+    std::string rp_id);
+
+// Returns a CBOR serialization of the command to be sent to the enclave
+// service which can then be encrypted and sent over HTTPS.
+// |command_callback| is used to generate the encoded MakeCredential or
+//     GetAssertion command.
+// |signing_callback| is used to generate the signature over the encoded
+//     command using the protected private key.
+// |device_id| is the unique identifier for this device which the server uses
+//     to look up the previously-registered public key.
+std::vector<uint8_t> BuildCommandRequestBody(
+    base::OnceCallback<cbor::Value()> command_callback,
+    base::RepeatingCallback<std::vector<uint8_t>(base::span<const uint8_t>,
+                                                 base::span<const uint8_t>)>
+        signing_callback,
+    base::span<uint8_t> handshake_hash_,
+    const std::vector<uint8_t>& device_id);
+
+// For testing only. (Also this is obsolete, the test service code needs to
+// be updated).
 std::string COMPONENT_EXPORT(DEVICE_FIDO)
     AuthenticatorGetAssertionResponseToJson(
         const AuthenticatorGetAssertionResponse& response);
 
-std::pair<absl::optional<AuthenticatorGetAssertionResponse>, std::string>
-AuthenticatorGetAssertionResponseFromJson(const std::string& json);
-
-void BuildGetAssertionRequestBody(
-    const sync_pb::WebauthnCredentialSpecifics& passkey,
-    scoped_refptr<JSONRequest> request,
-    std::string* out_request_body);
-
-// For testing only.
+// For testing only. (Also this is obsolete, the test service code needs to
+// be updated).
 bool COMPONENT_EXPORT(DEVICE_FIDO) ParseGetAssertionRequestBody(
     const std::string& request_body,
     sync_pb::WebauthnCredentialSpecifics* out_passkey,
diff --git a/device/fido/enclave/test/passkey_enclave_client.cc b/device/fido/enclave/test/passkey_enclave_client.cc
index 4c330b2..664adce 100644
--- a/device/fido/enclave/test/passkey_enclave_client.cc
+++ b/device/fido/enclave/test/passkey_enclave_client.cc
@@ -4,9 +4,14 @@
 
 #include <iostream>
 #include <memory>
+#include <vector>
 
 #include "base/at_exit.h"
+#include "base/base64.h"
+#include "base/command_line.h"
+#include "base/containers/span.h"
 #include "base/functional/bind.h"
+#include "base/i18n/icu_util.h"
 #include "base/json/json_reader.h"
 #include "base/logging.h"
 #include "base/memory/scoped_refptr.h"
@@ -16,6 +21,9 @@
 #include "base/task/thread_pool/thread_pool_instance.h"
 #include "base/values.h"
 #include "components/sync/protocol/webauthn_credential_specifics.pb.h"
+#include "crypto/ec_private_key.h"
+#include "crypto/ec_signature_creator.h"
+#include "crypto/sha2.h"
 #include "device/fido/authenticator_get_assertion_response.h"
 #include "device/fido/cable/v2_constants.h"
 #include "device/fido/ctap_get_assertion_request.h"
@@ -26,17 +34,37 @@
 namespace device {
 namespace {
 
-const GURL kLocalUrl = GURL("http://127.0.0.1:8880");
+// Default if unspecified on the command line.
+const GURL kLocalUrl = GURL("http://127.0.0.1:8080");
 
 const uint8_t kCredentialId[] = {10, 11, 12, 13};
 
-// Corresponds to identity seed {1, 2, 3, 4}.
 const uint8_t kPeerPublicKey[kP256X962Length] = {
-    4,   244, 60,  222, 80,  52,  238, 134, 185, 2,   84,  48,  248,
-    87,  211, 219, 145, 204, 130, 45,  180, 44,  134, 205, 239, 90,
-    127, 34,  229, 225, 93,  163, 51,  206, 28,  47,  134, 238, 116,
-    86,  252, 239, 210, 98,  147, 46,  198, 87,  75,  254, 37,  114,
-    179, 110, 145, 23,  34,  208, 25,  171, 184, 129, 14,  84,  80};
+    0x04, 0x6b, 0x17, 0xd1, 0xf2, 0xe1, 0x2c, 0x42, 0x47, 0xf8, 0xbc,
+    0xe6, 0xe5, 0x63, 0xa4, 0x40, 0xf2, 0x77, 0x03, 0x7d, 0x81, 0x2d,
+    0xeb, 0x33, 0xa0, 0xf4, 0xa1, 0x39, 0x45, 0xd8, 0x98, 0xc2, 0x96,
+    0x4f, 0xe3, 0x42, 0xe2, 0xfe, 0x1a, 0x7f, 0x9b, 0x8e, 0xe7, 0xeb,
+    0x4a, 0x7c, 0x0f, 0x9e, 0x16, 0x2b, 0xce, 0x33, 0x57, 0x6b, 0x31,
+    0x5e, 0xce, 0xcb, 0xb6, 0x40, 0x68, 0x37, 0xbf, 0x51, 0xf5};
+
+std::vector<uint8_t> Sign(crypto::ECPrivateKey* signing_key,
+                          base::span<const uint8_t> handshake_hash,
+                          base::span<const uint8_t> data) {
+  CHECK(handshake_hash.size() == 32);
+  std::array<uint8_t, 64> signing_data;
+  memcpy(signing_data.data(), handshake_hash.data(), 32);
+
+  std::string_view data_sv(reinterpret_cast<const char*>(data.data()),
+                           data.size());
+  crypto::SHA256HashString(data_sv, signing_data.data() + 32, 32);
+
+  std::vector<uint8_t> output;
+  auto signer = crypto::ECSignatureCreator::Create(signing_key);
+  if (!signer->Sign(signing_data, &output)) {
+    std::cout << "Signature generation failed.\n";
+  }
+  return output;
+}
 
 // This is an executable test harness that wraps EnclaveAuthenticator and can
 // initiate transactions.
@@ -46,22 +74,35 @@
  public:
   EnclaveTestClient() = default;
 
-  int StartTransaction();
+  int StartTransaction(const std::string& device_id,
+                       const std::string& signing_key,
+                       const std::string& service_url,
+                       const std::string& username,
+                       const std::string& sync_entity);
 
  private:
   void Terminate(CtapDeviceResponseCode result,
                  std::vector<AuthenticatorGetAssertionResponse> response);
 
   std::unique_ptr<enclave::EnclaveAuthenticator> device_;
+  std::unique_ptr<crypto::ECPrivateKey> signing_key_;
 
   base::RunLoop run_loop_;
 };
 
-int EnclaveTestClient::StartTransaction() {
-  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys{
-      sync_pb::WebauthnCredentialSpecifics::default_instance()};
+int EnclaveTestClient::StartTransaction(const std::string& device_id,
+                                        const std::string& signing_key,
+                                        const std::string& service_url,
+                                        const std::string& username,
+                                        const std::string& sync_entity) {
+  sync_pb::WebauthnCredentialSpecifics entity;
+  auto decoded_entity = base::Base64Decode(sync_entity);
+  CHECK(decoded_entity);
+  entity.ParseFromArray(decoded_entity->data(), decoded_entity->size());
+  std::vector<sync_pb::WebauthnCredentialSpecifics> passkeys{std::move(entity)};
+
   // Set RP ID and allow credentials only, for test purposes.
-  CtapGetAssertionRequest request("https://passkey.example", "");
+  CtapGetAssertionRequest request("https://webauthn.io", "");
   std::vector<uint8_t> cred_id(std::begin(kCredentialId),
                                std::end(kCredentialId));
   request.allow_list.emplace_back(PublicKeyCredentialDescriptor(
@@ -69,11 +110,33 @@
   passkeys[0].set_credential_id(cred_id.data(), cred_id.size());
   CtapGetAssertionOptions options;
   absl::optional<base::Value> parsed_json = base::JSONReader::Read(
-      R"({"attestation":"direct","authenticatorSelection":{"authenticatorAttachment":"platform","residentKey":"required","userVerification":"required"},"challenge":"dGVzdCBjaGFsbGVuZ2U","excludeCredentials":[{"id":"FBUW","transports":["usb"],"type":"public-key"},{"id":"Hh8g","type":"public-key"}],"extensions":{"appIdExclude":"https://example.test/appid.json","credBlob":"dGVzdCBjcmVkIGJsb2I","credProps":true,"credentialProtectionPolicy":"userVerificationRequired","enforceCredentialProtectionPolicy":true,"hmacCreateSecret":true,"largeBlob":{"support":"required"},"minPinLength":true,"payment":{"isPayment":true},"prf":{},"remoteDesktopClientOverride":{"origin":"https://login.example.test","sameOriginWithAncestors":true}},"pubKeyCredParams":[{"alg":-7,"type":"public-key"},{"alg":-257,"type":"public-key"}],"rp":{"id":"passkey.example","name":"Example LLC"},"user":{"displayName":"Example User","id":"dGVzdCB1c2VyIGlk","name":"user@example.test"}})");
+      R"({"allowCredentials":[{"id":"FBUW","transports":["usb"],"type":"public-key"},{"id":"Hh8g","type":"public-key"}],"challenge":"dGVzdCBjaGFsbGVuZ2U","rpId":"webauth.io","userVerification":"required"})");
   options.json = base::MakeRefCounted<JSONRequest>(std::move(*parsed_json));
 
+  std::vector<uint8_t> device_id_bytes;
+  if (!base::HexStringToBytes(device_id, &device_id_bytes)) {
+    std::cout << "Invalid device ID\n";
+    return -1;
+  }
+
+  std::vector<uint8_t> signing_key_bytes;
+  if (!base::HexStringToBytes(signing_key, &signing_key_bytes)) {
+    std::cout << "Invalid signing key hex string\n";
+    return -1;
+  }
+
+  signing_key_ =
+      crypto::ECPrivateKey::CreateFromPrivateKeyInfo(signing_key_bytes);
+  if (!signing_key_) {
+    std::cout << "Invalid signing key\n";
+    return -1;
+  }
+
   device_ = std::make_unique<enclave::EnclaveAuthenticator>(
-      kLocalUrl, kPeerPublicKey, std::move(passkeys));
+      service_url.empty() ? kLocalUrl : GURL(service_url), kPeerPublicKey,
+      std::move(passkeys), std::move(device_id_bytes),
+      username.empty() ? "testuser" : username,
+      base::BindRepeating(&Sign, signing_key_.get()));
   device_->GetAssertion(
       request, options,
       base::BindOnce(&EnclaveTestClient::Terminate, base::Unretained(this)));
@@ -87,30 +150,55 @@
     std::vector<AuthenticatorGetAssertionResponse> responses) {
   if (result == CtapDeviceResponseCode::kSuccess) {
     CHECK(responses.size() == 1u);
-
-    std::cout << "Returned credential for user: "
-              << *responses[0].user_entity->name << "\n";
+    std::cout << "Returned 1 assertion successfully.\n";
   } else {
-    std::cout << "Request completed with error: " << static_cast<int>(result);
+    std::cout << "Request completed with error: " << static_cast<int>(result)
+              << "\n";
   }
-
   run_loop_.Quit();
 }
 
 }  // namespace
 }  // namespace device
 
+void Usage() {
+  std::cout << "Usage: passkey_enclave_client <args>\n";
+  std::cout << "  Args:\n";
+  std::cout << "       --device-id=<device ID hex>\n";
+  std::cout << "       --signing-key=<signing key hex>\n";
+  std::cout << "       --url=<service url> (optional)\n";
+  std::cout << "       --user=<service user name>\n";
+  std::cout << "       --entity=<passkey protobuf Base64>\n";
+}
+
 int main(int argc, char** argv) {
   base::AtExitManager at_exit_manager;
+
+  base::CommandLine::Init(argc, argv);
   base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
   base::ThreadPoolInstance::CreateAndStartWithDefaultParams("passkey_enclave");
   base::ScopedClosureRunner cleanup(
       base::BindOnce([] { base::ThreadPoolInstance::Get()->Shutdown(); }));
+  base::i18n::InitializeICU();
   logging::LoggingSettings settings;
   settings.logging_dest =
       logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
   logging::InitLogging(settings);
 
+  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+  std::string device_id = command_line->GetSwitchValueASCII("device-id");
+  std::string private_key = command_line->GetSwitchValueASCII("signing-key");
+  std::string service_url = command_line->GetSwitchValueASCII("url");
+  std::string username = command_line->GetSwitchValueASCII("user");
+  std::string entity = command_line->GetSwitchValueASCII("entity");
+
+  if (device_id.empty() || private_key.empty() || username.empty() ||
+      entity.empty()) {
+    Usage();
+    return -1;
+  }
+
   device::EnclaveTestClient client;
-  return client.StartTransaction();
+  return client.StartTransaction(device_id, private_key, service_url, username,
+                                 entity);
 }
diff --git a/device/fido/value_response_conversions.cc b/device/fido/value_response_conversions.cc
index 308ba651..efa74cb 100644
--- a/device/fido/value_response_conversions.cc
+++ b/device/fido/value_response_conversions.cc
@@ -34,6 +34,7 @@
   if (!base::Base64UrlDecode(*b64url_data,
                              base::Base64UrlDecodePolicy::DISALLOW_PADDING,
                              &decoded)) {
+    FIDO_LOG(ERROR) << "Failed to decode key " << key;
     return absl::nullopt;
   }
   return decoded;
@@ -114,7 +115,7 @@
   std::vector<uint8_t> signature = ToByteVector(*signature_opt);
 
   auto [success, user_handle_opt] =
-      Base64UrlDecodeOptionalStringKey(response_dict, "user_handle");
+      Base64UrlDecodeOptionalStringKey(response_dict, "userHandle");
   if (!success) {
     FIDO_LOG(ERROR) << "Assertion response contained invalid user handle.";
     return absl::nullopt;
diff --git a/device/fido/value_response_conversions_unittest.cc b/device/fido/value_response_conversions_unittest.cc
index 544a1ade..6984452 100644
--- a/device/fido/value_response_conversions_unittest.cc
+++ b/device/fido/value_response_conversions_unittest.cc
@@ -65,7 +65,7 @@
   constexpr char kJson[] = R"({
       "authenticatorData": "EZQijaj9ve79JhvXtllc_XClDXDGQHvPAT3pbU77F94BAAAAAA",
       "signature": "dGVzdCBzaWduYXR1cmU",
-      "user_handle": "dXNlcmlkMQ"
+      "userHandle": "dXNlcmlkMQ"
       })";
 
   JSONStringValueDeserializer deserializer(kJson);
diff --git a/device/vr/public/mojom/vr_service.mojom b/device/vr/public/mojom/vr_service.mojom
index 8f2b906..b78f6eda 100644
--- a/device/vr/public/mojom/vr_service.mojom
+++ b/device/vr/public/mojom/vr_service.mojom
@@ -793,40 +793,6 @@
   XRTrackedImagesData? tracked_images;
 };
 
-// Used primarily in logging to indicate why a session was rejecting to aid
-// developer debuggability. To this end, rather than re-using an existing enum
-// value, if it doesn't match your error case, a new value should be added.
-// Note that this is converted to a console log, and thus the numbers can still
-// be re-arranged as needed (e.g. if a value should be removed).
-enum RequestSessionError {
-  // Indicates that another session is already using the hardware that a new
-  // session would need exclusive control over.
-  EXISTING_IMMERSIVE_SESSION = 1,
-  // Used to indicate that something happened to the connection between either
-  // renderer and browser or browser and device processes, and thus a session
-  // could not be brokered.
-  INVALID_CLIENT = 2,
-  // The user denied consent for one or more required features in the session
-  // configuration (or the mode).
-  USER_DENIED_CONSENT = 3,
-  // No runtime was present which supported both the requested mode and all
-  // required features.
-  NO_RUNTIME_FOUND = 4,
-  // The runtime could not successfully create a session.
-  UNKNOWN_RUNTIME_ERROR = 5,
-  // The runtime requires additional installation which could not be completed.
-  RUNTIME_INSTALL_FAILURE = 6,
-  // While processing/creating a session the "best fit" runtime changed (either
-  // a better runtime was added or the only supported runtime was removed).
-  RUNTIMES_CHANGED = 7,
-  // Something prevented the session from entering fullscreen, which was
-  // required by part of the session configuration.
-  FULLSCREEN_ERROR = 8,
-  // We are unable to determine why the request failed. This should be used very
-  // sparingly, (e.g. Crashes, destructors, etc.)
-  UNKNOWN_FAILURE = 9,
-};
-
 struct RequestSessionSuccess {
   XRSession session;
 
diff --git a/device/vr/public/mojom/xr_session.mojom b/device/vr/public/mojom/xr_session.mojom
index 5bd2daa..396bd7a 100644
--- a/device/vr/public/mojom/xr_session.mojom
+++ b/device/vr/public/mojom/xr_session.mojom
@@ -7,6 +7,40 @@
 import "skia/public/mojom/bitmap.mojom";
 import "ui/gfx/geometry/mojom/geometry.mojom";
 
+// Used primarily in logging to indicate why a session was rejecting to aid
+// developer debuggability. To this end, rather than re-using an existing enum
+// value, if it doesn't match your error case, a new value should be added.
+// Note that this is converted to a console log, and thus the numbers can still
+// be re-arranged as needed (e.g. if a value should be removed).
+enum RequestSessionError {
+  // Indicates that another session is already using the hardware that a new
+  // session would need exclusive control over.
+  EXISTING_IMMERSIVE_SESSION = 1,
+  // Used to indicate that something happened to the connection between either
+  // renderer and browser or browser and device processes, and thus a session
+  // could not be brokered.
+  INVALID_CLIENT = 2,
+  // The user denied consent for one or more required features in the session
+  // configuration (or the mode).
+  USER_DENIED_CONSENT = 3,
+  // No runtime was present which supported both the requested mode and all
+  // required features.
+  NO_RUNTIME_FOUND = 4,
+  // The runtime could not successfully create a session.
+  UNKNOWN_RUNTIME_ERROR = 5,
+  // The runtime requires additional installation which could not be completed.
+  RUNTIME_INSTALL_FAILURE = 6,
+  // While processing/creating a session the "best fit" runtime changed (either
+  // a better runtime was added or the only supported runtime was removed).
+  RUNTIMES_CHANGED = 7,
+  // Something prevented the session from entering fullscreen, which was
+  // required by part of the session configuration.
+  FULLSCREEN_ERROR = 8,
+  // We are unable to determine why the request failed. This should be used very
+  // sparingly, (e.g. Crashes, destructors, etc.)
+  UNKNOWN_FAILURE = 9,
+};
+
 // These correspond with the features from the WebXR Spec:
 // https://immersive-web.github.io/webxr/#feature-descriptor
 enum XRSessionFeature {
diff --git a/docs/README.md b/docs/README.md
index 08f0375d..c1aaa62 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -438,6 +438,22 @@
 ### UI
 *   [Chromium UI Platform](ui/index.md) - All things user interface
 
+### What's Up With That Transcripts
+
+These are transcripts of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq),
+a video series of interviews with Chromium software engineers.
+
+*   [What's Up With Pointers - Episode 1](transcripts/wuwt-e01-pointers.md)
+*   [What's Up With DCHECKs - Episode 2](transcripts/wuwt-e02-dchecks.md)
+*   [What's Up With //content - Episode 3](transcripts/wuwt-e03-content.md)
+*   [What's Up With Tests - Episode 4](transcripts/wuwt-e04-tests.md)
+*   [What's Up With BUILD.gn - Episode 5](transcripts/wuwt-e05-build-gn.md)
+*   [What's Up With Open Source - Episode 6](transcripts/wuwt-e06-open-source.md)
+*   [What's Up With Mojo - Episode 7](transcripts/wuwt-e07-mojo.md)
+*   [What's Up With Processes - Episode 8](transcripts/wuwt-e08-processes.md)
+*   [What's Up With Site Isolation - Episode 9](transcripts/wuwt-e09-site-isolation.md)
+
 ### Probably Obsolete
 *   [TPM Quick Reference](tpm_quick_ref.md) - Trusted Platform Module notes.
 *   [System Hardening Features](system_hardening_features.md) - A list of
diff --git a/docs/transcripts/wuwt-e01-pointers.md b/docs/transcripts/wuwt-e01-pointers.md
new file mode 100644
index 0000000..c0dee26
--- /dev/null
+++ b/docs/transcripts/wuwt-e01-pointers.md
@@ -0,0 +1,601 @@
+# What’s Up With Pointers
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 1, a 2022 video discussion between [Sharon (yangsharon@chromium.org)
+and Dana (danakj@chromium.org)](https://www.youtube.com/watch?v=MpwbWSEDfjM).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Welcome to the first episode of What’s Up With That, all about pointers! Our
+special guest is C++ expert Dana. This talk covers smart pointer types we have
+in Chrome, how to use them, and what can go wrong.
+
+Notes:
+- https://docs.google.com/document/d/1VRevv8JhlP4I8fIlvf87IrW2IRjE0PbkSfIcI6-UbJo/edit
+
+Links:
+- [Life of a Vulnerability](https://www.youtube.com/watch?v=HAJAEQrPUN0)
+- [MiraclePtr](https://www.youtube.com/watch?v=WhI1NWbGvpE)
+
+---
+
+0:00 SHARON: Hi, everyone, and welcome to the first installment of "What's Up
+With That", the series that demystifies all things Chrome. I'm your host,
+Sharon, and today's inaugural episode will be all about pointers. There are so
+many types of types - which one should I use? What can possibly go wrong? Our
+guest today is Dana, who is one of our Base and C++ OWNERS and is currently
+working on introducing Rust to Chromium. Previously, she was part of bringing
+C++ 11 support to the Android NDK and then to Chrome. Today, she'll be telling
+us what's up with points. Welcome, Dana!
+
+00:31 DANA: Thank you, Sharon. It's super exciting to be here. Thank you for
+letting me be on your podcast thingy.
+
+00:36 SHARON: Yeah, thanks for being the first episode. So let's just jump
+right in. So when you use pointers wrong, what can go wrong? What are the
+problems? What can happen?
+
+00:48 DANA: So pointers are a big cause in security problems for Chrome, and
+that's what we mostly think about when things go wrong with pointers. So you
+have a pointer to some thing, like you've pointed to a goat. And then you
+delete the goat, and you allocate some new thing - a cow. And it gets stuck in
+the same spot. Your pointer didn't change. It's still pointing to what it
+thinks is a goat, but there's now a cow there. And so when you go to use that
+pointer, you use something different. And this is a tool that malicious actors
+use to exploit software, like Chrome, in order to gain access to your system,
+your information, et cetera.
+
+01:39 SHARON: And we want to avoid those. So what's that general type of attack
+called?
+
+01:39 DANA: That's a Use-After-Free because you have freed the goat and
+replaced it with a cow. And you're using your pointer, but the thing it pointed
+to was freed. There are other kinds of pointer badness that can happen. If you
+take a pointer and you add to it some number, or you go to an offset off the
+pointer, and you have an array of five things, and you go and read 20, or minus
+2, or something, now you're reading out of bounds of that memory allocation.
+And that's not good. these are both memory safety bugs that occur a lot with
+pointers.
+
+02:23 SHARON: Today, we'll be mostly looking at the Use-After-Free kind of
+bugs. We definitely see a lot of those. And if you want to see an example of
+one being used, Dana has previously done a talk called, "Life of a
+Vulnerability." It'll be linked below. You can check that out. So that being
+said, should we ever be using just a regular raw pointer in C++ in Chrome?
+
+02:41 DANA: First of all, let's call them native pointers. You will see them
+called raw pointers a lot in literature and stuff. But later on, we'll see why
+that could be a bit ambiguous in this context. So we'll call them a native
+pointer. So should you use a native pointer? If you don't want to
+Use-After-Free, if you don't want a problem like that, no. However, there is a
+performance implication with using smart pointers, and so the answer is yes.
+The style guide that we have right now takes this pragmatic approach of saying
+you should use raw pointers for giving access to an object. So if you're
+passing them as a function parameter, you can share it as a pointer or a
+reference, which is like a pointer with slightly different rules. But you
+should not store native pointers as fields and objects because that is a place
+where they go wrong a lot. And you should not use a native pointer to express
+ownership. So before C++ 11, you would just say, this is my pointer, use a
+comment, say this one is owning it. And then if you wanted to pass the
+ownership, you just pass this native pointer over to something else as an
+argument, and put a comment and say this is passing ownership. And you just
+kind of hope it works out. But then it's very difficult. It requires the
+programmer to understand the whole system to do it correctly. There is no help.
+So in C++ 11, the type called `std::optional_ptr` - or sorry, `std::unique_ptr`
+- was introduced. And this is expressing unique ownership. That's why it's
+called `unique_ptr`. And it's just going to hold your pointer, and when it goes
+out of scope, it gets deleted. It can't be copied because it's unique
+ownership. But it can be moved around. And so if you're going to express
+ownership to an object in the heap, you should use a `unique_ptr`.
+
+04:48 SHARON: That makes sense. And that sounds good. So you mentioned smart
+pointers before. You want to tell us a bit more about what those are? It sounds
+like `unique_ptr` is one of those.
+
+04:55 DANA: Yes, so a smart pointer, which can also be referred to as a
+pointer-like object, perhaps as a subset of them, is a class that holds inside
+of it a pointer and mediates access to it in some way. So unique pointer
+mediates access by saying I own this pointer, I will delete this pointer when I
+go away, but I'll give you access to it. So you can use the arrow operator or
+the star operator to get at the underlying pointer. And you can construct them
+out of native pointers as well. So that's an example of a smart pointer.
+There's a whole bunch of smart pointers, but that's the general idea. I'm going
+to add something to what a native pointer is, while giving you access to it in
+some way.
+
+05:40 SHARON: That makes sense. That's kind of what our main thing is going to
+be about today because you look around in Chrome. You'll see a lot of these
+wrapper types. It'll be a `unique_ptr` and then a type. And you'll see so many
+types of these, and talking to other people, myself, I find this all very
+confusing. So we'll cover some of the more common types today. We just talked
+about unique pointers. Next, talk about `absl::optional`. So why don't you tell
+us about that.
+
+06:10 DANA: So that's actually a really great example of a pointer-like object
+that's not actually holding a pointer, so it's not a smart pointer. But it
+looks like one. So this is this distinction. So `absl::optional`, also known as
+`std::optional`, if you're not working in Chromium, and at some point, we will
+hopefully migrate to it, `std::optional` and `absl::optional` hold an object
+inside of it by value instead of by pointer. This means that the object is held
+in that space allocated for the `optional`. So the size of the `optional` is
+the size of the thing it's holding, plus some space for a presence flag.
+Whereas a `unique_ptr` holds only a pointer. And its size is the size of a
+pointer. And then the actual object lives elsewhere. So that's the difference
+in how you can think about them. But otherwise, they do look quite similar. An
+`optional` is a unique ownership because it's literally holding the object
+inside of it. However, an `optional` is copyable if the object inside is
+copyable, for instance. So it doesn't have quite the same semantics. And it
+doesn't require a heap allocation, the way unique pointer does because it's
+storing the memory in place. So if you have an `optional` on the stack, the
+object inside is also right there on the stack. That's good or bad, depending
+what you want. If you're worried about your object sizes, not so good. If
+you're worried about the cost of memory allocation and free, good. So this is
+the trade-off between the two.
+
+07:51 SHARON: Can you give any examples of when you might want to use one
+versus the other? Like you mentioned some kind of general trade-offs, but any
+specific examples? Because I've definitely seen use cases where `unique_ptr` is
+used when maybe an `optional` makes more sense or vise versa. Maybe it's just
+because someone didn't know about it or it was chosen that way. Do you have any
+specific examples?
+
+08:14 DANA: So one place where you might use a `unique_ptr`, even though
+`optional` is maybe the better choice, is because of forward declarations. So
+because an `optional` holds the type inside of it, it needs to know the type
+size, which means it needs to know the full declaration of that type, or the
+whole definition of that type. And a `unique_ptr` doesn't because it's just
+holding a pointer, so it only needs to know the size of a pointer. And so if
+you have a header file, and you don't want to include another header file, and
+you just want to forward declare the types, you can't stick an optional of that
+type right there because you don't know how big it's supposed to be. So that
+might be a case where it's maybe not the right choice, but for other
+constraining reasons, you choose to use a `unique_ptr` here. And you pay the
+cost of a heap allocation and free as a result. But when would you use an
+`optional`? So `optional` is fantastic for returning a value sometimes. I want
+to do this thing, and I want to give you back a result, but I might fail. Or
+sometimes there's no value to give you back. Typically, before C++ - what are
+we on now, was it came in 14? I'm going to say it wrong. That's OK. Before we
+had `absl::optional`, you would have to do different tricks. So you would pass
+in a native pointer as a parameter and return a bool as the return value to say
+did I populate the pointer. And yes, that works. But it's easy to mess it up.
+It also generates less optimal code. Pointers cause the optimizer to have
+troubles. And it doesn't express as nicely what your intention is. A return,
+this thing, sometimes. And so in place of using this pointer plus bool, you can
+put that into a single type, return an `optional`. Similar for holding
+something as a field, where you want it to be held in line in your class, but
+you don't always have it present, you can do that with an `optional` now, where
+you would have probably used a pointer before. Or a `union` or something, but
+that gets even more tricky. And then another place you might use it as a
+function argument. However, that's usually not the right choice for a function
+argument. Why? Because the `optional` holds the value inside of it.
+Constructing an `optional` requires constructing the whole object inside of it.
+And so that's not free. It can be arbitrarily expensive, depending on what your
+type is. And if your caller to your function doesn't have already an
+`optional`, they have to go and construct it to pass it to you. And that's a
+copy or move of that inner type. So generally, if you're going to receive a
+parameter, maybe sometimes, the right way to spell that is just to pass it as a
+pointer because a native pointer, which can be null, when it's not present.
+
+11:29 SHARON: Hopefully that clarifies some things for people who are trying to
+decide which one best suits their use case. So moving on from that, some people
+might remember from a couple of years ago that instead of being called
+`absl::optional`, it used to be called `base::optional`. And do you want to
+quickly mention why we switched from `base` to `absl`? And you mentioned even
+switching to `std::optional`. Why this transition?
+
+11:53 DANA: Yeah, absolutely. So as the C++ standards come out, we want to use
+them, but we can't until our toolchain is ready. What's our toolchain? So our
+compiler, our standard library, and unfortunately, we have more than one
+compiler that we need to worry about. So we have the NaCl compiler. Luckily, we
+just have Clang for the compiler choice we really have to worry about. But we
+do have to wait for these things to be ready, and for a code base to be ready
+to turn on the new standard because sometimes there are some non-backwards
+compatible changes. But we can forward port stuff out of the standard library
+into base. And so we've done that. We have a bunch of C++ 20 backport in base
+now. We had 17 back ports before. We turned on 17, now they should hopefully be
+gone. And so `base::optional` was an example of a backport, while `optional`
+was still considered experimental in the standard library. We adopted use of
+`absl` since then, and `absl` had also, essentially, a backport of the
+`optional` type` inside of it for presumably the same reasons. And so why have
+two when you can have one? That's a pretty good rule. And so we deprecated the
+`base` one, removed it, and moved everything to the `abslq one. One thing to
+note here, possibly interest, is we often add security hardening to things in
+`base`. And so sometimes there is available in the standard library something.
+But we choose not to use it and use something in `base` or `absl`, but we use
+it in `base` instead, because we have extra hardening checks. And so part of
+the process of removing `base::optional` and moving to `absl::optional` was
+ensuring those same security hardening checks are present in `absl`. And we're
+going to have to do the same thing to stop using `absl` and start using the
+standard one. And that's currently a work in progress.
+
+13:48 SHARON: So let's go through some of the `base` types because that's
+definitely where the most of these kind of wrapper types live. So let's just
+start with one that I learned about recently, and that's a `scoped_refptr`.
+What's that? When should we use it?
+
+13:59 DANA: So `scoped_refptr` is kind of your Chromium equivalent to
+`shared_ptr` in the standard library. So if you're familiar with that, it's
+quite similar, but it has some slight differences. So what is `scoped_refptr`?
+It gives you share ownership of the underlying object. And it's a smart
+pointer. It holds a pointer to an object that's allocated in the heap when all
+`scoped_refptr` that point to the same object are gone, it'll be deleted. So
+it's like `unique_ptr`, except it can be copied to add to your ref count,
+basically. And when all of them are gone, it's destroyed. And it gives access
+to the underlined pointer in exactly the same ways. Oh, but why is it different
+than `shared_ptr`? I did say it is. `scoped_refptr` requires the type that is
+held inside of it to inherit from `RefCounted` or `RefCountedThreadSafe`.
+`shared_ptr` doesn't require this. Why? So `shared_ptr` sticks an allocation
+beside your object and then puts your object here. So the ref count is
+externalized to your object being stored and owned by the shared pointer.
+Chromium took this position to be doing intrusive ref counting. So because we
+inherit from a known type, we stick the ref count in that base class,
+`RefCounted` or `RefCountedThreadSafe`. And so that is enforced by the
+compiler. You must inherit from one of these two in order to be stored and
+owned in a `scoped_refptr`. What's the difference? Ref counted is the default
+choice, but it's not thread safe. So the ref counting is cheap. It's the more
+performant one, but if you have a `scoped_refptr` on two different threads
+owning the same object, their ref counting will race, can be wrong, you can end
+up with a double free - which is another way that pointers can go wrong, two
+things free in the same thing - or you could end up with potentially not
+freeing it at all, probably. I guess I've never checked if that's possible. But
+they can race, and then bad things happen. Whereas, ref counted thread safe
+gives you atomic ref counting. So atomic means that across all threads, they're
+all going to have the same view of the state. And so it can be used across
+threads and be owned across threads. And the tricky part there is the last
+thread that owns that object is where it's going to be destroyed. So if your
+objects destructor does things that you expect to happen on a specific thread,
+you have to be super careful that you synchronize which thread that last
+reference is going away on, or it could explode in a really flakey way.
+
+17:02 SHARON: This sounds useful in other ways. What are some kind of more
+design things to consider, in terms of when a scope ref pointer is useful and
+does help enforce things that you want to enforce, like relative lifetimes of
+certain objects?
+
+17:15 DANA: Generally, we recommend that you don't use ref counting if you can
+help it. And that's because it's hard to understand when it's going to be
+destroyed, like I kind of alluded to with the thread situation. Even in a
+single thread situation, how do you know which one is the last reference? And
+is this object going to outlive that other object? Maybe sometimes. It's not
+super obvious. It's a little more clear with a `unique_ptr`, at least local to
+where that `unique_ptr`'s destruction is. But there's usually no
+`scoped_refptr`. You can say this is the last one. So I know it's gone after
+this thing is gone. Maybe it is, maybe it's not often. So it's a bit tricky.
+However, there are scenarios when you truly want a bunch of things to have
+access to a piece of data. And you want that data to go away when nobody needs
+it anymore. And so that is your use case for a `scoped_refptr`. It is nicer
+when that thing being with shared ownership is not doing a lot of interesting
+things, especially in its destructor because of the complexity that's involved
+in shared ownership. But you're welcome to shoot yourself in the foot with this
+one if you need to.
+
+18:33 SHARON: We're hoping to help people not shoot themselves in the foot. So
+use `scoped_refptr` carefully, is the lesson there. So you mentioned
+`shared_ptr`. Is that something we see much of in Chrome, or is that something
+that we generally try to avoid in terms of things from the standard library?
+
+18:51 DANA: That is something that is banned in Chrome. And that's just
+basically because we already have `scoped_refptr`, and we don't want two of the
+same thing. There's been various times where people have brought up why do we
+need to have both? Can we just use `shared_ptr` now? And nobody's ever done the
+kind of analysis needed to make that kind of decision. And so we stay with what
+we're at.
+
+19:18 SHARON: If you want to do that, there's someone that'll tell you what to
+do. So something that when I was using `scoped_refptr`, I came across that you
+need a WeakPtrFactory to create such a pointer. So weak pointers and WeakPtr
+factories are one of those things that you see a lot in Chrome and one of these
+base things. So tell us a bit about weak pointers and their factories.
+
+19:42 DANA: So WeakPtr and WeakPtrFactory have a bit of an interesting history.
+Their major purpose is for asynchronous work. Chrome is basically a large
+asynchronous machine, and what does that mean? It means that we break all of
+the work of Chrome up into small pieces of work. And every time you've done a
+piece, you go and say, OK, I'm done. And when the next piece is ready, run this
+thing. And maybe that next thing is like a user input event, maybe that's a
+reply from the network, whatever it might be. And there's just a ton of steps
+in things that happen in Chrome. Like, a navigation has a request, a response,
+maybe another request - some redirects, whatever. That's an example of tons of
+smaller asynchronous tasks that all happen independently. So what goes on with
+asynchronous tasks? You don't have a continuous stack frame. What does that
+mean? So if you're just running some synchronous code, you make a variable, you
+go off and you do some things, you come back. Your variable is still here
+right. You're in this stack frame. And you can keep using it. You have
+asynchronous tasks. You make a variable, you go and do some work, and you are
+done your task Boop, your stack's gone. You come back later, you're going to
+continue. You don't have your variable anymore. So any state that you want to
+keep across your various tasks has to be stored and what we call bound in with
+that task. If that's a pointer, that's especially risky. So we talked earlier
+about Use-After-Frees. Well, you can, I hope, imagine how easy it is to stick a
+pointer into your state. This pointer is valid, I'm using it. I go away, I come
+back when? I don't know, sometime in the future. And I'm going to go use this
+pointer. Is it still around? I don't own it. I didn't use a `unique_ptr`. So
+who owns it. How do they know that I have a task waiting to use it? Well,
+unless we have some side channel communicating that, they don't. And how do I
+know if they've destroyed it if we don't have some side channel communicating
+that. I don't know. And so I'm just going to use this pointer and bad things
+happen. Your bank account is gone.
+
+22:06 SHARON: No! My bank account!
+
+22:06 DANA: I know. So what's the side channel? The side channel that we have
+is WeakPtr. So a WeakPtr and WeakPtrFactory provide this communication
+mechanism where WeakPtrFactory watches an object, and when the object gets
+destroyed, the WeakPtrFactory inside of it is destroyed. And that sends this
+little bit that says, I'm gone. And then when your asynchronous task comes back
+with its pointer, but it's a WeakPtr inside of it and tries to run, it can be
+like, am I still here? If the WeakPtrFactory was destroyed, no, I'm not. And
+then you have a choice of what to do at that point. Typically, we're like,
+abandon ship. Don't do anything here. This whole task is aborted. But maybe you
+do something more subtle. That's totally possible.
+
+22:59 SHARON: I think the example I actually meant to say that uses a
+WeakPtrFactory is a SafeRef, which is another base type. So tell us a bit about
+SafeRefs.
+
+23:13 DANA: WeakPtr is cool because of the side channel that you can examine.
+So you can say are you still alive, dear object? And it can tell you, no, it's
+gone. Or yeah, it's here. And then you can use it. The problem with this is
+that in places where you as the code author want to believe that this object is
+actually always there, but you don't want a security bug if you're wrong. And
+it doesn't mean that you're wrong now, even. Sometime later, someone can change
+code, unrelated to where this is, where the ownership happens, and break you.
+And maybe they don't know all the users of a given object and change in its
+lifetime in some subtle way, maybe not even realizing they are. Suddenly you're
+eventually seeing security bugs. And so that's why native pointers can be
+pretty scary. And so SafeRef is something we can use instead of a native
+pointer to protect you against this type of bug. It's built on top of WeakPtr
+and WeakPtrFactory. That's its relationship, but its purpose is not the same.
+so what SafeRef does is it says - SafePtr?
+
+24:31 SHARON: SafeRef.
+
+24:31 DANA: SafeRef.
+
+24:31 SHARON: I think there's also a safe pointer, but there -
+
+24:38 DANA: We were going to add it. I'm not sure if it's there yet. But so two
+differences between SafeRef and WeakPtr then, ref versus ptr, it can't be null.
+So it's like a reference wrapper. But the other difference is you can't observe
+whether the object is actually alive or not. So it has the side channel, but it
+doesn't show it to you. Why would you want that? If the information is there
+anyway, why wouldn't you want to expose it? And the reason is because you are
+documenting that you as the author understand and expect that this pointer is
+always valid at this time. It turns out it's not valid. What do you do? If it's
+a WeakPtr, people tend to say, we don't know if it's valid. It's a WeakPtr.
+Let's check. Am I valid? And if I'm not, return. And what does that result in?
+It results in adding a branch to your code. You do that over, and over, and
+over, and over, and static analysis, which is what we as humans have to do -
+we're not running the program, we're reading the code - can't really tell what
+will happen because there's so many things that could happen. We could exit
+here, we could exit there, we could exit here. Who knows. And that makes it
+increasingly hard to maintain and refactor the code. So SafeRef you the option
+to say this is always going to be valid. You can't check it. So if it's not
+valid, go fix that bug somewhere else. It should be valid here.
+
+26:16 SHARON: So what kind of -
+
+26:16 DANA: The assumptions are broken.
+
+26:16 SHARON: So what kind of errors happen when that assumption is broken? Is
+that a crash? Is that a DCHECK kind of thing?
+
+26:22 DANA: For SafeRef and for WeakPtr, if you try to use it without checking
+it, or write it incorrectly, they will crash. And crashing in this case means a
+safe crash. It's not going to lead to a security bug. It's literally just
+terminating the program.
+
+26:41 SHARON: Does that also mean you get a sad tab as a user? Like when the
+little sad file comes up?
+
+26:47 DANA: Yep. It would. If you're in the render process, you take it down.
+It's a sad tab. So that's not great. It's better than a security bug. Because
+your options here are don't write bugs. Ideal. I love that idea, but we know
+that bugs happen. Use a native pointer, security problem. Use a WeakPtr, that
+makes sense if you wanted it to sometimes not be there. But if you want it to
+always be there - because you have to make a choice now of what you're supposed
+to do if it's not, and it makes the code very hard to understand. And you're
+only going to find out it can't be there through a crash anyhow. Or use a
+SafeRef. And it's going to just give you the option to crash. You're going to
+figure out what's wrong and make it no longer do that.
+
+27:38 SHARON: I think wanting to guarantee the lifetime of some other things
+seems like a pretty common thing that you might come across. So I'm sure there
+are many cases for many people to be adding SafeRefs to make their code a bit
+safer, and also ensure that if something does go wrong, it's not leading to a
+memory bug that could be exploited in who knows how long. Because we don't
+always hear about those. If it crashes, and they can reliably crash, at least
+you know it's there. You can fix it. If it's not, we're hoping that one of our
+VRP vulnerability researchers find it and report it, but that doesn't always
+happen. So if we can know about these things, that's good. So another new type
+in base that people might have been seeing recently is a `raw_ptr` which is
+maybe why earlier we were saying let's call them native pointers, not raw
+pointers. Because the difference between `raw_ptr` and raw pointer, very easy
+to mix those up. So why don't you tell us a bit about `raw_ptr`s?
+
+28:40 DANA: So `raw_ptr` is really cool. It's a non-owning smart pointer. So
+that's kind of WeakPtr or SafeRef. These are also non-owning. And it's actually
+very similar in inspiration to what WeakPtr is. So it has a side channel where
+it can see if the thing It's pointing to is alive or gone. So for WeakPtr, it
+talks to the WeakPtrFactory and says am I deleted? And for `raw_ptr`, what it
+does is it keeps a reference count, kind of like `scoped_refptr`, but it's a
+weak reference count. It's not owning. And it keeps this reference count in the
+memory allocator. So Chrome has its own memory allocator for new and delete
+called PartitionAlloc. And that lets us do some interesting stuff. And this is
+one of them. And so what happens is as long as there is `raw_ptr` around, this
+reference count is non-zero. So even if you go and you delete the object, the
+allocator knows there is some pointer to it. It's still out there. And so it
+doesn't free it. It holds it. And it poisons the memory, so that just means
+it's going to write some bit pattern over it, so it's not really useful
+anymore. It's basically re-initialized the memory. And so later, if you go and
+use this `raw_ptr`, you get access to just dead memory. It's there, but it's
+not useful anymore. You're not going to be able to create security bugs in the
+same way. Because when we first started talking about a Use-After-Free - you
+have your goat, you free it, a cow is there, and now your pointer is pointing
+at the wrong thing - you can't do that because as long as there's this
+`raw_ptr` to your goat, the goat can be gone, but nothing else is going to come
+back here. It's still taken by that poisoned memory until all the `raw_ptr`s
+are gone. So that's their job, to protect us from a Use-After-Free being
+exploitable. It doesn't necessarily crash when you use it incorrectly, you just
+get to use this bad memory inside of it. If you try to use it as a pointer,
+then you're using a bad pointer, you're going to probably crash. But it's a
+little bit different than a WeakPtr, which is going to deterministically crash
+as soon as you try to use it when it's gone. It's really just a protection or a
+mitigation against security exploits through Use-After-Free. And then we
+recently just added `raw_ref`, which is really the same as `raw_ptr`, except
+addressing null ability. So smart pointers in C++ have historically all allowed
+a null state. That's representative of what native pointers did in C and C++.
+And so this is kind of just bringing this along in this obvious, historical
+way. But if you look at other languages that have been able to break with
+history and make their own choices kind of fresh, we see that they make choices
+like not having null pointers, not having null smart pointers. And that
+increases the readability and the understanding of your code greatly. So just
+like for WeakPtr, how we said, we just check if it's there or not. And if it's
+not, we return, and so on. It's every time you have a WeakPtr, if you were
+thinking of a timeline, every time you touch a WeakPtr, your timeline splits.
+And so you get this exponential timeline of possible states that your
+software's in. That's really intense. Whereas every time you cannot do that,
+say this can't be null, so instead of WeakPtr, you're using SafeRef. This can't
+be not here or null, actually. WeakPtr can't just be straight up null. This is
+always present. Then you don't have a split in your timeline, and that makes it
+a lot easier to understand what your software is doing. And so for `raw_ptr`,
+it followed this historical precedent. It lets you have a null value inside of
+it. And `raw_ref` is our kind of modern answer to this new take on nullability.
+And so `raw_ref` is a reference wrapper, meaning it holds a reference inside of
+it, conceptually, meaning it just can't be null. That is just basically - it's
+a pointer, but it can't be null.
+
+33:24 SHARON: So these do sound the most straightforward to use. So basically,
+if you're not sure - or your class members at least - any time you would use a
+native pointer or an ampersand, basically you should always just put those in
+either a `raw_ptr` or a `raw_ref`, right?
+
+33:45 DANA: Yeah, that's what our style guide recommends, with one nuance. So
+because `raw_ptr` and `raw_ref` interact with the memory allocator, they have
+the ability to be like, turn on or off dynamically at runtime. And there's a
+performance hit on keeping this reference count around. And so at the moment,
+they are not turned on in the renderer process because it's a really
+performance-critical place. And the impact of security bugs, there is a little
+less than in the browser process where you just immediately get access to the
+whole system. And so we're working on turning it on there. But if you're
+writing code that's only in the renderer process, then there's no point to use
+it. And we don't recommend that you use it. But the default rule is yes. Don't
+use a native pointer, don't use a native reference. As a field to an object,
+use a `raw_ptr`, use a `raw_ref`. Prefer something with less states, always,
+because you get less branches in your timeline. And then you can make it cost
+if you don't want it to be able to rebound to an object, if you don't want the
+pointer to change. Or you can make it mutable if you wanted to be able to.
+
+34:58 SHARON: So you did mention that these types are ref counted, but earlier
+you said that you should avoid ref counting things. So
+
+35:04 DANA: Yes.
+
+35:11 SHARON: So what's the balance there? Is it because with a
+`scoped_refptr`, you're a bit more involved in the ref counting, or is it just,
+this is we've done it for you, you can use it. This is OK.
+
+35:19 DANA: No, this is a really good question. Thank you for asking that. So
+there's two kinds of ref counts going on here. I tried to kind of allude to it,
+but it's great to make it clear. So `scoped_refptr` is a strong ref count,
+meaning the ref count owns the object. So the destructor runs, the object is
+gone and deleted when that ref count goes to 0. `raw_ref` and `raw_ptr` are a
+witchcraft count. They could be pointing to something owned in a
+`scoped_refptr` even. So they can exist at the same time. You can have both
+kind of ref counts going at the same time. A weak ref count, in this case, is
+holding the memory alive so that it doesn't get re-used. But it's not keeping
+the object in that memory alive. And so from a programming state point-of-view,
+the weak refs don't matter. They're helping protect you from security bugs.
+They're helping to make - when things go wrong, when a bug happens, they're
+helping to make it less impactful. But they don't change your program in a
+visible way. Whereas, strong references do. That destrutor's is based on when
+the ref count goes to 0 for a strong reference. So that's the difference
+between these two.
+
+36:46 SHARON: So when you say don't use ref counting, you mean don't use strong
+ref counting.
+
+36:46 DANA: I do, yes.
+
+36:51 SHARON: And if you want to learn more about the raw pointer, `raw_ptr`,
+`raw_ref`, that's all part of the MiraclePtr project, and there's a talk about
+that from BlinkOn. I'll link that below also. So in terms of other base types,
+there's a new one that's called `base::expected`. I haven't even really seen
+this around. So can you tell us a bit more about how we use that, and what
+that's for?
+
+37:09 DANA: `base::expected` is a backport from C++ 23, I want to say. So the
+proposal for `base::expected` actually cites a Rust type as inspiration, which
+is called `std::result` in Rust. And it's a lot like `optional`, so it's used
+for return values. And it's more or less kind of a replacement for exceptions.
+So Chrome doesn't compile with exceptions enabled even, so we've never relied
+on exceptions to report errors. But we have to do complicated things, like with
+`optional` to return a bool or an enum. And then maybe some value. And so this
+kind of compresses all that down into a single type, but it's got more state
+than just an option. So `expected` gives you two choices. It either returns
+your value, like `optional` can, or it returns an error. And so that's the
+difference between `optional` and `expected`. You can give a full error type.
+And so this is really useful when you want to give more context on what went
+wrong, or why you're not returning the value. So it makes a lot of sense in
+stuff like File IO. So you're opening a file, and it can fail for various
+reasons, like I don't have permission, it doesn't exist, whatever. And so in
+that case, the way you would express that in a modern way would be to return
+`base::expected` of your file handle or file class. And as an error, some
+enumerator, perhaps, or even an object that has additional state beyond just I
+couldn't open the file. But maybe a string about why you couldn't open the file
+or something like this. And so it gives you a way to return a structured error
+result.
+
+39:05 SHARON: That's found useful in lots of cases. So all of these types are
+making up for basically what is lacking in C++, which is memory safety. C++, it
+does a lot. It's been around for a long time. Most of Chrome is written in it.
+But there are all these memory issues. And a lot of our security bugs are a
+result of this. So you are working on bringing Rust to Chromium. Why is that a
+good next step? Why does that solve these problems we're currently facing?
+
+39:33 DANA: So Rust has some very cool properties to it. Its first property
+that is really important to this conversation is the way that it handles
+pointers, which in Rust would be treated pretty much exclusively as references.
+And what Rust does is it requires you to tell the compiler the relationships
+between the lifetimes of your references. And the outcome of this additional
+knowledge to the compiler is memory safety. And so what does that mean? It
+means that you can't write a Use-After-Free bug in Rust unless you're going
+into the unsafe part of the language, which is where scariness exists. But you
+don't need to go there to write a normal program. So we'll ignore it. And so
+what that means is you can't write the bug. And so that doesn't just mean I
+also like to believe I can write C++ without a bug. That's not true. But I
+would love to believe that. But it means that later, when I come back and
+refactor my code, or someone comes who's never seen this before and fixes some
+random bug somewhere related to it, they can't introduce a Use-After-Free
+either. Because if they do, the compiler is like, hey - it's going to outlive
+it. You can't use it. Sorry. And so there's this whole class of bugs that you
+never have to debug, you never ship, they never affect users. And so this is a
+really nice promise, really appealing for a piece of software like Chrome,
+where our basic purpose is to handle arbitrary and adversarial data. You want
+to be able to go on some web page, maybe it's hostile, maybe not. You just get
+a link. You want to be able to click that link and trust that even if it's
+really hostile and wanting to destroy you, it can't. Chrome is that safety net
+for you. And so Rust is that kind of safety net for our code, to say no matter
+how you change it over time, it's got your back. You can't introduce this kind
+of bug.
+
+42:03 SHARON: So the first project sounds really cool. If people want to learn
+more or get involved - if you're into the whole languages, memory kind of thing
+- where can people go to learn more?
+
+42:09 DANA: So if you're interested in helping out with our Rust experiment,
+then you can look for us in the Rust channel on Slack. If you're interested in
+C++ language stuff, you can find us in the CXX channel on Slack, as well. As
+well as the same CXX@chromium.org mailing list. And there is, of course, the
+rust-dev@chromium.org mailing list if you want to use email to reach us as
+well.
+
+42:44 SHARON: Thank you very much, Dana. There will be notes from all of this
+also linked in the description box. And thank you very much for this first
+episode.
+
+42:52 DANA: Thanks, Sharon This was fun.
diff --git a/docs/transcripts/wuwt-e02-dchecks.md b/docs/transcripts/wuwt-e02-dchecks.md
new file mode 100644
index 0000000..537e03d
--- /dev/null
+++ b/docs/transcripts/wuwt-e02-dchecks.md
@@ -0,0 +1,453 @@
+# What’s Up With DCHECKs
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 2, a 2022 video discussion between [Sharon (yangsharon@chromium.org)
+and Peter (pbos@chromium.org)](https://www.youtube.com/watch?v=MpwbWSEDfjM).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+You've seen DCHECKs around and been asked to use them in code review, but what
+are they? What's the difference between a CHECK and a DCHECK? How do you use
+them? Here to answer that is special guest is Peter, who works on UI and
+improving crash reports.
+
+Notes:
+- https://docs.google.com/document/d/146LoJ1E3N3E6fb4zDh92HPQc6yhRpNI7DSKlJjaYlLw/edit
+
+Links:
+- [What's Up With Pointers](https://www.youtube.com/watch?v=MpwbWSEDfjM)
+
+---
+
+00:00 SHARON: Hello, and welcome to What's Up With That?, the series that
+demystifies all things Chrome. I'm your host, Sharon. And today, we're talking
+about DCHECKs. You've seen them around. You've probably been told to add one in
+code review before. But what are they? What are they for, and what do they do?
+Our guest today is Peter, who works on desktop UI and Core UI. He's also
+working on improving Chrome's crash reports, which includes DCHECKs. Today
+he'll help us answer, what's up with DCHECKs? Welcome, Peter.
+
+00:30 PETER: Thanks for having me.
+
+00:32 SHARON: Yeah. Thanks for being here. So the most obvious question to
+start with, what is a DCHECK?
+
+00:39 PETER: So a CHECK and a DCHECK are both sort of things that make sure
+that what you think is true is true. Right? So this should never be called with
+an empty vector. You might add a CHECK for it, or you might add a DCHECK for
+it. And it's sort of similar to a search, which you may have hit during earlier
+programming outside of Chrome. And what it means is when this line gets hit, we
+check and see if it's true. And if it's not true, we crash. DCHECKs differ from
+CHECKs in that they are traditionally only in debug builds, or local
+development builds, or on our try-bots. So they have zero overhead when Chrome
+hits stable, because the CHECK just won't be there.
+
+01:24 SHARON: OK. So like if the D stands for Debug. That make sense.
+
+01:28 PETER: Yeah. I want debug to turn into developer, because now we have
+them by default if you're no longer - if you're doing a release build, and
+you're not turning them off, and you're not doing an official build, you get
+them.
+
+01:42 SHARON: OK. Well, you heard it here first, or maybe you heard it before.
+I heard it here first. So you mentioned asserts. So something that I've seen a
+couple times in Chrome, and also is part of the standard library, is
+`static_assert`. So how is that similar or different to DCHECKs? And why do we
+use or not use them?
+
+02:00 PETER: Right. So `static_assert`s are - and you're going to have to ask
+C++ experts, who can probably take some of the sharp edges off of this - but
+it's basically, if you can assert something in compile time, then you can use a
+`static_assert`, which means that you don't have to hit a code path where it's
+wrong. It sort of has to always hold true. And whenever you can use a
+`static_assert`, use a `static_assert`, because it's free. And basically, you
+can't compile the program if it's not true.
+
+02:31 SHARON: OK. That's good to know, because I definitely thought that was
+one of the C++ standard library things we should avoid, because we have a
+similar thing in Chromium. But I guess that's not the case.
+
+02:41 PETER: Yeah. Assert is the one that is - OK, so this is a little
+complicated, right? `static_assert` is a language feature, not a library
+feature. And someone will tell me that I'm wrong about something about this.
+Asserts are just sort of a poorer version of DCHECKs. So they won't go through
+our crash handling. It won't print the pretty stacks, et cetera.
+`static_assert`s, on the other hand, are a compile time feature. And we don't,
+as far as I know, have our own wrapper around it. We just use `static_assert`.
+So what you would maybe use this for is like if you have a constant - like, say
+you have an array, and the code makes an assumption that some constant is the
+size of this array, you can assert that in compile time, and that would be a
+good use of a `static_assert`.
+
+03:26 SHARON: OK. Cool. So you mentioned that some things have changed with how
+DCHECKs work. So can you give us a brief overview of the history of DCHECKs -
+what they used to be, people who have been using them for a while, how might
+they have changed from the idea of what they have as a DCHECK in their mind?
+
+03:43 PETER: Sure. So this is as best I know. I'm just sort of extrapolating
+from what I've seen. And what I think originally was true is that a CHECK used
+to be this logging statement, where you essentially compile the file name and
+the line number. And if this ever hits, then we'll log some stuff and then
+crash. Right? Which comes with a little bit of overhead, especially on size,
+that you basically take the file name and line number for every instance, and
+that generates a bunch of strings and numbers that essentially add to Chrome's
+binary size. I don't know how many steps between that and where we currently
+are. But right now, our CHECKs are just, if condition is false, crash, which
+means that you won't, out of the CHECK, get file name and line number. We'll
+get those out of debugging symbols. And you also won't get any of the logging
+messages that you can add to the end of a CHECK, which means that your debug
+info will be poorer, but it will be cheaper to use. So they've gotten from
+being pretty heavy CHECKs to being really cheap.
+
+05:01 SHARON: OK. So that kind of leads us into the question that I think most
+people want to have answered, which is, when should I use a DCHECK? When should
+I use a CHECK? When should I use neither?
+
+05:13 PETER: I would say that historically, we've said CHECKs are expensive.
+Don't use them unless you sort of have to. And I don't think that holds true
+anymore. So basically, unless you are in really performance-critical code, then
+use a CHECK. If there's anything that you care about where the program state
+will be unpredictable from this point on if it's not true, CHECK it. It's not
+that expensive. Right? We have a lot of code where we push a string onto a
+vector, and that never gets flagged in code review. And it's probably like 10
+times more expensive, if not 100 times more expensive, than adding a CHECK. The
+exception to that is if you're in a really hot loop where you don't want to
+dereference a pointer, then a CHECK might add some cost. And the other is if
+the condition that you're trying to validate is really expensive. It's not the
+CHECK itself that's expensive. It's the thing you're evaluating. And if that's
+expensive, then you might not afford doing a CHECK. If you don't know that it's
+expensive, it's probably not expensive.
+
+06:20 SHARON: Can you give us an example of something expensive to evaluate for
+a CHECK?
+
+06:24 PETER: Right. So say that you have something in video code that for every
+video frame, for every pixel validates the alpha value as opaque, or something.
+That would probably make video conferencing a little bit worse performance.
+Another thing would just be if you have to traverse a graph on every frame, and
+it will sort of jump all over memory to see if some reachability problem in
+your graph is true, that's going to be a lot more expensive. But CHECKing that
+index is less than some vector bounds, I think that should fall under cheap.
+And -
+
+07:02 SHARON: OK.
+
+07:02 PETER: culturally, we've tried to avoid doing a lot of these. And I think
+it's just hurting us.
+
+07:09 SHARON: OK. So since most places we should use CHECKs, are there any
+places where a DCHECK would be better then? Or any time you would have normally
+previously used a DCHECK, you should just make that a check?
+
+07:23 PETER: So we have a new construct that's called `EXPENSIVE_DCHECK`s, or
+if `EXPENSIVE_DCHECK`s are on, I think we should add a corresponding macro for
+`EXPENSIVE_DCHECK`. And then you should be able to just say, either it's
+expensive and has to be a DCHECK, so use `EXPENSIVE_DCHECK`; otherwise, use
+CHECK. And my hunch would be like 95% of what we have as DCHECKs would probably
+serve us better as CHECKs. But your code owner and reviewer might disagree with
+that. And it's not yet documented policy that we say CHECKs are cheap; just add
+a billion of them. But I would like to get there eventually.
+
+08:04 SHARON: OK. So if you put in a CHECK, and your reviewer tells them this
+should be a DCHECK, the person writing the CL can point them to this video, and
+then they can discuss from there.
+
+08:13 PETER: I mean, yeah, you can either say Peter disagrees with you, or I
+can get further along this and say we make policy that CHECKs are cheap, so
+they are preferable. So a lot of foot-shooters with DCHECKs is that you expect
+this property to hold true, but you never effectively CHECK it. And that can
+lead to all sorts of bad stuff, right? Like if you're trying to DCHECK that
+some origin for some frame makes some assumptions of site iso - I don't know
+site isolation well enough to say this. But basically, if you're DCHECKing that
+the code that you're running runs under some sort of permissions, then that is
+effectively unchecked in stable, right? And we do care about those properties,
+and it would be really good if we crashed rather than leaked information
+between sites.
+
+09:12 SHARON: Right.
+
+09:14 PETER: Yeah.
+
+09:16 SHARON: So that seems like a good tie-in for the fact that within some
+security people, they don't have the most positive impression of DCHECKs, shall
+we say? So a couple examples of this, for listeners who maybe aren't familiar
+with this, is one person previously on security saying DCHECKs are pronounced
+as "code that's not tested". Someone else I told about this episode - I said,
+we're going to talk about DCHECKs - they immediately said, is it going to be
+about why DCHECKs are bad? So amongst the Chrome security folks, they are not a
+huge fan of DCHECKs. Can you tell us maybe why that is?
+
+09:51 PETER: So if we go back a little bit in time, it used to be that DCHECKs
+were only built for developers if they do a debug build. And Chrome has gotten
+so big that you don't want to do a debug build or the UI is incredibly slow.
+Unfortunately, it's sort of not that great an experience to work in a debug
+build. So people work in a release build. That doesn't mean that they don't
+care about the things they put under DCHECK. It just means they want to go on
+with their lives and not wait x minutes for the browser to launch, or however
+bad it is nowadays. And that means that they, unfortunately, lose coverage for
+the DCHECKs. So this means that if your code is not exercised well under tests,
+then this is completely not enforced. But it's slightly better than a comment,
+in that you're really expecting this thing to hold true, and that's clearly an
+expectation. But how good is the expectation if you don't look at it? So last
+year, I believe, we made it so that DCHECKs are on by default if you're not
+doing an official build. And this included release builds. So now, it's like at
+least if you're doing development and you hit this condition, it's going to
+explode, which is really good, because then you can find a lot of issues, and
+we can prevent a lot of issues from ever happening in the first place. It is
+really hard for you, as a developer, to make the assumption that if this
+invariant is ever false, I will find it during development, and it will never
+happen in the wild. And DCHECKs are essentially either, I will find this
+locally before I submit it, or all bets are off; or it is I don't care that
+much if this thing doesn't hold true, which is sort of a weird assertion to
+make. So I think we're in this little awkward in-between state. And this
+in-between state, remember, mostly exists as a performance optimization from
+when CHECKs used to be a lot more expensive, in terms of code size. So did I
+cover most of this?
+
+12:06 SHARON: Yeah. I think, based on that, I think it's pretty easy to see why
+people who are more concerned about security are not a fan of this.
+
+12:13 PETER: I mean, if you care about it, especially if it causes privacy or
+security or user-harm sort of things, just CHECK. Just CHECK, right? If it
+makes your code animate a thing slightly weirder, like it will just jump to the
+end position instead of going through your fence load, whatever. Maybe you can
+make that a DCHECK. Maybe it doesn't matter. Like it's wrong, but it's not that
+bad. But most of the cases, you DCHECK something, where it's like the program
+is going to be in some indeterminate state, and we actually care about if it's
+ever false. So maybe we can afford to make it a CHECK. Maybe we should look
+more about our sort of vector pushbacks than we should look at our CHECKs, and
+then just have more CHECKs. More CHECKs. Because it's also like when things
+break, it's a lot cheaper to debug a DCHECK than your program is in some
+indeterminate state, because it was allowed to pass through a DCHECK that you
+thought was - and when you read the code, unless you're used to reading it as
+DCHECKs - oh, that just didn't get enforced - it's sort of hard to try to
+figure out why the thing was doing the wrong thing in the first place.
+
+13:22 SHARON: OK. How is this as a summary? When in doubt, CHECK it out.
+
+13:27 PETER: I like that. I like that. And you might get pushback by reviewers,
+who aren't on my side of the fence yet. And then you can decide on which hill
+you want to die on, at least until we've made policy to just not complain about
+DCHECKs, or not complain about CHECKs.
+
+13:45 SHARON: All right. That sounds good. So you mentioned stuff failing in
+the wild. And for people who might not know, do you want to just briefly
+explain what failing in the wild means?
+
+13:54 PETER: OK. So there's two things. Just failing in the wild just means
+that when this thing rolls out to Canary, Dev, Beta, Stable, if you have a
+CHECK that will crash and generate a crash report as if you had a memory bug,
+but it crashes in a deterministic way, at a deterministic spot - so you can
+find out exactly what assumption was violated. Say that this should never be
+called with a null pointer. Then you can say, look at this line where it
+crashed. It clearly got hit with a null pointer. And then you can try to figure
+out, from the stack, why that happened, rather than after you post this pointer
+to a task, it crashes somewhere completely irrelevant from the actual call
+site. Well, so in the wild specifically means it generates a crash report so
+you can look at it, or in the wild means it crashes at a user computer rather
+than - in the wildness outside of development. And as for the other part of in
+the wild, it's that we have started running non-crashy DCHECKs for a percentage
+of Windows Canary. And we're looking to expand that. And we're gathering
+information, basically, about which assertions or invariants that we have are
+violated in practice in the wild, even though we don't think that they should
+be. And that will sort of also culturally move the needle so that we do care
+about DCHECKs. And when we care about DCHECKs, sort of similarly to how we care
+about CHECKs, is it really that important to make the big distinction between
+the two? Except for the case where you have really expensive DCHECKs, they
+might still be worth keeping separate. And those will be things like, if you do
+things for - say that you zero out memory or something for every memory block
+that you allocate and free, or you do things for every audio sample, or for
+every video frame pixel, those sort of things. And then we can sort of keep
+expensive stuff gated out from CHECKs. And then maybe we don't need this
+in-between where people don't know whether they can trust a DCHECK or not.
+
+16:04 SHARON: So you mentioned that certain release builds now have DCHECKs
+enabled. So for those in the wild versus regular CHECKs in the wild, if those
+happen to fail, do the reports for those look the same? Are they in the same
+place? Can they be treated the same?
+
+16:20 PETER: Yeah. Well, they are uploaded to the same crash-reporting thing.
+They show up under a special branch. And you likely will get bugs filed to you
+if they hit very frequently, just like you would with crashes. There's a sort
+of slight difference, in that they say dump without crashing. And that's just
+sort of a rollout strategy for us. Because if we made DCHECK builds incredibly
+crashy, because they hit more than CHECKs, then we can never roll this thing
+out. Or it gets a lot scarier for us to put this on 5% of a new platform that
+we haven't tested. But as it is right now, the first DCHECK that gets hit for
+every process gets a crash dump uploaded.
+
+17:07 SHARON: OK. So I've been definitely told to use dump without crashing at
+certain points in CLs, where it's like, OK, we think that this shouldn't
+happen. But if it does, we don't necessarily want to crash the browser because
+of it. With the changes you've mentioned to DCHECKs happening, should those
+just be CHECKs instead now or should those still be dump without crashing?
+
+17:29 PETER: So if you want dump without crashing, and you made those a DCHECK,
+then you would only have coverage in the Canary channels that we are testing.
+Right? So if you want to get dump reports from the platforms that we're not
+currently testing, including all the way up to Stable, you probably still want
+to keep that a dump without crashing. You want to make sure that you're not
+using the sort of - you want to make sure that you triage these, because you
+don't want to keep these generating crash dumps n forever. You should still
+treat them as if they were crashes. And I think the same thing should hold true
+for DCHECKs. You should only add them for an invariant that you care about
+being violated, right? So as it is violated, you should either figure out why
+your invariant was wrong, or you should try to fix the breakage. And you can
+probably add more information to logging to figure out why that happened.
+
+18:41 SHARON: So when you have a CHECK, and it crashes in the wild, you get a
+stack trace. And that's what you have to work on to figure out what went wrong
+for debugging. Right? So what are some things that you can do, as a developer,
+to make these CHECKs a bit more useful for you - ways to incorporate other
+information that you can use to help yourself debug?
+
+19:01 PETER: So some of the stuff that we have is we have something called
+crash keys, which are essentially, you can write a piece of string data,
+essentially - there's probably some other data types - and if you write those
+before you're running dump without crashing, or before you hit a CHECK, or
+before you hit a DCHECK, then those will be uploaded along the crash dump. And
+if you talk to someone who knows where to find them, you can basically go in
+under a crash report, and then under field product data, or something like
+that, you should be able to find your key-value pair. And if you have
+information in there, you'll be able to look at it. The other thing that I like
+to do, which is probably the more obvious thing, is if you have somewhat of a
+hypothesis that this thing should only fail if a or b or c is not true, then
+you can add CHECKs for those. Like, if a CHECK is failing, you can add more
+CHECKs to see why the CHECK was failing. In general, you're not going to get as
+much out of a mini-dump that you want. You're not going to have the full heap
+available to you, because that would be a mega-dump. You can usually find
+whatever is on the stack if you go in with a debugger. And I know that you
+wanted to lead me into talking about CHECK\_GT and CHECK\_EQ, which are
+essentially, if you want to check that x is greater than y, then you should use
+CHECK\_GT(x,y). The problem with those, in this sort of context, is that,
+similarly to CHECKs - so CHECK\_GT gets compiled into, basically, if not x is
+greater than y, crash. So unfortunately, the values of x and y are optimized
+out when you're doing an official build.
+
+21:02 SHARON: So this makes me think of some stuff we mentioned in the last
+episode, which was with Dana. Check it out if you haven't. But one of the types
+we mentioned there was SafeRef, which enforces a certain condition. And if that
+fails - so in the case of a SafeRef, it ensures that the value you have there
+is not null. And if that's ever not true, then you do get a crash similar to if
+a CHECK fails. So in general, would you say it's better practice to enforce and
+make sure your assumptions are held in these other, more structural ways than
+relying on CHECKs instead?
+
+21:41 PETER: So let me see if I can get at what you actually want out of that
+one. So if we look at - there's a RawRef type, right? So what's good with the
+RawRef is that you have a type that annotates that this thing cannot possibly
+be null. So if you assign to it, and you're assigning a null pointer, your
+program is going to crash, and you don't need to think about whether you throw
+a null pointer in or not. If you keep passing a RawRef around, then that's
+essentially you passing around a non-null pointer. And therefore, you don't
+have to check that it's not null pointer in every step of the way. You only
+need to do it when you're - I mean, the type will do it for you, but it only
+needs to happen when you're converting from a pointer to a ref, essentially, or
+a RawRef. And what's so good about that is now you have the - previously, you
+might just CHECK that this isn't called with null pointer or whatever. But then
+you would do that for four or five arguments. And you'd be like, null pointer
+CHECKs are this part of the function body. And then it just gets super-noisy.
+But if you're using the RawRef types, then the semantics of the type will
+enforce that for you. And you don't have to think about that when reading the
+code, because usually when you read the code, you're going to be like, it's a
+pointer. Can it be null or not? What does it point to? And this thing will at
+least tell you, it can't be null. And you still have the question of, what does
+it point to? And that's fine. So I like enforcing this through types more than
+checking those assumptions, and then checking inside of what happens. If you
+were assigned to this RawRef, then it's going to crash in the constructor if
+you have a null pointer. And then based on that stack trace, if we have good
+stack data, you're going to know at what line you created the RawRef. And
+therefore, it's equivalent to checking for not null pointer, because you can
+trust the type to do the checking. And since I know Dana made this, I can
+probably with 200% certainty say that it's a CHECK and not a DCHECK. But we do
+have a couple of other places where you have a WeakPtr that shouldn't be
+dereferenced on the wrong sequence. And those are complicated words. And that,
+unfortunately, is a DCHECK. So we're hitting some sort of - I don't know if
+that CHECK is actually expensive, or if it should be a CHECK, or if it could be
+a CHECK. I think, especially, if you're in core types, the size overhead of
+adding a CHECK is negligible, because all of the users of it benefit from that
+CHECK. So unless it's incredibly -
+
+24:28 SHARON: What do you mean by core types?
+
+24:30 PETER: Say that you make a `scoped_refptr` something, that ref pointer is
+used everywhere. So if you CHECKed in the destructor, then you're validating
+all of the clients of your scope ref pointer. So for one CHECK, you get the
+price of a lot of CHECKing. Whereas if in your client code you're validating
+some parameters of an API call that only gets called once, then that's one
+CHECK you add for one case. But if you're re-use, then your CHECK gets a lot
+more value. And it's also easier to get parameters wrong sometimes if you have
+500 clients that are calling your API. You can't trust all of them to get it
+right. Whereas if you're just developing your feature, and it's only used by
+your feature, then you can be a little bit more certain with how it's being
+called. I would say, still add CHECKs, because code evolves over time. It's
+sort of like how you can add unit tests to make sure that no one breaks your
+code in the future. If you add CHECKs, then no one can break your code in the
+future.
+
+25:37 SHARON: Mm-hmm. OK. So you mentioned a few things about how CHECKs and
+DCHECKs are changing. [AUDIO OUT] what is currently in the works, and what is
+the long-term goal and plan for CHECKs and DCHECKs.
+
+25:53 PETER: So currently what's in the work is we've made sure that some
+libraries that we use, like Abseil and WebRTC, which is a first-party
+third-party library, that they both use Chrome's crashing report system, which
+means that you get more predictable crash stacks because it's using the
+immediate crash macro. But also, you get the fatal logging field that I talked
+about. That gets logged as part of crash dumps. So you hopefully have more
+glanceable, actionable crash reports whenever a CHECK is violated inside of
+Abseil, or in WebRTC, as it were. And then upcoming is we want to make sure
+that we keep an eye out for our DCHECKs on other platforms, such as Mac. I know
+that there's some issues with getting that fatal log field in the GPU process,
+and I'm working on fixing that as well. So hopefully, it just means more
+reports for the things you care about and easier to action on reports. That's
+what we're hoping.
+
+27:03 SHARON: If people think that this sounds really cool, want to have some
+more involvement, or want to ask more questions, what's a good place for them
+to do that?
+
+27:11 PETER: I like Slack as a thing for this. So the #cxx channel on Slack,
+the #base channel on Slack, the #halp channel on Slack is really good. #halp is
+really, I think, unintimidating. You can just throw whatever question you have
+in there, and I happen to be around there. If you can find out what my last
+name is through sheer force of will, you can send me an email to my Chromium
+username. What else would we have? I think if they want to get involved, just
+add CHECKs to your code. That's a really good way to do it. Just make sure that
+your code does what you expect it to in more cases.
+
+27:48 SHARON: Maybe if you have a CL, and you're just doing some drive-by
+cleanup, you can turn some DCHECKs into CHECKs also?
+
+27:56 PETER: If your reviewer is cool with that, I'm cool with that. Otherwise,
+you can just try to hope for us making that policy that we use CHECKs - if it's
+something we care about, we use a CHECK instead of a DCHECK, unless we have a
+really good reason to use a DCHECK. And that would be performance.
+
+28:15 SHARON: That sounds good. And one last question is, what do you want
+people to take away as their main takeaway from this discussion?
+
+28:26 PETER: I think validating code assumptions is really valuable. So you
+think that you're pretty smart when you're writing something, or you remember -
+I mean, you're sometimes kind of smart when you're writing something. And
+you're like, this can't possibly be wrong. And in practice, looking at crash
+reports, these things are wrong all the time. So please validate any
+assumptions that you make. It's also, I would say, better than a comment,
+because it's a comment that doesn't get outdated without you noticing it. So, I
+think, validate your assumptions to make sure that your code is more robust.
+And validate properties you care about. And don't be afraid to use CHECKs.
+
+29:13 SHARON: All right. That sounds like a good summary. Thank you very much
+for being here, Peter. It was great to learn about DCHECKs.
+
+29:18 PETER: Yeah. Thanks for having me.
+
+29:24 SHARON: Action. Hello.
+
+29:26 PETER: Oh. Take four.
+
+29:29 SHARON: [LAUGHS] Take four. And action.
diff --git a/docs/transcripts/wuwt-e03-content.md b/docs/transcripts/wuwt-e03-content.md
new file mode 100644
index 0000000..cd19812
--- /dev/null
+++ b/docs/transcripts/wuwt-e03-content.md
@@ -0,0 +1,488 @@
+# What’s Up With //content
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 3, a 2022 video discussion between [Sharon (yangsharon@chromium.org)
+and John (jam@chromium.org)](https://www.youtube.com/watch?v=SD3cjzZl25I).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+What lives in the content directory? What is the content layer? How does it fit
+into Chrome and the web at large? Here to answer all that and more is today’s
+special guest, John, who not only is a Content owner, but actually split the
+codebase to create the Content layer.
+
+Notes:
+- https://docs.google.com/document/d/1EJnG5gK8rQwHkdZTKl8vIwx9oScP8TaKBgwzBafIh9M/edit
+
+Links:
+- [//content/README.md](https://crsrc.org/c/content/README.md)
+- [//content/public/README.md](https://crsrc.org/c/content/public/README.md)
+- [What's Up With Pointers](https://www.youtube.com/watch?v=MpwbWSEDfjM)
+
+---
+
+00:00 SHARON: Hello, and welcome to "What's Up with That", the series that
+demystifies all things Chrome. I'm your host, Sharon, and today, we're talking
+about content. What lives in the content directory? What is the content layer?
+How does it fit into Chrome and the web at large? Here to answer all of that
+and more is today's special guest, John. He's not only a content owner, but
+actually split the code base to create the content layer. Since then, a theme
+of his work has been Chrome's architecture, and how to make it usable by
+others. He's been involved far and wide across Chrome, but today, we're
+focusing on content. John, welcome to the program.
+
+00:33 JOHN: Hi, everyone, and thanks for setting this up, Sharon. My name's
+John, and I'm happy to try to shed some light and history on this part of the
+Chrome codebase. I've had the pleasure of working on a lot of different parts
+of Chrome over a number of years I've worked on it. A theme of my work has been
+on the architecture of Chrome and making it reusable by other products. And one
+of the projects has been splitting up the codebase and helping create this
+content layer.
+
+01:02 SHARON: So, can you tell us what the content layer is? Because content is
+a very overloaded term, and we're going to say it a lot today. So you mentioned
+the content layer. Can you tell us what that is?
+
+01:10 JOHN: Yes. The content layer is a part of the Chrome codebase that's
+responsible for the multiprocess sandbox implementation of our platform.
+
+01:24 SHARON: And another term that I had heard a lot tossed around before I
+really understood what was going on was the content public API. So is that the
+same as the content layer, or is that different?
+
+01:36 JOHN: It's part of it. So the content component is very large, and so,
+we've surrounded it by this small public API. So that you hide the
+implementation details and the private directories, and then, embedders just
+only have access to a small public layer.
+
+01:56 SHARON: How did we end up with this content layer? Can you give us a bit
+of history of how we came up with it? And also, maybe why it's called content?
+
+02:02 JOHN: Sure. The history is - in the beginning, Chrome, like all software
+projects begins nice and easy to understand. But over time, as you add a lot
+more features to go from zero users to billions of users, it becomes harder to
+understand. Small files, small classes become much larger small functions kind
+of get numerous hooks to talk to every feature, because they want to know when
+something happens. And so, this idea started that let's separate the product.
+Things that make Google Chrome what it is from the platform, which is what any
+browser, any minimal browser doing the latest HTML specs would need to
+implement them in a sandbox, a multiprocess way. And so, content was the lower
+part, and that's how it started.
+
+02:58 SHARON: How did we get the name content?
+
+02:58 JOHN: The name is like a pun. And when we started Chrome, one of the
+ideas was, we'll focus on content and not Chrome, and so, the browser will get
+out of the way. Chrome is a term used to refer to all the user interface parts
+of the browser. And so, we said, it's going to be content and not Chrome. And
+so, when you open Chrome, you just see a very small UI. Most of what you see is
+the content. And so, when we split the directory, it was originally called
+Source Chrome, and so, the content part, that's the pun. That's where it came
+from.
+
+03:34 SHARON: That's fun. Earlier, you mentioned embedders of content. Can you
+tell us what an embedder of content is? And this is part of why I was very
+excited about this episode, because I was working on a team where we were
+embedders of content for a long time. Well over a year, and it took me a long
+time to really understand what that was. Because, as you mentioned now,
+Chrome's grown a lot. You work on a very specific thing understanding these
+more general concepts of what is content? What is a content embedder are less
+important to what you do day-to-day. But can you tell us what an embedder of
+content is?
+
+04:13 JOHN: Sure. An embedder of content is simply anybody who chooses to use
+that code to build a browser on top of it. And so, in the beginning, right when
+we did this, the goal was just to have one embedder. Or not the goal, what we
+had was just one embedder. It was Chrome. But then, right away, we were like,
+you know what? It would be nice for people who work on content and not the
+feature part to build a smaller binary. It builds faster. It debugs faster,
+runs faster. And so, we built this minimal example also to other people called
+content shell. And then, we started running tests against that, and that was
+the first - or the second embedder of content. And then since then, what was
+unexpected, what we started for code health reasons turned out to be very
+useful for other projects to restart - or start building their browser from.
+And so, things like Android webview, which was using its own fork of web kit,
+then started using content. That was one first-party example. But then, other
+projects came along. Things like Electron and content-embedded framework, all
+started building not just products on top of it, but other frameworks.
+
+05:30 SHARON: That was really surprising to learn about, because it seems
+unsurprising that you would build another browser based on Chromium. And people
+have heard about this when Edge switched over to Chromium. But to learn that
+things like Electron are built around content seem really surprising, because
+that's very different from what a browser is.
+
+05:52 JOHN: But they have common needs. They have some HTML data, and they want
+to render it and do so in a safe, and stable, and secure way. And that's not
+their value add, working on that code. So it's better for them to use something
+else.
+
+06:11 SHARON: That makes sense. You also mentioned that Chrome is dependent on
+content. And when I first started working on Chrome as an intern, I had - it
+told to me so many times because I couldn't remember that Chrome can depend on
+content, but not the other way around. So can you tell us a bit about this
+layering, and why it's there?
+
+06:31 JOHN: I should also start by saying, content is not just - when we say
+content, often what we mean, you embed content. You embed content in everything
+that sits below it in the layer tree. So that includes things like Blink, our
+rendering engine. V8, our JavaScript Engine. Net, our networking library, and
+so on. And there's also you can talk to the content public APIs, but also,
+sometimes, you talk to the Blink API and the files, and V8, and so on.
+
+07:07 SHARON: So you have this many layer API or product? And, at the bottom,
+we have things like Net, Blink, and those probably have dependencies on them
+that I don't know about. And on top of that, we have content, and then, on top
+of that, we have Chrome?
+
+07:23 JOHN: Right. And so, Chrome as an embedder content can include directory
+in the content public API. But since content can have multiple embedders, it
+can't include Chrome. If content reached out directly to Chrome, then other
+people wouldn't be able to use it. Because if you try to bring in this code, it
+includes files from a directory that you're not using. So, instead, the content
+public API, it has APIs going two different directions. One direction is going
+into content, and then, one direction are these abstract interfaces that go out
+from content. And any embedder has to implement them. And so, these usually end
+up in terms like client or delegate. And these are implemented by Chrome, and
+that's how content is able to call back to it. But then, any other, of course,
+product or embedder can also implement these same interfaces.
+
+08:23 SHARON: You mentioned link and also some things called delegate and
+whatever. So we have a lot of things called something something host in
+content. Can you talk a bit about what the relationship between content and
+Blink is? Because there's a lot of mirroring in terms of how they might be set
+up, and how they relate to each other.
+
+08:37 JOHN: So Blink was the rendering engine that originally started as Web
+Kit. And we forked, and we named it Blink a number of years ago. And that did
+not have any concept of processes. So it was something that you call it in one
+process, and it does its job. And you give it whatever data it needs, and it
+gives you back the rendered data. And you can poke at it or whatever you want
+to do with it. But you needed to wrap that with some - you needed a bunch of
+code around it to make it multi-process. And also, to figure out when it needs
+something that's not available in the sandbox that it runs in, you have to
+provide that data. And so, this is where the content layer comes in. It's the
+one that wraps the rendering engine and uses the networking library and other
+things to be able to create a fully working browser.
+
+09:33 SHARON: More about processes. So it's easy to think, maybe, that the
+content - the relationship between the content layer and the browser process.
+So can you just talk a bit about how processes work in content? And what the
+content API provides in terms of accessing these processes?
+
+09:54 JOHN: So the content code runs in - it's the initial process that runs.
+Content starts up, and then - and so, it's in the browser process. But it also
+creates the render processes for where Blink runs. It creates a GPU process
+that talks to the GPU and where a bunch of the compositing happens. It creates
+a network process where we do networking. It creates other processes, things
+like audio on some platforms, storage process to isolate storage. And then, a
+lot of short lived processes for security and stability reasons. And so, you
+can have processes that run content code, but, sometimes, an embedder wants to
+run its own code in a different process. So it could re-use the same helpers
+that content has for creating a process, and we'll use that. And then, I think
+I didn't fully answer your previous question yet, which was the host part. So,
+often, you'll have classes in Blink that are running in the renderer process,
+and you need an equivalent class to drive it from the browser process. And
+that's where we often have the host suffix. So it'd be like a class for -
+
+11:11 SHARON: Can you give an example of -
+
+11:11 JOHN: Yes. So, for example, every renderer process has a class in content
+browser called render process host. And then, every tab object in Blink will
+have this class called render view, and then, in content browser, it will have
+this class called render view host.
+
+11:36 SHARON: Those are classes that, depending on what you work on, you might
+see pop up quite a bit. And there's a lot of them. They're all called render
+something host, and it's a bit tough to keep them straight. But that makes
+sense as to why they're called render and - why render and host are in the
+names for them. So you just listed a bunch of different process types. The GPU
+process, the browser process, render processes. And, usually, whenever we have
+different processes, we have some security boundary between them. Can you talk
+a bit about how security and the content layer overlap? Is the content API a
+security boundary? What happens if someone calls it maliciously? What could go
+wrong if they do and do it successfully?
+
+12:26 JOHN: So the security boundaries in any browser built on top of content
+is the processes. We separate things to not just have render processes per tab,
+but there are multiple render processes per tab thanks to the amazing work of
+the Site Isolation project. And that's what split up different iframes into
+different processes. And so, how they talk, all these processes talk through
+IPC, and our current IPC system's called Mojo. And so, any time you talk, you
+use Mojo between processes. You're usually talking from between processes of
+different privileges. And so, one could be sandboxed and the other one not
+sandboxed. Or one could be sandboxed, and the other one only partially
+sandboxed. So you have to scrutinize any time you use these Mojo calls to make
+sure that they can't inadvertently lead to a security vulnerability. Now, even
+those, as hard as you can, people could still misuse code. Or, also, embedders
+like Chrome or other content embedders can add their own IPCs. So content
+obviously doesn't know about the IPCs from other layers, and so, it's possible
+that it could be an embedder of content that has security vulnerability in
+their own Mojo calls. And so, content doesn't know about them, so it can't do
+anything about them. You could write insecure code in content. You can also
+write in secure code in an embedder, and if someone finds a vulnerability - so
+let's say someone finds a vulnerability in Blink, and maybe they're only
+running their code in a minimal content shell. Maybe they can't find any other
+Mojo calls that they can abuse to be able to get access to the browser process.
+But maybe someone else, an embedder, is a more full-featured browser. It has
+more IPC service, and that could be more of an attack surface for that - to
+start with that Blink vulnerability and then to hop into the browser process.
+
+14:38 SHARON: And if you gain control of the browser process, that's a very
+highly privileged process.
+
+14:44 JOHN: Because that has full access to your system. So that's the point
+where you can leave persistent changes to the user system, which is pretty bad.
+
+14:55 SHARON: That sounds not great. So if you're an average, say, Chrome
+engineer, that could be anyone. This is probably not too much of a concern. All
+the stuff we mentioned, this is good to know. How would a Chrome engineer who
+doesn't directly work on content or in the content directory interact with the
+content layer?
+
+15:20 JOHN: Well, they might need a signal from Blink, for example. That's
+often how someone will do that. They'll be working on a feature in the browser,
+and everything works great. But then, they'll be like, I just need something
+from Blink. But it's not there. And so, sometimes, they'll have to add an IPC
+between processes, and that might interact. They'll be like, how do I get it?
+It's in Blink. It's in the render view class. so I need an interface that talks
+between each render view host and each render view. And that's how they might
+get - well, that would be how they get interaction with the multiprocessor part
+of it. But if someone is just working on something only in a browser process,
+they might still be trying to get information about the current tab. And that's
+represented by a web content's class and content. So they'll look in content
+public browser, and they'll see web contents. And there will be a lot of
+interfaces that hang off it. So they'll be looking at it, going through a trail
+of interfaces and classes to be able to get more information on what's going on
+in the current tab.
+
+16:29 SHARON: Can you give us a quick overview of the Web Content class?
+Because it is one, massive, and two, called something like web contents. Which
+suggests it's important because content plus the web, and it's also something
+you see all over the place. So can you just give us a quick overview of what
+that class does? What it's for? What it represents?
+
+16:46 JOHN: Yes. Things now are a lot more complicated than before, but if you
+go back in a time machine and see how these things started, you can roughly
+think in initial Chrome. Every tab had a class to represent the content in that
+tab, and that was called web contents. And then, it was called web contents
+because we had other classes. We used to be able to put native stuff in a tab.
+And so, that would be called tab contents. But that's gone now, and we just
+have web contents. So that's where the name comes from. And then even, for
+example, there was render process host, which I mentioned earlier. And then,
+each tab, each web contents roughly translate into one render process. And so,
+now, it's a bit more complicated. There are examples where you can have web
+contents inside of web contents, and that's more esoteric that most people
+don't have to deal with. And then, so that's what web contents is for. It will
+do things like take input and feed it to the page. Every time there's a
+permission prompt, you usually go through that. If a page wants to access to a
+microphone, or video, and so on. It keeps track of this navigation going on.
+What's the current URL? What's the pending URL? It uses other classes to drive
+all that stuff as you send out the network request and get it back. And that's
+not inside of web contents itself, but it's driven by other helper classes.
+
+18:28 SHARON: I tend to think of content as being the home of navigation, which
+I think is a decent way to think about it and also is maybe biased because of
+the stuff I've been working on. But you have Chrome, and navigation, and
+content, and all the stuff here. And then, separately, you have the actual web,
+the internet. And that has things like actual websites. And there are web
+standards, and there's things like HTML. And these two things somehow have to
+intersect. But being on the Chrome side, working on Chrome, apart from writing
+some browser tests, maybe, you never really interact with any of the more web
+things. JavaScript, you don't really touch. That's more Blink and HTML only in
+a test kind of thing. So how do these web standards - there's navigation web
+standards and all that. How do we actually make sure that they're implemented
+in Chrome? And where does that happen?
+
+19:32 JOHN: So that happens all over the code, but there's a few critical
+directories. If you look at net at a low level, a lot of IETF - and some
+aspects will be implemented there at that layer. Either net or in the network
+service, which is a code that runs inside the network process. Then you've got
+V8, of course, our JavaScript engine, and that has to follow the ECMAScript
+standards. And then, there's a lot of the platform standards. Either some of
+them only don't need multiple processes to be - to implement them, so they'll
+just be completely inside Blink. But some of them require multiple processes,
+things that need access to devices and so on. And so, that implementation will
+be split across Blink and content browser. But then, how do you ensure that,
+not only do you implement this correctly, but also that you don't regress it?
+So there's a whole slew of tests. There's the Blink tests, which used to be
+called the layout tests. And those run across the simple, simple test cases for
+many features to make sure that each one works. And there's also this cool
+thing where we share now a lot of these tests with other embedders, and that
+way, you run the same test in every browser. And so, when you write a test, you
+don't have to write it n times. You can just write it once. So that's how we
+ensure that we meet the specs.
+
+21:10 SHARON: That makes sense. Because I've been pointed - when I was looking
+into a class. What does this do? I've been linked to, say, one of the HTML
+specs or web specs. But the whole time, I'm just thinking, how do we make
+sure - or who's checking that we're actually implementing this and correctly?
+But these tests seem like a good way to do it and also ensure some level of
+consistency across browsers. Assuming you know whether or not the browser you
+use chooses to run these tests or not, I guess.
+
+21:41 JOHN: And as an engineer on a project like that, the first time you'll
+hit them is when you're breaking them. You'll make a change, and I think this
+is fine. And then, you send it to the commit queue, and you break some layout
+tests. What's happening to me today? And then, you have to drill into it. And
+the nice thing about layout test is because each one is small, you - it's
+faster to figure out what you broke because it's just like, hopefully, you only
+broke a small number of tests.
+
+22:06 SHARON: For sure, and it's a good example of why we have all these tests,
+is to make sure things don't break. So that is pretty much all the questions I
+have written down. Is there anything else generally content layer, content
+public API-ish related that is interesting that maybe we didn't get a chance to
+cover?
+
+22:31 JOHN: Yes. The most common questions is people will be like, well, does
+this belong in content or not? So I can have a chance to point people towards
+their README files and content/README that describes what's supposed to go in
+or not. And then, there's also a content/public/README that describes the
+guidelines we have for the API to make it consistent.
+
+22:59 SHARON: I've definitely seen those questions before. You're updating one
+of the content public APIs. Does this belong? While we're here, can you give us
+a quick breakdown heuristic of what things generally would belong in the
+content public API versus you put it up for review, and the reviewer's like,
+no. This does not belong in content public?
+
+23:24 JOHN: So sometimes, for example, for convenience, maybe the Chrome layer
+wants to call other parts of Chrome layer, but they don't have a direct
+connection. Or maybe a Chrome layer wants to talk to a different component. And
+so, they'll be like, we'll add something to the content API, and then, that
+way, Chrome can talk to this other part of Chrome or this other component
+through content as a shortcut. We don't allow that, and the reason for that is
+anybody who's gone through the content public directory, it's already huge. And
+so, we feel that if Chrome wants to talk to Chrome or to another layer, they
+should have their own API to each other directly instead of hopping through
+content. Just because the content API's already very large, very complex, hard
+to understand. So we don't want to add things that are absolutely not necessary
+to it. And another thing we try to do is to not add multiple ways of doing
+something. We only add something to the content API when there's no other way
+of getting this data from inside content, or there's no other way of getting
+this data from them better to content. But if there's something similar that
+can do the same thing, we push back on that.
+
+24:39 SHARON: And also, test-only things? Are those generally OK, or do you
+want to generally avoid those?
+
+24:45 JOHN: Well, yes. test-only methods, we try really hard - not just for the
+public API, but inside, because we don't want to bloat the binary. But we do
+have content public tests, which is - gives you a lot more leeway to poke at
+things in your browser test, for example, or your unit tests. Another thing is,
+we also have guidelines for how the API should be. We don't have, really,
+concrete classes. It's mostly abstract interfaces. And so, there's a bunch of
+rules there, and they're all listed in content/public/README. Just so people
+know the guidelines we have for interfaces there.
+
+25:28 SHARON: On the Chrome binary point, how much is the size of the binary
+dependent on the size of the content public API? Is that a big part of the
+binary, or is it small enough where, sure, we want to keep it from being
+unnecessarily large but not too much of an issue?
+
+25:48 JOHN: The size is not going to come as much from the content/public API
+but just from the entire content and all its dependencies. And those are in the
+tens of megabytes. So, sometimes, for example, if you're bundling the content
+layer, you're not going to be a small binary. You'll just start off in the 30
+megabyte range or 40 megabyte range once you put everything together.
+
+26:12 SHARON: And I guess that's something you have to be more conscious of if
+you're working in content versus another directory even in Chrome. is that you
+have to be wary of your dependencies more so than anywhere else. Not only for
+Chrome, but also, any other embedders who might want to use content.
+
+26:31 JOHN: Yes. And so, for example, if someone's trying to add something in
+Chrome, we also ask, does this have to be in content? Of can this be part of
+Chrome, so that not every embedder has to pay that cost if they don't need it?
+Maybe we'll have an interface, and the embedder can plug the data in through
+that way but still not have it in content. Another problem, of course, with
+having data inside content is that not all embedders update at the same speed.
+So if you're putting something in content, it can quickly go stale, the
+content, whatever the data is if you're not updating quickly.
+
+27:08 SHARON: That make sense. So we mentioned a bit of what content is, a bit
+of the history of it. Can you tell us anything about what are upcoming changes
+that might happen in content? What is the future of the content directory, the
+layer, the API?
+
+27:28 JOHN: Well, it's always changing. It's not static, driven by the needs of
+the product. And so, you look at big changes happening today like MPArch to
+support various use cases that we didn't have, or we never thought about
+initially. And that's where the web contents, inside web content, some of that
+comes in. There are big changes like banning, for example, pointers and
+replacing them with a raw pointer. So we can try to address some of the
+security problems we have with Use-After-Frees. So that's where, when you look
+at the content code or the Chrome code in general, too, you might see a little
+bit different than that average C++ project that you see. You'll be like, I'm
+getting errors if I try to have a raw pointer, and that's why.
+
+28:15 SHARON: Check out episode one for more on that. We'll link it below.
+Anything else random content-related or otherwise you would like to share with
+us?
+
+28:27 JOHN: I think the only other thing I would add is familiarize yourself
+with the READMEs in content/README and content/public/README before making
+changes. That will make the author and reviewer's time more efficient. And if
+you're working on content and below, you can build Content Shell instead of
+Chrome. That would be faster to build and debug and hopefully make you more
+productive.
+
+28:52 SHARON: Good tips. Hopefully, our viewers follow them. They would never
+try to change a content/public API without reading the READMEs first. Well,
+thank you so much, John, for sitting down and chatting with me about content.
+This was great, and, hopefully, people find it useful.
+
+29:14 JOHN: And thank you for hosting me, Sharon.
+
+29:23 SHARON: Did you start working on Chrome from the very start, or just -
+obviously, pre-launch. Because, I think, based on your profile pictures, the
+picture of that comic book that released when Chrome did - which I was lucky
+enough to get a copy of when I was an intern. Shout-out Peter. But that
+obviously suggests you were a major contributor before the public launch of
+Chrome. So were you working on Chrome from the very beginning?
+
+29:47 JOHN: I was not. It took about six months. I tried to join from the
+beginning, but I couldn't join right at the beginning. So my sneaky way was I
+found another project under that same director who was running Chrome, and
+then, once that project finished in six months, then I jumped into Chrome.
+
+30:09 SHARON: And do you ever think about how crazy it is from this thing that
+you worked on, effectively, from the start before the public launch? To what it
+is now where Chrome is one of the foundational pieces of the internet at large?
+Any time the internet gets run period, probably something in Chrome is running
+like the next stack, if not, obviously, the browser? Do you ever think about
+that, and how crazy that is? And your place in that?
+
+30:38 JOHN: Yes. It's amazing how far Chrome has come, and it's really humbling
+to see it be the number one browser, the most widely-used browser. Because when
+we were working on Chrome at the beginning, we were just trying to guess what
+market share it would have. And people would be like, it'll be 10%, and we're
+like, no way. Even the people working on it, we didn't think that was going to
+be possible. So to see users really enjoy using it, and for us to keep
+demonstrating value by sticking to our four principles, security and stability,
+simplicity and speed. And seeing people not just adopt Chrome as a product, but
+Chromium as a platform is - it's beyond our wildest dreams. And it's a
+responsibility that we have every time we make a change to Chrome to all these
+users and developers using it. You were asking earlier, how does it feel to be
+here from the start? There's almost a sense of feeling super lucky. But also
+this humbling feeling where we started in Chrome when it was really small, and
+our knowledge built up incrementally as it got more complicated. But so, it's
+like, well, what if I was to jump in Chrome today? It seems like way too many -
+the code is so complicated now compared to before. This almost responsibility
+we have as being in Chrome for a long time to share knowledge, to help people
+pick it up. Because we would ourselves struggle if we were to jump in now.
+
+32:22 SHARON: Yes. As those people, we certainly did struggle. But people are
+pretty smart, I think, and they can figure it out. But that doesn't mean you
+can't make it easier for the people in the future figuring it out. Or even
+people who - you just work on a different part. If I were to do anything in
+Blink, I'm just like -
+
+32:44 JOHN: Same. I've been on it for a long time. I don't touch Blink.
+
+32:50 SHARON: Yes. Yes.
diff --git a/docs/transcripts/wuwt-e04-tests.md b/docs/transcripts/wuwt-e04-tests.md
new file mode 100644
index 0000000..05e2602
--- /dev/null
+++ b/docs/transcripts/wuwt-e04-tests.md
@@ -0,0 +1,968 @@
+# What’s Up With Tests
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 4, a 2022 video discussion between [Sharon (yangsharon@chromium.org)
+and Stephen
+(smcgruer@chromium.org)](https://www.youtube.com/watch?v=KePsimOPSro).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Testing is important! What kinds of tests do we have in Chromium? What are they
+all about? Join in as Stephen, who led Chrome's involvement in web platform
+tests, tells us all about them.
+
+Notes:
+- https://docs.google.com/document/d/1SRoNMdPn78vwZVX7YzcdpF4cJdHTIV6JLGiVC2dJUaI/edit
+
+---
+
+00:00 SHARON: Hello, everyone, and welcome to "What's Up With That," the series
+that demystifies all things Chrome. I'm your host, Sharon. And today we're
+talking testing. Within Chrome, there are so many types of tests. What are they
+all? What's the difference? What are the Chromium-specific quirks? Today's
+guest is Stephen. He previously led Chrome's involvement in web platform tests.
+Since then, he's worked on rendering, payments, and interoperability. As a fun
+aside, he's one of the first people I met who worked on Chrome and is maybe
+part of why I'm here today. So welcome, Stephen.
+
+00:33 STEPHEN: Well, thank you very much for having me, Sharon, I'm excited to
+be here.
+
+00:33 SHARON: Yeah, I'm excited to have you here. So today, we're in for maybe
+a longer episode. Testing is a huge topic, especially for something like
+Chrome. So grab a snack, grab a drink, and let's start. We'll start with what
+are all of the things that we have testing for in Chrome. What's the purpose of
+all these tests we have?
+
+00:51 STEPHEN: Yeah. It's a great question. It's also an interesting one
+because I wanted to put one caveat on this whole episode, which is that there
+is no right answer in testing. Testing, even in the literature, never mind in
+Chromium itself, is not a solved problem. And so you'll hear a lot of different
+opinions. People will have different thoughts. And I'm sure that no matter how
+hard we try, by the end of this episode, our inbox will be filled with angry
+emails from people being like, no, you are wrong. So all of the stuff we're
+saying here today is my opinion, albeit I'll try and be as useful as possible.
+But yeah, so why do we test was the question, right? So there's a lot of
+different reasons that we write tests. Obviously, correctness is the big one.
+You're writing some code, you're creating a feature, you want it to be correct.
+Other reasons we write them, I mean, tests can be useful as a form of
+documentation in itself. If you're ever looking at a class and you're like,
+what does - why is this doing this, why is the code doing this, the test can
+help inform that. They're also useful - I think a topic of this podcast is sort
+of security. Tests can be very useful for security. Often when we have a
+security bug, we go back and we write what are called regression tests, so at
+least we try and never do that security failure again. And then there are other
+reasons. We have tests for performance. We have tests for - our launch process
+uses tests. There's lots and lots of reasons we have tests.
+
+02:15 SHARON: Now that you've covered all of the different reasons why we test,
+how do we do each of these types of tests in Chromium? What are the test types
+we have?
+
+02:27 STEPHEN: Yeah. So main test types we have in Chromium, unit tests,
+browser tests, what we call web tests, and then there's a bunch of more
+specialized ones, performance tests, testing on Android, and of course manual
+testing.
+
+02:43 SHARON: We will get into each of these types now, I guess. The first type
+of test you mentioned is unit tests. Why don't you tell us a quick rundown of
+what unit tests are. I'm sure most people have encountered them or heard of
+them before. But just a quick refresher for those who might not.
+
+02:55 STEPHEN: Yeah, absolutely. So as the name implies, a unit test is all
+about testing a unit of code. And what that is not very well defined. But you
+can usually think of it as just a class, a file, a small isolated component
+that doesn't have to talk to all the other bits of the code to work. Really,
+the goal is on writing something that's testing just the code under test - so
+that new method you've added or whatever. And it should be quick and easy to
+run.
+
+03:22 SHARON: So on the screen now we have an example of a pretty typical unit
+test we see in Chrome. So there's three parts here. Let's go through each of
+them. So the first type - the first part of this is `TEST_P`. What is that
+telling us?
+
+03:38 STEPHEN: Yeah. So that is - in Chromium we use a unit testing framework
+called Google test. It's very commonly used for C++. You'll see it all over the
+place. You can go look up documentation. The test macros, that's what this is,
+are essentially the hook into Google test to say, hey, the thing that's coming
+here is a test. There's three types. There is just test, which it just says
+here is a function. It is a test function. `TEST_F` says that you basically
+have a wrapper class. It's often called a test fixture, which can do some
+common setup across multiple different tests, common teardown, and that sort of
+thing. And finally, `TEST_P` is what we call a parameterized test. And what
+this means is that the test can take some input parameters, and it will run the
+same test with each of those values. Very useful for things like when you want
+to test a new flag. What happens if the flag is on or off?
+
+04:34 SHARON: That's cool. And a lot of the things we're mentioning for unit
+test also apply to browser test, which we'll cover next. But the
+parameterization is an example of something that carries over to both. So
+that's the first part. That's the `TEST_P`, the macro. What's the second part,
+PendingBeaconHostTest? What is that?
+
+04:54 STEPHEN: Yeah. So that is the fixture class, the test container class I
+was talking about. So in this case, we're assuming that in order to write a
+beacon test, whatever that is, they have some set up, some teardown they need
+to do. They might want to encapsulate some common functionality. So all you
+have to do to write one of these classes is, you declare a C++ class and you
+subclass from the Google test class name.
+
+05:23 SHARON: So this is a `TEST_P`, but you mentioned that this is a fixture.
+So are fixture tests a subset of parameterized tests?
+
+05:35 STEPHEN: Parameterized tests are a subset of fixture test, is that the
+right way around to put it? All parameterized tests are fixtures tests. Yes.
+
+05:41 SHARON: OK.
+
+05:41 STEPHEN: You cannot have a parameterized test that does not have a
+fixture class. And the reason for that is how Google test actually works under
+the covers is it passes those parameters to your test class. You will have to
+additionally extend from the `testing::WithParamInterface`. And that says, hey,
+I'm going to take parameters.
+
+06:04 SHARON: OK. But not all fixture tests are parameterized tests.
+
+06:04 STEPHEN: Correct.
+
+06:04 SHARON: OK. And the third part of this, SendOneOfBeacons. What is that?
+
+06:10 STEPHEN: That is your test name. Whatever you want to call your test,
+whatever you're testing, put it here. Again, naming tests is as hard as naming
+anything. A lot of yak shaving, finding out what exactly you should call the
+test. I particularly enjoy when you see test names that themselves have
+underscores in them. It's great.
+
+06:30 SHARON: Uh-huh. What do you mean by yak shaving?
+
+06:35 STEPHEN: Oh, also known as painting a bike shed? Bike shed, is that the
+right word? Anyway, generally speaking -
+
+06:40 SHARON: Yeah, I've heard -
+
+06:40 STEPHEN: arguing about pointless things because at the end of the day,
+most of the time it doesn't matter what you call it.
+
+06:46 SHARON: OK, yeah. So I've written this test. I've decided it's going to
+be parameterized. I've come up with a test fixture for it. I have finally named
+my test. How do I run my tests now?
+
+06:57 STEPHEN: Yeah. So all of the tests in Chromium are built into different
+test binaries. And these are usually named after the top level directory that
+they're under. So we have `components_unittests`, `content_unittests`. I think
+the Chrome one is just called `unit_tests` because it's special. We should
+really rename that. But I'm going to assume a bunch of legacy things depend on
+it. Once you have built whichever the appropriate binary is, you can just run
+that from your `out` directory, so `out/release/components_unittests`, for
+example. And then that, if you don't pass any flags, will run every single
+components unit test. You probably don't want to do that. They're not that
+slow, but they're not that fast. So there is a flag `--gtest_filter`, which
+allows you to filter. And then it takes a test name after that. The format of
+test names is always test class dot test name. So for example, here
+PendingBeaconHostTest dot SendOneOfBeacons.
+
+08:04 SHARON: Mm-hmm. And just a fun aside for that one, if you do have
+parameterized tests, it'll have an extra slash and a number at the end. So
+normally, whenever I use it, I just put a star before and after. And that
+generally does - covers the cases.
+
+08:17 STEPHEN: Yeah, absolutely.
+
+08:23 SHARON: Cool. So with the actual test names, you will often see them
+prefixed with either `MAYBE_` or `DISABLED_`, or before the test, there will be
+an ifdef with usually a platform and then depending on the cases, it'll prefix
+the test name with something. So I think it's pretty clear what these are
+doing. Maybe is a bit less clear. Disabled pretty clear what that is. But can
+you tell us a bit about these prefixes?
+
+08:51 STEPHEN: Yeah, absolutely. So this is our way of trying to deal with that
+dreaded thing in testing, flake. So when a test is flaky, when it doesn't
+produce a consistent result, sometimes it fails. We have in Chromium a whole
+continuous integration waterfall. That is a bunch of bots on different
+platforms that are constantly building and running Chrome tests to make sure
+that nothing breaks, that bad changes don't come in. And flaky tests make that
+very hard. When something fails, was that a real failure? And so when a test is
+particularly flaky and is causing sheriffs, the build sheriffs trouble, they
+will come in and they will disable that test. Basically say, hey, sorry, but
+this test is causing too much pain. Now, as you said, the `DISABLED_` prefix,
+that's pretty obvious. If you put that in front of a test, Google test knows
+about it and it says, nope, will not run this test. It will be compiled, but it
+will not be run. `MAYBE_` doesn't actually mean anything. It has no meaning to
+Google test. But that's where you'll see, as you said, you see these ifdefs.
+And that's so that we can disable it on just one platform. So maybe your test
+is flaky only on Mac OS, and you'll see basically, oh, if Mac OS, change the
+name from maybe to disabled. Otherwise, define maybe as the normal test name.
+
+10:14 SHARON: Makes sense. We'll cover flakiness a bit later. But yeah, that's
+a huge problem. And we'll talk about that for sure. So these prefixes, the
+parameterization and stuff, this applies to both unit and browser tests.
+
+10:27 STEPHEN: Yeah.
+
+10:27 SHARON: Right? OK. So what are browser tests? Chrome's a browser. Browser
+test, seems like there's a relation.
+
+10:34 STEPHEN: Yeah. They test the browser. Isn't it obvious? Yeah. Browser
+tests are our version - our sort of version of an integration or a functional
+test depending on how you look at things. What that really means is they're
+testing larger chunks of the browser at once. They are integrating multiple
+components. And this is somewhere that I think Chrome's a bit weird because in
+many large projects, you can have an integration test that doesn't bring your
+entire product up and in order to run. Unfortunately, or fortunately, I guess
+it depends on your viewpoint, Chrome is so interconnected, it's so
+interdependent, that more or less we have to bring up a huge chunk of the
+browser in order to connect any components together. And so that's what browser
+tests are. When you run one of these, there's a massive amount of machinery in
+the background that goes ahead, and basically brings up the browser, and
+actually runs it for some definition of what a browser is. And then you can
+write a test that pokes at things within that running browser.
+
+11:42 SHARON: Yeah. I think I've heard before multiple times is that browser
+tests launch the whole browser. And that's -
+
+11:47 STEPHEN: More or less true. It's - yeah.
+
+11:47 SHARON: Yes. OK. Does that also mean that because you're running all this
+stuff that all browser tests have fixtures? Is that the case?
+
+11:59 STEPHEN: Yes, that is the case. Absolutely. So there is only - I think
+it's - oh my goodness, probably on the screen here somewhere. But it's
+`IN_PROC_BROWSER_TEST_F` and `IN_PROC_BROWSER_TEST_P`. There is no version that
+doesn't have a fixture.
+
+12:15 SHARON: And what does the in proc part of that macro mean?
+
+12:15 STEPHEN: So that's, as far as I know - and I might get corrected on this.
+I'll be interested to learn. But it refers to the fact that we've run these in
+the same process. Normally, the whole Chromium is a multi-process architecture.
+For the case of testing, we put that aside and just run everything in the same
+process so that it doesn't leak, basically.
+
+12:38 SHARON: Yeah. There's flags when you run them, like `--single-process`.
+And then there's `--single-process-test`. And they do slightly different
+things. But if you do run into that, probably you will be working with people
+who can answer and explain the differences between those more. So something
+that I've seen quite a bit in browser and unit tests, and only in these, are
+run loops. Can you just briefly touch on what those are and what we use them
+for in tests?
+
+13:05 STEPHEN: Oh, yeah. That's a fun one. I think actually previous on an
+episode of this very program, you and Dana talked a little bit around the fact
+that Chrome is not a completely synchronous program, that we do we do task
+splitting. We have a task scheduler. And so run loops are part of that,
+basically. They're part of our stack for handling asynchronous tasks. And so
+this comes up in testing because sometimes you might be testing something
+that's not synchronous. It takes a callback, for example, rather than returning
+a value. And so if you just wrote your test as normal, you call the function,
+and you don't - you pass a callback, but then your test function ends. Your
+test function ends before that callback ever runs. Run loop gives you the
+ability to say, hey, put this callback into some controlled run loop. And then
+after that, you can basically say, hey, wait on this run loop. I think it's
+often called quit when idle, which basically says keep running until you have
+no more tasks to run, including our callback, and then finish. They're
+powerful. They're very useful, obviously, with asynchronous code. They're also
+a source of a lot of flake and pain. So handle with care.
+
+14:24 SHARON: Yeah. Something a tip is maybe using the `--gtest_repeat` flag.
+So that one lets you run your test however number of times you've had to do it.
+
+14:30 STEPHEN: Yeah.
+
+14:36 SHARON: And that can help with testing for flakiness or if you're trying
+to debug something flaky. In tests, we have a variety of macros that we use. In
+the unit test and the browser tests, you see a lot of macros, like `EXPECT_EQ`,
+`EXPECT_GT`. These seem like they're part of maybe Google test. Is that true?
+
+14:54 STEPHEN: Yeah. They come from Google test itself. So they're not
+technically Chromium-specific. But they basically come in two flavors. There's
+the `EXPECT_SOMETHING` macros. And there's the `ASSERT_SOMETHING` macros. And
+the biggest thing to know about them is that expect doesn't actually cause - it
+causes a test to fail, but it doesn't stop the test from executing. The test
+will continue to execute the rest of the code. Assert actually throws an
+exception and stops the test right there. And so this can be useful, for
+example, if you want to line up a bunch of expects. And your code still makes
+sense. You're like, OK, I expect to return object, and it's got these fields.
+And I'm just going to expect each one of the fields. That's probably fine to
+do. And it may be nice to have output that's like, no, actually, both of these
+fields are wrong. Assert is used when you're like, OK, if this fails, the rest
+of the test makes no sense. Very common thing you'll see. Call an API, get back
+some sort of pointer, hopefully a smart pointer, hey. And you're going to be
+like, assert that this pointer is non-null because if this pointer is null,
+everything else is just going to be useless.
+
+15:57 SHARON: I think we see a lot more expects than asserts in general
+anecdotally from looking at the test. Do you think, in your opinion, that
+people should be using asserts more generously rather than expects, or do we
+maybe want to see what happens - what does go wrong if things continue beyond a
+certain point?
+
+16:15 STEPHEN: Yeah. I mean, general guidance would be just keep using expect.
+That's fine. It's also not a big deal if your test actually just crashes. It's
+a test. It can crash. It's OK. So use expects. Use an assert if, like I said,
+that the test doesn't make any sense. So most often if you're like, hey, is
+this pointer null or not and I'm going to go do something with this pointer,
+assert it there. That's probably the main time you'd use it.
+
+16:45 SHARON: A lot of the browser test classes, like the fixture classes
+themselves, are subclass from other base classes.
+
+16:53 STEPHEN: Mm-hmm.
+
+16:53 SHARON: Can you tell us about that?
+
+16:53 STEPHEN: Yeah. So basically, we have one base class for browser tests. I
+think its `BrowserTestBase`, I think it's literally called, which sits at the
+bottom and does a lot of the very low level setup of bringing up a browser. But
+as folks know, there's more than one browser in the Chromium project. There is
+Chrome, the Chrome browser that is the more full-fledged version. But there's
+also content shell, which people might have seen. It's built out of content.
+It's very simple browser. And then there are other things. We have a headless
+mode. There is a headless Chrome you can build which doesn't show any UI. You
+can run it entirely from the command line.
+
+17:32 SHARON: What's the difference between headless and content shell?
+
+17:39 STEPHEN: So content shell does have a UI. If you run content shell, you
+will actually see a little UI pop up. What content shell doesn't have is all of
+those features from Chrome that make Chrome Chrome, if you will. So I mean,
+everything from bookmarks, to integration with having an account profile, that
+sort of stuff is not there. I don't think content shell even supports tabs. I
+think it's just one page you get. It's almost entirely used for testing. But
+then, headless, sorry, as I was saying, it's just literally there is no UI
+rendered. It's just headless.
+
+18:13 SHARON: That sounds like it would make -
+
+18:13 STEPHEN: And so, yeah. And so - sorry.
+
+18:13 SHARON: testing faster and easier. Go on.
+
+18:18 STEPHEN: Yeah. That's a large part of the point, as well as when you want
+to deploy a browser in an environment where you don't see the UI. So for
+example, if you're running on a server or something like that. But yeah. So for
+each of these, we then subclass that `BrowserTestBase` in order to provide
+specific types. So there's content browser test. There's headless browser test.
+And then of course, Chrome has to be special, and they called their version in
+process browser test because it wasn't confusing enough. But again, it's sort
+of straightforward. If you're in Chrome, `/chrome`, use
+`in_process_browser_test`. If you're in `/content`, use `content_browsertest`.
+It's pretty straightforward most of the time.
+
+18:58 SHARON: That makes sense. Common functions you see overridden from those
+base classes are these set up functions. So they're set, set up on main thread,
+there seems to be a lot of different set up options. Is there anything we
+should know about any of those?
+
+19:13 STEPHEN: I don't think that - I mean, most of it's fairly
+straightforward. I believe you should mostly be using setup on main thread. I
+can't say that for sure. But generally speaking, setup on main thread, teardown
+on main thread - or is it shutdown main thread? I can't remember - whichever
+the one is for afterwards, are what you should be usually using in a browser
+thread. You can also usually do most of your work in a constructor. That's
+something that people often don't know about testing. I think it's something
+that's changed over time. Even with unit tests, people use the setup function a
+lot. You can just do it in the constructor a lot of the time. Most of
+background initialization has already happened.
+
+19:45 SHARON: I've definitely wondered that, especially when you have things in
+the constructor as well as in a setup method. It's one of those things where
+you just kind of think, I'm not going to touch this because eh, but -
+
+19:57 STEPHEN: Yeah. There are some rough edges, I believe. Set up on main
+thread, some things have been initialized that aren't around when your class is
+being constructed. So it is fair. I'm not sure I have any great advice unless -
+other than you may need to dig in if it happens.
+
+20:19 SHARON: One last thing there. Which one gets run first, the setup
+functions or the constructor?
+
+20:19 STEPHEN: The constructor always happens first. You have to construct the
+object before you can use it.
+
+20:25 SHARON: Makes sense. This doesn't specifically relate to a browser test
+or unit test, but it does seem like it's worth mentioning, which is the content
+public test API. So if you want to learn more about content and content public,
+check out episode three with John. But today we're talking about testing. So
+we're talking about content public test. What is in that directory? And how
+does that - how can people use what's in there?
+
+20:48 STEPHEN: Yeah. It's basically just a bunch of useful helper functions and
+classes for when you are doing mostly browser tests. So for example, there are
+methods in there that will automatically handle navigating the browser to a URL
+and actually waiting till it's finished loading. There are other methods for
+essentially accessing the tab strip of a browser. So if you have multiple tabs
+and you're testing some cross tab thing, methods in there to do that. I think
+that's probably where the content browser test - like base class lives there as
+well. So take a look at it. If you're doing something that you're like, someone
+should write - it's the basic - it's the equivalent of base in many ways for
+testing. It's like, if you're like, someone should have written a library
+function for this, possibly someone has already. And you should take a look.
+And if they haven't, you should write one.
+
+21:43 SHARON: Yeah. I've definitely heard people, code reviewers, say when you
+want to add something that seems a bit test only to content public, put that in
+content public test because that doesn't get compiled into the actual release
+binaries. So if things are a bit less than ideal there, it's a bit more
+forgiving for a place for that.
+
+22:02 STEPHEN: Yeah, absolutely. I mean, one of the big things about all of our
+test code is that you can actually make it so that it's in many cases not
+compiled into the binary. And that is both useful for binary size as well as
+you said in case it's concerning. One thing you can do actually in test, by the
+way, for code that you cannot avoid putting into the binary - so let's say
+you've got a class, and for the reasons of testing it because you've not
+written your class properly to do a dependency injection, you need to access a
+member. You need to set a member. But you only want that to happen from test
+code. No real code should ever do this. You can actually name methods blah,
+blah, blah for test or for testing. And this doesn't have any - there's no code
+impact to this. But we have pre-submits that actually go ahead and check, hey,
+are you calling this from code that's not marked as test code? And it will then
+refuse to - it will fail to pre-submit upload if that happens. So it could be
+useful.
+
+23:03 SHARON: And another thing that relates to that would be the friend test
+or friend something macro that you see in classes. Is that a gtest thing also?
+
+23:15 STEPHEN: It's not a gtest thing. It's just a C++ thing. So C++ has the
+concept of friending another class. It's very cute. It basically just says,
+this other class and I, we can access each other's internal states. Don't
+worry, we're friends. Generally speaking, that's a bad idea. We write classes
+for a reason to have encapsulation. The entire goal of a class is to
+encapsulate behavior and to hide the implementation details that you don't want
+to be exposed. But obviously, again, when you're writing tests, sometimes it is
+the correct thing to do to poke a hole in the test and get at something. Very
+much in the schools of thought here, some people would be like, you should be
+doing dependency injection. Some people are like, no, just friend your class.
+It's OK. If folks want to look up more, go look up the difference between open
+box and closed box testing.
+
+24:00 SHARON: For those of you who are like, oh, this sounds really cool, I
+will learn more.
+
+24:00 STEPHEN: Yeah, for my test nerds out there.
+
+24:06 SHARON: [LAUGHS] Yeah, Stephen's got a club. Feel free to join.
+
+24:06 STEPHEN: Yeah. [LAUGHTER]
+
+24:11 SHARON: You get a card. Moving on to our next type of test, which is your
+wheelhouse, which is web tests. This is something I don't know much about. So
+tell us all about it.
+
+24:22 STEPHEN: [LAUGHS] Yeah. This is my - this is where hopefully I'll shine.
+It's the area I should know most about. But web tests are - they're an
+interesting one. So I would describe them is our version of an end-to-end test
+in that a web test really is just an HTML file, a JavaScript file that is when
+you run it, you literally bring up - you'll remember I said that browser tests
+are most of a whole browser. Web tests bring up a whole browser. It's just the
+same browser as content shell or Chrome. And it runs that whole browser. And
+the test does something, either in HTML or JavaScript, that then is asserted
+and checked. And the reason I say that I would call them this, I have heard
+people argue that they're technically unit tests, where the unit is the
+JavaScript file and the entire browser is just, like, an abstraction that you
+don't care about. I guess it's how you view them really. I view the browser as
+something that is big and flaky, and therefore these are end-to-end tests. Some
+people disagree.
+
+25:22 SHARON: In our last episode, John touched on these tests and how that
+they're - the scope and that each test covers is very small. But how you run
+them is not. And I guess you can pick a side that you feel that you like more
+and go with that. So what are examples of things we test with these kind of
+tests?
+
+25:49 STEPHEN: Yeah. So the two big categories of things that we test with web
+tests are basically web APIs, so JavaScript APIs, provided by the browser to do
+something. There are so many of those, everything from the fetch API for
+fetching stuff to the web serial API for talking to devices over serial ports.
+The web is huge. But anything you can talk to via JavaScript API, we call those
+JavaScript tests. It's nice and straightforward. The other thing that web tests
+usually encompass are what are called rendering tests or sometimes referred to
+as ref tests for reference tests. And these are checking the actual, as the
+first name implies, the rendering of some HTML, some CSS by the browser. The
+reason they're called reference tests is that usually the way you do this to
+check whether a rendering is correct is you set up your test, and then you
+compare it to some image or some other reference rendering that you're like,
+OK, this should look like that. If it does look like that, great. If it
+doesn't, I failed.
+
+26:54 SHARON: Ah-ha. And are these the same as - so there's a few other test
+names that are all kind of similar. And as someone who doesn't work in them,
+they all kind of blur together. So I've also heard web platform tests. I've
+heard layout tests. I've heard Blink tests, all of which do - all of which are
+JavaScript HTML-like and have some level of images in them. So are these all
+the same thing? And if not, what's different?
+
+27:19 STEPHEN: Yeah. So yes and no, I guess, is my answer. So a long time ago,
+there were layout tests basically. And that was something we inherited from the
+WebKit project when we forked there, when we forked Chromium from WebKit all
+those years ago. And they're exactly what I've described. They were both
+JavaScript-based tests and they were also HTML-based tests for just doing
+reference renderings. However, web platform test came up as an external project
+actually. Web platform test is not a Chromium project. It is external upstream.
+You can find them on GitHub. And their goal was to create a set of - a test
+suite shared between all browsers so that all browsers could test - run the
+same tests and we could actually tell, hey, is the web interoperable? Does it
+work the same way no matter what browser you're on? The answer is, no. But
+we're trying. And so inside of Chromium we said, that's great. We love this
+idea. And so what we did was we actually import web platform test into our
+layout tests. So web platform test now becomes a subdirectory of layout tests.
+OK?
+
+28:30 SHARON: OK. [LAUGHS]
+
+28:30 STEPHEN: To make things more confusing, we don't just import them, but we
+also export them. We run a continuous two-way sync. And this means that
+Chromium developers don't have to worry about that upstream web platform test
+project most of the time. They just land their code in Chromium, and a magic
+process happens, and it goes up into the GitHub project. So that's where we
+were for many years - layout tests, which are a whole bunch of legacy tests,
+and then also web platform tests. But fairly recently - and I say that knowing
+that COVID means that might be anything within the last three years because who
+knows where time went - we decided to rename layout test. And partly, the name
+we chose was web tests. So now you have web tests, of which web platform tests
+are a subset, or a - yeah, subset of web test. Easy.
+
+29:20 SHARON: Cool.
+
+29:20 STEPHEN: [LAUGHS]
+
+29:20 SHARON: Cool. And what about Blink tests? Are those separate, or are
+those these altogether?
+
+29:27 STEPHEN: I mean, if they're talking about the JavaScript and HTML, that's
+going to just be another name for the web tests. I find that term confusing
+because there is also the Blink tests target, which builds the infrastructure
+that is used to run web tests. So that's probably what you're referring, like
+`blink_test`. It is the target that you build to run these tests.
+
+29:50 SHARON: I see. So `blink_test` is a target. These other ones, web test
+and web platform tests, are actual test suites.
+
+29:57 STEPHEN: Correct. Yes. That's exactly right.
+
+30:02 SHARON: OK. All right.
+
+30:02 STEPHEN: Simple.
+
+30:02 SHARON: Yeah. So easy. So you mentioned that the web platform tests are
+cross-browser. But a lot of browsers are based on Chromium. Is it one of the
+things where it's open source and stuff but majority of people contributing to
+these and maintaining it are Chrome engineers?
+
+30:23 STEPHEN: I must admit, I don't know what that stat is nowadays. Back when
+I was working on interoperability, we did measure this. And it was certainly
+the case that Chromium is a large project. There were a lot of tests being
+contributed by Chromium developers. But we also saw historically - I would like
+to recognize Mozilla, most of all, who were a huge contributor to the web
+platform test project over the years and are probably the reason that it
+succeeded. And we also - web platform test also has a fairly healthy community
+of completely outside developers. So people that just want to come along. And
+maybe they're not able to or willing to go into a browser, and actually build a
+browser, and muck with code. But they could write a test for something. They
+can find a broken behavior and be like, hey, there's a test here, Chrome and
+Firefox do different things.
+
+31:08 SHARON: What are examples of the interoperability things that you're
+testing for in these cross-browser tests?
+
+31:17 STEPHEN: Oh, wow, that's a big question. I mean, really everything and
+anything. So on the ref test side, the rendering test, it actually does matter
+that a web page renders the same in different browsers. And that is very hard
+to achieve. It's hard to make two completely different engines render some HTML
+and CSS exactly the same way. But it also matters. We often see bugs where you
+have a lovely - you've got a lovely website. It's got this beautiful header at
+the top and some content. And then on one browser, there's a two-pixel gap
+here, and you can see the background, and it's not a great experience for your
+users. So ref tests, for example, are used to try and track those down. And
+then, on the JavaScript side, I mean really, web platform APIs are complicated.
+They're very powerful. There's a reason they are in the browser and you cannot
+do them in JavaScript. And that is because they are so powerful. So for
+example, web USB to talk to USB devices, you can't just do that from
+JavaScript. But because they're so powerful, because they're so complicated,
+it's also fairly easy for two browsers to have slightly different behavior. And
+again, it comes down to what is the web developer's experience. When I try and
+use the web USB API, for example, am I going to have to write code that's like,
+if Chrome, call it this way, if Fire - we don't want that. That is what we do
+not want for the web. And so that's the goal.
+
+32:46 SHARON: Yeah. What a team effort, making the whole web work is. All
+right. That's cool. So in your time working on these web platform tests, do you
+have any fun stories you'd like to share or any fun things that might be
+interesting to know?
+
+33:02 STEPHEN: Oh, wow. [LAUGHS] One thing I like to bring up - I'm afraid it's
+not that fun, but I like to repeat it a lot of times because it's weird and
+people get tripped up by it - is that inside of Chromium, we don't run web
+platform tests using the Chrome browser. We run them using content shell. And
+this is partially historical. That's how layout tests run. We always ran them
+under content shell. And it's partially for I guess what I will call
+feasibility. As I talked about earlier, content shell is much simpler than
+Chrome. And that means that if you want to just run one test, it is faster, it
+is more stable, it is more reliable I guess I would say, than trying to bring
+up the behemoth that is Chrome and making sure everything goes correctly. And
+this often trips people up because in the upstream world of this web platform
+test project, they run the test using the proper Chrome binary. And so they're
+different. And different things do happen. Sometimes it's rendering
+differences. Sometimes it's because web APIs are not always implemented in both
+Chrome and content shell. So yeah, fun fact.
+
+34:19 SHARON: Oh, boy. [LAUGHTER]
+
+34:19 STEPHEN: Oh, yeah.
+
+34:19 SHARON: And we wonder why flakiness is a problem. Ah. [LAUGHS]
+
+34:19 STEPHEN: Yeah. It's a really sort of fun but also scary fact that even if
+we put aside web platform test and we just look at layout test, we don't test
+what we ship. Layout test running content shell, and then we turn around and
+we're like, here's a Chrome binary. Like uh, those are different. But, hey, we
+do the best we can.
+
+34:43 SHARON: Yeah. We're out here trying our best. So that all sounds very
+cool. Let's move on to our next type of test, which is performance. You might
+have heard the term telemetry thrown around. Can you tell us what telemetry is
+and what these performance tests are?
+
+34:54 STEPHEN: I mean, I can try. We've certainly gone straight from the thing
+I know a lot about into the thing I know very little about. But -
+
+35:05 SHARON: I mean, to Stephen's credit, this is a very hard episode to find
+one single guest for. People who are working extensively usually in content
+aren't working a ton in performance or web platform stuff. And there's no one
+who is - just does testing and does every kind of testing. So we're trying our
+best. [INAUDIBLE]
+
+35:24 STEPHEN: Yeah, absolutely. You just need to find someone arrogant enough
+that he's like, yeah, I'll talk about all of those. I don't need to know the
+details. It's fine. But yeah, performance test, I mean, the name is self
+explanatory. These are tests that are trying to ensure the performance of
+Chromium. And this goes back to the four S's when we first started Chrome as a
+project - speed, simplicity, security, and I've forgotten the fourth S now.
+Speed, simplicity, security - OK, let's not reference the four S's then.
+[LAUGHTER] You have the Comet. You tell me.
+
+36:01 SHARON: Ah. Oh, I mean, I don't read it every day. Stability. Stability.
+
+36:08 STEPHEN: Stability. God damn it. Let's literally what the rest of this is
+about. OK, where were we?
+
+36:13 SHARON: We're leaving this in, don't worry. [LAUGHTER]
+
+36:19 STEPHEN: Yeah. So the basic idea of performance test is to test
+performance because as much as you can view behavior as a correctness thing, in
+Chromium we also consider performance a correctness thing. It is not a good
+thing if a change lands and performance regresses. So obviously, testing
+performance is also hard to do absolutely. There's a lot of noise in any sort
+of performance testing. An so, we do it essentially heuristically,
+probabilistically. We run whatever the tests are, which I'll talk about in a
+second. And then we look at the results and we try and say, hey, OK, is there a
+statistically significant difference here? And there's actually a whole
+performance sheriffing rotation to try and track these down. But in terms of,
+yeah, you mentioned telemetry. That weird word. You're like, what is a
+telemetry test? Well, telemetry is the name of the framework that Chromium
+uses. It's part of the wider catapult project, which is all about different
+performance tools. And none of the names, as far as I know, mean anything.
+They're just like, hey, catapult, that's a cool name. I'm sure someone will
+explain to me now the entire history behind the name catapult and why it's
+absolutely vital. But anyway, so telemetry basically is a framework that when
+you give it some input, which I'll talk about in a second, it launches a
+browser, performs some actions on a web page, and records metrics about those
+actions. So the input, the test essentially, is basically a collection of go to
+this web page, do these actions, record these metrics. And I believe in
+telemetry that's called a story, the story of someone visiting a page, I guess,
+is the idea. One important thing to know is that because it's sort of insane to
+actually visit real websites, they keep doing things like changing - strange.
+We actually cache the websites. We download a version of the websites once and
+actually check that in. And when you go run a telemetry test, it's not running
+against literally the real Reddit.com or something. It's running against a
+version we saved at some point.
+
+38:31 SHARON: And how often - so I haven't really heard of anyone who actually
+works on this and that we can't - you don't interact with everyone. But how -
+as new web features get added and things in the browser change, how often are
+these tests specifically getting updated to reflect that?
+
+38:44 STEPHEN: I would have to plead some ignorance there. It's certainly also
+been my experience as a browser engineer who has worked on many web APIs that
+I've never written a telemetry test myself. I've never seen one added. My
+understanding is that they are - a lot of the use cases are fairly general with
+the hope that if you land some performance problematic feature, it will regress
+on some general test. And then we can be like, oh, you've regressed. Let's
+figure out why. Let's dig in and debug. But it certainly might be the case if
+you are working on some feature and you think that it might have performance
+implications that aren't captured by those tests, there is an entire team that
+works on the speed of Chromium. I cannot remember their email address right
+now. But hopefully we will get that and put that somewhere below. But you can
+certainly reach out to them and be like, hey, I think we should test the
+performance of this. How do I go about and do that?
+
+39:41 SHARON: Yeah. That sounds useful. I've definitely gotten bugs filed
+against me for performance stuff. [LAUGHS] Cool. So that makes sense. Sounds
+like good stuff. And in talking to some people in preparation for this episode,
+I had a few people mention Android testing specifically. Not any of the other
+platforms, just Android. So do you want to tell us why that might be? What are
+they doing over there that warrants additional mention?
+
+40:15 STEPHEN: Yeah. I mean, I think probably the answer would just be that
+Android is such a huge part of our code base. Chrome is a browser, a
+multi-platform browser, runs on multiple desktop platforms, but it also runs on
+Android. And it runs on iOS. And so I assume that iOS has its own testing
+framework. I must admit, I don't know much about that at all. But certainly on
+Android, we have a significant amount of testing framework built up around it.
+And so there's the option, the ability for you to test your Java code as well
+as your C++ code.
+
+40:44 SHARON: That makes sense. And yeah, with iOS, because they don't use
+Blink, I guess there's - that reduces the amount of test that they might need
+to add, whereas on Android they're still using Blink. But there's a lot of
+differences because it is mobile, so they're just, OK, we actually can test
+those things. So let's go more general now. At almost every stage, you've
+mentioned flakiness. So let's briefly run down, what is flakiness in a test?
+
+41:14 STEPHEN: Yes. So flakiness for a test is just - the definition is just
+that the test does not consistently produce the same output. When you're
+talking about flakiness, you actually don't care what the output is. A test
+that always fails, that's fine. It always fails. But a test that passes 90% of
+the time and fails 10%, that's not good. That test is not consistent. And it
+will cause problems.
+
+41:46 SHARON: What are common causes of this?
+
+41:46 STEPHEN: I mean, part of the cause is, as I've said, we write a lot of
+integration tests in Chromium. Whether those are browser tests, or whether
+those are web tests, we write these massive tests that span huge stacks. And
+what comes implicitly with that is timing. Timing is almost always the
+problem - timing and asynchronicity. Whether that is in the same thread or
+multiple threads, you write your test, you run it on your developer machine,
+and it works. And you're like, cool, my test works. But what you don't realize
+is that you're assuming that in some part of the browser, this function ran,
+then this function run. And that always happens in your developer machine
+because you have this CPU, and this much memory, and et cetera, et cetera. Then
+you commit your code, you land your code, and somewhere a bot runs. And that
+bot is slower than your machine. And on that bot, those two functions run in
+the opposite order, and something goes horribly wrong.
+
+42:50 SHARON: What can the typical Chrome engineer writing these tests do in
+the face of this? What are some practices that you generally should avoid or
+generally should try to do more often that will keep this from happening in
+your test?
+
+43:02 STEPHEN: Yeah. So first of all, write more unit tests, write less browser
+tests, please. Unit tests are - as I've talked about, they're small. They're
+compact. They focus just on the class that you're testing. And too often, in my
+opinion - again, I'm sure we'll get some nice emails stating I'm wrong - but
+too often, in my opinion people go straight to a browser test. And they bring
+up a whole browser just to test functionality in their class. This sometimes
+requires writing your class differently so that it can be tested by a unit
+test. That's worth doing. Beyond that, though, when you are writing a browser
+test or a web test, something that is more integration, more end to end, be
+aware of where timing might be creeping in. So to give an example, in a browser
+test, you often do things like start by loading some web contents. And then you
+will try and poke at those web contents. Well, so one thing that people often
+don't realize is that loading web contents, that's not a synchronous process.
+Actually knowing when a page is finished loading is slightly difficult. It's
+quite interesting. And so there are helper functions to try and let you wait
+for this to happen, sort of event waiters. And you should - unfortunately, the
+first part is you have to be aware of this, which is just hard to be. But the
+second part is, once you are aware of where these can creep in, make sure
+you're waiting for the right events. And make sure that once those events have
+happened, you are in a state where the next call makes sense.
+
+44:28 SHARON: That makes sense. You mentioned rewriting your classes so they're
+more easily testable by a unit test. So what are common things you can do in
+terms of how you write or structure your classes that make them more testable?
+And just that seems like a general good software engineering practice to do.
+
+44:50 STEPHEN: Yeah, absolutely. So one of the biggest ones I think we see in
+Chromium is to not use singleton accessors to get at state. And what I mean by
+that is, you'll see a lot of code in Chromium that just goes ahead and threw
+some mechanism that says, hey, get the current web contents. And as you, I
+think, you've talked about on this program before, web contents is this massive
+class with all these methods. And so if you just go ahead and get the current
+web contents and then go do stuff on that web contents, whatever, when it comes
+to running a test, well, it's like, hold on. That's trying to fetch a real web
+contents. But we're writing a unit test. What does that even look like? And so
+the way around this is to do what we call dependency injection. And I'm sure as
+I've said that word, a bunch of listeners or viewers have just recoiled in
+fear. But we don't lean heavily into dependency injection in Chromium. But it
+is useful for things like this. Instead of saying, go get the web contents,
+pass a web contents into your class. Make a web contents available as an input.
+And that means when you create the test, you can use a fake or a mock web
+contents. We can talk about difference between fakes and mocks as well. And
+then, instead of having it go do real things in real code, you can just be
+like, no, no, no. I'm testing my class. When you call it web contents do a
+thing, just return this value. I don't care about web contents. Someone else is
+going to test that.
+
+46:19 SHARON: Something else I've either seen or been told in code review is to
+add delegates and whatnot.
+
+46:25 STEPHEN: Mm-hmm.
+
+46:25 SHARON: Is that a good general strategy for making things more testable?
+
+46:25 STEPHEN: Yeah. It's similar to the idea of doing dependency injection by
+passing in your web contents. Instead of passing in your web contents, pass in
+a class that can provide things. And it's sort of a balance. It's a way to
+balance, if you have a lot of dependencies, do you really want to add 25
+different inputs to your class? Probably not. But you define a delegate
+interface, and then you can mock out that delegate. You pass in that one
+delegate, and then when delegate dot get web content is called, you can mock
+that out. So very much the same goal, another way to do it.
+
+47:04 SHARON: That sounds good. Yeah, I think in general, in terms of Chrome
+specifically, a lot of these testing best practices, making things testable,
+these aren't Chrome-specific. These are general software engineering-specific,
+C++-specific, and those you can look more into separately. Here we're mostly
+talking about what are the Chrome things. Right?
+
+47:24 STEPHEN: Yeah.
+
+47:24 SHARON: Things that you can't just find as easily on Stack Overflow and
+such. So you mentioned fakes and mocks just now. Do you want to tell us a bit
+about the difference there?
+
+47:32 STEPHEN: I certainly can do it. Though I want to caveat that you can also
+just go look up those on Stack Overflow. But yeah. So just to go briefly into
+it, there is - in testing you'll often see the concept of a fake version of a
+class and also a mock version of a class. And the difference is just that a
+fake version of the class is a, what I'm going to call a real class that you
+write in C++. And you will probably write some code to be like, hey, when it
+calls this function, maybe you keep some state internally. But you're not using
+the real web contents, for example. You're using a fake. A mock is actually a
+thing out of the Google test support library. It's part of a - Google mock is
+the name of the sub-library, I guess, the sub-framework that provides this. And
+it is basically a bunch of magic that makes that fake stuff happen
+automatically. So you can basically say, hey, instead of a web contents, just
+mock that web contents out. And the nice part about mock is, you don't have to
+define behavior for any method you don't care about. So if there are, as we've
+discussed, 100 methods inside web contents, you don't have to implement them
+all. You can be like, OK, I only care about the do Foobar method. When that is
+called, do this.
+
+48:51 SHARON: Makes sense. One last type of test, which we don't hear about
+that often in Chrome but does exist quite a bit in other areas, is manual
+testing. So do we actually have manual testing in Chrome? And if so, how does
+that work?
+
+49:03 STEPHEN: Yeah, we actually do. We're slightly crossing the boundary here
+from the open Chromium into the product that is Google Chrome. But we do have
+manual tests. And they are useful. They are a thing. Most often, you will see
+this in two cases as a Chrome engineer. You basically work with the test team.
+As I said, all a little bit internal now. But you work with the test team to
+define a set of test cases for your feature. And these are almost always
+end-to-end tests. So go to this website, click on this button, you should see
+this flow, this should happen, et cetera. And sometimes we run these just as
+part of the launch process. So when you're first launching a new feature, you
+can be like, hey, I would love for some people to basically go through this and
+smoke test it, make sure that everything is correct. Some things we test every
+release. They're so important that we need to have them tested. We need to be
+sure they work. But obviously, all of the caveats about manual testing out
+there in the real world, they apply equally to Chromium or to Chrome. Manual
+testing is slow. It's expensive. We require people - specialized people that we
+have to pay and that they have to sit there, and click on things, and that sort
+of thing, and file bugs when it doesn't work. So wherever possible, please do
+not write manual tests. Please write automated testing. Test your code, please.
+But then, yeah, it can be used.
+
+50:33 SHARON: In my limited experience working on Chrome, the only place that
+I've seen there actually be any level of dependency on manual test has been in
+accessibility stuff -
+
+50:38 STEPHEN: Yeah.
+
+50:38 SHARON: which kind of makes sense. A lot of that stuff is not
+necessarily - it is stuff that you would want to have a person check because,
+sure, we can think that the speaker is saying this, but we should make sure
+that that's the case.
+
+50:57 STEPHEN: Exactly. I mean, that's really where manual test shines, where
+we can't integration test accessibility because you can't test the screen
+reader device or the speaker device. Whatever you're using, we can't test that
+part. So yes, you have to then have a manual test team that checks that things
+are actually working.
+
+51:19 SHARON: That's about all of our written down points to cover. Do you have
+any general thoughts, things that you think people should know about tests,
+things that people maybe ask you about tests quite frequently, anything else
+you'd like to share with our lovely listeners?
+
+51:30 STEPHEN: I mean, I think I've covered most of them. Please write tests.
+Write tests not just for code you're adding but for code you're modifying, for
+code that you wander into a directory and you say, how could this possibly
+work? Go write a test for it. Figure out how it could work or how it couldn't
+work. Writing tests is good.
+
+51:50 SHARON: All right. And we like to shout-out a Slack channel of interest.
+Which one would be the - which one or ones would be a good Slack channel to
+post in if you have questions or want to get more into testing?
+
+52:03 STEPHEN: Yeah. It's a great question. I mean, I always like to - I think
+it's been called out before, but the hashtag #halp channel is very useful for
+getting help in general. There is a hashtag #wpt channel. If you want to go ask
+about web platform tests, that's there. There's probably a hashtag #testing.
+But I'm going to admit, I'm not in it, so I don't know.
+
+52:27 SHARON: Somewhat related is there's a hashtag #debugging channel.
+
+52:27 STEPHEN: Oh.
+
+52:27 SHARON: So if you want to learn about how to actually do debugging and
+not just do log print debugging.
+
+52:34 STEPHEN: Oh, I was about to say, do you mean by printf'ing everywhere in
+your code?
+
+52:41 SHARON: [LAUGHS] So there are a certain few people who like to do things
+in an actual debugger or enjoy doing that. And for a test, that can be a useful
+thing too - a tool to have. So that also might be something of interest. All
+right, yeah. And kind of generally, as you mentioned a lot of things are your
+opinion. And it seems like we currently don't have a style guide for tests or
+best practices kind of thing. So how can we -
+
+53:13 STEPHEN: [LAUGHS] How can we get there? How do we achieve that?
+
+53:19 SHARON: How do we get one?
+
+53:19 STEPHEN: Yeah.
+
+53:19 SHARON: How do we make that happen?
+
+53:19 STEPHEN: It's a hard question. We do - there is documentation for
+testing, but it's everywhere. I think there's `/docs/testing`, which has some
+general information. But so often, there's just random READMEs around the code
+base that are like, oh, hey, here's the content public test API surface. Here's
+a bunch of useful information you might want to know. I hope you knew to look
+in this location. Yeah, it's a good question. Should we have some sort of
+process for - like you said, like a style guide but for testing? Yeah, I don't
+know. Maybe we should enforce that people dependency inject their code.
+
+54:04 SHARON: Yeah. Well, if any aspiring test nerds want to really get into
+it, let me know. I have people who are also interested in this and maybe can
+give you some tips to get started. But yeah, this is a hard problem and
+especially with so many types of tests everywhere. I mean, even just getting
+one for each type of test would be useful, let alone all of them together. So
+anyway - well, that takes us to the end of our testing episode. Thank you very
+much for being here, Stephen. I think this was very useful. I learned some
+stuff. So that's cool. So hopefully other people did too. And, yeah, thanks for
+sitting and answering all these questions.
+
+54:45 STEPHEN: Yeah, absolutely. I mean, I learned some things too. And
+hopefully we don't have too many angry emails in our inbox now.
+
+54:52 SHARON: Well, there is no email list, so people can't email in if they
+have issues. [LAUGHTER]
+
+54:58 STEPHEN: If you have opinions, keep them to yourself -
+
+54:58 SHARON: Yeah. [INAUDIBLE]
+
+54:58 STEPHEN: until Sharon invites you on her show.
+
+55:05 SHARON: Yeah, exactly. Yeah. Get on the show, and then you can air your
+grievances at that point. [LAUGHS] All right. Thank you.
diff --git a/docs/transcripts/wuwt-e05-build-gn.md b/docs/transcripts/wuwt-e05-build-gn.md
new file mode 100644
index 0000000..74e4e47c
--- /dev/null
+++ b/docs/transcripts/wuwt-e05-build-gn.md
@@ -0,0 +1,923 @@
+# What’s Up With BUILD.gn
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 5, a 2023 video discussion between [Sharon (yangsharon@chromium.org)
+and Nico (thakis@chromium.org)](https://www.youtube.com/watch?v=NcvJG3MqquQ).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Building Chrome is an integral part of being a Chrome engineer. What actually
+happens when you build Chrome, and what exactly happens when you run those
+build commands? Today, we have Nico, who was responsible for making Ninja the
+Chrome default build system, to tell us more.
+
+Notes:
+- https://docs.google.com/document/d/1iDFqA3cZAUo0TUFA69cu5wEKL4HjSoIGfcoLIrH3v4M/edit
+
+---
+
+00:00 SHARON: Hello, and welcome to "What's Up With That," the series that
+demystifies all things Chrome. I'm your host, Sharon, and today, we're talking
+about building Chrome. How do you go from a bunch of files on your computer to
+running a browser? What are all the steps involved? Our special guest today is
+Nico. He's responsible for making Ninja, the Chrome default build system, and
+he's worked on Clang and all sorts of areas of the Chrome build. If you don't
+know what some of those things are, don't worry. We'll get into it. Welcome,
+Nico.
+
+00:29 NICO: Hello, Sharon, and hello, internet.
+
+00:29 SHARON: Hello. We have lots to cover, so let's get right into it. If I
+want to build Chrome at a really quick overview, what are all the steps that I
+need to do?
+
+00:41 NICO: It's very easy. First, you download `depot_tools` and add that to
+your path. Then you run fetch Chromium. Then you type `cd source`, run `gclient
+sync`, `gn gen out/GN`, and `ninja -C out/GN chrome`. And that's it.
+
+00:53 SHARON: Wow. Sounds so easy. All right. We can wrap that up. See you guys
+next time. OK. All right. Let's take it from the start, then, and go over in
+more detail what some of those things are. So the first thing you mentioned is
+`depot_tools`. What is that?
+
+01:11 NICO: `depot_tools` is just a collection of random utilities for - like,
+back in the day, for managing subversion repositories, nowadays for pulling
+things from git. It contains Ninja and GN. Just adds a bunch of stuff to your
+path that you need for working on Chrome.
+
+01:25 SHARON: OK. Is this a Chrome-specific thing, or is this used elsewhere,
+too?
+
+01:33 NICO: In theory, it's fairly flexible. In practice, I think it's mostly
+used by Chromium projects.
+
+01:39 SHARON: OK, all right. And there, you mentioned Ninja and GN. And for
+people - I think most people who are watching this have built Chrome at some
+point. But what is the difference between Ninja and GN? Because you have your
+build files, which are generally called Build.gn, and then you run a command
+that has Ninja in it. So are those the same thing? Are those related?
+
+01:57 NICO: Yes. So GN is short for Generate Ninja. So Ninja is a build system.
+It's similar to Make. It basically gets a list of source files and a list of
+build outputs. And then when you run Ninja, Ninja figures out which build steps
+do I have to run, and then it runs them. So it's kind of like Make but simpler
+and faster. And then GN - and Ninja doesn't have any conditionals or anything,
+so GN is - just a built - it describes the build. And then it generates Ninja
+files.
+
+02:34 SHARON: OK.
+
+02:34 NICO: So if you want to do, like, add these files only if you're building
+for Windows, this is something you can do, say, in GN. But then it only
+generates a Windows-specific Ninja file.
+
+02:46 SHARON: All right. And in terms of when you mention OS, so there's a
+couple places that you can specify different arguments for how you build
+Chrome. So you have your gclient sync - sorry, your gclient file, and then you
+have a separate args.gn. And in both of these places, you can specify different
+arguments. And for example, the operating system you use - that can be
+specified in both places. There's an OS option in both. So what is the purpose
+of the gclient file, and what is the purpose of the args.gn file?
+
+03:25 NICO: Yes. So gclient reads the steps file that is at the root of the
+directory, and the DEPS file basically specifies dependencies that Chrome pulls
+in. It's kind of similar to git submodules, but it predates git, so we don't
+use git submodules also for other reasons. And so if you run gclient sync, that
+reads the DEPS file, the Chrome root, and that downloads a couple hundred
+repositories that Chrome depends on. And then it executes a bunch of so-called
+hooks, which are just Python scripts, which also download a bunch of more
+stuff. And the hooks and the dependencies are operating system dependent, so
+gclient needs to know the operating system. But the build also needs to know
+the operating system. And GN args are basic things that are needed for the
+builds. So the OS is something that's needed in both places, but many GN args
+gclient doesn't need to know about. For example, if you enable DCHECKs, like
+Peter discussed a few episodes ago, that's a GN-only thing.
+
+04:26 SHARON: All right. That sounds good. So let's see. When you actually -
+OK. So when you run Chrome and you - say you build Chrome, right? A typical
+example of a command to do that would be, say, `autoninja -C out/default
+content`, right? And let's just go through each part of that and say what each
+of those things is doing and what happens there. Because I think that's just an
+example from one of the starter docs. That's just the copy and paste command
+that they give you. So autoninja seems like it's based on Ninja. What is the
+auto they're doing for us?
+
+05:15 NICO: Yeah. So autoninja is also one of the things that's just
+`depot_tools`. It's a very - or it used to be a very thin wraparound Ninja. Now
+it's maybe a little thicker, but it's optional. You don't have to use autoninja
+if you don't want to. But what it does is basically - like, it helps - So
+Chrome contains a lot of code. So we have this system called Goma, which can
+run all the C++ compilations in a remote data center. And if you do use the
+system, then you want to build with a very high build parallelism. You want to,
+say, `-j 1000` or what and run, like, a thousand bit processes in parallel. But
+if you're building locally, you don't want to do that. So what autoninja
+basically does - it looks at your args.gn file, sees if you have enabled Goma,
+and if so, it runs Ninja with many processes, and else, it runs it with just
+one process per core, or something like that. So that's originally all that
+autoninja does. Nowadays, I think it also uploads a bunch of stuff. But you can
+just run which autoninja, and that prints some path, and you can just open that
+in the editor and read it. I think it's still short enough to fairly quickly
+figure out what it does.
+
+06:17 SHARON: OK. What does `-C` do? Because I think I've been using that this
+whole time because I copied and pasted it from somewhere, and I've just always
+had it.
+
+06:28 NICO: It says - it changes the current directory where Ninja runs, like
+in Make. So it basically says, change the current directory to out/GN, or
+whatever you build directory is, and then run the build from there. So for
+Chrome, the build always - the current directory during the build is always the
+build directory. And then Ninja looks for a file called build.ninja in the
+current directory, so GN writes build.ninja to out/GN, or whatever you build
+directory is. And then Ninja finds it there and reads it and does its thing.
+
+06:57 SHARON: All right. So the next part of this would be out/default, or out
+slash something else. So what are out directories, and how do we make use of
+them?
+
+07:11 NICO: An out directory - it's just a build directory. That's where all
+the build artifacts go to, all the generated objects files, executables, random
+things that are generated during the build. So it can be any directory, really.
+You can make up any directory name that you like. You can build your Chrome in,
+I don't know, fluffy/kitten, or whatever. But I think most people use out just
+because it's in the global `.gitignore` file already. Then you want to use
+something that's two directories deep so that the path from the directory to
+the source is always `../..`. And that makes sure that this is deterministic.
+We try to have a so-called deterministic build, where you get exactly the same
+binary when you build Chrome at the same revision, independent of the host
+machine, more or less. And the path from the build directory to the source file
+is something that goes into debug info. So if you want to have the same build
+output as everyone else, you want a build directory path that's two directories
+deep. And the names of those two directories doesn't really matter. So what
+some people do is they use out/debug for the debug builds and out/release for
+their release builds. But it's really up to you.
+
+08:26 SHARON: Right. Other common ones are, like, yeah. ASan is a common one,
+different -
+
+08:33 NICO: Right.
+
+08:33 SHARON: OSes. Right. So you mentioned having a deterministic build. And
+assuming you're on the same version of Chrome, at the same checkout,
+tip-of-tree, or whatever as someone else, I would have expected that all of the
+builds are just deterministic, but maybe that's because of work that people
+like you and the build team have done. But what are things that could cause
+that to be nondeterministic? Because you have all the same files. Where is the
+actual nondeterminism coming from? Or is it just different configurations and
+setups you have on your local machine?
+
+09:09 NICO: Yeah, that's a great question. I always thought this would be very
+easy to - but turns out it mostly isn't. We wrote a very long blog post that we
+can link to it from the show notes about this. But there's many things that can
+go wrong. Like for example, in C++, there's the preprocessor macro `__DATE__`,
+which embeds the current date into the build output. So if you do that, then
+you're time dependent already. By default, I think you end up with absolute
+paths to everything in debug information. So if you build under
+`/home/sharon/blah`, then that's already different from all the people who are
+not called Sharon. Then there's - we run tools as part of the build that
+produce output. For example, the protobuf compiler or whatnot. And so if that
+binary iterates over some map, some hash map, and that doesn't have
+deterministic iteration order, then the output might be different. And there's
+a long, long, long, long, long list of things. Making the build deterministic
+was a very big project, and there's still a few open things.
+
+10:08 SHARON: OK, cool. So I guess it's - yeah, it's not true nondeterminism,
+maybe, but there's enough factors that go into it that to a typical person
+interacting with it, it does seem -
+
+10:21 NICO: Yeah, but there's also true nondeterminism. Like, every now and
+then, when we update the compiler, the compiler will write different object
+files on every run just because the compiler internally iterates about some -
+over some hash map. And then we have to complain upstream, and then they fix
+it.
+
+10:34 SHARON: OK. Oh, wow. OK. That's very cool. Well, thank you for dealing
+with this kind of stuff so people like us don't have to worry about it. OK. And
+the last part of our typical build thing is content. So what is content in this
+context? If you want to learn about content more in general, check out
+episode 3. But in this case, what does that mean?
+
+10:58 NICO: So just a build target. So I think people - at least I usually
+build some executable. I usually build, I don't know, `base_unittests` or
+`unit_tests` or Chrome or content shell or what. And it's just - so in the
+Ninja files, there's basically - there's many, many lines that go, if you want
+to build this file, you need to have these inputs and then run this command. If
+you want to build this file, instead, you need these other files. You need to
+run this other command. So for example, if you want to build `base_unittests`,
+you need a couple thousand object files, and then you need to run the linkers,
+what's in there. And so if you tell Ninja - the last thing you give it -
+basically, it tells Ninja, what do you want to build? So if you say, `ninja -C
+out/GN content_shell` or what, then Ninja is like, let's look at the line that
+says `content_shell`. And then it checks - I need these files, so it builds all
+the prerequisites, which usually means compiling a whole bunch of files. And
+then it runs the final command and runs the linker. So Ninja basically decides
+what it needs to do and then invokes other commands to do the actual work.
+
+12:08 SHARON: OK, makes sense. So say I run the build - so say I built the
+target Chrome, which is the one that actually is an executable, and that's
+what - if you run that, the browser is built from it. So say I've built the
+Chrome build target. How do I run that now?
+
+12:31 NICO: Well, it's written - so normally, the thing you give to Ninja is
+actually a file name. And the `-C` change current directory. So if you say, `-C
+out/release chrome`, then this creates the file `out/release/chrome`. It just
+creates that file in the out directory. So to run that, you just run
+`out/release/chrome`, and hopefully it'll start up and work.
+
+12:54 SHARON: Great. Sounds so easy. So you mentioned earlier something called
+Goma, which had remote data centers and stuff. Is this something that's
+available to people who don't work at Google, or is this one of the
+Google-specific things? Because I think so far, everything mentioned is anyone,
+anywhere can do all this. Is that the case with Goma, also?
+
+13:14 NICO: Yeah. For the other things - so Ninja is actually something that
+started in Chrome land, but that's been fairly widely adopted across the world.
+Like, that's used by many projects. But yeah, Goma - I think it's kind of like
+distcc. Like, it's a distributed compiler thing. I think the source code for
+both the client and the server are open source. And we can link to that. But
+the access to the service, I think, isn't public. So they have to work at
+Google or at a partner company. I think we hand out access to a few partners.
+And as far as I know, there's a few independent implementations of the
+protocol, so other people also use something like Goma. But as far as I know,
+these other services also aren't public.
+
+13:53 SHARON: OK. Right. Yeah, because I think one of the main things is - I
+mean, as someone who did an internship on Chrome, after, I was like, I'll
+finish some of these remaining to do items once I go back to school, right? And
+then I started to build Chrome on my laptop, just a decent laptop, but still a
+laptop, and I was like, no, I guess I won't be doing that.
+
+14:17 NICO: No, it's doable. You just need to be patient and strategic. Like, I
+used to do that every now and then. You have to start the build at night, and
+then when you get up, it's done. And if you only change one or two CC files,
+it's reasonably fast. It's just, full builds take a very long time.
+
+14:29 SHARON: Yeah, well, yeah. There was enough stuff going on that I was
+like, OK. We maybe won't do this. Right. Going back to another thing you
+mentioned is the compiler and Clang. So can you tell us a bit more about Clang
+and how compiling fits into the build process?
+
+14:50 NICO: Yeah, sure. I mean, compiling just means - almost all of Chrome
+currently is written in C++, and compiling just means taking a CC file, like a
+C++ file, and turning it into - turning that into an object file. And there are
+a whole bunch of C++ compilers. And back in the day, we used to use many, many
+different C++ compilers, and they're all slightly different, so that was a
+little bit painful. And then the C++ language started changing more frequently,
+like with C++ 11, 14, 17, 20, and so on. And so that was a huge drain on
+productivity. Updating compilers was always a year-long project, and we had to
+update, like, seven different compilers, one on Android, iOS, Windows, macOS,
+Android, Fuchsia, whatnot. So over time, we moved to - we moved from using
+basically the system compiler to using a hermetically built Clang that we
+download as a gclient DEPS hook. So when you run gclient sync, that downloads a
+prebuilt Clang binary. And we use that Clang binary to build Chrome on all
+operating systems. So if one file builds for you on your OS, then chances are
+it'll build on all the other OSes because it's built by the same compiler. And
+that also enables things like cross builds, so you can build Chrome for Windows
+on Linux if you want to because your compiler is right there.
+
+16:11 SHARON: Oh, cool. All right. I didn't know that. Is there any reason,
+historically, that Clang beat out these other compilers as the compiler of
+choice?
+
+16:24 NICO: Yes. So it's basically - I think when we looked at this - so Clang
+is basically the native compiler on macOS and iOS, and GCC is kind of the
+system compiler on Linux, I suppose. But Clang has always had very good GCC
+compatibility. And then on Windows, the default choice is Visual Studio. And we
+still want to link against the normal Microsoft library, so we need a compiler
+that's ABI-compatible with the Microsoft ABI. And GCC couldn't do that. And
+Clang also couldn't do that, but we thought if we teach Clang to do that, then
+Clang basically can target all the operating systems we care about. And so we
+made Clang work on Windows, also with others. But there was a team funded by
+Chrome that worked on that for a few years. And also, Clang has pretty good
+tooling interface. So for code search, we also use Clang. So we now use the
+same code to compile Chrome and to index Chrome for code search.
+
+17:28 SHARON: Oh, cool. I didn't know that either, so very interesting. OK.
+We're just going to keep going back. And as you mention more things, we'll
+cover that, and then go back to something you previously mentioned. So next on
+the list is gclient sync. So I think for everyone who's ever worked on Chrome,
+ever, especially at the start, you're like, I'll build Chrome. You build your
+target, and you get these weird errors. And you look at it, and you think, oh,
+this isn't some random weird spot that I definitely didn't change. What's going
+on? And you ask a senior team member, and they say to you, did you run gclient
+sync? And you're like, oh, I did not. And then you run it, and suddenly, things
+are OK. So what else is going - you mentioned a couple of things that happen.
+So what exactly does gclient sync do?
+
+18:13 NICO: Yeah. So as I - that's this file at the source root called DEPS,
+D-E-P-S, all capital letters. And when you update - if you git pull the Chrome
+repository, then that also updates the DEPS file. And then this DEPS file
+contains a long list of revisions of dependent projects. And then when you run
+gclient sync, it basically syncs all these other git repositories that are
+mentioned in the DEPS file. And after that, it runs so-called hooks, which like
+do things download a new Clang compiler and download a bunch of other binaries
+from something called the CIPD, for example, GN. But yeah, basically makes sure
+that all the dependencies that are in Chrome but that aren't in the Chrome
+repository are also up to date. That's what it does.
+
+19:06 SHARON: OK. Do you have a rough ballpark guess of how many dependencies
+that includes?
+
+19:13 NICO: Its operating system dependent. I think on Android we have way
+more, but it's on the order of 200. Like, 150 to 250.
+
+19:25 SHARON: Sounds like a lot. Very cool. OK. In terms of - speaking of other
+dependencies, one of the top-level directories in Chrome is `//third_party`,
+and that seems in the same kind of direction. So how does stuff in
+`//third_party` work in terms of building? Can you just build them as targets?
+What kind of stuff is in there? What can you and can you not build? Like, for
+example, Blink is one of the things in `//third_party`, and lots of people -
+that's a big part of it, right? But a lot of things in there are less active
+and probably less big of a part of Chrome. So does `//third_party` just build
+anything else, or what's different about it?
+
+20:09 NICO: And that's a great question. So Blink being in `//third_party` is a
+bit of a historical artifact. Like, most things - almost all of the things in
+`//third_party` is basically code that's third-party code. That's code that we
+didn't write ourselves. And Chrome's secret mission is to depend on every other
+library out there in the world. No, we depend on things like libpng for reading
+PNG files, libjpeg for reading all of - libjpeg-turbo these days, I guess, for
+reading JPEG files, libxml for reading XML, and so on. And, well, that's many
+dependencies. I won't list them all. And some of these third-party dependencies
+are just listed in the DEPS file that we talked about. And so they basically -
+like, when gclient sync runs, it pulls the code from some git repository that
+contains the third-party code and puts it into your source tree. And for other
+third-party code, we actually check in the code into the Chrome main repository
+instead of DEPSing it in. There are trade-offs, which approach to choose. We do
+both from time to time. But yeah. Almost no third-party dependency has a GN
+file upstream, so usually what you do is you have to write your own BUILD.gn
+file for the third-party dependency you want to add. And then after that, it's
+fairly normal. So for a library, if you want to add a dependency on libfoo,
+usually what we do is you add - you create third-party libfoo, and you put
+BUILD.gn in there. And then you add a DEPS entry that syncs the actual code to
+a third-party libfoo source or something. Yes.
+
+21:37 SHARON: All right. Sounds good. Again, you mentioned BUILD.gn files, and
+that's, as, expected a big part of how building works. And that's probably the
+part that most people have interacted more with, outside of just actually
+running whatever command it is to build Chrome. Because if you create, delete,
+rename any files, you have to update it in some BUILD.gn file. So can you walk
+us through the different things contained in a BUILD.gn file? What are all the
+different parts?
+
+22:12 NICO: Sure. So there's a great presentation by Brett, who wrote GN, that
+we can link to. But in a summary, it's - BUILD.gn contains build targets, and
+the build target normally is like - it doesn't have to be, but usually, it's a
+list of CC files that belong together and that either make up a static library
+or a shared library on executable. So those are the main target types for CC
+code. But then you can also have custom build actions that run just arbitrary
+Python code, which, for example, if you compile a protobuf - proto files into
+CC and H - into C++ and header files, then we basically have a Python script
+that runs protoc, the proto compiler, to produce those. And so in that case,
+the action generates C++ files, and then those get built. But the other, simple
+answer is libraries or executables.
+
+23:11 SHARON: OK. One part of GN files that has caused me personally some
+confusion and difficulty - which I think is maybe, depending on the part of
+Chrome you work on, less of an issue - is DEPS. So you have DEPS in your GN
+files, and there's also something called external DEPS. And then you have
+separate DEPS files that are just called capital D-E-P-S.
+
+23:30 NICO: Yes. Yes, there, that's some redundant - that's, again, I guess for
+historical reasons. So in gclient, DEPS just means to build this target, you
+first have to build these other targets. Like, this target depends - uses this
+other code. And in different contexts, it kind of means different things. So
+for example - I think if an executable depends on some other target, then that
+external executable is linked - that other target is also linked in. If base
+unit test depends on the base library, which in a normal build is a static
+library - like in a normal build? Like in a release build, by default, it's a
+static library. And so if base unit test is built, it first creates a static
+library and then links to it. And then base itself might depend on a bunch of
+third-party things, libraries, which means when base unit tests is linked, it
+links base, but then it also links against basis dependencies. So that's one
+meaning of DEPS. Another meaning, like these capital DEPS files, that's
+completely distinct. Has nothing to do with GN, I'm sad to say. And that's just
+for enforcing layering. Those predate GN, and they are for enforcing layering
+at a fairly coarse level. They say, code in this directory may include code
+from this other directory but not from this third directory. For example, a
+third - like, Blink must not - may include stuff from base, but must not
+include anything from, I don't know, the Chrome layer or something.
+
+25:18 SHARON: Right, the classic content Chrome layering, where Chrome -
+
+25:18 NICO: Right. And I think -
+
+25:18 SHARON: content, but -
+
+25:18 NICO: Right. And there's a step called check-deps, and that checks the
+capital DEPS files.
+
+25:24 SHARON: OK. Yeah, because before, I worked on some Fuchsia stuff, and
+because we're adding a lot of new things, you're messing around with different
+DEPS and stuff a lot more than I think if you worked in a typical part. Like,
+now, I mostly just work within content. Unlikely that you're changing any
+dependencies. But that was always a bit unclear because, for the most part, the
+targets have very similar names - not exactly the same, but very similar. And
+if you miss one, you get all these weird errors. And it was, yeah, generally
+quite confusing.
+
+25:55 NICO: Yeah, that's pretty confusing. One thing of the capital DEPS things
+that they can do that the GN DEPS can't is if someone adds a DEPS on your
+library and they add an entry to their DEPS file, that means that now at code
+review time, you need to approve that they depend on you. And that's not
+something we can do at the GN level. And the advantage there is, I don't know,
+if you have some library and then 50 teams start depending on it without
+telling you, and now you're on the hook for keeping all these 50 things
+working, then with this system, you at least have to approve every time someone
+adds a dependency on you, you have to say, this is fine with me. Or you can
+say, actually, this is - we don't want this to be used by anyone else.
+
+26:45 SHARON: Is there an ideal state where we don't have these DEPS files and
+maybe that functionality is built into the BUILD.gn files, or is this something
+that's probably going to be sticking around for a while?
+
+26:52 NICO: That's a great question. I don't know. It seems weird, right? It's
+redundant. So I think the current system isn't ideal, but it's also not
+horrible enough that we have to fix it immediately. So maybe one day we'll get
+around to it.
+
+27:10 SHARON: Yeah. I think I've mostly just worked on Chrome, so I've gotten
+pretty used to it. But a common complaint is people who work in Google internal
+things or other, bigger - the main build system of whatever company they work
+on, they come to Chrome and they're like, oh, everything's so confusing. But if
+you - you just got to get used to it, but -
+
+27:27 NICO: Right. I think if you're confused by anything, it's great if you
+come to us and complain. Because you kind of become blind to these problems,
+right? I've been doing this for a long time. I'm used to all the foot guns. I
+know how to dodge them. And yeah. So if you're confused by anything, please
+tell me personally. And then if enough people complain about something, maybe
+we'll fix it.
+
+27:55 SHARON: All right. Yeah. That's what you said. The outcome of that -
+we'll see. We'll see how that goes. We'll see how many complaints you suddenly
+get. Right. OK. So another thing I was interested in is right now there's a lot
+of work around Rust, getting more Rust things, introducing that, memory safety,
+that's good. We like it. What is involved from a build perspective for getting
+a whole other language into Chrome and into the build? Because we have most of
+the things C++. There's some Java in all of the Android stuff. And in some
+areas, you see - you'll see a list of - you'll see a file name, and then you'll
+see file name underscore and then all the different operating systems, right?
+And most those are some version of C++. The Mac ones are .mm. And you have Java
+ones for Android. But if you want to add an entirely different language and
+still be able to build Chrome, at a high level, what goes into that?
+
+29:00 NICO: Yeah, there's also some Swift on iOS. It's many different things.
+So at first, you have to teach GN how to generate Ninja files for that
+language. So when a CC file is built, then basically the compiler writes out a
+file that says, here are all the header files I depend on. So if one of them
+gets touched, the compiler - or Ninja knows how to rebuild those. So you need
+to figure out how the Rust compiler or the Swift compiler track dependencies.
+You need to get that information out of the compiler into the build system
+somehow. And C++ is fairly easy to build. It's like a per-file basis. I think
+most languages are more on a module or package base, where you build a few
+files as a unit. Then you might want to think about, how can I make this work
+with Goma so that the compilation can work remotely instead of locally? So
+that's the build system part. Then also, especially for us, we want to use this
+for some performance critical things, so it needs to be very fast. And we use a
+bunch of toolchain optimization techniques to make Chrome very fast with
+three-letter acronyms, such as PGO and LTO and whatnot. And LTO in particular,
+that means a Link Time Optimization. That means the C++ or the Rust code is
+built - is compiled into something called "bitcode." And then all the bitcode
+files at link time are analyzed together so you can do cross-file in-lining and
+whatnot. And for that work, the bitcodes - all the bitcode versions need to be
+compatible, which means Clang and Rust need to be built against the same
+version of LLVM, which is some - it's some internal compiler machinery that
+defines the bitcode. So that means you have to - if you want to do
+cross-language LTO, you have to update your C++ compiler and your Rust compiler
+at the same time. And you have to build them at the same time. And when you
+update your LLVM revision, it must break neither the C++ compiler nor the Rust
+compiler. Yeah. And then you kind of want to build the Rust library from
+source, so you have bit code for all of that. So it's a fairly involved - but
+yeah, we've been doing a lot of work on that. Not me, but other people.
+
+31:24 SHARON: Right. Sounds hard. And what does LTO stand for, since you used
+it?
+
+31:30 NICO: Link Time Optimization.
+
+31:30 SHARON: All right.
+
+31:30 NICO: And there's a blog post on the Chromium blog about this that we can
+link to in the show notes that has a fairly understandable explanation what
+this does.
+
+31:43 SHARON: Yeah, all right. That sounds good. So linking, that was my next
+question. As you build stuff, you sort out all of your just compile errors, you
+got all your spelling mistakes out. The next type of error you might get is
+linking error. So how does - can you tell us a bit more about linking in
+general and how that fits into the build process?
+
+32:01 NICO: I mean, linking - like, for C++, the compiler basically produces
+one object file for every CC file. And then the linker takes, like, about
+50,000 to 100,000 object files and produces a single executable. And every
+object file has a list of functions that are defined in that object file and a
+list of functions that are undefined in that object file that it calls that are
+needed from elsewhere. And then the linker basically makes one long list of all
+the functions it finds. And at the end, all of them should be defined, and all
+the non-inline ones should be defined in exactly one object file. And if
+they're not - if that doesn't happen, then it emits an error, and else, it
+emits a binary. And the linker is kind of interesting because the only thing
+you really care about is that it does its job very quickly. But it has to read
+through gigabytes of data before it writes the executable. And currently, we
+use a linker called `ld`, which was also written by people on the Chrome team,
+and which is also fairly popular outside of Chrome nowadays. And so we wrote on
+ELF linker, which is the file format used on Linux and Android, and on COFF
+linker, which is the file system used on Windows, and our own Mach-O linker,
+which is the file system on Apple - macOS and iOS. And our linkers are way,
+way, way faster than the things that they replace. On Windows, we were, like,
+10 times faster than the Windows linker. And on Mac, we're, like, four times
+faster than the system linker and whatnot. The other linker vendors have caught
+up a little bit, but we - I feel like Chrome has really advanced the state and
+performance of linking binaries across the industry, which I think is really
+cool.
+
+33:44 SHARON: Yeah, that is really cool. And in a kind of similar vein to the
+different OSes and all that kind of stuff is 32- versus 64-bit. There's some
+stuff happening. I've seen people talk about it. It seems pretty important. Can
+you just tell us a bit more about this in general?
+
+34:04 NICO: Well, I guess most processors sold in the last decade or so are
+64-bit. So I think on some platforms, we only support 64-bit binaries, like -
+and the bit just means how wide is a pointer and has some implications on which
+instructions can the compiler use. But it's fairly transparent too, I think, at
+the C++ level. You don't have to worry about it all that much. On macOS, we
+only support 64-bit builds. Same on iOS. On Windows, we still have 32-bit and
+64-bit builds. On Linux, we don't publicly support 32-bit, but I think some
+people try to build it. But it's really on Windows where you have both 32-bit
+and 64-bit builds. But the default bits is 64-bit, and you can say, if you say
+target CPU equals x86, I think, in your args.gn, then you get a 32-bit build.
+But it should be fairly transparent to you as a developer, unless you write
+assembly.
+
+35:02 SHARON: How big of an effort would it be to get rid of 32-bit on Windows?
+Because Windows is probably the biggest Chrome-using platform, and also,
+there's a lot of versions out there, right? So -
+
+35:15 NICO: Oh, yeah.
+
+35:15 SHARON: How doable?
+
+35:15 NICO: I think that the biggest platform is probably Android. But yeah,
+Android is also 32-bit, at least on some devices at the moment. That's true. I
+don't know. I think we've looked into it and decided that we don't want to do
+that at the moment. But I don't know details.
+
+35:33 SHARON: And you mentioned ARM. So is there any - how much does the Chrome
+build team - are they concerned with the architecture of these processors? Is
+that something that, at the level that you and the build team have to worry
+about, or is it far enough - a few layers down that that's -
+
+35:47 NICO: It's something we have to worry about at the toolchain team. So we
+update the scaling compiler every two weeks or so, which means we pull in all -
+around 1,000 changes from upstream contributors that work on LVM spread across
+many companies. And we have to make sure this doesn't break from on 32-bit ARM,
+64-bit ARM, 32-bit Intel, 64-bit Intel, across seven different operating
+systems. And so fairly frequently, when we try to update Clang tests start
+failing on, I don't know, 32-bit Windows or on 64-bit iOS or some very specific
+configuration. And then we have to go and debug and dissect and figure out
+what's going on and work with upstream to get that fixed. So yeah. That's
+something we have to deal with at the toolchain team, but hopefully, it's -
+hopefully, like the normal Chrome developer is isolated from that for the most
+part.
+
+36:45 SHARON: I think so. It's not - if I weren't asking all these other
+questions, it's something that almost never crosses my mind, right? So that
+means you're all doing a very good job of that. Thank you very much. Much
+appreciated. And jumping way back, you mentioned earlier indexing the code
+base, code search. So I make a change. I submit it. I upload it. It eventually
+ends up in code search. So how does that process work? And what goes into
+indexing? Because before, when I was working on Fuchsia all the Fuchsia code
+wasn't indexed, so you couldn't do the handy thing of clicking a thing and
+seeing where it was defined. You had to actually look it up. And once you got
+that, it was like, oh my gosh, so much better. So can you just tell us a bit
+more about that process?
+
+37:30 NICO: Sure, yeah. The Chrome has a pretty good code search feature, I
+think, codesearch.chromium.org or cs.chromium.org. Basically, we have a bot
+that runs, I think, every six hours or so, pulls the latest code, bundles it
+up, sends it to some indexer service that then also uses Clang to analyze the
+code. Like, for C++, I think we also index Java. We probably don't index Rust
+yet, but eventually we will. And then it generates - for every word, it
+generates metadata that says, this is a class. This is an identifier. And so if
+you click on it, if you click on a function, you have the option of jumping to
+the definition of the function, to the declaration, to all the calls, all the
+overrides, and so on. And that updates ideally several times a day and is
+fairly up to date. And we built the index, I think, for most operating systems.
+So you can see this is called here on Linux, here on Windows, and what not.
+
+38:32 SHARON: OK. Sounds good. Very useful stuff. And I don't know if this is
+part of the build team's jurisdiction, but when you are working on things
+locally, you have some git commands, and then you have some git-cl commands.
+
+38:43 NICO: Mm-hmm.
+
+38:48 SHARON: So the git commands are your typical ones - git pull, git rebase,
+git stash, that kind of thing. And then you have git-cl commands, which relate
+more to your actual CL in Gerrit. So git-cl upload, git-cl status. That'll show
+you all your local branches and if they have a Gerrit change associated with
+them. So what's the difference between git and git-cl commands?
+
+39:18 NICO: I'm sorry. So this is basically a git feature. If you call git-foo,
+then git looks for git-foo on your path. So you can add arbitrary commands to
+git if you want to. And git-cl is just something that's in `depot_tools`.
+Again, there's git-cl in `depot_tools`, and you can open that and see what it
+does. And it'll redirect to `git_cl.py`, I think, which is a fairly long and
+hairy Python script. But yeah. It's basically Gerrit integration, as you say.
+So you can use that to send try jobs, `git cl try`. To upload, as you say, you
+can use `git cl issue` to associate your current branch with a remote Gerrit
+review, `git cl patch` to get a patch off Gerrit and patch it into your local
+thing, `git cl web` to open the current thing in a web browser. Yeah, git-cl is
+basically - git-cl help to see all the git-cl commands, or - yeah. If you have
+a change that touches, like, 1,000 files, you can run `git cl split`, and it'll
+upload 500 reviews. But that's usually too granular, and I wouldn't recommend
+doing that. But it's possible.
+
+40:25 SHARON: Right. Do you have a - [DOORBELL DINGS]
+
+40:25 NICO: Oops, sorry.
+
+40:25 SHARON: commonly - yeah.
+
+40:30 NICO: Oh, sorry. There was - the door just rang. Maybe you didn't hear
+it. Sorry.
+
+40:30 SHARON: All right. It's all good. Do you have a lesser known git or
+git-cl command that you use a lot or -
+
+40:41 NICO: Well, I -
+
+40:41 SHARON: is your favorite? [LAUGHS]
+
+40:46 NICO: It's not lesser known to me, so I wouldn't know. I don't know. I
+use `git cl upload` a lot.
+
+40:53 SHARON: Right. Well, you have to use `git cl upload`, right?
+
+40:53 NICO: I use -
+
+40:53 SHARON: Well, you don't - maybe not but -
+
+40:53 NICO: `git cl try` to send try jobs from my terminal, `git cl web` to see
+what's going on, `git cl patch` a lot to patch stuff in locally. If I'm doing a
+code review and I want to play with it, I patch it in, build a local, and see
+how things are working.
+
+41:12 SHARON: Yeah. When I patch in a thing, I go from the cl page on Gerrit
+and then click the down patch thing, but -
+
+41:21 NICO: No, even `git cl patch -b` and then some branch name, and then you
+just patch - paste the Gerrit review URL.
+
+41:28 SHARON: Oh, cool.
+
+41:28 NICO: So it's just, yeah, Control-L to focus the URL bar. Control-C
+Alt-Tab `git cl patch -b blah`, Paste, Enter, and then you have a local branch
+with the thing.
+
+41:36 SHARON: All right. Yeah, a lot of these things, once you learn about
+them - at first you're like, whoa, and then you use them, and then they're not
+lesser known to you, but you tell other people also a common - so another one
+would be `git cl archive`, which will -
+
+41:47 NICO: Oh, yeah, yeah.
+
+41:47 SHARON: get rid of any local branches associated with a closed Gerrit
+branch, so that's very handy, too.
+
+41:53 NICO: Yes.
+
+41:53 SHARON: So it's always fun to learn about things like that.
+
+41:59 NICO: Are you fairly tidy with your branches? How many open branches do
+you usually have?
+
+41:59 SHARON: [LAUGHS] I used to be more tidy. When I tried to do a cleanup
+thing, I had more branches. I think right now I've got around 20-something
+branches. I like having not very many. I think to some people, that's a lot. To
+some people, that's not very many. I mean, ideally, I have under five, right?
+[LAUGHS] But -
+
+42:18 NICO: I don't know. I usually have a couple 10, sometimes. Have a bunch
+of machines. I think on some of them it's over 100, but yeah. Every now and
+then, I run `git cl archive` and it removes half of them, but -
+
+42:29 SHARON: Yes. All right, cool. Is there anything that we didn't cover so
+far that you would like to share? So things that maybe you get asked all the
+time, things that people could do better when it comes to build-related things?
+Things that you can do that make the build better or don't make it worse, that
+kind of thing? Or just anything else you would like to get out there?
+
+42:58 NICO: I guess one thing that's maybe implicitly stated, but currently not
+explicitly documented, as far as I know, but I'm hoping to change that, is - so
+Chrome tries to have a quiet build. Like, if you build this zero build output,
+except that one Ninja file, Ninja line that's changing, right? There's, well,
+another code basis - I think it's fairly common - that there's many screenfulls
+of warning that scroll by. And we very explicitly try not to do that because if
+the build emits lots of warnings, then people just learn to ignore warnings. So
+we think something should either be a serious problem that people need to know
+about, then it should be an error, or it should be not interesting. Then it
+should be just quiet. So if you add a build step that adds a random script, the
+script shouldn't print anything, just about progress. Shouldn't say, doing
+this, doing this, doing this. Should either print something and say something's
+wrong and fail those build step or not say anything. So that's one thing.
+
+43:51 SHARON: That's - yeah, that's true.
+
+43:51 NICO: And the other thing -
+
+43:51 SHARON: Like, you only really get a bunch of terminal output if you have
+a compile or a linker error, whatever.
+
+43:57 NICO: Right.
+
+43:57 SHARON: I hadn't ever considered that. If you build something and it
+works, you get very few lines of output. And I hadn't ever thought that was
+intentional before, but you're right in that if it was a ton, you would just
+not look at any of it. So yeah, that's very cool.
+
+44:09 NICO: Yeah. And on that same note, we don't do deprecation warnings
+because we don't do any warnings. So if people - like, people like deprecating
+things, but people don't like tidying up calls to deprecated functions. So if
+you want to deprecate something in Chrome, the idea is basically, you remove
+all callers, and then you remove the deprecated thing. And we don't allow you
+to say - to add a warning that tells everyone, hey, please, everyone, remove
+your calls. The onus is on the person who wants to deprecate something instead
+of punting that to everyone else.
+
+44:46 SHARON: Yeah, I mean, the thing that I was working on has a deprecating
+effect, so removing callers, which is why I have so many branches. But I've
+also seen presubmit warnings for if you include something deprecated. So - oh,
+yeah, and there's presubmit, too. OK, we'll get to that also. [LAUGHS] Tell us
+more about all of this.
+
+45:05 NICO: About presubmits? Yeah, presubmits - presubmits are terrible.
+That's the short summary. So if you run a `git cl presubmit`, it'll look at a
+file called presubmit.py, I think, in the current directory, and maybe in all
+the directories of files - of directories that contain files you touched or
+something like that. But you can just open the top-level presubmit.py file, and
+there's a couple thousand lines of Python where basically everyone can add
+anything they want without much oversight, so it's a fairly long - at least
+historically, that used to be the case. I don't know if that's still the case
+nowadays. But yeah, it's basically like a long list of things that random
+people thought are good if they - like, presubmits are something that are run
+before you upload, also, implicitly. And so you're supposed to clean them up.
+And [INAUDIBLE] many useful things. For example, nowadays we require most code
+to be autoformatted so that people don't argue about where semicolons should go
+or something silly like that. So one of the things it checks is, did you run
+`git cl format`, which runs, I guess, Clang format for C++ code and a bunch of
+custom Python scripts for other files. But it's also - presubmits have grown
+organically, and there isn't - they're kind of unowned and they're very, very
+slow. And I think some people have tried to improve them recently, and they're
+better than they used to be, but I don't love presubmits, I guess is the
+summary. But yeah, it's another thing to check invariants that we would like to
+be true about our code base.
+
+46:48 SHARON: Yeah. I mean, I think - yes, spelling is something I think it
+also checks.
+
+46:54 NICO: It checks spelling? OK.
+
+46:54 SHARON: Or maybe that's a separate bot in Gerrit.
+
+46:59 NICO: Oh, yeah, yeah, yeah, yeah. Like, there's this thing called -
+what's its name?
+
+47:06 SHARON: Trucium? Tricium?
+
+47:06 NICO: Tricium, yeah. Tricium, right. Tricium is something that adds
+comments to your - automatically adds comments to your change list when you
+upload it. And Tricuium can do spelling correction, but it can also - it runs
+something called Clang Tidy, which is basically a static analysis engine which
+has quite a few false positives, so sometimes it complains about something
+that - but it's actually incorrect, and so we don't put that into the compiler
+itself. So we've added a whole bunch of warnings to the compiler for things
+that we think are fairly buggy. But Clang Tidy is - but these warnings have to
+be - they have to have a very low false positive rate. Like, if they complain,
+they should almost always be right. But sometimes, for static analysis, it's
+hard to be right. Like, you can say this might be wrong. Please be sure. But
+this is not something the compiler can say, so we have this other system called
+Clang Tidy which also adds a comment to your C++ code which says, well, maybe
+this should be a reference instead of a copy, and things like that.
+
+48:04 SHARON: Yeah. And I think it - I've seen it - it checks for unused
+variables and other - there's been useful stuff that's come from comments from
+there, so definitely. All right. Very cool. So if people are interested in all
+this build "infra-y" kind of stuff and they want to get more into it, what can
+they do?
+
+48:32 NICO: We have a public build@chromium.org mailing list. It's very low
+volume, but if you want to reach out, you can send an email there and a few of
+us will see your email and interact with you. And there's also I think the tech
+build on crbug. So you can just look for build bugs and fix all our bugs for
+us. That'd be cool.
+
+48:51 SHARON: [LAUGHS]
+
+48:51 NICO: And if there's anything specific, just talk to local OWNERS. Or if
+you feel this is just something you're generally interested in and you're
+looking for a project, you can talk to me, and I probably have a long list of -
+I do have a long list of somewhat beginner-friendly projects that people could
+help out with, I guess.
+
+49:15 SHARON: Yeah. I mean, I think being able to - if you're looking for a
+20%y kind of project or something else. But knowing how things actually get put
+together is always a good skill and definitely applicable to other things. It's
+the kind of thing where the more low level-knowledge you have, the more - it
+works - it applies to things higher up, but not necessarily the other way
+around, right?
+
+49:34 NICO: Mm-hmm.
+
+49:34 SHARON: So having that kind of understanding is definitely a good thing.
+All right. Any last things you'd like to mention or shout out or cool things
+that you want people to know about? [LAUGHS]
+
+49:48 NICO: I guess -
+
+49:48 SHARON: Or what - yeah, quickly, what is the future of the whole build
+thing? Like, what's the ideal situation if -
+
+49:55 NICO: Ideally, it'll all be way faster, I guess is the main thing. But
+yeah, yeah, I think build speed is a big problem. And I'm not sure we have the
+best handle on that. We're working on many things, but - not many. A bunch of
+things. But it's - like, people keep adding all that much code, so if y'all
+could delete some code, too, that would help us a lot. I mean, having -
+supporting more languages is something we have to - this is something that's
+happening. Like, Rust is happening. We are also on iOS also using Swift.
+Currently, we can't LTO Swift with the rest because that's on a different OEM
+version. There's this - in C++ - we keep upgrading C++ versions. So Peter
+Kasting is working on moving us to C++20. And then 23, we'll have them, and so
+on. There's maybe C++ modules at some point, which may or may not help with
+build speed. And there's a bunch of tech debt that we need to clean up, but
+that's not super interesting.
+
+51:24 SHARON: I don't know. I think people in Chrome in general are more
+interested and care about reducing tech debt in general, right? A lot of people
+I know would be happy to just do tech debt clean-up things only, right?
+Unfortunately, it doesn't really work out for job reasons. But a lot of people,
+I think, are interested in, I think, in higher proportions than maybe other
+places.
+
+51:47 NICO: It depends on the tech debt. Some of it might work out for job
+reasons. But, yeah.
+
+51:54 SHARON: Yeah. I mean, some of it is easier than others, too, right? Some
+of it is like, yeah, so, OK, well, go delete some code. Go clean up some
+deprecated calls. [LAUGHS] All that.
+
+52:08 NICO: Yeah, and again, I think finishing migrations is way harder than
+starting them, so finish more migrations, start fewer migrations. That'd be
+also cool.
+
+52:16 SHARON: All right. I am sure everyone listening will go and do that right
+away.
+
+52:21 NICO: Yep.
+
+52:21 SHARON: And things will immediately be better.
+
+52:27 NICO: They've just been waiting to hear that from me, and now they're
+like, ah, yeah, right. That makes sense.
+
+52:27 SHARON: Yeah, yeah. All right. Well, you all heard it here first. Go do
+that. Things will be better, et cetera. So all right. Well, thank you very
+much, Nico, for being here answering all these questions. I learned a lot. A
+lot of this is stuff that - everyone who works on Chrome builds Chrome, right?
+But you can get by with a very minimal understanding of how these things are.
+Like, you see your - you follow the Intro to Building Chrome doc. You copy the
+things. You're like, OK, this works. And then you just keep doing that until
+you have a problem. And depending on where you work, you might not have
+problems. So it's very easy to know very little about this. But obviously, it's
+so important because if we didn't have any of this infrastructure, nothing
+would work. So one, I guess, thank you for doing all the stuff behind the
+scenes, determinism, OSes, all that, making it a lot easier for everyone else,
+but also thank you for sharing about it so people understand what's actually
+going on when they run the commands they do every day.
+
+53:31 NICO: Sure. Anytime. Thanks for having me. And it's good to hear that
+it's possible to work on Chrome without knowing much about the build because
+that's the goal, right? It should just work.
+
+53:44 SHARON: Yeah.
+
+53:44 NICO: Sometimes it does.
+
+53:44 SHARON: [LAUGHS] Yeah. Well, thank you for all of it, and see you next
+time.
+
+53:51 NICO: Yeah. See you on the internet. Bye.
+
+54:03 SHARON: OK. So we will stop recording -
+
+54:03 NICO: Wee. Time for the second take.
+
+54:03 SHARON: [LAUGHS] Let's do that, yeah, all over again.
+
+54:11 NICO: Let's do it.
+
+54:11 SHARON: I will stop recording.
diff --git a/docs/transcripts/wuwt-e06-open-source.md b/docs/transcripts/wuwt-e06-open-source.md
new file mode 100644
index 0000000..a7398ef
--- /dev/null
+++ b/docs/transcripts/wuwt-e06-open-source.md
@@ -0,0 +1,978 @@
+# What’s Up With Open Source
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 6, a 2023 video discussion between [Sharon (yangsharon@chromium.org)
+and Elly
+(ellyjones@chromium.org)](https://www.youtube.com/watch?v=zOr64ee7FV4).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+What does it mean for Chrome to be open source? What exactly is Chromium? Can
+anyone contribute to the browser? Answering all that is today's special guest,
+Elly. She worked all over Chrome and ChromeOS, and is passionate about
+accessibility, the open web, and free and open-source software.
+
+Notes:
+- https://docs.google.com/document/d/1a6sdrspJgAHDdQMMNGV0t7zo8QWgqq0hgsyV55tc_dk/edit
+
+Links:
+- [What's Up With BUILD.gn](https://www.youtube.com/watch?v=NcvJG3MqquQ)
+- [What's Up With //content](https://www.youtube.com/watch?v=SD3cjzZl25I)
+- [What are Blink Intents?](https://www.youtube.com/watch?v=9cvzZ5J_DTg)
+
+---
+
+00:00 SHARON: Hello, and welcome to "What's Up With That?" the series that
+demystifies all things Chrome. I'm your host, Sharon. And today, we're talking
+about open source. What does it mean to be open source? I've heard of Chrome,
+but what's Chromium? What are all the ways you can get involved? Answering
+those questions and more is today's special guest, Elly. Elly currently works
+on the Chrome content team, which is focused on making the web more fun and
+interesting to use. Previously, she's worked all over Chrome and Chrome OS.
+She's passionate about accessibility, the open web, and free and open-source
+software. Welcome, Elly.
+
+00:34 ELLY: Thank you, Sharon.
+
+00:34 SHARON: All right. First question I think is pretty obvious. What is open
+source? What does that mean?
+
+00:40 ELLY: Yeah, so open source is a pretty old idea. And it basically just
+means, in the purist sense, that the source code for a program is open to be
+read by others.
+
+00:51 SHARON: OK. And Chrome, the source code is available to be read by
+anyone. What else is it? Open source - I've heard of open-source community. It
+seems like there's a lot to it. So why don't you just tell us more about open
+source, generally?
+
+01:03 ELLY: Yeah, for sure. There's quite a bit of nuance here. And there's
+been differing historical interpretations of some of these terms, so I'll -
+there's two big camps that are important to talk about. One is open source,
+which means what I said - the source is available to be viewed. There's also
+the idea of free software, which is software that actually has license terms
+that allow for people to modify it, to make their own derivative versions of
+it, and that kind of thing. And so historically, there was a pretty big
+difference between those things. These days, the two concepts are often talked
+about pretty interchangeably because a lot of open-source projects are free
+software, and all free software projects basically are open source. But the
+distinction used to be very important and is still pretty important, I guess.
+Chromium is both open source and free software. So we ship under a license that
+allows for - not only for everyone to read and look at our code, but also for
+other folks to make their own versions of Chromium. So, yeah, Chromium, both
+open source and free software.
+
+01:56 SHARON: OK, very cool. And you mentioned Chromium in there. But I think
+for most people, when they think of the browser, they call it Chrome. So what
+is the difference between Chrome and Chromium? Are they the same thing? I think
+people, myself included, sometimes use those interchangeably, especially when
+you work on it. So what is the difference there?
+
+02:16 ELLY: Yeah, fantastic question. So Chromium is an open-source and free
+software web browser that is made by the Chromium Foundation, which is like an
+actual .org that exists somewhere on the internet. Chrome is a Google-branded
+web browser that is basically made by taking Chromium, which is an open-source
+and free software web browser, adding some kind of Google magic to it, like
+integrations with some Google services, some kind of media codecs that maybe
+aren't themselves free software, that kind of thing, bundling that up into a
+more polished product which we call Google Chrome, and then shipping that as a
+web browser. So Chromium is an open-source project. Google Chrome is a Google
+product that is built on top of Chromium.
+
+03:03 SHARON: OK. So Google Chrome is a Chromium-based browser, which is a term
+I think that people who work in any browser stuff - it's a term that they've
+all [INAUDIBLE] before.
+
+03:08 ELLY: Yeah, exactly. And in fact, you alluded to the fact that we
+sometimes use those terms interchangeably. And especially at Google, we
+sometimes get a little confused about what we're talking about sometimes
+because we're - the Google Chrome team are the biggest contributors to
+Chromium, the open-source project. And so we tend to sometimes talk about the
+two things as though they're the same. But there's a really important
+difference for folks who are working on other Chromium-derived browsers. So if
+you're working on a Chromium derivative that a Linux distribution ships, for
+example, your browser is based on Chromium, and it's really not Chrome. It's
+Chromium, right? It is the open-source browser that Chrome is based on. But
+it's not the same thing at all.
+
+03:52 SHARON: Yeah, if you want to learn a bit more about basing things on
+Chromium, the content episode is a good one to check out. We talk a bit about
+that and embedding Chrome in Chromium and what that means. So -
+
+04:03 ELLY: Yeah, absolutely.
+
+04:03 SHARON: check it out if you [INAUDIBLE]...
+
+04:03 ELLY: And there's also, in the Chromium source tree, there's actually a
+thing called Content Shell, which is a minimal demonstration browser. It's like
+the rendering engine from Chromium wrapped in the least amount of browser
+possible to make it work. And we use it for testing, but it's also a really
+good starting point if you're trying to learn how to build a Chromium
+derivative browser.
+
+04:22 SHARON: OK, very neat. So I think a next very natural question to come
+out of this is, why is Chrome or Chromium - Chromium rather - going to try to
+be good about using those correctly here - but why is Chromium open source?
+
+04:40 ELLY: Yeah, so this is the decision that we made right when we were
+starting the project actually. And it's based on this really fundamental idea
+that the web benefits when users have better browsers. So if we, like the
+Chromium project, come up with some super clever way of doing something, or we
+come up with some really ingenious optimization to our JavaScript Engine or
+something like that, it's better for the web, better for everyone, and
+ultimately even better for Google as a business if those improvements are
+actually adopted by other people and taken by other people and used by them. So
+it is better for us if other people make use of anything clever that we do. And
+separately from that, there's this idea that's really prevalent in open-source
+communities of, if people can read the code, they're more likely to find bugs
+in it. And that's something that Chromium constantly benefits from, is folks
+who are outside the project, just kind of looking through our code base,
+reading and understanding it, spotting maybe security flaws that are in there.
+That kind of research is so much easier to do when the source code is just
+there, and you're not trying to reverse-engineer something you can't see the
+source to. So we get a lot of benefit from being open-source like that. And
+those are the reasons we had originally, and those still all hold totally true
+today, I think.
+
+05:51 SHARON: That makes sense. Yeah, it seems, at first, a bit odd for a big
+company like Google to make something like this open source. But there are
+other massive open-source things at Google - Android, I think, being the other
+canonical example, which we don't know too much about, but we won't be getting
+too into that. But there are other big open-source projects around.
+
+06:08 ELLY: Yeah, absolutely. And there's also, like - there's Go. That's an
+open-source programming environment, like a language and a compiler and a bunch
+of tools around it that is open source built by Google. There are plenty of
+other open-source and free software projects built by large corporations, often
+for really the same reasons. We benefit because the entire web benefits from
+better technology.
+
+06:32 SHARON: Yeah, I think some of the Build stuff we do is open source. Check
+out the previous episode for that. And that's, yeah, exactly - not strictly
+only used by -
+
+06:37 ELLY: Yeah, and by the way, partly because we're open source - like, for
+example, the Chromium base library, which is part of our C++ software
+environment - our base library is regularly used in other projects, even things
+that are totally unrelated to browsers, because it provides a high-quality
+implementation of a lot of basic things that you need to do. And so that code
+is being used in so many places we would never have anticipated and has done
+honestly more good in the world than it would do if it was just part of a
+really excellent browser.
+
+07:13 SHARON: Something that someone on my first team told me was, if you've
+changed anything in base, that probably is going to get run any time the
+internet gets run, somewhere in that stack, which, if you think about it, is so
+crazy.
+
+07:26 ELLY: Oh, Yeah. Absolutely. Early in my career, I added a particular
+error message to part of the Chrome network stack. And that network stack, too,
+is one of those components that gets reused in a lot of places. And so
+occasionally, I'll be running some completely other program. Like, I'll be
+running a video game or something, and I'll see that error message that I added
+being emitted from this program. And I'm like, oh, my code is living on in a
+place I would never have really thought of.
+
+07:51 SHARON: Oh, that's very cool.
+
+07:51 ELLY: Yeah.
+
+07:51 SHARON: Yeah.
+
+07:51 ELLY: It's one of those unique open-source experiences in my book, of
+seeing your own work being used like that by other folks you wouldn't have
+anticipated.
+
+07:57 SHARON: Yeah, that's very cool. So something I think I've heard you say
+before that I thought sounded very cool was the open-source dream. So can you
+tell us a bit more about what that is. What is that vision? It sounds very
+nice.
+
+08:09 ELLY: Yeah, so I talked about this a little bit. And earlier, I cautioned
+against conflating open-source and free software. But it really is more of the
+free software dream than the open-source dream, in some sense. That dream is
+this idea that if we have software that is made available for free, under
+licenses that let people modify it and make derivative works and keep using it,
+that over time, everyone will get access to really high-quality and
+freely-available software. And we will have a situation where the software that
+people need is built by their communities, built by the people who are in those
+communities, instead of being something that they have to buy from a company
+that makes it. It'll be something they can instead produce for themselves. And
+over time, I think that this has really played out in that way. If you look at
+the state of operating systems today, for example, there are these really
+high-quality, freely-available open-source free software operating systems that
+are readily available and anyone can use, and they really do meet the needs for
+a lot of folks. And then, in fact, it kind of circles back to where Linux is a
+high-quality, free software open-source operating system that Google can then
+turn around and make really good use of to build something like Chromium OS,
+which is another free software open-source project that uses Linux as one of
+its major components. And then we get to produce a product that the Chromium OS
+engineering team would have had to spend a lot of time if we weren't able to
+make use of that existing Linux kernel work. So you get into this cycle of
+giving back and sharing and benefiting from the effects of other people
+sharing. That's the free software dream to me.
+
+09:57 SHARON: It does - yeah, that sounds great. And for sure - I try to use
+open-source options when I can. When I edit these videos, I use something
+open-source. It feels appropriate for what we're doing here. So, yeah, that
+sounds like it would be - it's a good system that everyone contributes to and
+everyone benefits from. And that's really nice.
+
+10:10 ELLY: Yeah, absolutely.
+
+10:16 SHARON: So going away from that towards the more less open-source part,
+so what kind of things in Chrome, the browser, are not open source? You
+mentioned a couple of things earlier. Can you tell us a bit more about some of
+those things?
+
+10:27 ELLY: Yeah, I'm going to caveat this by saying that I don't personally
+work on the stuff I'm about to talk about. And so my knowledge is more
+superficial. There's a couple things I'm pretty confident about. So one is, for
+example, there's a few video formats that Chrome can play that Chromium cannot
+play because Google has agreements with the companies that make those codecs
+that allow us to basically license and embed their thing and ship it as part of
+Chrome. But those agreements, we can't really extend them to everyone who might
+make a Chromium browser. And so it ends up in a situation where there is a
+closed-source component that's included in Chrome to make that possible. I'm
+struggling to think of another example right off the top of my head. I believe
+that there's also a couple things in Chrome that are integrating with Google
+APIs, where they're features that are Chrome-specific because they're
+Google-specific. And one of the things that is generally true between the two
+products is that Chrome will have more Google integrations and more Google
+magic and more Google smarts than Chromium will. And so I think some of those
+are actually closed-source components that come from Google that get embedded
+into Chrome. But because they're a closed-source, we wouldn't want to put them
+into Chromium.
+
+11:37 SHARON: Right. It seems like, yeah, I can sign into Chrome. I don't
+expect that I'd be able to sign in with my gmail.com into, say, Chromium. I'm
+not sure it's actually part of it, but that's a guess.
+
+11:49 ELLY: Yeah, so that does work, except that you need to - any Chromium
+distributor needs to go and talk to - basically, talk to the sign-in team to
+get an API key that allows their browser to sign in. There is a process for
+doing that. It doesn't actually require any closed-source code components. But
+there is still a thing where you have to talk to the accounts team and
+basically be like, hey, we're a legitimate web browser, and we want to allow
+users to sign in. Because we don't want a situation where bots or malware are
+doing fake user sign-ins from - pretending to be Chromium. That's bad.
+
+12:25 SHARON: Right. That makes sense. Yeah, and I think because of where
+Chrome and Chromium are positioned, I think there will be some interesting
+comparisons and differences between Chrome, Chromium, and other internal
+google3 projects. So that's kind of the term for things that are closed-source
+Google - the typical Maps, Search, all that stuff - and also comparing Chromium
+to other open-source projects. So we've talked a bit about the similarities and
+differences between Chrome and Google internal. Are there any other things you
+can think of that are either similar or different between Chrome the project
+and the people who work on it and how people do things internally at Google?
+
+13:11 ELLY: Yeah. So internally at Google, there's this very powerful, very
+custom-built whole technology stack around the projects. There is a continuous
+integration system. There's an editor. There's a source control system. There's
+all of this stuff. Within Google, all of that is custom. And it's all fitted to
+Google's needs. And a lot of it is just built from scratch, frankly. Whereas
+for Chromium, we're using essentially off-the-shelf open-source stuff to meet a
+lot of those needs. So, for example, for version control, we're just using Git,
+which is I think the most popular version control system in the world right
+now. It's definitely open source. And our build system, for example, which is
+like GN and Ninja put together, those are both free software open-source
+projects. Admittedly, both of them were, I think, started as part of Chromium
+because we had those needs. But they, themselves, are free software components
+that anyone else can also use to build a Chromium. And the reason why that's
+done that way - like, why doesn't - it's actually a really good question. Why
+doesn't Chrome, which is a Google project, use all of this amazing
+infrastructure for engineering that Google has? And the answer is, we want the
+Chromium project to be possible to work on for people who don't work at Google.
+And so we can't say, oh, hey, whenever you're going to make a change, you have
+to commit it into Google's internal source control system. That wouldn't work
+at all. So we're almost - because we want to be an open-source project, and
+because we want to have contributors from outside of Google, we end up almost
+pushed into using this pretty open free software stack, which I - to be honest,
+from my perspective, has a lot of other benefits. When we have new folks
+joining the team, we can actually offer them tools they're already pretty
+familiar with. They don't have the feeling that new Googlers sometimes get,
+where they're totally disoriented. Like, everything they know about programming
+doesn't apply anymore. We actually be like, hey, here's Git. You know how to
+use this. Here's Gerrit, which is another piece of open-source software that we
+use. They may not have used Gerrit before, but a lot of projects do. And so
+they might have run into it previously. So it has pluses and minuses,
+definitely. So that's a big difference. There's also a bit of what I would say
+is a cultural difference more than anything else because most Google projects
+that are not open source - so I'm not talking about things like Android or Go
+or something like that - but projects that are really just not open source,
+like Search, their ecosystem of discussion and culture and stuff is very much
+inside Google. Whereas for Chromium, we constantly are getting ideas and
+suggestions and code changes and stuff from outside of Google. And so we also
+tend to have perspectives from outside of Google in our discussions more often
+as we work on Chromium. So part of that is at the level of, if we're going to
+make a change, we would have maybe input coming in on that change from Mozilla
+even. They're a group we collaborate with a ton on web standards. And so we
+would have their perspective in the discussion. Whereas if we were working
+entirely within Google, we might not have those external perspectives. So
+culture-wise, I feel like Chromium has more perspectives in the room sometimes
+when we're thinking about stuff.
+
+16:26 SHARON: That makes sense because browsers exist across other companies
+too, and there's a lot of compatibility and standards and stuff. So just in
+that nature of things, you have to have a lot more of this collaboration. If
+you make a change, it'll affect all of the embedders maybe, and then you have
+to think about this. And, yeah, there's a lot more discussion - [INTERPOSING
+VOICES]
+
+16:42 ELLY: Yeah, absolutely.
+
+16:42 SHARON: If you're Search, you're like, OK, we're going to, I don't know,
+do our thing.
+
+16:47 ELLY: Yeah, you have more - I don't know if "autonomy" is the right word.
+But, yeah. I want to caveat this by saying I'm not on Search. And so maybe it's
+totally different. But that's how it looks to me as a person who works on
+Chrome.
+
+16:59 SHARON: Yeah. Yeah. And I think in terms of actual development and making
+code changes and stuff, I think probably the biggest difference is that because
+anyone can download the source repository and make changes and all that, the
+actual programming and changes you do, you do those on a computer. Maybe that's
+a machine you SSH into or a cloud top or whatever. But you have to actually
+download all of the code. Whereas with all of the google3 stuff, everything
+happens in a cloud somewhere. So everything is all connected, and you just do
+things through the browser pretty much.
+
+17:29 ELLY: That's very true. Actually, there's another important facet that
+just occurred to me, which is, because Chromium is open source - and in
+particular, some open-source projects will use this model where they send out a
+release every so often. So they'll be like, we're shipping a new major release
+of our program, and here's the source that corresponds to that. So there are
+companies that do that. But we actually do what's called developing in the
+open. So our main Git repository that stores our source is public. Which means
+that as soon as you put in a commit, or even if you just put it up for code
+review, that's public. Everyone on the internet can see what we're doing live,
+which is really pretty interesting in terms of its effects on you. So for
+example, if you're in - you're working inside google3, and you're like, I have
+this really cool, wild idea, I'm going to go and make an experimental branch
+and just make a prototype of it and see what happens, you can just go do that.
+It's not a problem. But if you're working in Chromium, and you go and make your
+wild prototype experimental branch, you have a pretty good chance that
+someone's going to notice that. And then maybe you get a news story that's
+like, hey, Chromium might be adding this amazing feature. And you're like, oh,
+no, that was my wild, experimental idea. I didn't intend for this to happen.
+But now people have really picked up on it, and people outside of the company
+that you've never met are starting to get excited about something that you
+never really intended to build and just wanted to try. So it's a different way
+of working. You're sort of always in the public eye a little bit. And you want
+to be a little bit more considerate about how something might look to people
+way outside of your team and outside of your context. Whereas teams that are
+inside google3 I don't think have to think about that as much.
+
+19:07 SHARON: Yeah, I mean, for me, I've only really worked in Chromium full
+time and all that. And I've just gotten used to the fact that all of my code
+changes are fully public and anyone can look at them. Whereas I think people
+who work in anything that's not like that - people in the company you work, I
+can see it. But not just anyone out there. So I don't know. I've gotten used to
+it, but I think it's not a typical thing to [INAUDIBLE].
+
+19:30 ELLY: Oh, yeah. Absolutely. And in fact, this is something that folks who
+are transferring into Chrome from other parts of Google sometimes have a little
+difficulty with, is if you're used to writing a commit message where maybe the
+only description in the commit message is go/doc about my project, for Chromium
+that doesn't fly because only Googlers can actually follow those links. And so
+the commit message to a non-Googler doesn't say anything. And so you actually
+have to start thinking, how am I going to explain this whole thing I'm doing to
+a non - to a person who doesn't have any of this Google-specific context about
+what it is. You go through this little mental - you cross this little mental
+bridge where you actually are forced to reframe your own work away from, what
+are Google's business goals, and towards, how does this fit Chromium, the
+open-source project, that other people also use? It's interesting and
+occasionally a little frustrating, but interesting and usually really
+beneficial.
+
+20:26 SHARON: Yeah, for sure. And I think from people I've talked to, it just
+seems like another, briefly, difference between internal Google stuff and
+Chromium is that internal Google just has a ton of tools you can use.
+
+20:37 ELLY: Yes, absolutely.
+
+20:37 SHARON: Which both means a lot of things that are maybe a bit challenging
+in Chromium are probably easier, but also maybe finding the right tool is hard.
+But -
+
+20:42 ELLY: Oh, yeah. That is very much the case. I have only limited
+experience working inside google3. But I definitely have experienced the
+profusion of tools and also the fact that the tools are just honestly amazing.
+And it makes total sense. Google has many, many engineers whose whole job is to
+build great tools. And Chromium is just not that big of a project. We just
+don't have that many folks that are working on it. The folks who do build
+infrastructure work for Chromium do amazing work, but there's not hundreds of
+them. And so it's not on the same level.
+
+21:12 SHARON: Yeah. And what you said earlier makes me have - gives me - has -
+makes me wonder - and this ties us into the next thing - of other open-source
+projects, they just do a release, and they don't maybe do development in the
+open. And having not actually worked on other open-source projects really, I
+kind of assumed that this development in the open was the norm. So how common
+do you think or you know that that practice is?
+
+21:45 ELLY: Gosh, I would really be guessing, to be honest with you. But I
+would say the development in the open is by far the norm these days. And when
+you see projects that follow the big release model instead, the way that looks
+is they'll be like, hey, version 15 is out, and here's the source for
+version 15. You can look at it. But the development, as it happens, happens
+internally. I would tend to associate that with being maybe big company
+projects that have a lot of confidentiality concerns. So for example, if you're
+building the software that goes with some cool, new hardware for your company,
+you don't want to start checking that software into Git publicly because then
+people are going to read it and be like, ooh, this has support for a
+billion-megapixel camera. That must be coming in the new thing. And so I think
+that the big release model might be, these days, more prevalent when people are
+doing hardware integrations, where there's other components that are shipping
+at a fixed time and you don't want your source to be open until that point. But
+honestly, the developing in the open model is, I think, much more common these
+days. Historically, back in the '70s and '80s, when you would buy an operating
+system and it would come with source, that was just a thing that you got as
+part of the package, then it was much more of the source is released with the
+OS model. Whereas these days, because distributed development is so easy with
+modern version control systems, it's just so common to just develop in the open
+like we do.
+
+23:11 SHARON: Oh, cool. I didn't know that. So compared to other open-source
+projects, what are some similarities and differences that Chromium has to
+others that you may be familiar with?
+
+23:25 ELLY: Ooh. All the ones I'm familiar with are quite a bit smaller than
+Chromium. And so it's going to be hard to talk about it because, frankly -
+
+23:32 SHARON: That's probably the common difference, though, right? Probably
+very few are as big as Chromium.
+
+23:32 ELLY: Oh, yeah. So in particular, one of the hardest problems in open
+source - in running an open-source project is managing how humans relate to
+other humans. The code problems are often relatively easy. The problems of how
+do we make decisions about the direction of a project that maybe has a hundred
+contributors who speak 10 different languages across a dozen time zones, that's
+a hard problem. And so I often talk about the idea between open source, open
+development, and then open governance. And so open source is just, like, you
+can see the source. Open development is you can see the development process. So
+the Git repo is open. The bug tracker is open. The mailing lists, where we do a
+lot of our discussion, are open. So we do open development. But then you have
+this next step of open governance, where the big decisions about where the
+project is going are made in the open. And for Chromium, some of those are made
+in the open, especially when it's really about the web platform or that kind of
+thing. But some of them are not. For example, if we're deciding that we're
+going to do some cool new UI design, that design and the initial development of
+it might not necessarily be - or sorry, the development would be done in the
+open, but the designing of it might not. That might be a discussion between a
+few UX designers who all work at Google in a Google internal place. And so
+Chromium has a bit of open governance but not all the way. A lot of smaller
+projects have super open governance. So they'll literally be like, hey, should
+we rewrite this entire thing in Rust? And they'll make that decision by arguing
+about it on a mailing list, where everyone can see. And that's totally, totally
+fine. Because Chromium is so big, we can't make those kinds of decisions by
+having every Chromium engineer have their opinion and just post. It would be
+complete chaos. And because we're big and prominent, a lot of the work that we
+do is very much in the public eye. And so even discussions that are maybe
+relatively speculative - like that example I gave before, where you have an
+idea and you're like, wouldn't it be neat if we did this? It's easy for that to
+turn into people inferring what Google's intentions are with Title Case, like,
+Big Important Thing, and turning that into a lot when you would not have
+intended it to be that way. And so we do end up keeping our governance
+relatively on the closed side compared to other open-source projects I've
+worked on. Other than that, in terms of engineering practices and what we do to
+get the code written, we uphold a super high standard of quality. And in
+particular - which is not to say that most open-source projects don't, because
+they totally do. But Chromium, in my opinion, is really, really thoughtful
+about not just, hey, how should code review work, but really evolving stuff
+like, how should we bring new developers into this project? What should that
+feel like? Those are discussions that we have. And I often feel like those are
+discussions that other open-source projects don't talk about as much. What else
+is different for us? I'm not sure. I think that those are some of the big ones.
+The differences in scale are such that it's almost hard to talk about. The
+difference between an open-source project that maybe has 5 contributors and one
+that has 500 is very, very large.
+
+27:07 SHARON: With the open governance thing you mentioned, something that that
+made me think of is maybe Blink Intents, where you submit a thing to a list and
+then that gets discussed. So that's part of the Chromium project, I think,
+right? That falls under that category.
+
+27:20 ELLY: Yep. Yep.
+
+27:20 SHARON: And so that's where, if you want to make a change to Blink, the
+rendering engine, you do this process of posting it to a list, and then people
+weigh in.
+
+27:25 ELLY: Yeah, absolutely. So Blink really does do open governance in a way
+that I, honestly, very much admire. Blink and the W3C and a lot of these groups
+that are setting standards for the internet do do open governance. Because,
+frankly, it's the only way for them to work. It would not be good or healthy
+for the web if it was just like, we're going to do whatever - whatever we,
+Google, have decided to do and good luck everyone else. That would be very bad.
+So yeah, Blink definitely does do open governance. But when it gets to things
+that are more part of the browsers' behavior and features, we tend to have the
+governance a little more closed.
+
+28:08 SHARON: Right. And I think an example of Blink being more open governance
+is the fact that BlinkOn is open to anyone to participate to. And that's the
+channel that we're posting this on right now. It just happened to make sense
+that I figured most of the audience who is watching Blink [INAUDIBLE] already
+are interested in these, too. So that's why - [INTERPOSING VOICES]
+
+28:27 ELLY: Yeah, absolutely.
+
+28:27 SHARON: And for people who may not have - may have found these videos
+that don't know about BlinkOn. That's what that is.
+
+28:34 ELLY: Yeah. And just in that vein of open governance for Blink,
+especially, there's also this idea of being a standard and then having things
+be compatible with it. So the web platform is a collection of standards. And
+other browsers have to implement those standards, too. And so for example, if
+we make up a standard that is very difficult or impossible for, like, Firefox
+to implement, that's not good. That's fragmenting the web platform. That's a
+bad thing. Whereas the Chromium UI, like how the omnibox works in Chromium, for
+example, isn't a standard. It doesn't matter whether Firefox or Edge or Opera
+or whoever have the same omnibox behavior as us, right? And so there's much
+less of a need to all agree. And instead, it's almost a little bit better to
+have some variety there so that users can get a little bit more of a choice and
+that collectively more things get tried in that vein. So there's places where
+agreement and standardization are really important. And then there's places
+where it's actually OK for each individual browser to go off on its own a bit
+and be like, hey, we thought of this cool, new way to do bookmarks. And so we
+have built this. And it doesn't matter whether the other browsers agree about
+it because bookmarks are not a thing that interoperates between browsers.
+
+29:44 SHARON: Yeah, that makes sense. So now let's talk about some of the
+actual details of what it's like to work on Chromium and make changes, write
+code, and new ideas. So I think you mentioned a few things, like bug tracking.
+That's all public, in the open, apart from, of course, security-sensitive
+things and other [INAUDIBLE] are hidden. What else is there? Code review - that
+was Gerrit. You mentioned that. So You can see all the comments that everyone
+leaves on everyone's changes.
+
+30:16 ELLY: Oh, Yeah. And for better or for worse, by the way. It's good to
+bear in mind that if you're like - you're going to type like a slightly jerk
+message to someone on a code review, that's going to be preserved for all time,
+and everyone's going to be able to see it.
+
+30:29 SHARON: Yeah. Yeah. Be nice to people. [CHUCKLES] Version control -
+that's Git. Probably people will know about that. Something that might be worth
+mentioning is that a lot of people who contribute to Chromium, and if you look
+at things like Gerrit and Chromium Code Search - that's also public, of
+course - looks a lot like Google internal code search, but obviously it's open
+source. So a lot of people have @chromium.org emails.
+
+31:00 ELLY: Yes.
+
+31:00 SHARON: So why are there separate emails? Because you can use at a
+google.com or a GMail or any email. So why have this @chromium.org email thing?
+
+31:05 ELLY: Yeah, so there's a few different reasons for that. So chromium.org
+emails are available to members of the project, which is a little bit
+nebulously defined, but it's definitely not just Googlers. And so there's a
+couple reasons why people like having those. So for some folks, it's sort of a
+signal that you are acting as a member of the open-source project rather than
+acting with your Google hat on, if you like. And so for example, I help run the
+community moderation team for Chromium. And so when I'm doing work for that
+team, I'm very careful to use my chromium.org account because I want it to be
+clear that I'm enforcing the Chromium community guidelines, which are something
+that was agreed upon by a whole bunch of Chromium members, not just Googlers.
+And so I'm not enforcing Google's code of conduct. I'm enforcing Chromium's
+code of conduct in my role as a Chromium project person. So sometimes you
+deliberately put on your Chromium hat so that you can make it clear that you
+are acting on behalf of their project. Some folks - and I'm also one of these
+folks, by the way - just happen to really be big fans and supporters of free
+software and of open source. And so if I have the choice between wearing my
+corporate identity and wearing my open-source project member identity, I might
+just wear my open-source project member identity and decide to actually
+contribute that way. And so a lot of the folks who've been on Chromium - or
+have been on Chrome, I should say, for a while, that's part of their reasoning.
+They joined because they were excited to work on something that was open. And
+so they have this open-source identity, this Chromium identity, that they use
+for that. There's a third factor, and this touches on one of the sometimes less
+pleasant parts of working in open source, which is our commit log and our bug
+tracker and all of that stuff are public. And what that means is that everyone
+on the internet can go see them. And that is often great, but it's occasionally
+not great. So for example, if you go and make an unpopular UI change, people on
+the internet know that that was you. And that might not be something that
+you're necessarily super ready to deal with. So for example, way, way, way, way
+early in my career, I made a change to Chromium OS because I was working - I
+was on the Chrome OS team as a brand Noogler. So this is I've been at Google
+maybe five or six months. I made a change to Chrome OS. Somebody happened to
+notice it and take issue with it. I don't even remember what the change was or
+the issue. But they happened to notice it and take issue with it. They showed
+up in our IRC channel, because we used IRC at the time, which was also public
+because the whole project was very open like that, and really just started
+yelling at me personally about it. And I'm like, this is not a cool experience.
+This is something that if this was a Google coworker of mine, I would be
+talking to HR about this. But it's actually just a random person on the
+internet. And so there are some folks who use their Chromium username as a
+little bit of a layer of insulation almost, where it's like, I want to work on
+this project, but I don't - maybe my Google username has my full name in it. I
+don't necessarily want every change I make to be done like that. And so if you
+don't do that, you can end up in a situation where you make a change, and then
+it's really attributed to you as though it was your personal idea and you did
+this bad thing. And that's not a risk that everyone wants to take as part of
+doing their work. And so sometimes people have a chromium.org account really
+because they want an identity that's separate from their Google account - that
+has a different name on it, that has different stuff like that. And so one of
+the things that I'm always cautious to remind folks of on my team is, if you're
+working with someone who has a chromium.org account, always use that
+chromium.org account when you're speaking in public, always, always, always,
+because you don't want to break that veil if someone is relying on it.
+
+35:09 SHARON: Right. Yeah, that makes sense. And I think, in general, whenever
+you are signing up for interacting in these public spaces, generally, I think
+it's encouraged to use your chromium.org account. So for example, Slack, which
+is the modern - current IRC often -
+
+35:27 ELLY: It hurts my soul to hear you say that.
+
+35:32 SHARON: Well - [LAUGHS]
+
+35:32 ELLY: I'm a die-hard IRC user. I've been using IRC for 30 years. And I
+was one of the few people who was I think very sad when we decided to move off
+IRC. But you're right, that it is the modern IRC option.
+
+35:44 SHARON: I think a lot of people are very die hard about IRC. So, you
+know, but modern or not, that's what's currently being used.
+
+35:49 ELLY: Absolutely.
+
+35:55 SHARON: So Slack is where anyone can join and discuss Chromium stuff. And
+generally, that kind of thing, you're encouraged to use your chromium.org
+account.
+
+36:01 ELLY: Yeah, absolutely. And to be fair to Slack also, the Slack has
+probably 30 times as many people in it as the IRC channel ever did. So I think
+that it's pretty clear that Slack is more popular than IRC was. But, yeah, no,
+we use our Chromium identities a lot, really, really on purpose. And to be
+honest, I would like it if we use them even more. Sometimes you will see folks
+who actually have both identities signed up. So they'll have their google.com
+and their Chromium, and that's always confusing for everyone. So if it was up
+to me, I would say everyone has a Chromium identity, and they'd just all use it
+when they're contributing.
+
+36:39 SHARON: Yeah, that's definitely one of these unique two Chromium
+[INAUDIBLE] pain points of someone [INAUDIBLE] use their maybe - often, they're
+the same for most people. But sometimes they're different. Sometimes they're
+very subtly different, and it's -
+
+36:53 ELLY: Absolutely.
+
+36:53 SHARON: you end up sending your [INAUDIBLE]...
+
+36:53 ELLY: I also - I have met a couple folks who the Google username they
+really wanted wasn't available, but it was available for chromium.org. And so
+they picked a shorter, cooler username for chromium.org, which is totally -
+totally fine to do. But then, every time you have to remember, oh, I know them
+by this longer Google username, but actually they use this shorter username for
+Chromium.
+
+37:13 SHARON: Yeah, you have to remember their real life name. You have to
+remember their work email. And then now you have to remember another work
+email.
+
+37:19 ELLY: Well, we have software that can help with that a bit.
+
+37:25 SHARON: Yeah, for sure. So as part of that, and that's, in a way - a
+thing that to me feels very related is there's a thing called being a committer
+in Chromium. So what does it mean to be a committer? And what does it entail?
+
+37:37 ELLY: Yeah, so committers are basically people who are trusted to commit
+to CLs, for want of a better way of putting it. So the way the project is
+structured, anyone can upload a CL. And anyone anywhere on the internet can
+upload a CL. It has to be reviewed by the OWNERS of the directories that it
+touches or whatever. But there are some files that are actually, like, OWNERS
+equals star. So for example, the build file in Chrome browser, because
+everybody needs to edit it all the time, it just has OWNERS equal star. And
+there's a comment that's like, hey, if you're making a huge change, ask one of
+these people. But otherwise, you're just freely allowed to edit it. And so if
+the committer system didn't exist, anyone on the internet would be allowed to
+edit a bunch of parts of the project without any review, which is pretty bad.
+And so there's this extra little speed bump where it's like, you have to send
+in a few CLs to show that you're really a legit person who's contributing to
+the project. And once you've done that, you get this committer status, which
+actually allows you to push the button that makes Gerrit commit your change
+into the tree. And that's what it does mechanically. We culturally tend to have
+it mean something a little different than that, but it's - culturally, it's
+like a sign of trust of the other project members in you. So getting that
+committer status really means, we collectively trust you to not totally screw
+things up. That's what it is. And so you have to be a committer to actually be
+in an OWNERS file, for example. You can't be listed as an owner until you're a
+committer. Because if you're not a committer yet, we're not really - if we're
+not trusting you to commit code, we're not really going to trust you to review
+other people's code. And, yeah, when you're new joining the project, it's
+actually a pretty big milestone to become a committer. You become a committer
+after you've been working for anywhere from three to six months, I would say.
+And it's definitely this moment of being like, yeah, I've really arrived. I'm
+no longer new on the project. I'm now a full committer.
+
+39:51 SHARON: Can you briefly tell us what the steps, mechanically, to becoming
+a committer are?
+
+39:51 ELLY: Yeah, so you need to have landed enough CLs to convince people you
+know what you're doing. And there is no hard and fast limit, but it's like - it
+should be convincing. And so I often hear maybe 15 to 20 of nontrivial CLs is a
+pretty good number. Having done that, you need someone to propose you or
+nominate you for committership. So there's actually - there's a mailing list
+for having these discussions. And so whoever's going to nominate you, who has
+to already be a committer, they'll send mail to that list, basically being
+like, I would like to nominate this person for committer. There's a comment
+period during which people can reply. And then if there's nobody who is raising
+a big objection to you being a committer, after - I don't know what the actual
+time period is - but after some amount of time, the motion carries with no
+objections, and then your Chromium account becomes a committer. I think Google
+accounts can also be committers as well, but I've only ever done this process
+for Chromium accounts. And so those threads - what's going on in those threads
+is mostly people endorsing the request. So let's say that I have someone who's
+new on my team who I want to propose as a committer. I'll start the thread
+nominating them as a committer, and then I'll go and talk to maybe two or three
+of the people who have reviewed a lot of their changes, and I'll be like, hey,
+would you endorse this person for a committer? If so, please post in this
+thread. And so in the thread, there will actually be a couple of replies that
+are like, plus 1, or, yes, this seems like a good fit. Very rarely, there might
+be a reply, which is like, hey, I saw some - I saw some stuff on this CL that
+shows that maybe this person isn't quite ready. We had a whole bunch of back
+and forth comments, and eventually it really didn't seem like they understood
+what I was asking for. And I feel like they're not really ready yet. Sometimes
+that will happen. But usually the threads - by the time someone's nominating
+you, you're already in good shape. So that's the mechanical process. And then
+there is - it might actually just be Eric, individually, who goes through and
+flips the bits on people being committers based on the threads. I'm not sure.
+But there's some process by which those threads turn into people being
+committers.
+
+42:14 SHARON: OK, cool. Is there an analog of this either internally at Google
+or in other open-source projects? Because internally at Google, there's the
+concept of readability, which means you are vouched for that you know how to
+code in this one language, which has some similarities. That's maybe a similar
+thing. Are there any similar notions in other projects you've seen?
+
+42:38 ELLY: Yeah, so many projects have this notion of being a member. And that
+often combines our notions of committer and sometimes code owner. And so they
+might - or for some open-source projects, you'll actually hear "maintainer" as
+the thing. And so they'll be like, only people who are project members can
+upload changes in the first place. And only people who are maintainers can
+merge those changes. So that little speedbump on entry is pretty common.
+Because it's a fact of life that if you are on the public internet and you have
+no barriers to entry, you're going to have spam in your community no matter
+what you do. And so that kind of split is super, super common. For some
+projects that don't do open development, the entire thing might happen inside a
+company or inside an organization anyway. And then there is no notion of
+committer status because you're just hired onto that team and then you can
+commit. But for projects that do open development and free software projects,
+there is often a sense of, these are the people who are roughly trusted to land
+code. And for a lot of projects, especially bigger ones, there's actually a
+two-tiered model, where maybe you have people who are domain experts on a
+specific thing, like, they maintain some subsystem. And they're trusted to make
+whatever changes they need or approve other people's changes in that area. But
+then at the wider scale, there's what's often called a steering committee or a
+core group or something. And those groups have authority over the whole project
+and the direction of everything that's going on. And so you'll often see that
+kind of model in larger projects. At smaller scales, it's often literally a
+list of one to five people who all have commit access to the same Git repo, and
+there's no - no structure on top of that. But for bigger projects, governance
+becomes a real concern. And so people start thinking about that.
+
+44:35 SHARON: All right. Now, let's switch topics to talking about the more
+day-to-day logistics of working on Chromium. So if you're not a Googler, don't
+work at Google, to what extent can you effectively contribute to Chromium, the
+project?
+
+44:48 ELLY: Yeah, so that depends where you're coming from, both whether you're
+part of another large organization, like maybe you work at Microsoft, you work
+at Opera, Vivaldi, one of those companies, or if you're really an IC lone
+contributor. If you're in a large organization, probably your org will have its
+own structure around how you should contribute anyway. And so you might just
+want to talk about that. So I'll really focus on the individual contributor
+angle. And so for engineers specifically, like if you're a programmer who wants
+to contribute to the code base, that's awesome. The best approach I think is
+really to find an area that you're passionate about because it's so much more
+fun and enjoyable to contribute when you're doing something you care about. So
+find an area you care about. Get in touch with the team that works on that
+area, either through their mailing lists or find their component in Monorail or
+find them in the OWNERS files or whatever. Get in touch with those folks. Ask
+them what are good places for you to contribute as a new person. That's often a
+really great way to get started. And you'll have a person you can go to for
+advice to be like, hey, how do I go about doing this thing? My experience has
+been that Chromium contributors are pretty much all super helpful. And so
+they're very willing to just give you guidance or do whatever. And you'll then
+know who to send your code reviews to.
+
+46:01 SHARON: Cool. Yeah. And if you're not an engineer, what are some ways you
+can also contribute?
+
+46:06 ELLY: Yeah, so there's a whole bunch of these. And by the way, these all
+apply to basically every open-source project, so not just Chromium
+specifically. So open-source projects, if you are a good writer, if you enjoy
+doing technical writing or you enjoy doing UX writing or you want to do that
+kind of thing, almost every open-source project out there is looking for people
+to contribute documentation. And Chromium is no exception at all to that. So
+high-quality documentation, we love that stuff. Or even if you're just honing
+that craft and you want to practice, Chromium is not a bad spot to do that. If
+you're a UX designer or a visual designer, a lot of open-source projects will
+actually appreciate your contributions of you bringing in, like, hey, I thought
+of a way that this user experience could feel or how the screen could look or
+something like that. They'll often appreciate that kind of input or design
+work. If you are someone who speaks multiple languages, translations are
+another great way to contribute to open-source projects. A lot of open-source
+projects don't have access to the same kind of - Chromium has access to a
+translation team within Google who do a lot of our translations. A lot of
+open-source projects don't have that. And so contributing translations of
+documentation, of user-facing interface, stuff like that, can be super
+valuable. And the last thing I'll say, which can be done by really anyone - you
+don't even need special skills for this one - is try early releases of stuff.
+So try development branches. If you're a Chrome user, try running Beta or Dev
+or Canary. And then when something doesn't feel right or when it's - when it
+doesn't work for you or it crashes or whatever, file bugs. And try to get
+practiced at filing good bugs, with details and info and steps to reproduce the
+bug and stuff like that. That's such a huge help as a developer of any
+open-source projects - to get that early-user feedback and be able to correct
+problems before they make it to the stable channel. And on Chromium, I've run
+into a few folks who just - their main contribution to the project is really
+just that they file great bugs all the time. There's a few folks who all they
+really do is they run Canary on Mac, and they notice when something doesn't
+feel quite right. And so they file stuff that's like, maybe the engineering
+team wouldn't necessarily have noticed it. But when someone calls it out, we're
+like, oh, that actually does feel kind of janky, and now we can go fix that.
+And getting that feedback early is so, so valuable. So there's a lot of
+different ways. Those are some, but there's plenty more, too.
+
+48:21 SHARON: OK. Cool. Yeah, and a few things on that. If you want to really
+try out random things, you can go to chrome://flags, play around there, see
+what happens. In terms of going back a bit for being an engineer, there's other
+web-adjacent stuff that you can do that we won't get into too much now. But
+that can be things like adding web platform test, web standard stuff. And for
+people who are into security, we have a VRP, Vulnerability Rewards Program. But
+if you know about that, probably you're into the whole security space. This is
+not how you're going to - maybe this is how you heard about it, and you want to
+get into it. But, anyway -
+
+48:59 ELLY: Yeah. I will say, if you're a security researcher and you aren't
+familiar with the Chromium VRP, you should go take a look because it's -
+Chromium is a really interesting project to audit for security. And the VRP can
+make it very worth your while to do so if you find good bugs.
+
+49:12 SHARON: Mm-hmm. Yeah. And going back a bit earlier to being an engineer,
+like an IC, who is not at Google or any of these other big companies, there are
+other barriers to entry to being a contributor, right?
+
+49:28 ELLY: Oh, yeah.
+
+49:28 SHARON: So I definitely encountered this after my internship. I worked on
+Chrome. I was like, hey, I know what's going on now at the end of it. A couple
+things we didn't finish. I'll go home, and I will keep working on this - good
+intentions. And I got home, got my laptop, which was a pretty good laptop, but
+still a laptop. I downloaded Chrome. That took a very long time. I built it for
+the first time, which always takes a bit longer. But that took so long. And
+even the incremental builds just took so long that I was like, OK, this is not
+happening. I'm in school right now. I've got other things to worry about. So
+how feasible is it for a typical person, let's say, to actually make changes in
+Chromium?
+
+50:05 ELLY: Yeah, that is unfortunately probably the biggest barrier to entry
+for individuals who want to make technical contributions. Obviously, it doesn't
+affect you if you're contributing documentation translations, whatever. But if
+you're trying to modify the code, yeah, the initial build is going to be very
+slow, and then the incremental builds are going to be very slow. And a lot of
+the ancillary tasks are slow too, like running the test suite or running stuff
+in a debugger. The project is just very big. And that's something that I think
+a lot of folks on the Chromium team wish we could reduce. But Chromium is big
+because the web is big and because what people want it to do is big. And so
+it's not just big for no reason. But it does make it harder to get started as a
+contributor. I've had this experience, too. I have a modern laptop sitting on
+the desk over there. And it takes seven to eight hours to do a clean Chromium
+build on that. Whereas on my work workstation, which has access to Goma,
+Google's compile farm, it takes a few minutes. And the large organizations that
+contribute also all have compile farms for the same reason. It's just so slow
+to work when you're only doing local building and don't have access to a ton of
+compilation power.
+
+51:12 SHARON: Mm-hmm. Yeah. I wonder if we could, I don't know, do a thing for
+people who are individuals who contribute more. Probably that would be really
+hard to do. Probably people have thought about it. But, yeah.
+
+51:24 ELLY: It would be nice if we could. I don't know what the challenges
+would be offhand, but it would be very cool if we could somehow make that
+available.
+
+51:30 SHARON: All right. That all sounds very cool. I know I learned a lot.
+Hopefully some of you learned a lot, too. I think if you are working within
+Google, it's really easy to not really interact with any of this more
+open-source stuff, depending on which part you work on. Maybe you work on a
+part that's very Google Chrome specific. I know before I was working on
+Fuchsia, so that was before Launch. So that was not really something we were
+open to the public about anyway. And a lot of even the typical Chrome tools I
+was unfamiliar with. So I think depending on which part you work on, this
+stuff - it's all there, but you might not have had a chance to interact with.
+So thank you, Elly, for telling us about it and giving us some context about
+free and open-source software in general.
+
+52:08 ELLY: Yeah, of course.
+
+52:08 SHARON: Is there anything you would like to give a shout out? Normally,
+we shout out a specific Slack channel. I think in this case, the Slack in
+general is the shout out. Anything else?
+
+52:20 ELLY: The Slack, in general, definitely deserves it. Honestly, I'm going
+to go a little bit larger scale here. I'm going to shout out all of the folks
+who have contributed to Chromium, both at Google and elsewhere. It is the work
+of many hands. And it would not be what it is without the contributions from
+the folks at Google, the folks at Microsoft, folks at Yandex, folks at Naver.
+All of these different browsers and projects and all of the different
+individuals that have contributed, like everyone in the AUTHORS file - so shout
+out to all of those folks. And also, I really want to shout out the open-source
+projects not even part of Chromium that we use and rely on every day. So for
+example, we use LLVM, which is a separate open-source project for our
+compilation toolchain. And I think I would not be exaggerating to say that
+Chromium couldn't exist in its current form without the efforts of a bunch of
+other open-source projects that we're making use of. And so I'm really hopeful
+and optimistic that Chromium can live up to that. We're standing on the
+shoulders of a lot of other open-source projects to build the thing that we've
+built. And I'm hopeful that, in turn, other projects are going to stand on our
+shoulders to build yet cooler stuff and yet - yet better programs and build a
+yet better open-source community. So shout out to all of the authors of all the
+open-source software that Chromium uses, which is a lot of people. But they
+deserve it.
+
+53:37 SHARON: Yeah, for sure. It's very cool how it's very - all very related.
+And even within Chrome, I think people stick around longer than typical other
+projects. And it's cool to see people around, like a decent number of them,
+from before Chrome launched. And that's probably [INAUDIBLE] to a generally
+more positive engineering culture. So that's very good.
+
+53:58 ELLY: I think so. But I'm biased, of course.
+
+53:58 SHARON: Yeah, maybe. [LAUGHS] Cool. You mentioned mailing lists a bunch.
+Any favorites that you have?
+
+54:08 ELLY: Oh, yeah. chromium-dev is the mailing list of my heart, I would
+say. It's the main open-source development mailing list for us. It's a great
+place for all of your newbie questions. If you're just like, how the heck do I
+even check out the source, that's a good place to ask. The topic-specific
+mailing lists, especially net-dev and security-dev, are really good if you have
+questions in those specific areas. But honestly, all of the mailing lists on
+chromium.org are good. I haven't yet encountered one where I'm like, that
+mailing list is bad. So check them all out.
+
+54:33 SHARON: Cool. All right. Check out every single mailing list. Sounds
+good.
+
+54:38 ELLY: Yeah, every mailing list, every Slack channel.
+
+54:38 SHARON: All right. Great.
+
+54:38 ELLY: You're all good.
+
+54:38 SHARON: Every Slack channel, I think - yeah, I'll add myself to the rest
+of them. All right. Well, thank you very much, Elly.
+
+54:45 ELLY: Of course.
+
+54:45 SHARON: Thank you for chatting with us. And see you all next time.
+
+54:51 ELLY: All right. Thank you, Sharon. Easter egg - in the second part of
+this video, Elly is drinking soda.
diff --git a/docs/transcripts/wuwt-e07-mojo.md b/docs/transcripts/wuwt-e07-mojo.md
new file mode 100644
index 0000000..f2964426
--- /dev/null
+++ b/docs/transcripts/wuwt-e07-mojo.md
@@ -0,0 +1,1057 @@
+# What’s Up With Mojo
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 7, a 2023 video discussion between [Sharon (yangsharon@chromium.org)
+and Daniel (dcheng@chromium.org)](https://www.youtube.com/watch?v=zOr64ee7FV4).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Due to technical issues, timestamps were not available for this episode. The
+transcript below uses 00:00 placeholders instead.
+
+---
+
+
+Mojo is used to communicate between processes. How does that happen? What can
+go wrong? Is mojo the same as mojom? Today’s special guest telling us all about
+it is Daniel. Daniel is an IPC reviewer and has written much of the guidance
+and documentation around it. He’s also worked on cross-process synchronization,
+navigation and hardening measures to mitigate security risks.
+
+Notes:
+- https://docs.google.com/document/d/15VD6WT-R3MN93gUmPAR_BXee5s0BfYL823Qtj9EHP9A/edit
+
+Links:
+- [Mojo - Chrome’s inter-process communication system](https://www.youtube.com/watch?v=o-nR7enXzII)
+- [IPC 101](https://www.youtube.com/watch?v=ZdB5P88-w8s)
+- [Life of a Navigation](https://www.youtube.com/watch?v=OFIvyc1y1ws)
+- [Long IPC review doc](https://docs.google.com/document/d/1Kw4aTuISF7csHnjOpDJGc7JYIjlvOAKRprCTBVWw_E4/edit)
+- [Mojo overview](https://chromium.googlesource.com/chromium/src/+/HEAD/mojo/README.md)
+- [Intro to Mojo](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/mojo_and_services.md)
+- [Mojo Style Guide](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/mojo.md)
+
+---
+
+00:00 SHARON: Hello. And welcome to "What's Up with That," the series that
+demystifies all things Chrome. I'm your host Sharon. And today, we're talking
+about Mojo. How do we communicate between processes? What can go wrong? What is
+mojom? Today's special guest to answer all of that and more is Daniel. You know
+him from the unparalleled volume of code reviews he does, including IPC Review.
+For which, he wrote the documentation and guidelines. And in addition, he has
+worked on navigation, cross-process synchronization, and hardening measures to
+help mitigate security bugs. So hello, Daniel. Welcome to the program.
+
+00:00 DANIEL: Thank you.
+
+00:00 SHARON: Thank you for being here. First question, what is Mojo?
+
+00:00 DANIEL: Mojo is basically Chrome's IPC system for talking between
+processes.
+
+00:00 SHARON: All right, that sounds pretty good. That sounds like what we're
+here to talk about. So today, we're going to cover some questions around Mojo.
+There are a couple of Chrome University talks and some documentation that are
+really good to explain the basics of how Mojo works. So those will be linked
+below. Check those out too. Today are questions you might have, if you've
+watched those videos, maybe some followup questions that you might have. So you
+mentioned IPC. Does that include RPC? Or is it just Inter Process
+Communication?
+
+00:00 DANIEL: So personally, I kind of think of them as the same thing. But I
+guess RPC is probably more general. Because it could include calls over the
+network, right? Mojo doesn't go over the network today.
+
+00:00 SHARON: OK. So it mostly is between the processes we have in Chrome.
+
+00:00 DANIEL: That's correct. Yeah. You also have things like gRPC, right,
+Google for making network API calls. But yeah, that's not under the scope of
+Mojo.
+
+00:00 SHARON: OK. Cool. Very briefly, we have a thing called Legacy IPC that I
+think is a long-term project in the works to get it removed. Anything briefly
+there?
+
+00:00 DANIEL: Yeah. Legacy IPC is what we used before Mojo. It was based on a
+bunch of clever or horrible hacks, depending how you're looking at it, using C
+preprocessor macros. We still have it around because NaCl and PPAPI actually
+use a CIPC. So eventually, when we don't have NaCl support, we can get rid of
+Legacy IPC altogether hopefully.
+
+00:00 SHARON: Any day now.
+
+00:00 DANIEL: Any day now.
+
+00:00 SHARON: Any day now. OK. So what we'll do now is I think we'll just
+rattle through some definitions because we'll come up with a bunch throughout
+it. And they're words that probably you've heard before but have maybe a
+special meaning in the context of Mojo. So the first of these is Mojo versus
+.mojom. I've seen both of them. What is the difference?
+
+00:00 DANIEL: So I think people kind of use them interchangeably in some
+contexts. But usually, mojom is specifically the file that defines your
+interfaces, structs, and other types that are going over Mojo IPC. Mojo is just
+kind of the general name for this system, right? Mojom is specifically a file
+that defines these kind of types.
+
+00:00 SHARON: OK. That's cool. Next is pipes.
+
+00:00 DANIEL: OK, yeah, so Mojo, basically, all the higher-level stuff that we
+actually use, most of the time, is built on top of this primitive called a
+message pipe. So Mojo message pipe always has two ends. It's actually
+bidirectional. So basically, the idea is you can create a pipe. And then you
+give the endpoints to whoever you want. And those two endpoints can talk to
+each other.
+
+00:00 SHARON: And that seems related to the next one, which is capabilities, in
+terms of passing things around.
+
+00:00 DANIEL: Yeah. So capabilities is kind of a pretty generic term. In Mojo,
+I think we would kind of think of it as using interfaces to grant capabilities
+to processes. So for example, if your renderer has permission to, say, use file
+system stuff, right, we would give it an interface, like a message pipe with an
+interface that's bound to an interface for accessing the file system. Or if it
+can record audio for WebRTC, right, we would give it an interface for recording
+audio, right? But the idea is we wouldn't just have this giant interface with
+all these methods and then have to permission check, at each time like someone
+calls a method, that they have permission, right? We would only give you the
+interface if you have permission. And if you don't have permission, you don't
+have the interface at all. And you can't use the capability.
+
+00:00 SHARON: Can you have multiple capabilities and interfaces per pipe?
+
+00:00 DANIEL: So that probably kind of gets into the associated stuff.
+
+00:00 SHARON: OK. We'll get there. We'll get there. That's coming up. OK. Next
+one on our list of words is bindings.
+
+00:00 DANIEL: Yeah, so I think when most people think of Mojo and using Mojo,
+the bindings layer is probably what they're thinking of. So this is stuff like
+the remotes, receivers, and the glue that actually makes these calls between
+processes. There's a lot of Mojo underneath that backing it all. In fact,
+rockot actually rewrote the entire backend that Mojo is built on top of
+recently to use something called IPCZ for efficiency and other reasons.
+
+00:00 SHARON: OK. He's one of the ones that ones that gave one of those Chrome
+University talks, which is very good. So go check that out. Cool. Moving along,
+we have remotes, one of the things you just mentioned, I think.
+
+00:00 DANIEL: Yeah. So earlier, I mentioned message pipes. Remotes, and
+receivers - they kind of come as a pair - are kind of an abstraction on top of
+message pipes to make it a bit easier to use. Because, with message pipes, it's
+basically you stuff bytes in one end, and you get bytes out the other end,
+right? And no one wants to deal with that. And basically, the idea with remotes
+and receivers, remotes are basically a way of making a Mojo call. A receiver is
+a way of handling a Mojo call. Yeah.
+
+00:00 SHARON: OK. Neat. And then up next, we have pending.
+
+00:00 DANIEL: OK, yeah. So to take a step back to get the broader picture, when
+you use the bindings, you can create a remote. And that always comes with
+another endpoint, right? Because a Mojo message pipe has two endpoints. So you
+always get a remote and a receiver together. Pending is basically the form of
+remotes and receivers that they are in when you can transfer them, right? So
+something has to be pending if you want to, say, send it from one thread to
+another. Because Mojo message pipe endpoints, they're all thread-bound - I
+think sequence-bound, technically. But yeah, so if you want to move things
+between threads or between processes, they have to be in pending form. Pending
+just kind of means it's not handling - it's not reading things off the message
+pipe or trying to send things. You can't use it in that form. You would have to
+turn it from a pending into an actual remote or receiver to use it, right? And
+we have pending forms of both remotes and receivers for type safety.
+
+00:00 SHARON: Right. Can you briefly explain what sequence-bound means?
+
+00:00 DANIEL: Yeah, so I think a few years ago now, we kind of rewrote the task
+scheduling system in Chrome. And the idea was to abstract out some of the ideas
+and make things a bit more flexible, right? Because, otherwise, a lot of people
+in code was just creating threads, even though it didn't always need like a
+dedicated OS thread, right? And so sequences are an abstraction on top of that.
+And a sequence just promises that, when you PostTask to it, it runs tasks in
+that order. But we could have multiple sequences on the same thread. That's
+kind of an implementation detail. That same sequence could potentially even run
+on different threads at times, right? So it's an abstraction. But in theory,
+people shouldn't have to think about it.
+
+00:00 SHARON: Right.
+
+00:00 DANIEL: Not always true, but usually true.
+
+00:00 SHARON: OK, so it's kind of like - in other places, it would be kind of a
+thread. It's the thing you interact with. This is a unit of stuff happening.
+
+00:00 DANIEL: Yeah. It's kind of Chrome's thread basically.
+
+00:00 SHARON: OK. Cool. Another thing you mentioned already, associated.
+
+00:00 DANIEL: Yeah. So the kind of tricky part sometimes with Mojo is message
+ordering is only guaranteed on the same message pipe. So if you have a
+remote-end receiver and you send stuff, it's a guarantee that the receiver will
+get things in the order you sent it in, right? If you call ABC, it will get
+ABC. But if you have two remote and receiver endpoints - if I call ABC on one
+and then DEF on the other, assuming they both go through the same process,
+there's actually no guarantee that ABC will happen before DEF, right? It could
+be any kind of interleaving of those kind of things.
+
+00:00 SHARON: Right.
+
+00:00 DANIEL: So associated is basically a way for remotes and receivers to
+share an underlying message pipe.
+
+00:00 SHARON: Oh, OK.
+
+00:00 DANIEL: Yeah. It's a bit tricky because the way it actually happens is,
+when you create an associated remote and receiver, it kind of gets tied to the
+message pipe. It's passed over, right? So when you have a remote, you pass a
+pending associated receiver or a pending associated remote over it. It gets
+tied to use that same underlying message pipe. It's kind of implicit. It
+usually just works. But yeah, sometimes you have to think about the details,
+and it gets complicated.
+
+00:00 SHARON: OK, this sounds - this feels a bit like this strong ref counting
+of, maybe we don't want to do this ourselves. But we can get into that more
+later.
+
+00:00 DANIEL: Yeah. Yeah. Yeah.
+
+00:00 SHARON: OK. And the last thing on the list of definitions is entangled.
+
+00:00 DANIEL: Yeah, so that's I think -
+
+00:00 SHARON: Quantum Mojo.
+
+00:00 DANIEL: Yes. Quantum Mojo. I think that's usually referring to the
+receiver-remote pair that Mojo has. It's not a super precise term. And I don't
+think we use it widely. But it does show up in a bunch of the comments, I
+guess. But yeah, usually, when it means entangled, if you have a remote, the
+entangled endpoint is the receiver on the other side or vice versa. If you have
+the receiver, then it's the remote on the other end.
+
+00:00 SHARON: Right. Yeah. OK. Probably all the other words that mean a similar
+thing have been heavily overloaded already, like connected.
+
+00:00 DANIEL: Yeah. Yeah. It's a bit hard to write comments for Mojo. We know
+it could use improvements. But yeah, trying to find ways to write this sort of
+information precisely without like writing novels is always a bit tricky.
+
+00:00 SHARON: It is tough. OK. So let's briefly talk about how Mojo is used. So
+I think the most typical case - the canonical case, I feel like, is between the
+browser and the renderer.
+
+00:00 DANIEL: Yeah.
+
+00:00 SHARON: Right? Is that the case?
+
+00:00 DANIEL: Yeah, I think that's fair to say that maybe that's where most of
+the IPC in Chrome happens because Chrome is a web browser.
+
+00:00 SHARON: Right. And I've heard it described as letting web pages get
+things that they want from the browser. So Mojo is used in that process. Like a
+web page wants maybe - I don't know - a file or something. And it uses Mojo to
+get that. So apart from - what are all the kinds of things a web page might
+want from the browser or want it to do that it would use Mojo for?
+
+00:00 DANIEL: Yeah, so I think that's a pretty big question. So there's kind of
+a set of core capabilities like a web page always has, right? So for example,
+it can always navigate somewhere, kind of various things to manage the loading
+state or to load some resources and that sort of stuff, right? So every web
+page will probably have all URL-loader factories or the frame interface for
+managing this sort of thing, right? And then there are additional capabilities
+that aren't necessarily exposed to everything, right? Obviously, on the web,
+you have all sorts of things gated by permissions, like file system access,
+clipboard, audio recording, video recording, and that sort of thing, right? And
+that's the thing where the renderer could go to the browser and be like, hey,
+give me an interface for geolocation or something, right? And assuming it
+passes the permission checks and other checks, we would give it back the
+geolocation interface, right? We would grant it the capability by passing it
+that interface.
+
+00:00 SHARON: OK.
+
+00:00 DANIEL: Yeah. That's the general sort of idea. It gets - as always, it
+gets a bit messy, right? Because there are edge cases where things have to work
+slightly differently. But in general, that's kind of the flow we try to follow.
+
+00:00 SHARON: So basically, it sounds like the renderer wants something that is
+kind of OS-level, right, like camera or audio. And because we don't trust
+renderers, we have to do that through the browser. So this is how it gets to
+the browser. And then, through whatever other magic happens -
+
+00:00 DANIEL: Right. So yeah, there's some central places where we register
+what interfaces are even exposed to a process, right? But that registration is
+usually also - has other logic, like, should we even grant this thing, right?
+Does the origin - does the document requesting this have a secure origin? Did
+the user give it permissions potentially? It all kind of depends. There's a
+wide gamut of things you might want to check. But yeah, that's the general
+idea, this central point to kind of broker these sort of capabilities out.
+
+00:00 SHARON: OK. Cool. So within the browser still, are there - what are other
+examples of not browser-to-renderer or back uses of Mojo? Are there
+render-to-render?
+
+00:00 DANIEL: Yeah. So like any other kind of thing that evolves over time,
+Chrome has gotten quite complicated. So there's, I think, a bunch of our things
+actually running utility processes now. Like I think - but don't quote me on
+this - like a lot of devices' code like can do this. And so what actually
+happens is the renderer will talk to the browser, right? And the browser will
+be like, you can use it, right? And it will actually maybe spin up the utility
+even for the renderer and give it access. It can pass the message-type
+endpoints. It can pass a remote back to the renderer and the receiver off to
+the utility process. And then the renderer can talk to the utility directly.
+And that actually kind of comes in for the other question about
+renderer-to-renderer communication. We have these things called service
+workers, which can do interesting things with page loads, like support offline
+apps and that sort of thing. And the way that works is you can't necessarily,
+from the renderer, go directly to another renderer. But the renderer, if we
+know it's controlled by a service worker in that document, we can give it a
+URL-loader factory that will actually go and talk to the service worker. In
+that sense, there is renderer-to-renderer communication happening, but it's
+brokered. It's not just a free for all.
+
+00:00 SHARON: Why don't we want free for all, direct renderer-to-renderer
+communication?
+
+00:00 DANIEL: Well, it would probably complicate the kind of trying to - so the
+thing with Mojo is it's very flexible. It's very easy to be - let any two
+endpoints in Chrome talk to each other. But with that flexibility is also a
+certain amount of danger, basically. We want to be able to - when things are
+exposed to another process, we want to be able to audit them, from a security
+perspective and just from a stability perspective as well. If we just kind of
+made it a free-for-all, it would probably become pretty hard to figure out what
+can talk to what? How is the permission checked? Where is it checked? So by
+kind of centralizing these checks in the browser interface broker, for example,
+the idea is we make it a bit easier to understand how the system - like, what
+it's exposing, and what the attack surface is, and that sort of thing.
+
+00:00 SHARON: Yeah. There's a lot of stuff that's very combinatorial explosion
+to me, and this seems like it's trying to limit that a little bit.
+
+00:00 DANIEL: Yeah. There's always going to be things that we can't catch,
+obviously. But that is kind of the general idea. By kind of limiting it through
+a central kind of broker area, we can figure out, if someone wants to audit it,
+they can be like, OK, we are exposing these things to the renderer process. Oh,
+no, we're exposing WebUI. Is that checked? It is, so we're OK. But that sort of
+thing, yeah.
+
+00:00 SHARON: OK. Can you explain a bit more about what service workers are?
+For those of us who might not be familiar, it sounds like they're kind of
+between a browser and a renderer process, maybe.
+
+00:00 DANIEL: So I'm actually not the best person to talk about service
+workers. But at a very high level, they're workers that aren't confined to the
+lifetime of a page, of a document necessarily. And that's why they can
+intercept network loads. They can also do some storage stuff. And I think some
+notifications are tied to service workers and other capabilities. I'm not super
+familiar with them. I just know how they work at a high level and that they can
+be used to implement offline support for apps, as one example. But all sorts of
+other things you could think.
+
+00:00 SHARON: All right. That makes sense. Cool. So those are, within Chrome
+browser, uses of Mojo. So let's talk about some adjacent Mojo use cases. So
+before I used to work on Fuchsia, and they have something called FIDL. It
+stands for Fuchsia Interface Definition Language. And to anyone who might have
+seen it, it looks a lot like Mojo. So can you tell us a bit about that and how
+that works?
+
+00:00 DANIEL: So I wasn't actually super involved with Mojo at that point. But
+my understanding is FIDL was basically forked from an earlier version of Mojo,
+and then they evolved it in their own direction. And FIDL has kind a lot of
+interesting things about it. And if we had infinite time in Chrome, it would be
+nice to integrate some of those features back. But my understanding is FIDL is
+very specific to Fuchsia. But they also have kind of this similar idea to
+Chrome where I think you only expose a FIDL interface - if you give someone a
+FIDL interface, you're granting them the capability to do that thing. So in
+that sense, it's quite similar to Mojo. But yeah, because of the shared
+heritage, I expect it probably looks pretty similar, but there are definitely
+some differences.
+
+00:00 SHARON: Yeah. Something I heard a lot was that Fuchsia was a
+capabilities-based operating system. And it wasn't until I started seeing more
+Mojo stuff that I was like, Oh, that's what that means!
+
+00:00 DANIEL: Yeah, yeah, yeah.
+
+00:00 SHARON: That's the same capabilities. And it looks a lot like Mojo. And I
+think, from the case of using it, I think the only thing you might notice is
+that they have more bindings in different languages. So in Chrome, it's mostly
+C++. Are there any non-C++ Mojo usages, really?
+
+00:00 DANIEL: There are, actually. So there's Java. That was one of the
+motivations for doing this is to make it a bit easier to implement an endpoint
+in Java. Because before people had to write a bunch of JNI boilerplate to jump
+from the C++ IPC handling over to Javaland. Mojo kind of abstracts that away at
+some cost. There's been some persistent concerns about binary size from the
+Java bindings from the Android team. And they could probably be improved.
+There's also the JavaScript and TypeScript bindings. I believe Chrome mostly
+uses the TypeScript bindings these days for things like WebUI. I know some WPTs
+also use the JavaScript endpoints for injecting test fakes or mocks and that
+sort of thing.
+
+00:00 SHARON: Oh, cool! I didn't know about that. Cool. So that's that. And
+then another kind of OSey thing is LaCrOS. I'm not super familiar with this,
+but I understand that Mojo is used in an interesting way in LaCrOS. So can you
+tell us about that?
+
+00:00 DANIEL: So LaCrOS is basically an effort to make it easier to update
+Chrome on ChromeOS devices. Before, it was kind of this monolithic thing
+because Chrome was also responsible for the Window environment Ash on ChromeOS.
+And so it was sometimes a bit difficult to uprev Chrome if there is a critical
+security fix or whatever. And LaCrOS is an effort to kind of decouple these. So
+basically, it turns Chrome OS into more of an OS kind of environment. And
+what's left on the LaCrOS Chrome - it's what it's called - is really just
+browser related. So it's still kind of a work in progress. But in the future,
+Ash the Chrome - right now we have Ash Chrome, which can show WebUI still. But
+in the future, that would actually - WebUI would be displayed in LaCrOS Chrome.
+And it would just be like an Ash backend without any blink renderer and that
+sort of thing. And there's a bunch of Mojo to basically communicate between Ash
+Chrome and LaCrOS Chrome. There's some constraints there. It uses versioned
+interfaces, which is something you won't find too much of elsewhere in Chrome,
+other than some ARC stuff.
+
+00:00 SHARON: What are these interfaces?
+
+00:00 DANIEL: So versioned just means that these interfaces have backwards
+compatibility constraints because Ash Chrome and LaCrOS Chrome don't
+necessarily ship together. We want to be able to update LaCrOS Chrome.
+
+00:00 SHARON: That's the point.
+
+00:00 DANIEL: Yeah, exactly. So we have to be able to tolerate some amount of
+skew between the interfaces. But we have to do it in a way that's backwards
+compatible. And so versioned interfaces are a way to more or less guarantee
+that, assuming you follow the rules. And we have some checks to make sure you
+don't break the rules, generally speaking. But yeah, there's some complexity
+because of that. If you want to deprecate methods or remove fields, you can
+deprecate methods and remove them eventually, but fields are a bit trickier,
+and that sort of thing.
+
+00:00 SHARON: It's like the whole Proto thing of you want them to optional
+because they're never going away, or something.
+
+00:00 DANIEL: Yeah. So Proto has an advantage over Mojo in this respect,
+because they identify their fields with tag numbers. And so you can just omit
+fields completely. Whereas, Mojo, we actually reserve space in the struct for
+it. And that means, once you have a field there in a versioned interface, you
+can never really get rid of it. You have to keep it there even if you're not
+using it. In the future, maybe you might use it for something else if it's no
+longer needed. But yeah, it becomes a bit tricky because of that sort of thing.
+
+00:00 SHARON: Yeah. Because I guess with regular Mojo, it's meant to just work
+within one monolith of the browser. So that, at least, has all the same
+version, and is not - the version skew is not something that was initially
+planned for.
+
+00:00 DANIEL: Right. It all ships as kind of one monolithic block. You can kind
+of refactor freely across the system. When you have versioned interfaces, it
+becomes trickier. You have to follow a deprecation process. I think LaCrOS, at
+one point, was kind of like a three-milestone, three-version thing before you
+could remove old APIs. But don't quote me on that.
+
+00:00 SHARON: Right. OK, interesting. Changing gears a bit here, so let's go
+back to talking about receivers and remotes and the different states they can
+be in. So some - these are all kind of words I've seen. I'm not that familiar
+with Mojo. I haven't done too much cross-process stuff. But you see words like,
+bound, connected, disconnected. I've seen all these words before. I know what
+they mean, but I don't think I know what they mean in this context. So can you
+explain?
+
+00:00 DANIEL: Yeah. So I think maybe the simplest way to think of it is bound
+is when a remote or receiver isn't null. Why would it be null? If you just
+default construct a Mojo remote that's not bound to - you just default
+construct on, it won't be bound to anything. It'll be null internally. If you
+try to make a method call on it, it will crash. You actually have to create
+that Mojo message pipe that's backing it to, quote, unquote, "bind" it. So when
+you create that underlying Mojo message pipe, that's what it means to go from
+unbound to bound. And this is kind of a bit tricky sometimes. I notice this
+kind of mistake pretty often. Sometimes it's very easy to call
+BindNewPipeAndPass, like, pending - I don't even know what the function is
+called. We gave it a really long name to try to be descriptive, and now no one
+can ever remember what the actual invocation is. But when you call that thing,
+the remote or receiver that you're calling it on becomes bound synchronously at
+that point. Even though there's no other side attached to the entangled
+endpoint, it's still considered bound because it's no longer null. You could
+create a Mojo remote. You could bind it. You could immediately start making
+method calls on it, even though the other end hasn't been passed anywhere. And
+what will happen is all that stuff would just be queued internally. And so when
+it becomes connected is when the other endpoint basically goes from pending
+to - actually, no, that's not true. Sorry. It's actually considered connected,
+too.
+
+00:00 SHARON: OK.
+
+00:00 DANIEL: Yeah. When you bind it, it's considered both bound and connected.
+
+00:00 SHARON: OK.
+
+00:00 DANIEL: Yeah. The disconnection, if there is one, is always kind of
+asynchronous. Internally, there's some control IPCs that do heartbeats and sort
+of stuff to see what's alive and that sort of thing. I don't know those
+details. You would have to ask rockot, who is probably the only person who
+knows those details at this point.
+
+00:00 SHARON: Oh, no!
+
+00:00 DANIEL: So yes, let us all hope for rockot's continual safety. But yeah,
+when you create a remote or receiver and you bind it, it's both bound and
+connected. If you have a remote, you can start making method calls on it
+immediately. You don't have to wait for the other side to turn from pending to
+a receiver, for example. Everything would just get queued. And disconnected is
+just when either endpoint is dropped. So if you drop the remote, the receiver
+will become disconnected, if you destroy the remote. Or if you destroy the
+receiver, the remote will become disconnected. But that's an asynchronous
+process because it's always asynchronous, even if you're in process. But it
+just happens at some point. And the tricky part here is if you have a bound
+thing, it can be disconnected. You can still make method calls on it. And
+that's OK. But your method calls will just disappear into thin air. Whether or
+not that's desirable kind of depends on what you're doing.
+
+00:00 SHARON: So going back to what you just said, can you have a case where
+you have one of the ends of a pipe disconnect, and then reconnect it? Or is the
+only way to disconnect one of the ends after you have connected it is to
+destroy the object that represents one of those ends?
+
+00:00 DANIEL: So disconnection is a permanent thing. You can't reconnect
+something that was disconnected. There's some Mojo underlying system - I don't
+know I would call it - but like low level Mojo APIs that you can use to fuse
+message pipes together. But even those won't turn a disconnected message pipe
+back into a connected one. The idea with the kind of endpoints is, once they're
+entangled, they're always kind of that pair. So if either endpoint gets
+destroyed, it becomes disconnected. And this could also happen if the other
+process crashes. Your endpoint that's remaining alive, whether that's a remote
+or receiver, will become disconnected at some point, but no guarantee when
+exactly. There's no ordering guarantees there.
+
+00:00 SHARON: OK. So whenever ordering and stuff comes up, like a concern - a
+common concern is like deadlocks or all sorts of synchronizing issues. So what
+are some of the concerns? Are deadlocks a common concern? How do we handle
+this? Because this seems very fraught with all of the typical, distributed,
+async problems that exist.
+
+00:00 DANIEL: So if you're not using synchronous IPCs, you probably won't hit
+deadlocks unless you're actually writing code that is blocking on receiving a
+remote IPC. In general, I haven't seen code written like this in Chrome because
+I think most developers are like, well, I probably shouldn't block waiting for
+that reply because that's not a great thing. Obviously, you'll see this sort of
+thing in tests because it's much more convenient in tests. But in actual
+production code, I don't think this is a thing that happens. Where this could
+run into problems more is with sync IPCs. So by default, Mojo methods are all
+async. You have to actually give it a sync attribute if you want to be able to
+make an async call in it. And what that means is, if you use the synchronous
+version of the method, it will actually just wait until it gets - until the
+remote process, or whatever, the other end calls the reply callback to let you
+know that it's done. And there's a lot of trickiness involved there because,
+when you're just waiting for the remote thing to reply, there were concerns
+because - before Mojo IPC, with legacy IPC, you could also have sync calls. But
+the way we tried to ensure safety was to make sure that the sync IPCs only ever
+went in one direction. So they only go renderer to browser, and not browser to
+renderer as well.
+
+00:00 SHARON: Because we don't want to block the browser ever.
+
+00:00 DANIEL: I mean, we don't want to block the browser. But we also don't
+want to end up with sync call cycles where the browser process is waiting for a
+sync reply from the renderer, and the renderer is waiting for a sync reply from
+the browser. That would be bad.
+
+00:00 SHARON: That would be bad.
+
+00:00 DANIEL: Mojo tries to avoid this problem by saying, if I'm waiting for a
+reply to my message, to that sync call I made, and someone else makes a sync
+call to me, I better let that through and handle it and let them know just to
+avoid deadlocks. But this is also problematic in another way, because it means
+the messages you're getting sent may be reordered, basically. So what this
+means is, say, I make a sync call from the renderer to the browser. The browser
+sends us some async IPCs, like A and B. And we see those. And we're like, OK,
+we're in the middle of a sync call. We're not going to handle them right now.
+And then, for some reason, someone added a sync call from the browser to the
+renderer. And so the browser goes to the renderer. And the renderer is like,
+hey, I better handle that sync - that incoming sync IPC. And it handles C. But
+at this point, you haven't handled A or B yet. And if you were kind of assuming
+that A and B would happen before C, that's no longer the case. It's pretty
+messy, which is why we've actually considered switching the behavior of sync
+IPCs to no interrupt by default rather than allowing sync interrupts,
+basically, is how it currently works. We actually had some security bugs kind
+of around this sort of message reordering thing. Really, the whole takeaway
+from this is don't use sync IPCs if you can avoid it in any way. They do add a
+lot of complexity, just for the considerations. Obviously, they aren't great
+performance-wise because they are blocking - if you don't need it, please,
+please, don't use them.
+
+00:00 SHARON: Is that the main takeaway of today is don't use sync IPCs, if at
+all possible.
+
+00:00 DANIEL: I mean, that is definitely one thing I would like people to
+remember just because, yeah, if you can avoid it, it will make things - it will
+make life much easier down the road, most likely.
+
+00:00 SHARON: So to make your life and Daniel's life easier down the road, try
+to minimize use of sync IPCs. So of course, what are some cases where they are
+used now and cases where they are currently used, and we would hope to
+transition away from them also.
+
+00:00 DANIEL: Hmm. That's a hard question, mostly because I don't have Code
+Search pulled up right now.
+
+00:00 SHARON: Right, fair enough.
+
+00:00 DANIEL: I know there's some sync stuff around GPU and render stuff. A lot
+of the older web APIs weren't written with promises in mind. So for example, I
+think document.cookie involves a sync IPC to go get whatever the latest cookie
+is from the cookie jar. We've added some caching there to make it better, but
+fundamentally, those sorts of things need to happen synchronously. So we don't
+have much of a choice. Interestingly enough, I think Android WebView actually
+has some sync IPCs from the browser to the GPU, I want to say. Don't quote me
+on that. I don't understand that code at all, despite having reviewed a lot of
+those CLs. But I'm given to understand that it's necessary. So yeah, I mean, I
+don't know that we're actively migrating anything away from sync IPC at this
+point. I know people have worked on optimizing cookie access. And so we will
+reduce the amount of sync IPCs, but never completely eliminate, I think.
+Luckily, I think a lot of the new web APIs are using promises, so they can be
+async. They don't need to be synced. And end life is great.
+
+00:00 SHARON: OK. That's good.
+
+00:00 DANIEL: Yeah. There is also some, I think, additional kind of Google
+integrations with Chrome. I think previously they were pretty complex because
+it was just trying to translate a Java code base into C++. There was a bunch of
+assumptions around sync calls. So they wrote sync IPCs kind of to wrap all that
+in their helper utility process. And that definitely led to some problems with
+deadlocks because we would make a Mojo sync IPC. And then to simulate the
+environment Java would have had, it would have - it spun a run loop internally.
+But it got into deadlocks. So don't write sync IPCs. Do yourself a favor.
+
+00:00 SHARON: Do yourself a favor. That's right. So when it comes to all of
+this async/sync, mostly the async stuff - and you mentioned binding earlier.
+Something we see a lot in Chrome is callbacks. So these are used for async
+stuff. And you also see them bound. Is that the same binding as Mojo binding or
+is that - no.
+
+00:00 DANIEL: No, it's completely different.
+
+00:00 SHARON: It's completely different. Is there much intersection between
+callbacks and Mojo? These are both heavily used in async situations. Do they
+intersect?
+
+00:00 DANIEL: Yeah. So it's actually kind of a known - I guess I would call it
+a wart at this point that our way of writing async code leads to kind of
+hard-to-follow code. If you want to make a Mojo message call and do something
+after it replies, you bind a reply callback. And that's kind of the case of how
+async code in Chrome often works. You create callbacks, and then you wait for
+this other thing to be done, and call your async callback. But it kind of means
+that trying to read the control flow of the program can be pretty tricky
+sometimes. You have to be like, oh, this thing has an async callback. Let me
+see what it's bound to. So you go in Code Search. You look at the caller.
+You're like, oh, it bounded to this onFooDone thing. Let me go look it
+onFooDone. And then if onFooDone has more async work, you're just kind of
+chasing these chains all over the place. And that's kind of the case with Mojo.
+I think Mojo used callback just because that's kind of our language for it in
+Chrome. It would be nice to do better. There was a bunch of exploration around
+some sort of promise-based idea a while back. Ultimately, we didn't implement
+that because it was felt it would be hard to migrate everything. And it was
+kind hard to justify prioritizing that. But we've played with a lot of other
+ideas since then to try to make these sorts of things a bit easier to write. If
+you're chaining two callbacks, you can use a callback helper called then.
+There's also something called a sequence bound which can help you if you have
+two objects that live on different sequences. You don't have to post task
+yourself. Sequence bound can happen - handles that under the hood for you and
+binds the callbacks and whatever.
+
+00:00 SHARON: Right, right. Yeah, we're still migrating off of legacy IPC. So
+to introduce another migration at this point seems ambitious.
+
+00:00 DANIEL: There's kind of varying opinions on this, obviously.
+
+00:00 SHARON: Well, they're not here right now. So what are your opinions, if
+you want to share them.
+
+00:00 DANIEL: I mean, it would be really nice if we could improve on this. I
+know that now that we're slowly getting C++20, thanks to Peter Kasting's work.
+I think there will probably be some exploration around co-routines and if
+that's something that we could use to help us migrate to simpler patterns for
+async code. It is kind of a very open-ended question now because there's also
+things like Rust that are up and coming, and figuring how to do async Rust and
+async in Chrome, in C++, and making that all mesh together is probably going to
+be a pretty complex problem.
+
+00:00 SHARON: Probably.
+
+00:00 DANIEL: Yeah.
+
+00:00 SHARON: Probably.
+
+00:00 DANIEL: Yeah.
+
+00:00 SHARON: So kind of transitioning a bit to more security things, and also
+as it ties into callbacks and async, is when you bind a thing - because memory
+safety and use-after-free and whatnot are a major problem that we have from a
+security perspective, especially because C++ and all of that. So when it comes
+to passing around these things that are async, you don't know when they'll be
+done, if you're passing in things that you're calling from - like in the
+callbacks, how do you make sure that they're still around when you need them
+and that call doesn't become either a crash, like null dereference, or worse, a
+use-after-free? Is this a big concern we have? How are we dealing with it?
+
+00:00 DANIEL: Yeah. So if you're using Mojo, quote, unquote, "the normal way",
+you're probably safe-ish. So when I mean the normal way is, you have a class.
+It needs to make Mojo calls. And it owns the Mojo remote. And the way that
+works is if you make calls on the remote, but then your class is destroyed, it
+will kind of cancel any reply callbacks. You will never get them. So you don't
+have to worry about that case. And that's kind of nice. But there's, obviously
+a lot of other ways for things to go wrong. In particular, if the lifetime of
+the class is tied to the lifetime of the Mojo message pipe, like, if it gets
+disconnected, you destroy this. That's kind of an area that's a bit fraught
+with peril. We've had this problem with self-owned receivers. A self-owned
+receiver is basically a shorthand way of creating an implementation for
+handling Mojo messages that deletes itself as soon as the message pipe is
+disconnected. And at first glance, this kind of seems a very natural pattern.
+If I'm disconnected, I don't need to be there. Just delete this. But it becomes
+problematic if other people are holding pointers to you. We had this problem, I
+think, a lot with - so a common kind of scope - for IPCs between browser and
+renderer, a common kind of anchoring point is the RenderFrame(Host) or
+RenderFrame rate. And what would happen is we -
+
+00:00 SHARON: What is a RenderFrame or RenderFrame(Host)?
+
+00:00 DANIEL: Yeah. So it kind of corresponds to, basically, either the main
+frame or an iframe. And it's just kind of responsible for dealing with all the
+fun logic of navigating, loading the page, and if the page wants to do other
+stuff, figuring out how to get it to the code that actually knows how to do the
+extra stuff, like the capabilities thing. So a common problem we had was the
+RenderFrame host could be destroyed, like if you remove an iframe from the
+document. The RenderFrame(Host) could be destroyed. But what would happen is
+people would grant capabilities using interfaces, but these interfaces would be
+self-owned receivers. And what would happen is the self-owned receiver would
+have a raw pointer to the RenderFrame(Host), but it wouldn't destroyed with the
+RenderFrame(Host) because it's a self-owned receiver. And the thing controlling
+its lifetime is whoever holds the other endpoint. In this case, that's a
+renderer that might be malicious or compromised. And so without any way to
+guarantee that the RenderFrame(Host) will outlive the self-owned receiver, it
+becomes dangerous. We had a lot of use-after-free bugs from this, actually. And
+that's why we added something called Document Service. And if you're writing
+web APIs and you need to implement IPCs, and your thing is kind of roughly
+scoped to the lifetime of the document, it's highly encouraged to use something
+like Document Service rather than a self-owned receiver. That way you don't
+need to hold a raw pointer to RenderFrame(Host) yourself. We guarantee the
+lifetimes are more or less correct. Obviously, kind of with anything of this
+nature, if other people hold pointers to you, you still need to be sure that
+you're clearing them, or your ref counted or something. It's hard to give a
+one-size-fits-all fix for this sort of thing. Document Service is kind of the
+closest we have. There's a couple other helpers along those lines. And if your
+code can fit within that framework, it will probably make your code a bit more
+robust against those kind of problems.
+
+00:00 SHARON: It sounds like, yeah, avoiding ref counting, or strong ref
+counting, we want to generally do that because that's easy to get wrong. And
+probably just general good advice or good practices to not use a `T*` to use a
+global pointer.
+
+00:00 DANIEL: Well -
+
+00:00 SHARON: `raw_ptr` instead.
+
+00:00 DANIEL: Ref counting has its place. But it's a bit tricky to use
+correctly. And in Chrome, we've traditionally tried to discourage it if it's
+not needed. And then, also, with the `T*` thing, with the MiraclePtr and
+BackupRefPtr work, I think we've actually turned on some enforcement that you
+can't actually have `T*` fields anymore.
+
+00:00 SHARON: Oh, cool.
+
+00:00 DANIEL: So that's an additional layer of safety, which is nice.
+
+00:00 SHARON: Things that have changed since the first episode. Wow!
+
+00:00 DANIEL: Yes. It's great. You can use `raw_ptr` or `raw_ref`. And you
+should be doing that where possible, just because that way, if you mess up, or
+you forget about an edge case, it turns into, hopefully, a mostly
+nonexploitable kind of stability bug, rather than an, oh my gosh. It's a
+critical-severity security bug. We must ship a fix out ASAP.
+
+00:00 SHARON: So that's how lifetimes can cause problems. So in the case of
+this - so it sounds like the bad thing that will happen in this case is a
+general memory safety, use-after-free problem. So there's nothing necessarily
+Mojo-specific about what can go wrong in this case where the problems are being
+sync and async.
+
+00:00 DANIEL: So yeah, it's not so much about async and sync but just
+remembering that the thing - like if you're implementing an interface, the
+other thing calling into you, whether it's a remote process or not, may be
+malicious, especially if it's from the renderer. We have to assume that the
+renderer is compromised. And that means it's better to try to structure things
+in a way that either Mojo will enforce invariants, or that impossible things
+can't happen. So one common area where we have these sort of issues is maybe
+something will pass like two arrays of stuff. And I don't know - say instead of
+passing a bunch of pixels, it passes all the reds in one array, all the greens
+in one array, and all the blues in one array. And then it just assumes those
+are the same length. That's not a safe assumption if it's coming from the
+renderer, so you would have to check that. But it would be better to structure
+a code in ways that didn't require checking all these assumptions. So in this
+contrived case, it would be better to have a pixel type, and then have an array
+of pixels, because then you have to specify RGB. And it's guaranteed that you
+won't have an array mismatch because you won't be passing multiples of them. So
+just stuff like that. It's really hard to go over all the ways things can go
+wrong. We did try to do that. And I think the document is 20-plus pages. It's a
+doc of guidelines for IPCs, like what reviewers and reviewees could, in theory,
+look for. But it is massive. It'd be nice if it could be more compact, but I
+think that's kind of the nature of people can write whatever they want. And
+there are all sorts of creative ways to get into trouble with these sort of
+things.
+
+00:00 SHARON: Yeah. As an IPC reviewer, when you look when someone is making a
+change, adding, removing - maybe not removing, but adding things, what are the
+first things you check for when you are reviewing a new or updated IPC?
+
+00:00 DANIEL: So the first things I will look at are the CL description and the
+comments in the module. And if I can't really figure out what the change is
+about from there, if I have extra time on my hands, I will go look at the bug.
+I will go read any design docs that were linked and try to kind of reverse
+engineer. But in general, that is the first thing I look for because I want to
+understand what they want to do at a high level. There's no point in trying to
+nitpick like things here and there in the implementation details if the
+operation that's being exposed is fundamentally unsafe. If someone's writing a
+file system interface, and it provides the capability to read any file, and
+they want to pass that to the renderer, that is fundamentally unsafe. And
+there's no point in reviewing the implementation. So you want to review the
+overall high-level ideas, and make sure you understand those. That's what I
+personally go for because sometimes I think it's very easy, if you're writing a
+CL, to be, like, I know the context behind it. I'm fixing X bug or fixing Y
+bug. But it's easy to forget that someone else coming in reading it - the IPC
+reviewer is not going to know every feature like the back of their hands. And
+so giving them the context to be, like, oh, this is a fix for Y, and we need it
+because Z, really helps the review. And also having these comments in the
+mojom, can help document constraints, or what is this going to be used for, or
+how will it be used, what is it expected to do, if you implement it? If you
+call it with - if something is nullable, you can pass nothing for it. What does
+that mean? Is that just a I didn't feel like figuring out the test, kind of
+thing, or it actually has some significance? Like documenting those sort of
+things.
+
+00:00 SHARON: Who would do something like that and not have figured out the
+tests first?
+
+00:00 DANIEL: I have never done anything like that.
+
+00:00 SHARON: Yeah.
+
+00:00 DANIEL: Yeah. But once those kind of high-level things are more out of
+the way, then it's easier to review the rest of the CL in the context of that.
+But without that background context, it can be quite tricky to do IPC reviews
+sometimes. And the other thing I would say is I would encourage people to send
+out reviews to IPC Reviewer Center. I kind of understand that people don't want
+the spam, like the people that are asking to review. But people, if they don't
+feel like they don't need to review it, they can ignore the CL until it is
+ready to review. But sometimes it's useful to peek in and glance and be like,
+yeah, this is about the right shape. I have no concerns that require immediate
+action. Because what's really unfortunate is if you're at the end of - I don't
+know - a three-week review, and you're like, oh, you shouldn't do it this way.
+You actually need to re-engineer this entire thing and hook it up this other
+different way over here. That's just not fun for anyone. It's not fun for the
+reviewer to give that kind of feedback. And it's not fun to get that kind of
+feedback either.
+
+00:00 SHARON: Yeah. I'm sure we've all been on at least one end of this kind of
+interaction before, so for sure. So would you say IPC review is basically a
+security review for IPC? Or are you reviewing for additional stuff beyond that?
+
+00:00 DANIEL: That's the minimal scope. Some people, depending on how they're
+familiar with the area, may have ideas beyond that. But the kind of expected
+scope - it's expected the cover is, basically, does this IPC make sense to add?
+Is it safe? What are some additional things we need to consider if the sender
+or the receiver is malicious? And this extra layer of scrutiny is just because,
+historically, before we had IPC review, we actually had a lot of security bugs
+due to - it's really easy to write this code because day to day, you're like,
+oh, I'm just working the same process. Everything is fine. I can assume that
+people won't violate my invariants. If I say this thing must always be called
+with at least one item in the array, I can assume there will always be one item
+in the array. But that all goes out the window if you have to assume a
+malicious attacker in the renderer. And so the IPC reviewer is usually just
+coming in more with a hostile mindset, like ways things could go wrong,
+basically. In that sense, very much a security review. But to be clear, it's
+very different from the security review for launches. That's an entirely
+different thing. Sometimes there might be times when an IPC review is like, I
+don't know. This seems a bit potentially dangerous. Has this gone through any
+sort of launch review yet? And at that point, you might punt it to a security
+review. It's not super common, though.
+
+00:00 SHARON: OK.
+
+00:00 DANIEL: Yeah.
+
+00:00 SHARON: OK. Yeah. Lots of reviews of all kinds. And I think what you said
+about the reviewer not having all the context applies to lots of reviews. In a
+launch review, you have so many fields you need to get approved. All of these
+people don't have the same context as you. And the same is true for IPC
+reviews. So are there any cases where something about the actual design of the
+Mojo interface itself went wrong that caused a problem that you can tell us
+about?
+
+00:00 DANIEL: I don't think I have a prepared example.
+
+00:00 SHARON: That's fine. It's cool.
+
+00:00 DANIEL: We can edit one in in post-production.
+
+00:00 SHARON: We can edit one in in post-production. So you're going to sort
+out an example very shortly.
+
+00:00 DANIEL: Sure. Let's go with that.
+
+00:00 SHARON: Yeah, let's go with that. And then moving - so best practices,
+any - when it comes to introducing new IPCs? So you mentioned getting review
+early, just a quick kind of sanity-check situation. Do you have any other tips
+for best reviews for best practices for IPC reviews?
+
+00:00 DANIEL: Well, you could go read the 20-plus page IPC guidelines doc and
+try to memorize it. I don't recommend that, though. I would say, in general, it
+probably comes down just to several things. It's better not to have stateful
+interfaces. And so what I mean by that is an interface where it's like, hey,
+you must call the init method before you do anything else, or else it will
+explode. We don't want that because that means all your other methods have to
+check that init has been called. And otherwise, they'll explode. Depending on
+who your caller is, they may or may not be trustworthy, and that sort of thing.
+They kind of - sorry.
+
+00:00 SHARON: Do we want a lot of Mojo calls to generally be idempotent, too?
+
+00:00 DANIEL: They don't need to be idempotent, necessarily. But when it's a
+very complex set of state transitions, that is where things can get into
+trouble. And obviously, there are some situations where this is unavoidable.
+And you'll just have to deal with it. But if you can avoid it, like if you have
+an init method, it might be worth it to create a factory interface. This is
+what I usually recommend. Obviously, it's a bit more boilerplate, and it's not
+the nicest always. But it can also save some headache down the road. We
+definitely had some IPCs in the past where this was a problem, just because
+malicious code could not call the init method. Or it could call it twice and
+cause a use-after-free. So if you can factor these out into separate
+interfaces, that can be a very helpful thing. And the other thing is - and I
+mean, it really goes along with the first - try to structure things in a way
+that a malicious - if the other end, if they're malicious, they can't violate
+the invariants. So the contrived pixel example, but also using things like
+struct traits, rather than having each thing be like, hey, let me validate all
+the data, or call a function to validate all the data, try to write struct
+traits if you have this sort of validation logic. And so that validation kind
+of happens centrally in one place. And everyone using the type, does it need to
+go, I don't know - data is valid, or something. Because if someone forgets,
+then, boom, potential security bug. So yeah, that sort of thing. It's very
+general. But if we wanted to get into specifics, we would be here for a couple
+of days.
+
+00:00 SHARON: OK, OK, a couple of days, all right. I think we might have lost
+people after at least the second day. I think we might.
+
+00:00 DANIEL: Yeah.
+
+00:00 SHARON: Yeah. And then moving on from that now, mostly a personal
+question, sometimes you have a function. It's a Mojo call. You click it, and
+there are no callers, like in Code Search, I mean. So why are there no callers?
+Why are they not shown? Does it mean I can just delete this interface? OpenURL,
+who needs that?
+
+00:00 DANIEL: OK. Yeah. So if you want to find out what's calling a Mojo
+method, the most reliable way is to go to the mojom definition first, and then
+click - get the cross references from there. And the reason for this is
+because, I guess, it's a quirk. I don't know what you want to call it.
+
+00:00 SHARON: A feature.
+
+00:00 DANIEL: A feature, yeah, we'll go with that. It sounds nicer. When we
+generate the C++ definitions for a mojom-like interface or struct, we actually
+generate two, what's called, variants. So one is - I call it the regular
+variant. It uses STL types as `std::string`, `std::map`, all the fun things
+that you're normally - sorry - `base::flat_map`. It doesn't use `std::map`. But
+you get the idea. It's all the kind of regular container types. And the other
+variant is what's called the Blink variant. And Blink uses `WTF::String`. It
+has its own hash map type, its own vector type, et cetera. And so if you have a
+Blink variant of an interface, when you pass arrays, it'll be passed as
+`WTF::Vector`. And you're probably like, why did we do this? Why are we hurting
+ourselves?
+
+00:00 SHARON: [INAUDIBLE] like WTF Mojo.
+
+00:00 DANIEL: Yeah, something like that. And the idea behind this is we already
+had to do a conversion in the past. The way things worked is we handled IPCs in
+the content layer, like in content render, or if you have Chrome render, or
+whatever. But then we had to pass the data across what's called the Blink
+public API. And the Blink public API would take all these STL types and marshal
+it into the WTF types. And that means copying a bunch of string data or copying
+a bunch of vectors or maps or whatever. And so it's not great from an
+efficiency perspective. So we were like, well, we have to deserialize this data
+already for Mojo. So why don't we just turn it into the right type to begin
+with? So that's kind of what that's all about. So the problem with this is,
+especially if you're in Blink, or in Content Browser, or something, if you
+click on a Mojo - like on a call that you know is a Mojo call, it will find the
+callers to that variant. So if you're on the browser side, there might - sorry
+- that wasn't [INAUDIBLE]. So if you're in the renderer, you're like, who calls
+this method? It's a Mojo - I want to know who is calling it from the browser
+side. I click on it. Because it's a Blink variant, Code Search actually won't
+go find the regular variant's caller. But if you go from the mojom definition,
+it will. So that's the most reliable way to do it. It can also help if you
+filter out generated files. Because, otherwise, it shows all the boilerplate
+from the generated files. But usually, if you do that, it should work. If it
+doesn't work, that's probably a bug. Please, file one, and we will try to fix
+it.
+
+00:00 SHARON: OK. When you say the Mojo file, there are - typically, there's
+the .mojom file, and there's like .mojom.h. So you mean the first?
+
+00:00 DANIEL: Yeah, I mean the first. Don't look at the generated files for
+Code Search.
+
+00:00 SHARON: In general.
+
+00:00 DANIEL: It's because of this feature with variants that sometimes you'll
+kind of get zero callers. But actually, your caller's in content, but you're
+handling it in Blink - yeah, it's a mess.
+
+00:00 SHARON: Yeah, all right. Because I've done that before, where I click a
+function. I don't realize it's a Mojo call because it's overriding something.
+And it's not immediately obvious. And you're like, oh, no one's calling it. We
+should just remove it. But it's something that's very long and very clearly
+important looking.
+
+00:00 DANIEL: Yeah, yeah, yeah.
+
+00:00 SHARON: And you're like, why are there no callers? Good tip! All right, I
+think that is all of our questions. If someone watched this and was like, wow,
+Mojo, this is so cool. Where can they go to learn more? We'll link the long
+20-page doc and some other documentation. But beyond that, what can people do
+if they're just like, I love me some IPC?
+
+00:00 DANIEL: Well, I think one thing that's in pretty shabby shape perpetually
+is the documentation for Mojo. We have tried to sort of incrementally improve
+it. We did sit down and try to write docs for it a while back. But over time, I
+think people have questions. And we haven't always had the time to go back and
+update the documentation to reflect the questions people are having. But if you
+do have questions, please, always ask them. There's a chromium-mojo mailing
+list for public questions. There's a chrome-mojo one for internal questions.
+And there's also the Mojo channel on the Slack. If you have questions, if
+you're hitting weird compile errors with struct traits, I know that's always
+kind of a big mess. Please, please, do ask questions. There's usually someone
+lurking on there who's happy to help with -
+
+00:00 SHARON: They're all very helpful.
+
+00:00 DANIEL: But don't be silent. Because if you're silent, we don't know
+things are a problem. And if we don't know it's a problem, it's kind of hard to
+fix. But in general, we do try. Reach out. Mojo is not supposed to be
+intentionally hard to use. And if you do find that's the case, please, ask us,
+because people who work on Mojo don't always understand the tricky parts.
+They're like, oh, this all make sense. But they already have that entire
+framework in their mind. Whereas, someone kind of coming into, it's kind of
+like, this makes no sense. This is dumb. We should - why doesn't it work like
+X? And then we might change it to work like X, or we might update the
+documentation to be like, it can't work like X because some reason. And that's
+just helpful for everyone in the long run.
+
+00:00 SHARON: I mean, as people often say, if you're new, you have perspective,
+which is you are seeing this. You're not just used to how it works, including
+the good and the bad parts. So yeah, it's a good time to ask questions. All
+right, well, that sounds great. Thank you very much, Daniel. Thank you for
+being here on the show. And we will see you all -
+
+00:00 DANIEL: Thank you!
+
+00:00 SHARON: next time. Cool, cool. We're relatively centered. No.
diff --git a/docs/transcripts/wuwt-e08-processes.md b/docs/transcripts/wuwt-e08-processes.md
new file mode 100644
index 0000000..41f27b3f
--- /dev/null
+++ b/docs/transcripts/wuwt-e08-processes.md
@@ -0,0 +1,1091 @@
+# What’s Up With Processes
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 8, a 2023 video discussion between [Sharon (yangsharon@chromium.org)
+and Darin (darin@chromium.org)](https://www.youtube.com/watch?v=SD3cjzZl25I).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Chrome has a lot of process types. What is a process? What are all the types?
+How do they work together? Today’s special guest to tell us more is Darin.
+Darin is one of the founding members of the Chrome team, and wrote the initial
+implementation of the multi-process architecture.
+
+Notes:
+- https://docs.google.com/document/d/1uXF-ncJ98LWQMN7M3NA_2oYkVmW9Vzp0v-wkJaNpsDQ/edit
+
+Links:
+- [Chrome comic](https://www.google.com/googlebooks/chrome/small_00.html)
+- [What's Up With Mojo](https://www.youtube.com/watch?v=at_35qCGJPQ)
+- [What's Up With Open Source](https://www.youtube.com/watch?v=zOr64ee7FV4)
+- [What's Up With //content](https://www.youtube.com/watch?v=SD3cjzZl25I)
+- [Life of a Process](https://www.youtube.com/watch?v=5im7SGmJxnA)
+- [Chrome Compositing](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/how_cc_works.md)
+- [Site Isolation papers by Charlie](https://charlesreis.com/research/publications/)
+
+---
+
+00:00 SHARON: Hello, and welcome to "What's Up With That," the series that
+demystifies all things Chrome. I'm your host, Sharon, and today, we're talking
+about processes. There are so many process types in Chrome. How do they form
+the multi-process architecture? What exactly is a process? Here to answer all
+of that and more is today's special guest, Darin. Darin was one of the founding
+members of the Chrome team and pretty much did the first implementation of the
+multi-process architecture, so it is well-suited to answer all of this. Plus,
+created the IPC channels that Chrome started with. If you want to learn more
+about IPC and Mojo, check out the last episode with Daniel for lots more on
+that. So hello. Welcome, Darin. Welcome to the show. Thanks for being here.
+
+00:38 DARIN: Thank you. Great to be here.
+
+00:38 SHARON: Yeah, cool. So first question, what is a process?
+
+00:44 DARIN: Right, so process is the container in which applications run on
+your system. Every process has both its own executing set of threads, but it
+also has its own memory space. That way, processes have their own independent
+memory, their own independent data, and their own independent execution. The
+system is multitasking across all of the processes on the system.
+
+01:13 SHARON: Cool. Chrome is basically an operating system that runs on top of
+your operating system. So there probably are parallels between Chrome's
+representation of a process and the actual operating system ones. So what are
+the similarities and differences, and how do they interact?
+
+01:30 DARIN: Well, yeah, I mean, you can talk about a lot of different things.
+I mean, so Chrome is made up of multiple processes. We run different tasks in
+different processes. That's done for multiple reasons. One is so that they can
+run independently, so that there's performance benefits that come from the fact
+that they're running independently. Back in the day, the original idea was that
+it would allow us to take advantage of the operating system's preemptive
+multitasking that it already has and to actually allow web pages to run
+concurrently and to be managed just like any other concurrent task that the
+operating system would manage. So that's the original idea there. And in that
+way, this model of Chrome divided into multiple processes just allows the
+Chrome itself and all of the tasks that it has to really take advantage of
+multi-core systems so that if you have more computing power, if you have more
+cores, you have more hyperthreading going on in your system, then it's possible
+for more things to happen concurrently. And Chrome's workload can be spread out
+that way because Chrome is broken into all of these different processes and all
+of these different threads. In that way, it's taking advantage of and mirroring
+the capabilities of the OS and providing that as a substrate for web and for
+browser and for how all these things work. How Chrome then has to be similar is
+that also, like an OS, Chrome has to manage all this stuff. And from simple
+things like how much resource should a background tab be using, should its
+timers be running when it's in the background, to much more complicated things
+when you talk about even should a process stay alive or not. If you look at
+Chrome OS where system resources can be so limited, it's necessary, or on
+mobile, necessary to terminate some of those background processes to close some
+of those tabs behind the scenes, even if the application makes it look like
+those tabs are still open. So the level of management is a big part of - in
+that way, it's being kind of like an OS.
+
+03:42 SHARON: Is Chrome's representation of a process, are those generally
+one-to-one with a system process, depending on which system you're on -
+
+03:48 DARIN: Absolutely.
+
+03:48 SHARON: or is that an abstraction layer?
+
+03:55 DARIN: No, well, absolutely when we talk about a process in Chrome, we
+mean an OS process. And so we might have multiple web pages being served by
+that single renderer process. We do try to spread the load across multiple
+processes, but we also independently decide how many processes to actually
+create. And it can be based on - there could be good reasons from, like I said,
+a performance perspective to having tabs assigned across multiple processes,
+but there can also be good security properties, like letting the web pages be
+allocated to different processes means that those web pages are not running in
+the same process, meaning they're not running in the same address space. And
+from a security perspective, that has really great properties because it means
+if a web page is able to tickle a bug in the rendering engine in the V8 or in
+part of Blink and somehow get a privilege escalation, like start to be able to
+do things that JavaScript normally can't do, it's still going to be limited by
+the capabilities of that process and what it has access to. And so if that
+process has really only the data for the web page that was providing the
+problematic JavaScript, well, it's not really getting access to anything it
+didn't already have. And that's kind of the whole idea of process isolation and
+sandboxing. And then on top of that, you limit the capabilities of that process
+by really leveraging the OS process primitive and the kinds of restrictions and
+capabilities that can be removed from that process to achieve an isolation for
+web pages for an origin or for a set of web pages. I say set because we might
+not want to allocate a process for every single tab or for every single origin
+because that might just use up way too many system resources. So we have to be
+thoughtful there, too.
+
+05:50 SHARON: Yeah, so this is quite closely related to site isolation, which
+isn't the topic of this video - maybe the next one. So terms that are used
+often and sometimes interchangeably are multi-process architecture and process
+model. So these aren't exactly the same thing, but I think can you explain the
+difference between them and what each one is for? Because there are
+similarities, but.
+
+06:16 DARIN: Sure. I mean, I think to me, the phrase "process model," it's
+talking about, what does a particular process represent, what does it do. And
+then when I say multi-process architecture, I'm thinking of the whole thing.
+It's all packaged up. It's a multi-process architecture to build a browser. At
+the end of the day, user is hopefully not so aware of the fact that this is how
+it's built. I mean, earlier on in Chrome's history, the Windows Task Manager
+didn't do a very good job of grouping processes by their parent. And so if you
+opened the Task Manager at the OS level, you'd see just a spew of processes
+that Chrome was responsible for. And it could be a little disconcerting for
+people. A little tangent, but now more modern versions of Windows, they do kind
+of group it all to the parent task. And so it's a little easier and less sort
+of in-your-face that Chrome is creating all these processes. But yeah, at the
+end of the day, it's just the multi-process architecture is like that's the
+embodiment of the whole thing. And we have these different process types that
+make up that whole thing. There's the browser process, the main one, and then a
+renderer process is the name we give to the processes responsible for running
+web pages. And then we have a few other process types that are part of the
+puzzle, a networking process, a GPU process, utility process, and occasionally,
+in the lifespan of Chrome, other types of processes. We had plugin processes,
+for example, when we were hosting Flash in Chrome. And the Native Client had
+its own type of processes as well. So what's that all about? Really, I can go
+into it if you want me to go into all the details there. But -
+
+08:05 SHARON: Yeah, I think we'll run through - this is a, yeah, perfect segue.
+We'll run through each of those process types you just mentioned and mention a
+bit about what they do, how much privilege they have, maybe how many of them
+there are because some of them, there's only one of. So I think it makes sense
+to start with the browser process, which is the process and is often likened to
+the kernel in an operating system.
+
+08:30 DARIN: Yeah, so the browser process kernel operating system broker, these
+are kind of good analogies for what the browser process's role is. So it's the
+application process, the main one, that starts up initially, and it's the one
+that hosts the whole UI of the app. And it's going to spawn these child
+processes, the renderer processes, the GPU process, and so on, to help fulfill
+its goals. So very early on, we started with this design where WebKit, the
+rendering engine we were using from Apple, it could be built as a COM control
+and register it on the system and load it as a DLL. And then in order to run
+that in a child process, it was using HWNDs and all the standard Win32 isms to
+do its job. And we started out by just literally trying to capture a bitmap
+rendering of WebKit and send it over to the browser process where we could
+present that bitmap. Actually, rewind even further. The very first version took
+advantage of the fact that Windows supports having HWNDs hosted in different
+processes and threads. And so we literally just took that HWND from WebKit and
+that child process and stuck it into the window hierarchy of the browser
+process. And we drew our browser UI around it, and WebKit was there, but it was
+running in a different process. And if we ever needed to tell that process to
+do something, we just send a WM user event postmessage to it. And that's
+something Windows lets you do. So it felt like a very simple toy kind of way to
+try it all out. A lot of limitations to that design. Pretty quickly, we
+realized we didn't want to just be in that kind of setup, and we moved to
+building our own IPC channel, a pipe, so that we could communicate and really
+get to the point where WebKit's running there without an HWND, without its own
+Win32 windowing constructs, but instead, it's just kind of an image generator.
+And we take the image that it generates, the bitmap, send it over our IPC
+channel to the browser process. And the browser process is where we have our
+window hierarchy browser process. We display that bitmap browser process where
+we collect user input and send it to the pipe to the renderer where we then
+feed it into WebKit.
+
+10:46 DARIN: That was the original architecture of Chrome. So in that world,
+the browser process is your application process. It has all the UI. And it's
+really like this glorified image viewer. And the renderer process is literally
+just like it's running WebKit - now Blink. It's running the rendering engine,
+and it's producing those images whenever. Like, an update occurs. A layout
+occurs or some invalidation occurs. And we got a little fancy. It was producing
+just the sub. It would know, oh, I really only have a small damage rect, so I
+don't have to produce the whole image. I just produce a small part. And send
+that over, and then we paint that into the part that the browser is retaining
+an old image of. And it can update just that one part. And so that's a very
+simple approach that we took when building this whole thing. And so those
+render processes become very much just very simplistic in that they aren't
+interacting with the rest of the OS in a very deep way. They are just taking
+input events from this pipe and sending images back. When they need other
+services like they need network access, instead of going straight to the
+network from the renderer process, because we started to realize, hey, we might
+want a sandbox and restrict those child processes, and also, we needed the
+notion of cookie jar that was shared across all web pages, so that if you visit
+GMail in one tab and visit GMail in another tab, you're still logged in, we
+needed the network stack to be in a unified place. So it meant that not just
+would we send images up to the browser, but now we would send network requests
+to the browser. And the browser would respond with the network data. And as a
+result, we started to go down this path of centralizing access to system
+services and resources in the browser process.
+
+12:44 DARIN: It's becoming therefore like a broker to the system that the
+renderer now is unable to - not unable - it's asking the browser for everything
+it needs. It's communicating to the browser to get access to all the different
+resources. And that allowed us to then restrict the renderer process
+considerably so that it doesn't even have access if it wanted to touch the file
+system, to touch the network TCP/IP implementation or any system resources. So
+the sandbox really is all about how we apply those restrictions, taking away
+the capabilities of a windows process. So in the very early days, there was
+just the browser process and renderer processes. And we would allow multiple
+renderer processes to be created as tabs were opened. And we put some
+restriction on the number of processes based on the amount of RAM that your
+system would have, thinking that processes maybe have some inherent overhead,
+which they do. Certainly, there's the overhead of the V8 heap that is allocated
+once per process or once per isolate, if you're familiar with the details of
+V8. And so, we didn't want to have so much of that kind of - so we thought
+there was some limit to how many processes we should have. Later on, other
+processes types started to emerge. The next one that came was the Plugin
+process because in order to get YouTube to work back in 2006, you needed to
+support Flash. And Flash has two modes - it did. It had a windowed mode and a
+windowless mode. And the difference is whether it drew itself into an HWND or
+if it would just produce a bitmap itself. But regardless of what mode it was
+rendering in, it still wanted direct system access, like it wanted to touch the
+file system. And so if we were going to run it in our browser, it can't run in
+the renderer process. It has to run somewhere else. And so, yeah, in the frenzy
+of, gee, wouldn't it be nice if we could have sandboxing, it was, how the heck
+are we going to sandbox and isolate plugins? Because the way plugins integrated
+with WebKit is that WebKit just directly called into them and said, hey, if
+it's a windowless one, give me your bitmap. I'm going to include it in my
+rendering. If it's a windowless one, it also means it's dependent on WebKit to
+feed it events. And so, how does that work? So we ended up building a process
+type called the Plugin process type for NPAPI plugins, Netscape-style plugins,
+all stuff that doesn't exist anymore. It's wonderful. And NPAPI is this
+interface that was once upon a time, I want to say, kind of, like - my head is
+going to some unsavory words. It was kind of pooped out by somebody at Netscape
+to make Acrobat Reader work over the weekend. And then it became a stable API.
+And lots of regret and sadness probably followed, but as a result, things like
+Flash were created, and web became very interesting in some ways. A wonderful
+story about Flash, I think.
+
+16:02 DARIN: But anyways, supporting that stuff meant dealing with some gnarly
+frozen APIs and figuring out how to stitch all that together, and the renderer
+process of WebKit would talk to something that wasn't actually in its process
+that was - or, again, another IPC channel, running a whole other process. We
+wanted plugins to still not run in our browser process, but to, instead, run in
+their own process so that if they crashed, they wouldn't take down the whole
+browser. And Flash and other plugins were notorious for crashing. So it was a
+must that they run in their own process. But we figured they couldn't be
+sandboxed as tightly as the renderer as WebKit because they already were
+accessing the system in very deep ways.
+
+16:55 SHARON: Cool, lots of -
+
+16:55 DARIN: Lots more processes got added later, like the networking, the GPU
+process, and NaCl. I can tell the story about those, too, if you're interested.
+
+17:08 SHARON: Oh, sure. Yeah, let's hear it.
+
+17:08 DARIN: OK, so 2009 era, I think, maybe 2010 - I don't know - somewhere
+along the way, we started building Chrome for Android. And you might recall I
+described how the renderer was really kind of a glorified image viewer, or the
+browser, browser was sort of an image viewer and the renderer's job was to
+produce a bitmap. And then we send it over to the browser, the browser would
+draw the bitmap. Mobile systems were not going to work very well if this is the
+way the drawing was going to work. If you think about how scrolling works or
+worked back then, scrolling a web page back then meant telling the computer to
+please memmove all the pixels, and then to draw another bitmap where pixels are
+not existing yet and need to be drawn. So you do a memmove followed by a
+memcpy. And so this is how original Chrome was built. If you were scrolling, it
+would be, oh, we need to shift pixels, and here's the bitmap. We need to stick
+in the part that's exposed. Do that all quickly, and do it over and over again.
+And that kind of operation is just not good if your goal is like nice
+responsive scrolling on a touch screen. Instead, the way mobile systems were
+built is using GPU rendering and compositing engines powered by GPUs, so that,
+instead, you are offloading a lot of that work to the GPU. So it was necessary
+to restructure Chrome's rendering pipeline for mobile, at least. But because we
+were doing that, we can also take advantage of it on desktop. Meanwhile, we
+were also on desktop starting to invent things like WebGL. Initially, WebGL,
+the precursor to that was this plugin called O3D, which is a 3D graphics plugin
+using the wonderful plugin APIs that I talked about before. But it provided
+this way to have 3D graphics scenes and build immersive kind of 3D content.
+That team, at some point, switched their sights on how to make that a standard
+through WebGL. Wonderful stories around that. But it also entailed figuring out
+how to do OpenGL, essentially, because WebGL was just OpenGL ES, and how to do
+that from a renderer, from that blink child process, how to do it there. And
+really, that meant that, OK, this process is going to be - these sandbox
+renderers are going to be generating a stream of GL commands. Where do they go?
+What do we do with that? And also, we know that it's possible to write shaders
+and possible to write GPU commands that can really wreck - can cause havoc, can
+be problematic, can cause the system to crash your process. So we don't want
+that happening in the browser process because we want the browser process to
+stay up so it can [INAUDIBLE] the manager.
+
+20:21 DARIN: So the GPU process was born. This will be the process that
+actually talks to the OpenGL driver or DirectX under the hood via ANGLE on
+Windows. And so now, we set up another pipe from the renderer over to the GPU
+process, and the stream of GL commands are being sent over there. And over
+there, it's talking to the driver. And if you sent something bad, driver is
+going to say no bueno and crash your process. And we would find that the
+browser would see the GPU process died, and it would maybe give you a warning
+or let you reload the page, and it will try again. As that's done, that's how
+we therefore were able to leverage processes to give us that isolation, but
+also give us that robustness, give us that capability. And that led to a lot of
+complexity, but also a lot of really amazing sophistication around the
+compositing engine. Chrome CC library was born subsequently, and all these
+things that have led to the modern way that we render the web on Chrome now.
+Skia learned how to render to OpenGL, et cetera, and the GPU process.
+
+21:35 DARIN: Next one came along was the network process, which was really born
+out of the idea of, gee, wouldn't it be nice to isolate the networking code
+into its own process that could be more tightly sandboxed? Because the
+networking stack tends to be a surface area that's accessible by attackers.
+Just like the V8 and JavaScript engine is parsing lots of stuff and very
+exposed to attack surface from would-be attackers, the network stack, same
+thing. You've got HTTP parsing and various other kinds of processing happening
+very close to content that attackers can control. And so this project, quite
+rather elaborate project to move the networking stack out of the browser
+process out of that broker process, but to, instead, its own process and have
+all the pipes go various IPC channels connecting to there, instead, was born.
+And I think this was more born in the era of Mojo IPC, where we had a more
+flexible IPC system that could help support that kind of transition, but still
+tons of work and quite a radical change to the flow of data and the way the
+system works. Previously, just to give a little aside, when a renderer is
+making a network request, the browser process acting as a broker needs to
+audit, is it OK for that guy to be requesting this thing? Think about all the
+kinds of rules that might be there, CSP, other kinds of things, and the
+security origin privileges associated with it and what we want to allow a
+renderer to actually access. Simple stuff like we support WebUI like Chrome
+colon pages in the context of, they load in a renderer process, that renderer
+process should be allowed to access other things from Chrome colon, right? But
+a web page shouldn't be able to. We don't want the arbitrary web pages to be
+poking around and seeing what's available in the Chrome colon URL. So that's
+like a simple example of where we honor that isolation. And so the browser
+process, having the network stack in the original incantation of Chrome makes
+no sense. It can apply these rules right there. Safe browsing was integrated
+there. Lots of different kinds of network filtering could be done there. Moving
+that to another process was a big change because now browser is the one that
+has the smarts to do auditing, but the data and all the requests are going to
+this other process. So making that work meant a lot more plumbing. And I think
+complexities ensued. But it's awesome to see it happen.
+
+24:20 DARIN: Anyways, I mentioned Native Client. So that was a precursor to
+Wasm that was a big investment by the Chrome team to find a way to bring native
+code to the web in a safe, secure manner. The initial take on it was, if you're
+running native code that came from the web on a system, that's scary. It could
+do like anything, right? Well, no, let's restrict the process capabilities, but
+even with a restricted set of capabilities, you can't necessarily restrict
+everything on Windows or Mac or Linux. There's always some limitation to the
+sandbox capabilities. And in many ways, the sandboxes that we implemented are
+kind of just an extra level of defense. If you think about it, the JavaScript
+Engine is already a sandbox, right? It already limits the capabilities. The web
+rendering engine, all the different kinds of security checks throughout the
+code are various forms of sandboxing. And then finally, the process in the way
+we restrict its capabilities is that next last defense. Well, running native
+code with only that last defense in place is not enough. So Native Client was
+designed to be not only to be native code that could be highly auditable, so
+that you could make sure that it's not allowed to jump to an address that it
+doesn't have code for, that it's not allowed to do things outside the set of
+things that it's allowed to do. So it had a lot of complexity as well in terms
+of how the process has to be set up in terms of the memory layout and various
+other details, which maybe I'm happy to not remember. And - but it meant it
+needed its own process type. Even though it integrated kind of like a plugin,
+it couldn't just be a plugin. It needed its own process type. And there had to
+be 64-bit variants and 32-bit variants, depending on the actual OS, actual
+underlying hardware that you were running on Arm versus Intel, all these
+differences. So yeah, we ended up with leveraging this process model
+extensively to enable these kinds of things.
+
+26:32 DARIN: I think I mentioned the utility process. In Chrome, the utility
+process is this thing you reach for when you want to do something that's
+potentially - like maybe you're dealing with some untrusted input, like you
+want to decode an image, or you want to run something in a process, and you
+just want to make sure that if it's going to do anything, it just dies over
+there and doesn't take down the whole browser process. I think some extension
+install manifest parsing, maybe various other kinds of things like that, would
+happen in a utility process as like a safety measure. Generally speaking,
+parsing input from the web or even the Web Store or things like that, doing
+that parsing in the browser process is a scary thing because you're taking
+input from a third party. And if you're parsing it there, you might have a bug
+in your parser, and that could lead to the most trusted process having been
+compromised.
+
+27:29 SHARON: Yeah, that falls into the whole Rule of Two thing, right, of
+untrusted data. We have a [INAUDIBLE] process. It's in C++. The thing that we
+decided to change is where it gets parsed, so.
+
+27:44 DARIN: That's right.
+
+27:44 SHARON: That makes sense.
+
+27:44 DARIN: Yeah, so the sandbox processes get used as this primitive to give
+us that extra safety measure.
+
+27:57 SHARON: So the other process type I can think of that wasn't just covered
+there was extensions. Is there anything to say there?
+
+28:02 DARIN: Sure, of course.
+
+28:02 SHARON: Of course.
+
+28:02 DARIN: In some ways, an extension process will show up that way in
+Chrome's Task Manager, but I believe it's usually just powered by a renderer,
+an ordinary renderer, because so extensions have background pages or background
+event in, I guess, the Manifest V2, it was background pages. Manifest V3, it's
+now just event pages or service worker type construct. And those need a process
+to run in. So the extensions get to inject some code that runs in the renderer
+of the web page, usually in an isolated world, so it can see the same DOM. If
+you've given the permission for the extension to read website data or to
+manipulate website data, it can do that by injecting a content script that will
+run in the same process as the web page that it's reading or modifying. But it
+will run in an isolated JavaScript context so that it's not seeing the same
+JavaScript variables and such. But it can still see the DOM. And that's meant
+to give a lot of capability, but also have a little bit of protection because
+it's so easy to accidentally interfere with the same JavaScript variables and
+things like this. OK, so extensions have that piece that injects a content
+script, but they also have a - usually, they can have this event service worker
+or background page that is their central place, process place for code to run.
+And so we do run that in a renderer process. And so for example, if the
+extension that's injected into a page wants to get some capabilities, it would
+talk to its service worker, who would then have the capability to ask for
+certain extension APIs to maybe understand all the tabs that are in your
+system, depending on what permissions it was granted. And then finally, with
+extensions, you also have the extension button and a dropdown that can occur
+there, which a web page can be drawn there by the extension. And that's going
+to be hosted in a renderer process, too. But that would be a web page that
+lives at a Chrome extension colon URL. And so you have these different pieces
+of the extension model where code from the extension can be running, and it,
+via some messaging channel, can talk to the other parts of itself that run in
+potentially likely different processes.
+
+30:37 SHARON: You mentioned service workers there, and those are kind of
+related to all this, too. So can you tell us a bit more about those?
+
+30:43 DARIN: Yes, so - well, OK, so backing up, in the context of extension, if
+we talk about background page first, the original idea with extensions was, OK,
+I'm injecting stuff into pages so I can modify things, but I also need like my
+home base. I need my context where - I need a place where my persistent script
+is running or where I can manage my databases, and I have just one place for
+that. And it's also a place where I can get elevated permissions to access
+other Chrome extension APIs. So that idea of a background page that the
+extension can create that's ever present so it's like a web page, but it's
+hidden, it's in the background, and content scripts that are injected into web
+pages can talk to it. So they can say, oh, I'm on this page. Give me some rules
+that I should apply to it or something, depending on the nature of that
+extension. OK, so but background pages are, unfortunately, persistent. And they
+live for the whole life of the browser. And they use up memory. They use up
+resources, even if nothing else about the extension needs doing. Even if the
+extension is not loaded into any web pages, that background page is sitting
+there. And so this was [INAUDIBLE] quickly realized, this is not great. This is
+a waste of resources for the system. We should have some policy for how we
+should close that background page down and only need to create it when
+necessary. In the context of, I think, Chrome apps, which is a thing that's no
+longer a thing, we created this concept called event pages, which allowed for
+these background pages to be a little more transient, that come into being only
+as needed, which is a much more efficient approach.
+
+32:28 DARIN: However, when it came time to bring that to extensions, at the
+same time, Service Worker had been created, which was a tool for web pages to
+be able to do background event processing. So the decision was to adopt that
+standards-based approach to how to do background processing. And so Service
+Worker is the construct that Manifest V3 allows extensions to use for that sort
+of background processing. Big difference between service workers are that they
+are not web pages. They're just JavaScript. But they can listen to different
+kinds of events. So just like a web worker, shared worker, service worker, they
+are without UI. They are without any HTML. They just have the ability to - but
+they have some functions that are given to them on the global scope that lets
+them talk to the outside world, to talk to the web page that created them, or
+in the case of Service Worker, they actually have events they can receive to
+handle network requests on behalf of the page. That's one of the main uses for
+them in the context of the web. A web page would have a Service Worker register
+it with the browser to say, hey, please contact my service worker if you are
+making a request for my origin. And that gives the Service Worker the
+opportunity to specify what content should be used to satisfy a URL. It could
+load that content out of a cache, and the Service Worker API includes APIs for
+managing caches and things like this. So all of that system that was built to
+kind of enable web pages to operate more robustly in the context of poor
+network connectivity or to get performance improvements for applications that
+are more single page applications that have a basic fixed shell that should
+load out of cache and then they make network requests to the server to get the
+data that populates some application UI, that model Service Worker was really
+designed for. But it seemed a very good fit for extensions. And it gets us out
+of the world of having these persistent extension background pages. So Manifest
+V3 says, if you want your content script to have access to privileged things,
+you go through a system, a Service Worker. And the Service Worker will get
+spawned in a renderer process. What renderer process? You don't know. It's up
+to the system. Chrome will make a decision there based on all of its usual
+rules around what other origins are in that process, thinking from a security
+isolation perspective, and so on, and so forth.
+
+35:22 SHARON: Cool. A lot of these process types have been added over time as
+the need for them arises. Like, oh, we want to put network stuff in a separate
+process. So apart from adding more process types, what have been other big
+changes to the multi-process architecture and processes in Chrome in the many
+years since launch?
+
+35:44 DARIN: The biggest one by far is the per site isolation, the site
+isolation work that was done.
+
+35:51 SHARON: We'll talk about that more next.
+
+35:56 DARIN: Yeah, so, I mean, well, I'll just say, so Charlie Reis was an
+intern on Chrome team back in the day during the pre-release period of Chrome.
+And I remember the conversations where we were like, gee, wouldn't it be nice
+if instead of isolating based on per tab, it was isolating per origin? And I
+think he was doing research on that topic, too. And he had all these ideas for
+this kind of a thing. And so it was really kind of very early on that we were
+having these conversations. But even very early on, it was like, this is going
+to be a big change, you know? No longer is it the idea that it's a big change
+to the rendering engine itself, like how frames could be served by different
+processes. So in order to isolate based on origin, you have to say a frame
+where an ad might live would actually have to be served by the process for that
+origin. And so now no longer is the whole frame tree just in one process.
+That's a big change. But built on top of the infrastructure we had, it was
+possible to imagine it, and it was quite a journey to get there. So that was
+probably the biggest change to the architecture. But like I mentioned before,
+actually, other big changes were definitely the introduction of the GPU
+process, definitely the introduction of Mojo IPC. Before Mojo IPC, the way
+things worked was, basically, messaging was much simpler, in some ways, easier
+to understand, but also much more the case that there were these files that
+really needed to know about everything in their world, like the render process
+host and the render process, the render view host and the render view, the
+render frame. The render frame host didn't exist then, but they came about
+because of site isolation, really. But the render view, render view host became
+this thing that represented the web page, and render view host in the browser,
+render view in the render. And for any feature that required brokering out to
+the browser to get access to something, essentially, the render view, the
+render view host had to be participants in that because they had to be kind of
+routers for that traffic. That's not very scalable. You start adding lots of
+engineers, building lots of different features that need lots of different
+capabilities. And these files start growing hairs and knowing about too many
+things. And it becomes really hard to manage.
+
+38:38 DARIN: On top of that, you start to have things where you say, gee, I
+really wish this system could be live in a different process. I mentioned the
+networking process. All these events were coming through these different kinds
+of crossroads of hell files. That was how I liked to call them. And in order to
+take a subset of that and move it to a different process, now you have to redo
+all that plumbing. And so the amount of layers of repeating yourself for
+plumbing IPCs felt very out of control for - maybe how much work you had to do
+to unlock a certain feature just seemed out of control. And so Mojo really was
+inspired by how to eliminate a lot of that, to have a system that's more
+endpoint to endpoint-based and all the flow of data would no longer be
+dependent on all of these kinds of routing classes that handled all this
+routing. And instead, you could just say, I have an endpoint. I have an
+endpoint over here. This one's privileged. This one's not. And if I want this
+one to live over here, I can do that. I can just move it around freely. And all
+the routing is taken care of for me. And so that was a big change. And there's
+many artifacts in the code base that sort of reveal the old system, right? In
+many ways in which the product is built still resembles that old system. The
+idea that if you look at a render view, render view host, there's an ID, a
+routing ID associated with that. The concept of routing IDs are not needed in
+Mojo anymore because the pipe itself, the Mojo pipe is like an identifier, in
+some sense. Of course, so much of our system is built up around the idea that
+tabs have these render view IDs, and frames have render frame IDs, and
+processes have process IDs. And so many systems deal with those integers that
+it's been unthinkable to not have those anymore. But in some sense, they aren't
+really needed. If we were to build things from scratch from anew with the Mojo
+system, you wouldn't need it.
+
+40:50 SHARON: Do you think if you were to start redesign the whole
+multi-process thing now, given how not just the internet is used, but also the
+devices that are out there, I think you would probably want to have multiple
+processes for things. But do you think there would be significant changes to
+how the system overall is designed or put together if one were to start now?
+
+41:16 DARIN: Well, yeah, I mean, it's always a question of where you're
+starting from and what the constraints are that you're dealing with. We were
+dealing with taking WebKit, which we didn't really have a lot of ownership of.
+And it was open source, but we also had limited bandwidth to go and fork it and
+manage that fork. And so to kind of try to create multi-process in the context
+of this big significant piece that we really can't change or do much about
+definitely limited us. So we had early ambitions and ideas. Like I said with
+Charlie about site isolation, it wasn't going to be then that we could realize
+it. It needed to be in a place where we had ownership of Blink. And not just
+ownership, I mean capability to go and change it and to own the consequences of
+changing it, to be able to manage that. We needed that, and we needed a lot of
+other pieces. So if I'm starting over, I also have to - it's sort of like,
+well, what am I starting from, right? But certainly, I feel like a lot of
+lessons along the way inspired Mojo and the design there. And I feel like
+that's a system that that sort of system would allow for an architecture that I
+think would be better in many ways. And I'm very biased because that's
+something I've worked on, and it was inspired by things I saw that weren't
+great about the way that we built Chrome originally, although, in many ways,
+the original setup with Chrome was born of pragmatism and minimalist in many
+ways, trying to achieve - Chrome was very focused on being a product first, not
+a browser construction kit. And so the idea that it needed to morph into a lot
+of different things wasn't there in the beginning. In the beginning, it was,
+you're just building a browser for Windows XP Service Pack 2. That's it,
+nothing else. OK, now Vista. You got to worry about Vista, too, sorry. But just
+that's it. And then later on, you add Mac. You add Android. You had Chrome OS,
+iOS, Chromecast, et cetera, et cetera. And suddenly your world is very
+complicated, and the needs of this system is way more. And the value of
+malleability becomes higher. Look at the investment in views, et cetera, to
+allow cross-platform UI, and then Mojo to allow a much more flexible system
+under the hood. So it depends on your constraints in a lot of ways.
+
+43:43 SHARON: Yeah, that makes sense. Something you said about even now in the
+code base, you can see remnants or suggestions of how obsessed maybe of how
+things used to be. So one of the things that makes me think of is about the IO
+and UI threads because I feel like people used to talk about those more. And
+now that's maybe changing a bit. So how come these are the only times we hear
+the term "thread," really, in all of this? And what are the IO and UI threads
+that can you just tell us a bit about?
+
+44:20 DARIN: Oh, yeah, threading is a super fun topic. Now we have all these
+task runner concepts and systems for giving you a task runner that's on an
+isolated thread or whatever. And systems like Mojo allow you to not really have
+to do a lot of plumbing to compensate for your choice of thread where you want
+something to run. You can just indicate where it should go, and that happens.
+But OK, originally, the design of the system was there was a UI thread, and
+that's where all the UI lives. So the HWNDs, the Window handles and all the
+Win32 stuff would go there. Input painting come in there. Then there was - so
+early on, I like to tell this story because one of the very first versions of
+Chrome, we had just that UI thread sending data to a renderer processes. And
+the renderers would have their main thread where they ran JavaScript and
+everything. So there was just these two threads in two different processes.
+That was kind of it. In the browser process, there might have been the system
+was probably doing a lot of other stuff with its networking stack and DNS
+threads and such. But we weren't doing any. That wasn't us. That was probably
+libraries we were using. So we had these two threads in two different processes
+and IPC channel. And so you send the input down to the renderer. The renderer
+sends you a bitmap. OK, Google Maps. Imagine Google Maps. And imagine you're on
+a single core, non hyper-threaded laptop. And you take your mouse, and you
+click on that map, and you start dragging it around. And you expect to see the
+image tiles moving around, right? And but for some reason, in Chrome, on that
+device, [SNAP] nothing happens. You just move your mouse around, and the image
+is stuck there. You're like, what's going on? It works fine on this other
+laptop. Why not on this laptop? Turns out that on that device, in that setup,
+the input stream was coming in. And basically, we were sending all this input,
+and the input events were taking priority in the Windows Event pump over any
+painting and/or reading from our IPC channels. And so, as a result, we were
+just sending input events to the renderer. It was doing work, generating new
+images. Those images were coming to the browser and backed up in some pipe and
+not really being serviced, not really making their way. And so we kind of came
+to the realization of several things. One is, we need to throttle that input
+going to the renderer, but we also probably need to have some highly responsive
+IO threads that could be dedicated to servicing the pipes, the channels, the
+IPC channels, both in the browser and the renderer, actually. And so what was
+born from that was the IO thread. And the IO thread was meant to be highly
+responsive thread for processing asynchronous IO. That's really what its name
+should be - highly responsive, non-blocking IO thread - because the name IO
+thread subsequently confused lots of people who wanted to do blocking IO on
+that thread, like read a file or something. And we had to put in some
+restrictions in the code to always let you know not to - that this function is
+going to - there's certain runtime assertions if you try to use certain
+blocking IO functions in base on the wrong threads. And alongside that, we
+invented something called the file thread. Said, this is the thread where you
+read files. This is the thread where you write files because we don't want you
+doing that on the UI thread because the UI thread needs to be responsive to
+user input. So don't do blocking file IO on the UI thread. Don't do it on the
+IO thread either. Do it on the file thread. So -
+
+48:14 SHARON: That means they're all running in the browser process.
+
+48:20 DARIN: In the browser process. The renderer got its own IO thread, too.
+So the renderer would have its main WebKit thread and its IO thread. So it was
+sort of a symmetric system. You had IPC channel, which was wrapped with a class
+called `ipc_channel_proxy`. These things still exist in the code base. And
+ChannelProxy was a way to use an IPC channel from a different thread. But the
+IPC channel would be bound to the IO thread. All of those things I just
+mentioned still exist, and Mojo was built on top of those channels. But the IPC
+channel provides that underlying pipe. So it's kind of IPC channel is
+one-to-one with an OS pipe. Mojo has this concept of pipes which are more like
+virtual pipes, and they're multiplexed over OS pipe, over an OS pipe.
+
+49:08 SHARON: OK. Yeah, because I think, yeah, now you hear non-blocking IO,
+but I feel like maybe it's just what part of the code base you work in. But
+running things, making sure things run on the right thread seems to be less of
+a problem than it used to be.
+
+49:27 DARIN: Yes. I think there's a lot of reasons for that, a lot of maturity
+in the system. But also, I think some of the primitives are set up nicely so
+that you can more easily have things running. In some ways, we used to have
+this concept of, yeah, we very much had this. Still, in some ways, still have
+this, but the idea that there is a UI thread, that there's an IO thread, and
+that there is a file thread, and you pick which thread you're going to use.
+Now, there's a whole pool of blocking IO threads. And you don't specifically
+say, I want the file thread. You say, I have blocking IO I want to do, or give
+me a - I want to put it on a thread pool. The IO thread used to be like where -
+it may be still the case that some systems would just live there only because
+maybe for latency reasons - like, cookies is a good example. We knew that we
+wanted to be able to respond quickly to the renderer if it was querying a
+cookie database. So we want to be able to service that directly on the IO
+thread. And so there'd be a collection of these things that were maybe somewhat
+sensitive, and but we wanted to have them live and be on the IO thread. And so
+that idea of some things live on the IO thread was born. But I think those
+things are few. And you really have to highly justify why you should be on that
+thread. And so most things don't need to be. Just be on the UI thread. It's OK.
+Or structure your work so that the part that is expensive and blocking goes to
+a blocking queue.
+
+51:00 SHARON: So partly for these threads, sometimes you see checks. Like,
+check that this is running on a certain thread. But in general, is there a good
+way to find out what process a certain block of code runs on? Because some
+things we know - if you go to a third party Blink, whatever, you kind of know
+that that's going to run in a render process, but just looking at the code,
+like looking in code search, can you know where something is going to -
+
+51:25 DARIN: [INAUDIBLE] very early on to try to deal with this. So like if you
+go to the content directory, it's a good one to look at. You'll see a browser
+directory, subdirectory, a renderer subdirectory, and a common directory. And
+there's some other ones that have these familiar names. We use that structure
+all throughout the code base for different components. So if you go components,
+components foo, you'd see browser, renderer, common, maybe a subset of those,
+depending on. And so the idea is, if it's code that should only run in the
+renderer, it lives in the render directory. If it's code that should only run
+in the browser, it lives in the browser directory. If it's code that could run
+in either, it lives in the common directory. So you'll see mojom definitions in
+common directories because mojom is where you define the Mojo interface that's
+going to be used in both processes.
+
+52:12 SHARON: Oh.
+
+52:12 DARIN: Yeah, we also have this code separation was also kind of born out
+of this idea at one point in time that we might generate a totally different
+binary for browser and renderer. And we used to have browsR. I'm calling it
+that way because it didn't have an E at the end, so browsR and capital R, and
+then rendR or something like this. And these were the two processes, the two
+executables. And they could just compile whatever code they needed for their
+purpose. Like WebKit would be in the renderer, and browser would have not
+WebKit. It would have other things. And so these separate directories also
+helped because it was like, that's the code that's going to go into that
+process literally. And fast forward when Sandbox came along, the team was like,
+nope, it's got to be the same executable for both browser and renderer and
+should probably be called chrome.exe instead. And then that idea kind of that
+they were separate executables and separate code kind of went away. And
+instead, all the code for Chrome went into just this big DLL on Windows. And
+the amount of shared code between the EXE and the DLL is very small, maybe a
+little bit from base and such. But yeah, this idea of tagging the directory
+structure in such a way that makes it obvious of like what process this code
+belongs in, I think it was a big help, and it was a good choice. And it gives
+people a little clarity of where they are and what they can use.
+
+53:49 SHARON: What about for non-browser renderer processes? What about GPU
+network? How do you know that this is running on the network process versus
+this is how this part of this section of the code is interacting maybe with the
+network process?
+
+54:05 DARIN: Sometimes it can be a little bit of good luck. And sometimes it
+might not be as obvious. I don't think this sort of - this structure that I
+described was used for plugins, so there's a plugins directory, which may still
+be around in some fashion or might be mostly gone. I don't know if when the
+network process transition occurred, if this annotation was really maintained.
+I actually don't think it was because I don't remember seeing network
+directories. But I could be wrong. There might be some of them. I'm not as
+familiar with the code for the networking process. But I think this convention
+has helped us a lot and would be valuable to use in more places. For GPU,
+there's a lot of symmetric code, probably code that runs in all processes, but
+still this convention probably would make sense. But yeah, I think that for
+some of those things, when you get like into the network world or you get into
+the GPU world, you're also kind of in a more focused world, a smaller world.
+And there's probably many other things you have to learn about that domain.
+
+55:16 SHARON: Yeah, the GPU stuff seems very, very difficult. And I certainly
+don't know how that works. OK, so -
+
+55:23 DARIN: [INAUDIBLE] on there.
+
+55:23 SHARON: Yes, so when it comes to process limits and performance and all
+that kind of thing, so we have process limits, but you can go over them. And
+can you tell us a bit about process limits, how they work, what happens when
+you reach the limit?
+
+55:39 DARIN: Hmm, yeah. So process limits, they exist to just have a reasonable
+number of processes allocated for some definition of reasonable. At least early
+on, that definition was based on how much RAM you had on your system. And as
+computers got more and more RAM, that definition needed to be adjusted. We
+assumed some overhead for individual processes. It's probably wise to put some
+limits on how many we create. The allocation of those processes, it's best to -
+kind of viewed as best to distribute the tabs across them as best as we can and
+the origins across them now and the side isolation world to give more isolation
+between different origins, to give more isolation between the different apps.
+But at some level, you run out, and you need to now allocate across the ones
+that are already in use. There's some hard rules around privileged content,
+like Chrome colon URLs. They should not mix with ordinary web pages. But if
+push comes to shove, we'll put a whole bunch of different origins content
+together into the same process, just ordinary web pages, not trusted content.
+
+56:52 SHARON: What happens if you just open a ton of tabs with a whole bunch of
+different pages open, and you're basically stress testing what Chrome can do?
+What happens in that case?
+
+57:08 DARIN: It creates a lot of processes. It uses a lot of system resources.
+It uses a lot of RAM. I think that this has been, I'd say, a battle for Chrome
+across a lot of its lifetime and more recently, is how to manage these extreme
+cases. And increasingly, these extreme cases are not actually odd or unusual.
+They'll do a lot of browsing. People click on a lot of links. People create a
+lot of tabs. People don't really close their browsers. They just leave it
+running. And they come back the next day, and they continue where they left
+off. And they open more tabs, and they do more surfing. And they just collect
+and collect and collect tabs. And maybe they create more windows because maybe
+they have some task that they're researching, and then they get interrupted and
+they come back to it later. But they start to accumulate these windows full of
+things that maybe they mean to come back to. And so that problem of just having
+lots and lots of stuff and lots and lots of processes, well, Chrome under the
+hood is like, I'll do my best. You wanted me to do all this stuff. I'm going to
+do it. Let's see what I can do. And on a system like Windows or Mac where
+there's a lot of RAM maybe, Chrome's thinking, OK, you wanted me to use the
+RAM. I'm going to use the RAM. You wanted all those tabs. And then even on
+those systems where maybe you're running out of RAM, but there's virtual
+memory, there's disk space, all right, let's use it. Let's go. And so I think
+it's really quite a challenge, actually.
+
+58:44 DARIN: The original idea of Chrome was, yeah, make it possible for web
+pages to take advantage of the resources of your computer. Let it allow web
+pages to be more capable because of it, and not be - the old world prior to
+Chrome was single-threaded browser, all web pages on the same thread. Like, you
+could have a dual core machine, and it wouldn't matter. It wouldn't make your
+browser any faster. But now with Chrome, no problem. You got dual core. You got
+eight cores, whatever you got. We can have all of those things saturated with
+work and allow you to multitask on the web and do lots of amazing things. But I
+think it's still a resource management challenge for the browser because on one
+hand, you want to give that capability, but on the other hand, you also don't
+want to - how much power should you be using? What if the laptop's not plugged
+into the wall? What if it's just running on battery? What is the right resource
+utilization for Chrome? I don't think that's a solved problem at all. There's
+various systems in place to throttle the resource utilization of background
+tabs. Timers, for a long time now, have been throttled, but throttling other
+things. I know there was a lot of research done into freezing tabs, so
+literally suspending them and not letting them do any work. But with that comes
+challenges of what do you do with all the IPCs that are inbound to those
+processes? They're backing up on pipes, and that's not great. If you unfreeze
+them, now there's a blast of IPCs coming in that they suddenly have to service.
+That doesn't seem great. Do you drop those IPCs on the floor? Probably not.
+Now, the process would be in some weird state, and you might as well have to
+just kill it, which, of course, is the case on dev systems like Chrome OS and
+Android. They do have to just kill the processes because of the limits of those
+devices. So, yeah, I've been a proponent of just being aggressive about killing
+processes on desktop in general. I think there's some balance there that's
+right. It's probably not right to keep all the tabs open, all the processes
+open. We should be, I think, judicious about what we keep open, keeping the
+workload reasonable, instead of making it like a, oh, yeah, I will rise to the
+challenge of dealing with thousands of tabs or thousands of web pages across
+100 processes, even if - maybe it's somehow possible through heroic effort to
+make Chrome capable of doing such a thing in an efficient manner. But does it
+mean we should? Who needs 1,000 tabs all running around doing work at once you
+know? You don't. You really don't. Nobody does.
+
+61:32 SHARON: So this is kind of the basis of the goal for Arc, right, which
+is I think it closes your tabs overnight or something. And Arc is what you work
+on now and is a Chromium-based browser. So for embedders of Chromium, let's say
+the browser kind, how much control do you have over how processes are used,
+allocated, if you embed content? Like, are you able to just say, oh, I don't
+want a network process. I will just put this all in the browser process. Can
+you do that?
+
+62:07 DARIN: Hm. You can do anything you want. It's just code. No, but as a
+browser embedder, as a Chromium embedder, you're shipping Chromium. So Arc
+browser ships a copy of Chromium. And Arc browser includes changes to Chromium
+as needed to make it work. Of course, that's possible. Of course, you could
+change a lot of stuff and make a big headache to manage it all, right? So
+there's some natural limits. You don't want to change too many things, or else
+you won't be able to really manage it going forward. You want to take updates
+from the mainline, incorporate improvements, but you also want to preserve some
+differences that you've made. Well, how do you do that? And so change
+management is a challenge. So there's a natural limit to how much you want to
+alter the base functionality. Instead, it's - anyways, the product like Arc is
+not so much differentiating on the basis of Chromium code or content layer.
+It's not really its purpose or goal. Its purpose is to differentiate at the UI
+layer and with things like what you mentioned and other things as well. Yeah,
+and so, of course, if one were to go down the path of could we optimize process
+model better, that would be in the realm of things that would be great to
+contribute to Chromium, so that it could be part of the mainline and therefore
+not be something that you have to maintain yourself. That's how I would
+approach it as a Chromium embedder.
+
+63:47 SHARON: OK, that makes sense. Yeah, if it's in Chromium, you don't have
+to worry about the updates, and you just get -
+
+63:53 DARIN: Turns out there's an army of engineers who would make sure it's
+never broken. You just gotta write some tests.
+
+63:59 SHARON: Oh, wow.
+
+63:59 DARIN: [INAUDIBLE] those tests.
+
+64:05 SHARON: So with non-browser embedders of Chromium, like, say, Electron, I
+don't know how familiar you are with that, but they presumably would have
+different needs out of how Chromium works, basically. I don't know if you know
+what they're doing with any processes.
+
+64:25 DARIN: I mean, I've used VS Code. That's a famous example of a Chromium
+embedder that you might not realize is using Chromium or built on top of it,
+that one might not realize that. But if you open up Task Manager and you look
+at VS Code, you'll see all the glorious processes under there. And so have they
+or Electron or any of these, have they altered things there? Maybe. I mean,
+there's some configuration one might do. If you're building an application
+that's very single purpose, like VS Code or Slack or - what are some other good
+examples, there's quite a few that are built on top of Chromium - they're more
+single purpose towards a single app, right? Of course, VS Code is pretty
+sprawling with all the things you can do in it, but at the same time, it could
+be the case that they don't have the same security concerns. They don't have
+the same idea of hosting content from so many different sources. So maybe they
+would tune the process model a little differently. Maybe they would decide, I
+don't really need as many processes because I'm managing things in a different
+way. It's not a browser.
+
+65:34 SHARON: Yeah, you're not handling all of the untrusted JavaScript of the
+web that you have to be -
+
+65:42 DARIN: Right, I'm not so worried about this part of my application dying
+and then wanting to keep the rest of it still running or something because that
+would still be considered a bug because part of my app died. And so some of the
+reasons for multi-process architecture might be a little different.
+
+66:01 SHARON: Right. And more just for fun, having worked on now an embedder of
+Chromium, how has that experience been in terms of decisions that were made
+when you were putting together the multi-process architecture? Are there things
+where you were like, oh, no, past me, if you'd done this differently, this
+would be easier now.
+
+66:20 DARIN: I would say I'm very thankful for Mojo IPC, made it very easy to -
+one thing that I've found is that it's possible to do a lot of amazing things
+on top of Chromium without actually modifying Chromium. And the Content API and
+Mojo IPC makes a lot of that really possible. So it's a very flexible system.
+There's a lot of really great hooks that let you interact with the system all
+the way from extending the renderer to extending the browser. And to be able to
+build stuff and layer it on top of a stable system is amazing. When I was
+working on building an Android browser, I built a tracking prevention ad
+blocking system for Android and was able to do it without modifying Chromium. I
+thought that was amazing.
+
+67:19 SHARON: How are you using Mojo? Because Mojo is typically going between
+the processes. So if you're not really changing how the processes work, what do
+you use Mojo for?
+
+67:26 DARIN: Oh, well, in that case, it was used to communicate a rule set down
+to the renderer. And then at the renderer level, I would inject a stylesheet to
+do content blocking or to apply a network filtering at the link layer. So there
+are a combination of Blink Public APIs and Content Public APIs. There are
+actually enough hooks to be able to filter network requests and insert
+stylesheets that would apply display none to a set of DOM elements. So but to
+do that efficiently, it was necessary to bundle up those rules into a blob of
+memory that you would just send down to the renderer process, to all render
+process, so it'd have it available to them so they could just directly inspect
+like a big hash map of rules. And so being able to - like I said before, when
+the IPC system is just like - when it's decoupled like that with Mojo, it makes
+it possible to kind of graft on these systems that they interact with APIs over
+here, and that endpoint talks to some endpoint over here in the browser
+process, which can have, like I said, like a rules data that it might want to
+send over and that kind of thing. And so being able to build those kinds of
+systems, and I think if you look at just how a lot of features in Chrome are
+built, they're built very similarly, too. They build on top of the Content API
+that provides the various hooks. They build on top of Blink API. Sometimes a
+feature needs to live in the renderer and the browser process. Like autofill is
+always the classic example of this early on in Chrome or password manager.
+These are systems that need to crawl the DOM. They need to poke at the DOM.
+They need to understand what's there. They need to be able to insert content or
+put overlays in, or they need to be able to talk to the browser where the
+actual database is, all that kind of stuff, and looking at different load
+events and various things to know in the lifecycle of the page. So, yeah, I'd
+say I'm thankful for a lot of these design choices along the way because I
+think it's led to Chromium being so useful to so many people in so many
+different ways. Obviously, it empowered building a really great browser and a
+really great product, but it also has empowered a lot of follow-on innovation.
+And I think that's pretty cool.
+
+69:53 SHARON: It is pretty cool. So Chrome was released in 2008. It is
+now 2023. So as math tells, it's been 15 years. We like numbers that end in 5
+and 0. So - I don't know - it's very cool. I remember when Chrome came out. And
+I don't know. Do you have any -
+
+70:08 DARIN: Yeah, for me, it's more like 17 years because we started in 2006.
+
+70:14 SHARON: Right. So do you have any general reflections on all the stuff
+that's changed in that time?
+
+70:22 DARIN: It's wild. I have a higher density of memories from the early
+days, too. It's amazing. I guess that's how memories work when everything's new
+and changing so much. But yeah, no, I'm very thankful for the journey and very
+thankful to have been part of it. And it was a lot of fun to work on. I mean,
+prior to Chrome, when I was working on Firefox, I did a little exploration on
+adding like a multi-process thing to Firefox, which I thought - just, I was
+learning about how to do IPC, and I was learning - but I was doing it for what
+purpose back then. I think I was just toying around with DCOM. I don't know if
+anybody knows what COM is, but Microsoft's Component Object Model that was like
+all the rage back then. And it allowed for like integrating different languages
+together. WinRT is all built on top of this stuff now. But anyways, Mozilla had
+its own version of COM called XPCOM. And wouldn't it be cool if you could have
+a component that - so you could have components back then that were built in
+JavaScript, and you could talk to them from C++, or they were built in C++ more
+commonly, and you talked to them from JavaScript. But wouldn't it be cool if
+one endpoint could be in another process? So that was something I was playing
+around with in 2004 when I was still working on Firefox. And then when Chrome
+opportunity came along - maybe that was 2005 - I don't know. But when the
+Chrome opportunity came along, I was like, all right, let's do it. IPC channel
+was basically those ideas, but kind of more polished slightly.
+
+72:02 SHARON: OK. Yeah, very cool. I mean, when I first started working on
+Chrome stuff, someone on my team said, any time you change something in base,
+that pretty much is going to get run anytime the internet gets run, which I
+thought was super crazy for just some random software engineer like me to be
+able to do, right? But -
+
+72:20 DARIN: And now it's even more than that if you think about [INAUDIBLE]
+code and [INAUDIBLE]..
+
+72:20 SHARON: Yeah, all the stuff. So do you ever just think about it, and
+you're just like, oh, my god, wow.
+
+72:26 DARIN: Yeah, it's pretty amazing.
+
+72:31 SHARON: So crazy.
+
+72:31 DARIN: It is one of the special things about working on Chromium, is
+that, yes, you can have such an amazing impact with the work that you do there.
+
+72:38 SHARON: Have there been any cases - these are just now unrelated
+miscellaneous questions. But in terms of surprising usages of Chromium, be it
+like maybe the base or the net stack or something, have there been any cases
+where you were really surprised by like, oh, this is being used here?
+
+72:56 DARIN: Well, for sure, the first time I heard about Electron, I was like,
+oh, this is not a good idea. House of cards, you know? It just seems like it's
+such a complicated system to build your app on top of, right? But at the same
+time, I totally get it and appreciate it, and I understand why people would
+reach for it. There's so much good sauce there, so much good stuff and so
+many - there is a lot of really good infrastructure there to build on. Early
+on, I kind of imagined more that things like Skia and V8 and some of the other
+libraries would be the thing that people would make lots of extra use out of,
+right? So I didn't quite imagine people taking the browser's framework like
+this. And we absolutely didn't build it with that purpose. Pretty much every
+choice along the way was highly motivated by making Chrome team's life better.
+Like, Content API was, when we came to the realization we needed it, it was
+like we desperately need it. Just the complexity of Chrome was getting
+unwieldy. We needed to cleave part of it and say, that is this part. We needed
+to somehow draw a line in the sand and say, this is the set of concerns over
+here. And so the idea that all of this could be used for other purposes is
+cool, but it was never really in the initial cards. And I came from working on
+Mozilla, which was, in many ways, browser construction kit first, product
+second. So Chrome was very much like, let's go the other extreme - product
+first, maybe a platform later. And to see it be this platform now is pretty
+cool. But it's pretty far from where we started.
+
+74:50 SHARON: Yeah, kind of - I watched some of the earlier talks you gave
+about the multi-process architecture and Content, not Chrome, came up a bunch.
+And this is, things, I guess, like Electron are the result of that, right?
+Where -
+
+75:01 DARIN: Yeah, it's pretty wild. Yeah, I mean, so Mozilla built this very
+elaborate system called XUL, or X-U-L, which was a XML language for doing UI.
+And it's very interesting, intellectually interesting, maybe different than
+XAML. XAML is way better probably in many ways. But XUL was kind of XHTML
+minus, minus, with a bunch of stuff added on for like UI things. And then it
+had this thing called XBL, which is a bindings language that you could do
+custom bindings. And so anyways, then you build your application in JavaScript
+and Firefox, Mozilla, it was all built this way. So it was like a web page
+hosting a web page. The outer web page was like this XML DOM. The product
+engineers working on that, in order to get some modern Windows sort of thing
+come through, they had to basically go through the rendering engine team to get
+them to do something. And so it really greatly limited the ability for product
+team to actually build product. And there were so many sacred cows around the
+shape of Gecko and how that structure was, that while this cross-platform
+toolkit seemed glorious at first, it ended up being handcuffs for product
+engineering, I think. So, yeah, Chrome started out with Win32 native UI for
+browser UI. You have all the choices you want to make, browser front-end
+engineers. You also have to build a lot of code, but no cross-platform
+toolkits. Views came later.
+
+76:43 SHARON: Right. Well, this was great. Thank you very much. Normally, we do
+a shout-out section at the end. Do you have anything - normally, it's like a
+Slack channel or something like the Mojo Slack channel. I think in this case,
+it's maybe - I don't know if there is a specific thing, but is there anything?
+
+76:57 DARIN: Shout-out to all the team and the engineers making everything
+great.
+
+77:03 SHARON: All right.
+
+77:03 DARIN: Yeah.
+
+77:03 SHARON: Cool. Awesome. Well, thank you very much for chatting with us.
+That was super cool, lots of really interesting background and good
+information. So thank you very much.
+
+77:15 DARIN: Yeah, a pleasure. Thank you so much for having me.
+
+77:21 SHARON: Talk about threads, so IO, UI thread.
+
+77:27 DARIN: Do I get credit for the confusingly named IO thread?
+
+77:27 SHARON: OK, all right, we can cover that. That's cool. Yeah, why is it
+called IO thread when it doesn't do IO?
diff --git a/docs/transcripts/wuwt-e09-site-isolation.md b/docs/transcripts/wuwt-e09-site-isolation.md
new file mode 100644
index 0000000..cd793dd6
--- /dev/null
+++ b/docs/transcripts/wuwt-e09-site-isolation.md
@@ -0,0 +1,691 @@
+# What’s Up With Site Isolation
+
+This is a transcript of [What's Up With
+That](https://www.youtube.com/playlist?list=PL9ioqAuyl6ULIdZQys3fwRxi3G3ns39Hq)
+Episode 9, a 2023 video discussion between [Sharon (yangsharon@chromium.org)
+and Charlie (creis@chromium.org)](https://www.youtube.com/watch?v=zOr64ee7FV4).
+
+The transcript was automatically generated by speech-to-text software. It may
+contain minor errors.
+
+---
+
+Site Isolation is a major part of Chrome's security. What exactly is it? How
+does it fit into navigation? What about security? Today’s special guest telling
+us all about it is Charlie, who made it happen. He's also worked all over
+navigation, making sure it works with all its complexities and remains secure.
+
+Notes:
+- https://docs.google.com/document/d/19LTLcwd2_JfiIklPXY0yu0ktpy-p8za2ZZXXzqBBVIY/edit
+
+Links:
+- [What's Up With Processes](https://www.youtube.com/watch?v=Qfy6T6KIWkI)
+- [Life of a Navigation](https://www.youtube.com/watch?v=OFIvyc1y1ws)
+
+---
+
+0:00 SHARON: Hello, and welcome to "What's Up With That?" the series that
+demystifies all things Chrome. I'm your host, Sharon, and today we're talking
+about site isolation, what exactly is it? How does it fit into navigation? What
+about security? Today's special guest telling us all about it is Charlie. He
+helped make site isolation happen. He's worked on Chrome since before the
+launch, though as an intern, and since then, he has worked all over navigation
+including things like the process model, site isolation, and just making sure
+that changes to that are all secure and that things still work. So welcome,
+Charlie.
+
+0:30 CHARLIE: Thank you for having me.
+
+0:30 SHARON: OK, let's start off with what is site isolation?
+
+0:36 CHARLIE: So site isolation is a way to use Chrome's sandbox to try to
+protect websites from each other. So it's a way to improve the browser security
+model.
+
+0:43 SHARON: OK, we like security. And can you tell us a bit about what a
+sandbox is?
+
+0:50 CHARLIE: Yeah. So sandbox is a mechanism that tries to keep web pages
+contained within the renderer process even if something goes wrong. So if they
+find a bug to exploit, it should still be hard for them to get out and install
+malware on your computer or do things outside the renderer process.
+
+1:05 SHARON: OK. Last video, we talked all about the different types of
+processes and what they all do. So why are we particularly concerned about
+renderer processes in this case?
+
+1:17 CHARLIE: Sure. So renderer processes really have the most attacked
+surface. So browser's job is to go out and get web pages from websites you
+don't necessarily trust, pull down code, and run that on your machine. And most
+of that code is running within this sandbox renderer process. So an attacker
+may be able to run code in there and try and find bugs to exploit. The renderer
+process is where most of those bugs are going to be. It's where the attacker
+has the most options and direct control. So we want that to be locked down as
+much as possible.
+
+1:55 SHARON: OK. Right. So how exactly does this work? How am I getting
+attacked?
+
+2:02 CHARLIE: Right. So all software tends to have bugs, and an attacker will
+try to find ways to exercise those bugs in the code to let them accomplish
+their goals. So maybe they find that there's some parsing error, and so the
+code in the web browser does the wrong thing when you give it some input. And
+for an attacker on the web, that input could be something in HTML or JavaScript
+that makes the browser do something wrong, and maybe they can use that to their
+advantage.
+
+2:36 SHARON: So say I do get attacked. What's the worst that can happen? Should
+I really be concerned about this?
+
+2:42 CHARLIE: Well, that's exactly what we think about in the browser security
+model is, what's the worst that can happen? How can we make that not be as bad
+as it could be? So in the old days when browsers were first introduced, it was
+basically just a program, it's all one process. And it would fetch content from
+the web, and so if something went wrong, there was no sandbox. There was no
+other protection. You were just relying on there not being bugs in the browser.
+But if something did go wrong, that web page could then install malware in your
+computer and your whole machine would be compromised. And so that might give
+them access to files on your disk or other things that you have access to on
+the network like your bank account or so on, which, obviously, is a big deal.
+
+3:28 SHARON: Right. Yeah, it would like to not have other people have that. OK,
+cool. So can you tell us a bit about how site isolation actually works? What is
+the mechanism behind it? What is going on?
+
+3:41 CHARLIE: Sure. So when Chrome launched, we were using the sandbox to try
+and prevent that first type of attack of installing malware in your machine or
+having access to the file system or to network, but we wanted it to do more to
+protect websites from each other. And to do that, you have to treat each
+renderer process like it can only load pages from one website. And if you go to
+visit a different website, that should be in a different process. And so
+there's a bunch of aspects of site isolation for, well, OK, as you go from one
+website to another, we need to use a different process, but the big one that
+made this such a large change to the browser was making cross-site iframes run
+in a different process.
+
+4:30 SHARON: What is an iframe?
+
+4:30 CHARLIE: So an iframe is basically a web page embedded inside of another
+web page. So you can think about this as an ad or a YouTube video. It might be
+from a different origin from the top level page that you're viewing, but it's
+another web page embedded inside it. And so that has a different security
+context that it's running on.
+
+4:54 SHARON: You mentioned it might be from a different origin, and it might be
+useful to know what the difference between a site and an origin is, especially
+as it relates to what we call site isolation.
+
+5:00 CHARLIE: Yeah, so we're being specific in using the word site isolation
+instead of origin isolation. A site is a little broader, so it's a registered
+domain name plus a scheme, so https://example.com would be an example of a
+site, but you might have many origins within that as you get into subdomains.
+So if you had foo.example.com and bar.example.com, those would be different
+origins within the example.com site. Web security models all about origins.
+Those foo.example.com and bar.example.com shouldn't be able to access each
+other, but there are some old web APIs that have stuck with us like being able
+to modify something called document.domain, where two different origins in the
+same site can sometimes access and modify each other, and we don't know in
+advance if they're going to do this. So therefore, we have to put everything
+from a site in the same process because we can't move things from one process
+to another later. We hope that someday we can get rid of that. There is some
+work in progress for that to go away. Maybe we can do origins.
+
+6:10 SHARON: Cool. So the site isolation stuff is all in the browser, so that's
+the browser security model. What's the difference between that and the web
+security model? Are these the same?
+
+6:16 CHARLIE: They're certainly related to each other, but they're a little
+different. So the web security model is conceptually what can web pages do, in
+general, what are they allow to access for another website or for another
+origin or for things on your machine, camera, and microphone, and things like
+that. And the browser security model is more about how we build that and how do
+we enforce the web security model, but also, provide some extra lines of
+defense in case things go wrong. So that incorporates things like the sandbox,
+the multi-process architecture, site isolation. What can we do to make it
+harder for attackers to accomplish their goals, even if there are bugs.
+
+7:04 SHARON: It seems like good stuff to have. So a couple other, maybe
+definitions to get through. So what is a security context?
+
+7:10 CHARLIE: Yeah. So that's the environment where this code is running. In
+the web, it's something like an HTML document or a worker, like a service
+worker, someplace where code is running from what we would call security
+principal, which is, for the web, something like an origin. So if you have an
+HTML document you've gotten from example.com, that's running in a web page in
+the browser that has a security context. And an ad from a different origin
+would be a different security context.
+
+7:49 SHARON: And a security context and security principal always the same, or
+are there times where those are different?
+
+7:55 CHARLIE: No, you can have two different security contexts, like two
+different documents that had the same security principal, and they might be
+able to access each other. Or they might be living in different processes, but
+still have access to the same cookies or local storage, things on disk. So the
+principal is, this is the entity that has access to something.
+
+8:16 SHARON: When people think of site isolation, often, they think about
+navigation as well, partly because that's how our teams are structured, so how
+exactly do these relate, and where in the life of a navigation - name of a
+talk, want to go watch - does site isolation stuff happen?
+
+8:34 CHARLIE: Yeah, so they're definitely related. So navigation is about how
+you get from one web page to another, and that might be a different security
+context, different security principal. And I got interested and involved with
+navigation because of site isolation, my interest in that. And as you think of
+the web browser as an operating system for running programs, it's how you're
+getting from one program to another. So it would make sense that as you go from
+one website to another, you get a new container for that, a new process. So
+that was one part of how I got involved with navigation was building what we
+call a cross-process navigation. So you have to start in one renderer process
+and then be able to end up in a different renderer process with all the various
+parts of the life of a navigation, where you go out to the network and ask for
+the web page. And maybe you have to run some - before, unload events first to
+see if you were actually allowed to leave, or maybe the user has some unsaved
+data. All the timing of that is tricky, and then switch to the new process at
+the right time. So navigation has a lot of different corner cases and
+complexity that then get involved with the process model so that you can do
+this in any type of navigation, in any frame. And so that's where our team ends
+up involved in both site installation work and the navigation code and the
+browser.
+
+10:06 SHARON: Right. What a cool team. So you mentioned the process model, and
+that is related, but not the same as the multi-process architecture. So let's
+just quickly mention what the differences there are, because in this case, it
+is important.
+
+10:22 CHARLIE: Yes. So the process model for the browser is how we decide what
+goes into each process, and specifically, we're talking about renderer
+processes and web pages here, where we can decide, as we create new tabs and we
+visit websites on those tabs which renderer processes are we going to use. So
+without site isolation, maybe it's that each newly created tab gets its own
+process. But anything you visit within a given tab stays in the same process.
+Or maybe you can do some cross-process transitions within that tab as long as
+you're not breaking scripting between existing pages. So site isolation defines
+a process model that says you can never put web pages from two different
+websites in the same renderer process, and then that provides a bunch of
+constraints for how navigation works.
+
+11:16 SHARON: And then the multi-process architecture is more just the fact
+that we have these different processes.
+
+11:22 CHARLIE: Right. It makes this possible, because it gives us this ability
+to run browser code and renderer code separately and plug-in code and other
+utilities and network service that - yeah.
+
+11:27 SHARON: Yeah, because back in the day, that wasn't the case. That's what
+made Chrome different.
+
+11:34 CHARLIE: Right. So when Chrome launched, we were moving from this more
+monolithic browser architecture that was common at the time, where everything
+ran in one process to separate browser process, renderer process that was
+sandbox, and we could play around with different process models. So when Chrome
+launched, part of the internship that I was doing was looking at what should go
+in each renderer process? What process model should we use? And we thought site
+isolation would be great, but you can't really do that yet. It's too
+complicated to get the iframe things to work. So maybe we can do a hybrid where
+sometimes we swap to a new renderer process as you go from one website to
+another at the top level, but then other times, you'll end up with multiple
+sites in the same process. And it was like that until we were able to ship site
+isolation much later.
+
+12:23 SHARON: Cool. So this sounds, conceptually, like it makes sense. You want
+to have different sites/different origins in different renderer processes, and
+it sounds like it shouldn't be that hard, but it is/was/still is very hard. So
+can you briefly just tell us about how and why navigation is hard? Because
+other people who don't work on browsers at all or tech or even people in
+Chrome, I feel like, they're just like, isn't navigation just done? This just
+works, right? So why is there still a team doing this, and what is so hard
+about it?
+
+12:59 CHARLIE: That was often the most common question we would get when we
+were explaining what work we were doing on site isolation was, oh, doesn't it
+already work that way? And it's like, yeah, I wish. Yeah, so there's two parts
+of that. There is, why is navigation hard, and why is site isolation hard? So
+tying into any kind of navigation thing is tricky because of how many different
+types of navigation and corner cases there are. As you're going from one page
+to another, is it redirecting to a different website, or does it end up not
+actually giving you a web page back? Maybe it's a download. Is it not moving to
+a new document at all and it's just a navigation within the same document,
+which has different properties. There's a lot of things that we need to keep
+track of in the navigation system and how it affects the back-forward history
+that makes it tricky. And then it continues to get more complicated over time,
+as we add new fancy features to the browser. So there's lots of things that
+we've layered on top of that with back-forward cache and pre-rendering and new
+navigation APIs for interacting with session history, which make things faster
+and nicer for web developers, but also, provide even more ways that navigation
+can get into interesting corner cases, like why didn't we think that about
+pre-rendering a page with a sandbox iframe that might cause a different path to
+happen? So that's where a lot of the complexity in navigation comes from and
+why there's ongoing challenges, even though it's something that seems like it
+has worked from the beginning. Site isolation being hard is related to the fact
+that you can navigate in any frame in a page, and iframes being embedded is
+something that we used to just handle entirely within the renderer process. So
+this is a fun way to think about the multi-process architectures that shipped
+around when Chrome was launched and then other browsers that did similar things
+was we could take the rendering engines that had existed already for a decade
+or so from existing browsers and just run multiple copies of them. So as you
+open up a new tab, we've got another copy of WebKit, which is the rendering
+engine we were using at the time, and we had to make changes to make it work in
+the renderer process talking to the browser process, but we didn't really need
+to change fundamentally how it rendered a web page. And so it was in charge of
+deciding what network requests it was going to make for getting iframe content
+and then rendering the iframe and where a click was going to go, that kind of
+thing. And to do out-of-process iframes, you need the iframe inside the page to
+be rendered in an entirely separate renderer process. And that is a big change
+to how the rendering engine works. And so that was what took all the time and
+what made site isolation a multi-year project, where we had to fundamentally
+introduce these new data structures, like render frame host and representations
+of each frame in the browser process, change how the rendering engine worked,
+and then change all the features in the browser that assumed the renderer would
+take care of this. And now, we need to handle them spread across multiple
+processes.
+
+16:28 SHARON: How did that fit in with the forking of WebKit into Blink, which
+is what the rendering engine in Chrome is now?
+
+16:34 CHARLIE: Yeah, so the fork was absolutely necessary to do this. We pretty
+much had to wait until that happened, because we didn't have as much
+flexibility to make large, architectural changes to WebKit as we were sharing
+it with other browsers, like Safari and so on. We were looking into ways that
+we might be able to of approximate what we want, but as the decision to fork
+WebKit into Blink was made, it opened the door and gave us a chance to say, we
+can do this now. Let's go ahead and dive in and make site isolation happen.
+
+17:14 SHARON: That makes sense. In a quite early talk, it was probably from 10
+years ago now, Darin gave a talk, and he was saying how having per site, having
+each renderer have just one site in it was like the Holy Grail, and he seemed
+very excited about it. So that makes sense because of the -
+
+17:34 CHARLIE: Yeah, and it feels like the natural use of a sandbox in a
+browser. The same reason that we got all these questions, like isn't that how
+it already works? Is that it's such a natural fit for we have a container for
+running a web page, what is this unit that you want to put in the container?
+It's a website that you're visiting. And the fact that we couldn't easily pull
+them apart into different processes was totally an artifact of how web browsers
+were originally built that didn't foresee this - oh, they're being used as
+complicated programs with different security principles.
+
+18:13 SHARON: Yeah, in a different talk, John from Episode 3 content had
+mentioned that site isolation was basically the biggest change to Chrome since
+it launched and probably is still the case. So yeah, it was a project.
+
+18:29 CHARLIE: Yeah, it was a long project, and we had a lot of help from many
+people across the Chrome team, but it was cool to get to this outcome, where we
+could then say, now we have processes that are locked to a single security
+principal, so it's nice to get to that outcome.
+
+18:47 SHARON: So for people on the Chrome team now, what do you wish they knew
+about site isolation/navigation in terms of as an engineer? Because before, I
+was on a different team, and someone on my team said, oh, you should know how
+navigation works. And I said, yeah, that sounds like a great idea, but how? So
+what are things that people should just keep in mind when they're out and about
+doing their stuff that usually isn't directly interacting with navigation even?
+
+19:14 CHARLIE: Right. Yeah, so I think that the biggest thing to keep in mind
+is to limit what we put into a renderer process or what a renderer process has
+access to, to not include cross-site data. And we already have to have this
+mentality in Chrome that we don't trust the renderer process. If it sends an
+IPC or Mojo call to the browser process, we should assume that it might be
+lying or asking for things that it shouldn't have access to. And I think it's
+in the back of a lot of people's heads already that, OK, I shouldn't let it
+like go get a file from disk, but also, we don't want it to mix data from
+different sites. It shouldn't be able to ask for something from - to lie and
+say, oh, I'm origin x, please give me data from there. Because that's often how
+APIs used to work in Chrome was, the renderer process would say what origin
+it's asking for, and please give me the cookie for that.
+
+20:12 SHARON: That sounds bananas.
+
+20:12 CHARLIE: Yeah. Now, it sounds crazy. And so we think that the browser
+process should already know based on who's asking what they have access to. So
+that's really the thing that, in order to avoid site isolation bypasses, that's
+what developers should keep in mind. So for features like Autofill or something
+where it's easy to think, oh it would be nice for me to just have that data on
+hand in the renderer process and I can just put it in when it's needed. No, you
+should keep it out of the renderer, and then only provide the data that's
+needed.
+
+20:51 SHARON: In security-discuss circles, another term you hear often is a
+renderer escape or renderer bypass or whatever. Is that the same as a site
+isolation bypass, or are those different?
+
+21:00 CHARLIE: Yeah, so sandbox escape is a common term that is used for when
+an attacker has found some bug already, and then they are able to escalate
+their privilege to affect the browser process or get out of the browser process
+and to the operating system. So a sandbox escape is a lot worse than a site
+isolation bypass. It would give the attacker control of your computer and
+installing malware and things. So sandbox escapes, we want to have as many
+boundaries as possible to try to prevent that from happening. A site isolation
+bypass is not as bad as a full sandbox escape, but it would be a way that an
+attacker could find some way to get access to another website's data or attack
+that website. So maybe it's able to trick the browser into giving it cookies
+from that site or using the permissions that have been granted to another
+website. And then renderer compromise would be another type of exploit that
+happens entirely within the renderer process. That's one where the attacker has
+found some bug, they can run whatever native code they want within the renderer
+process, and that's what we're trying to contain with the sandbox and what site
+isolation tries to make even less useful to the attacker. Because even if you
+can run any code you want within the renderer process, you shouldn't be able to
+install malware because of the sandbox, and you shouldn't be able to access
+other site's data because of site isolation
+
+22:47 SHARON: Yeah, I think when I was learning about site isolation and stuff,
+I was like, whoa, this is a lot going on, and most people just have no idea
+about it. And in terms of how other bugs and whatnot, something that is often
+mentioned is Spectre and that still affect thing. And the only mention, on
+Wikipedia in the Mitigation section of Spectre, they mentioned site isolation,
+but I was like, this should have its own page, so maybe one day -
+
+23:20 CHARLIE: Maybe one day.
+
+23:20 SHARON: one of us is going to write a thing about that. But yeah, that's
+kind of the bug, right? So can you just talk about that?
+
+23:25 CHARLIE: Yeah, so Spectre and Meltdown were certainly a big change to the
+security landscape for browsers. At a high level, those are attacks that are
+based on the micro-architectural parts of the CPU. The way that the basic CPU
+hardware works, there are ways to leak data that weren't anticipated. And we
+can view it as it gives attackers what we call an arbitrary read primitive,
+something that can access anything in your address space in a process. You can
+think about it as the CPU wants to not stop and wait for going and accessing
+data from RAM, so it thinks, well, I'll just guess what the answer is going to
+be and then keep running some instructions. And if I was right in my guess, the
+next several steps are done already, and I can just move on from there. And if
+I was wrong, well, I just throw away that work, and I do the right thing, and
+we move on, and everybody is fine. But attackers found that while you're doing
+those extra steps ahead of time, you're also affecting the caches on the CPU,
+and cache timing attacks let you find out what work was done there. So some
+very clever researchers found that you can do some things in those extra steps
+that happen in this speculative state to find out what data is in addresses you
+don't have access to. And so places where we thought some check in the renderer
+process could say, oh, you don't have access to this thing from another
+website. We're fine. Now, you could get access to it, just based on how CPUs
+work, without needing any bugs in the browser. So now, we're thinking, OK,
+we're running JavaScript, and if it can leak things from the renderer process,
+we can't have data we're stealing in the renderer process. You could try to
+find ways to prevent those attacks, but those ended up being difficult. And
+ultimately, we found that it wasn't really feasible to prevent the attacks in
+all the forms that they could happen. So site isolation became the first line
+of defense to say, data from other websites, data we're stealing should not be
+in the render process where a Spectre attack could get access to it. Now, that
+was actually one of the big, exciting events that helped us accelerate the work
+on site isolation and get it launched when that was discovered in 2017 or 2018.
+
+26:24 SHARON: So at that point, site isolation was mostly done, and it was just
+getting it out?
+
+26:24 CHARLIE: Yeah, it was really interesting. So we'd been working on it for
+several years for a different reason for the fact that we wanted it to be a
+second line of defense against compromised rendering processes. We assume
+people are going to find bugs in the renderer process, in V8 or in Blink or
+things like that, and we wanted that to not be as big of a problem. We wanted
+to say, OK, whatever. There isn't data we're stealing in that process. We had
+already shipped some initial uses of out-of-process iframes in 2017 for
+extensions, and we were working on trying to do some sort of initial steps
+towards using site isolation for some websites and see how that goes when we
+found out about Spectre and Meltdown. And so that next six months or so was a
+very accelerated, OK, we've got to get everything else working with the way
+that site isolation interacted with DevTools and extensions and printing and a
+bunch of other features in the browser that we needed to get working. And so it
+was an interesting accelerated rollout, where we even had an optional mode and
+an enterprise policy where you could say, I don't care if printing doesn't
+work, turn on site isolation so that Spectre attacks won't find other data
+we're stealing in the process. And then we got to where it was working well
+enough we could ship it for all desktop users in, I think it was Chrome 67 in
+mid 2018. So it was good that far along that we were able to ship the full
+thing within a few months.
+
+28:19 SHARON: Very cool. Yeah, I mean, those are all the things that make
+navigation hard, like extensions as part of it, and there's just all these
+things and all of these go-through navigation and effective, so that's very
+exciting. So what is the state of site isolation now, and are there still going
+to be changes? That was a few years ago, so are things still happening?
+
+28:45 CHARLIE: Yeah, we're still trying to make several different improvements.
+We've made several improvements since the launch, so that initial launch, since
+it was mostly focused on Spectre, didn't have all the defenses we wanted
+against compromise renderer processes, because the Spectre attack can't affect
+actual running code. It can't go and lie to the browser process. It won't give
+you full control over what's running in the renderer process, but it can leak
+all the data that's in there. So anything that a web page can pull into a
+renderer process can be leaked. So after that initial launch, we needed to go
+and actually finish the compromise renderer defenses and say, OK, all the IPCs
+that come out of the renderer, make sure they can't lie and steal someone
+else's data, so get all the browser process enforcements in place. Another big
+thing after that was getting it to work on Android, where we wanted this
+defense. We have a much different set of resource constraints on mobile
+devices, where there's not nearly as much memory and renderer processes are
+often killed or just discarded. So there, we couldn't isolate all websites from
+each other. We had to use heuristics to say, here are the sites that need it
+the most, so sites where users log in, in general, or sites where this
+particular user is logged in or other signals that this site probably needs
+some protection, we'll give those isolation, and then other ones can share a
+renderer process. So we've tried to improve those heuristics and isolate as
+many sites as we can there. And then things that we weren't initially isolating
+from each other, we have been able to. So extensions was an example where we
+started by just making sure extensions didn't share a process with web pages,
+but now, we make sure that no extensions can share a process with each other.
+And we're trying to get to where we could isolate all origins from each other,
+depending on what resources are available, but there's some changes with,
+basically, deprecating document.domain that are in flight that might make that
+possible.
+
+30:57 SHARON: So say I have a fancy computer, and I just want maximum site
+isolation because I care about security. How do I go get that?
+
+31:03 CHARLIE: Yeah, so there are some experimental ways to do that. You can go
+into the chrome://flags page, where you can turn on and off different features
+and experiments that are in progress. And there's one there called strict
+origin isolation, which will ensure that all origins within various sites are
+isolated from each other, and that works on desktop and Android. It'll just
+create slightly more processes than we do today. Similarly, on Android, if you
+wanted to isolate all sites, there is an option for full site isolation there
+called site-per-process, which you could use that or strict origin isolation to
+get maximum site isolation today.
+
+31:51 SHARON: So another platform that Chrome does exist on is iOS. So can we
+do anything there? Why is that not in [INAUDIBLE]
+
+31:58 CHARLIE: So Chrome for iOS has to use Apple's WebKit rendering engine
+today, and current versions doesn't have site isolation, and we don't have the
+ability to run our own rendering engine that has support for it. So we don't
+have it today, but my understanding is that WebKit is working on site isolation
+as well, and actually, Firefox has also shipped their version of site
+isolation, which is pretty cool to see other browser vendors building this as
+well. And so if that were made available to other third-party browsers on iOS,
+then maybe it could be used there. But at the moment, we're constrained, and we
+can't ship it on that platform.
+
+32:47 SHARON: In terms of how the internet happens, this seems like a good
+thing to just have generally. So is it possible that this could be a spec one
+day that any browser should implement, or is it - because it's under the hood
+and it's not something that's maybe necessarily visible to websites, maybe
+that's not part of it, but is this an option?
+
+33:04 CHARLIE: Yeah. I think it ties back to the earlier question about web
+security model versus browser security model, where the web visible parts of
+this, it's meant to be transparent to the websites. There's no behavior changes
+to the web platform by turning on site isolation. There's not meant to be. And
+so it's not really a spec visible thing, it's more part of the browser's
+architecture, the same way that there's no spec for sandboxes in a browser. You
+could build a browser that doesn't have a sandbox, but today, the best practice
+is to have better security by having a sandbox. So I think the relevant thing
+for web specs is just that we don't introduce APIs that don't work when
+different origins are in different processes. And that sounds like, well OK,
+that makes sense, and thankfully, we were sort of in that state to begin with,
+and in some places we got lucky. Like postmessage is asynchronous, which is a
+mechanism for sending a message to another origin, but they don't need to run
+in the same process because that message will be delivered at a later time. So
+we can send it to a different process running on a different thread. Some
+places we got unlucky, like document.domain, where web APIs said that different
+origins can script each other if they agree that it's OK, as long as they're in
+the same site, and that constrained us in the process model. So we're trying to
+improve things about the web spec. You could almost say that deprecating
+document.domain is a way of seeing that the browser security model and the web
+security model aligning with each other to say, OK, we want to use processes.
+We want this asynchronous boundary. You shouldn't be able to script other
+origins from the same site. So I think that's the closest is making sure that
+specced APIs fit well with this multi-process site isolation world.
+
+35:12 SHARON: There are some headers and tags and whatever that websites can
+use to alter how the browser handles things though, right?
+
+35:23 CHARLIE: Yes, absolutely. And those are both good ways that websites can
+more effectively isolate themselves, in general, both from web visible behavior
+and from the browser's architecture and ways that browsers that don't have
+full-site isolation, that don't have out-of-process iframes in all cases, web
+pages might still be able to get some of the isolation benefits using those
+APIs. And so those are things like cross-origin opener policies that says, for
+example, if I open a pop up to a different website, there's not going to be any
+communication between me and that pop up. So it's OK to put them in different
+processes, and they can be better isolated from each other. That's good from an
+architecture perspective. It's also nice from a web perspective in that you
+don't have to worry about is the window.opener variable in the pop up able to
+be used to do sneaky things to the page that opened it. So there's nice,
+web-visible reasons to use something like a cross-origin opener policy to keep
+them protected from each other. So that's one example of that. There's others
+as well.
+
+36:46 SHARON: Something I've seen around that is a web spec is content security
+policy. Is that related to any of this at all?
+
+36:52 CHARLIE: It kind of is. Yeah, so content security policy is another way
+for websites to tell the browser better ways to secure that site. And so some
+of it is useful for saying I want to do a better job preventing cross-site
+scripting attacks on my page, so don't run a script if you find it in these
+random places. It should only come from these URLs or in these contexts on my
+page. So that's more about what happens in a given renderer process, but there
+are some places where content security policy does overlap a bit with site
+isolation. There is a sandbox value you can put into a content security policy
+header that makes it get treated like a sandbox iframe. And while we don't yet
+have support for putting sandbox iframes in another process, that was work
+that's in progress and we're hoping to ship before long. And so CSP headers
+that say sandbox will also be able to be isolated from the rest of their site.
+So if they have some kind of untrustworthy content in them, that won't be able
+to attack the rest of the site.
+
+38:04 SHARON: OK. Yeah, so it's that difference between the web versus browser,
+what's visible, what's an option versus how it's actually implemented.
+
+38:11 CHARLIE: Right.
+
+38:11 SHARON: Cool. So a lot of this, we've talked about security a lot, and I
+think for people who don't know about security, the image you have is people
+trying to break into - like I'm in, that whole thing, and that's very much not
+what's going on here, because we're not trying to break things. So can you tell
+us just a bit about the difference between offensive and defensive security and
+how this is one of those.
+
+38:38 CHARLIE: Yeah, so a lot of attention in the security space goes to big,
+exciting, flashy attacks that are found. On the offensive side, look, I found a
+way to break the security of this thing, and we have big vulnerability reward
+bounties to reward when people find these things so we can get them fixed. So
+even on the defensive side, you want people working on offensive security,
+looking for these bugs, looking for things that need to be fixed so we can
+defend users. But the defensive side is super important and I find it a
+satisfying place to be, even if it isn't always as glamorous. It's like, you
+have to have all the defenses in place and all of these different attacks that
+are found, it's like, yeah, we need to fix them, and we need to find ways to
+make that less likely. But ultimately, this is the real goal, is we want to
+have systems that we can trust, that are safe to use, and that we can go and
+visit untrustworthy web content and not have to worry about it. You need these
+extra lines of defense. You need all these different ways of defending the
+product and shipping security fixes fast, all the things that security works on
+in a defensive sense so that people can use these systems and depend on them in
+their lives. So that's the fun and fulfilling part of this, even if it isn't
+quite as glamorous as I found a sandbox escape, but those are fun to look at
+too.
+
+40:17 SHARON: I heard security described as a bunch of layers of Swiss cheese.
+So you have all these different layers of mitigations to try to keep bad things
+from happening, but each of them is not perfect. And if the holes in those
+layers line up, then that's where you get a vulnerability. So in this very
+approximate metaphor, what are the neighboring slices of cheese to site
+isolation? What other defensive things are related to this and are trying to
+achieve the same goal sure?
+
+40:46 CHARLIE: Sure. Yeah, so there's going to be holes in any layer that you
+build we. Have bugs in software, and in site isolation's case, it's trying to
+put this boundary between the renderer process, where we assume everything is
+compromised already and the data that the attacker wants to get to, other
+websites, data on your machine and so on. So the adjacent layers of Swiss
+cheese would be within the render process, we do have security checks that try
+to say we have same origin policy checks, things that try to keep certain data
+opaque to a web page so the JavaScript can't look at it. Those checks in the
+renderer process do matter. Today, we do have multiple origins from the same
+site in the same process. The renderer process' job is to make sure that they
+don't attack each other. But there's some fairly large Swiss cheese holes in
+that layer that we try to fix whenever we find them. And so site isolation's
+job is to be the next layer, which won't have holes in the same places,
+hopefully. Its holes, site isolation bypasses, might be, oh, there's some way
+for the renderer process to ask the browser process for something it shouldn't
+have access to, and it tricks it, and it gets access to that. We hope that it's
+tough to line those holes up, that an attacker has to find both a bug in the
+renderer process and a bug in site isolation and luck out in that those bugs
+line up and you can get to one from the other in order to get access to another
+website's data. And then the next layer of Swiss cheese would be all the things
+that the browser process does to keep the renderer isolated from the user's
+machine and the sandbox itself that you shouldn't have access to the OS APIs
+and so on. So those would be other ways to try and get beyond site isolation to
+other things.
+
+42:48 SHARON: That makes sense. Yeah, when I first heard about it, I was like,
+oh, that's such a fun way to think about it, really. It's a good visual seeing,
+OK, this is how things go wrong. All right, cool. Do you have any other fun
+stories about site isolation, making it happen, stuff since then?
+
+43:08 CHARLIE: I mean, it's been a really fun journey the whole way. There's
+been different projects and different exploratory phases, where we weren't sure
+what was going to work or what we needed to get done. I've worked with a bunch
+of great interns and people who have been on the team on early phases like
+getting postmessage to work across renderer processes, later phases about what
+would it look like to build out a process iframes using something like the
+plugin infrastructure, just is this feasible? Or what is it that we could
+protect that a particular renderer process is allowed to ask for. If can we
+keep allowing JavaScript data from other websites into a renderer process,
+while blocking your bank account information from getting it, those both look
+like network responses from different websites, but one has to be let through
+for compatibility reasons, and one has to be blocked. Can we build that? Are we
+doing a good job of keeping that sensitive data out? These are things that. We
+had some great PhD interns working with us on, and ultimately, got us to where
+we could ship this and protect a lot of data. So it's fun working with all
+those people along the way.
+
+44:35 SHARON: Yeah, that sounds very cool. These days, so earlier on, you
+mentioned people whose questions were like, why doesn't this already happen? So
+these days, it does happen more or less like that. So what kind of questions or
+misconceptions do you still see folks who typically work on Chrome still have
+when it comes to this kind of stuff?
+
+44:52 CHARLIE: I think it's often assuming that navigation is simpler than it
+is and not realizing how many corner cases matter and how all of these
+different features that have built on top of navigation interact with each
+other. So I think that's where we spend a lot of our time these days beyond the
+we want to improve site isolation. We want to make these abstractions easier
+for other people to understand. So I think that's one of the big challenges now
+is how many different directions the navigation code has been pulled and how
+those things interact with each other.
+
+45:24 SHARON: Right. And that's kind of - was intentional initially, right? You
+don't want everyone who works on Chrome to have to know how all of this works,
+but then when you hide it so well, they're like, oh, this is fine. I'll just do
+my thing. It'll just be my one thing, but then everyone has such a thing, and
+then it becomes too many things. Yeah, I used to work on a different part of
+Chrome that was not related to this, and you see some of these big classes,
+like web content or whatever. You're like, oh, I'll just get what I need from
+that, and things will be fine, but you just don't even have any idea of all the
+things that could go wrong. So it's cool that someone is out here trying to
+keep that under control.
+
+46:00 CHARLIE: And I'm glad there's a lot of efforts to try to improve the APIs
+for how we expose these things, web content to web content, observer which is
+growing into quite a large API with many users, looking at ways to make these
+APIs easier to use and harder to make mistakes with. So I think those are
+worthwhile efforts.
+
+46:20 SHARON: OK. Cool. Well, I think that covers all of it. Now, folks know
+how isolation works. Problem solved. This is great. All right, thank you very
+much. Great.
+
+46:34 CHARLIE: Thanks. Oh, no. What? OK, hold on.
diff --git a/extensions/renderer/api/automation/automation_internal_custom_bindings.cc b/extensions/renderer/api/automation/automation_internal_custom_bindings.cc
index 017c93d..745518a5 100644
--- a/extensions/renderer/api/automation/automation_internal_custom_bindings.cc
+++ b/extensions/renderer/api/automation/automation_internal_custom_bindings.cc
@@ -48,12 +48,14 @@
 AutomationInternalCustomBindings::AutomationInternalCustomBindings(
     ScriptContext* context,
     NativeExtensionBindingsSystem* bindings_system,
+    scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
     int worker_thread_id)
     : ObjectBackedNativeHandler(context),
       bindings_system_(bindings_system),
       should_ignore_context_(false),
       automation_v8_bindings_(
           std::make_unique<ui::AutomationV8Bindings>(this, this)),
+      io_task_runner_(io_task_runner),
       worker_thread_id_(worker_thread_id) {
   // We will ignore this instance if the extension has a background page and
   // this context is not that background page. In all other cases, we will have
@@ -241,9 +243,7 @@
 }
 
 void AutomationInternalCustomBindings::NotifyTreeEventListenersChanged() {
-  scoped_refptr<base::SingleThreadTaskRunner> task_runner =
-      context()->web_frame()->GetTaskRunner(blink::TaskType::kInternalDefault);
-  task_runner->PostTask(
+  io_task_runner_->PostTask(
       FROM_HERE,
       base::BindOnce(&AutomationInternalCustomBindings::
                          MaybeSendOnAllAutomationEventListenersRemoved,
diff --git a/extensions/renderer/api/automation/automation_internal_custom_bindings.h b/extensions/renderer/api/automation/automation_internal_custom_bindings.h
index e06304e2..7b4a836 100644
--- a/extensions/renderer/api/automation/automation_internal_custom_bindings.h
+++ b/extensions/renderer/api/automation/automation_internal_custom_bindings.h
@@ -12,6 +12,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/memory/weak_ptr.h"
+#include "base/task/single_thread_task_runner.h"
 #include "extensions/common/api/automation.h"
 #include "extensions/renderer/object_backed_native_handler.h"
 #include "ui/accessibility/ax_enums.mojom-shared.h"
@@ -37,6 +38,7 @@
   AutomationInternalCustomBindings(
       ScriptContext* context,
       NativeExtensionBindingsSystem* bindings_system,
+      scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
       int worker_thread_id);
 
   AutomationInternalCustomBindings(const AutomationInternalCustomBindings&) =
@@ -95,6 +97,7 @@
 
   std::unique_ptr<ui::AutomationV8Bindings> automation_v8_bindings_;
 
+  scoped_refptr<base::SingleThreadTaskRunner> io_task_runner_;
   const int worker_thread_id_;
 
   base::WeakPtrFactory<AutomationInternalCustomBindings> weak_ptr_factory_{
diff --git a/extensions/renderer/api/automation/automation_internal_custom_bindings_unittests.cc b/extensions/renderer/api/automation/automation_internal_custom_bindings_unittests.cc
index 253392f..f42dfc4 100644
--- a/extensions/renderer/api/automation/automation_internal_custom_bindings_unittests.cc
+++ b/extensions/renderer/api/automation/automation_internal_custom_bindings_unittests.cc
@@ -37,9 +37,15 @@
     script_context->set_url(extension->url());
     bindings_system()->UpdateBindingsForContext(script_context);
 
+    // Currently the TaskRunner is not used, because the thread ID is
+    // kMainThreadId.
+    // When testing with a different thread ID, a runloop will be needed to
+    // allow the TaskRunner to complete.
+    // TODO(crbug/1487002) Add tests for service worker.
     auto automation_internal_bindings =
         std::make_unique<AutomationInternalCustomBindings>(
-            script_context, bindings_system(), kMainThreadId);
+            script_context, bindings_system(),
+            base::SingleThreadTaskRunner::GetCurrentDefault(), kMainThreadId);
     automation_internal_bindings_ = automation_internal_bindings.get();
     script_context->module_system()->RegisterNativeHandler(
         "automationInternal", std::move(automation_internal_bindings));
diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc
index c48b3a15..04773097 100644
--- a/extensions/renderer/dispatcher.cc
+++ b/extensions/renderer/dispatcher.cc
@@ -971,10 +971,19 @@
   module_system->RegisterNativeHandler(
       "runtime",
       std::unique_ptr<NativeHandler>(new RuntimeCustomBindings(context)));
+
+  scoped_refptr<base::SingleThreadTaskRunner> io_task_runner = nullptr;
+  // RenderThread::Get() returns nullptr from some tests.
+  if (context->IsForServiceWorker() && RenderThread::Get()) {
+    io_task_runner = RenderThread::Get()->GetIOTaskRunner();
+  } else if (context->web_frame()) {
+    io_task_runner =
+        context->web_frame()->GetTaskRunner(blink::TaskType::kInternalDefault);
+  }
   module_system->RegisterNativeHandler(
-      "automationInternal",
-      std::make_unique<AutomationInternalCustomBindings>(
-          context, bindings_system, content::WorkerThread::GetCurrentId()));
+      "automationInternal", std::make_unique<AutomationInternalCustomBindings>(
+                                context, bindings_system, io_task_runner,
+                                content::WorkerThread::GetCurrentId()));
 }
 
 bool Dispatcher::OnControlMessageReceived(const IPC::Message& message) {
diff --git a/google_apis/gaia/gaia_auth_fetcher.h b/google_apis/gaia/gaia_auth_fetcher.h
index e297025..61d3e4c 100644
--- a/google_apis/gaia/gaia_auth_fetcher.h
+++ b/google_apis/gaia/gaia_auth_fetcher.h
@@ -19,7 +19,6 @@
 #include "google_apis/gaia/google_service_auth_error.h"
 #include "net/base/net_errors.h"
 #include "net/traffic_annotation/network_traffic_annotation.h"
-#include "services/network/public/cpp/http_raw_request_response_info.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
 #include "url/gurl.h"
 
diff --git a/google_apis/gcm/engine/registration_request.cc b/google_apis/gcm/engine/registration_request.cc
index 3ea57da3..e694f04f 100644
--- a/google_apis/gcm/engine/registration_request.cc
+++ b/google_apis/gcm/engine/registration_request.cc
@@ -48,9 +48,12 @@
 const char kAuthenticationFailed[] = "AUTHENTICATION_FAILED";
 const char kInvalidSender[] = "INVALID_SENDER";
 const char kInvalidParameters[] = "INVALID_PARAMETERS";
-const char kInternalServerError[] = "InternalServerError";
+const char kInternalServerError[] = "INTERNAL_SERVER_ERROR";
 const char kQuotaExceeded[] = "QUOTA_EXCEEDED";
 const char kTooManyRegistrations[] = "TOO_MANY_REGISTRATIONS";
+const char kTooManySubscribers[] = "TOO_MANY_SUBSCRIBERS";
+const char kInvalidTargetVersion[] = "INVALID_TARGET_VERSION";
+const char kFisAuthError[] = "FIS_AUTH_ERROR";
 
 // Gets correct status from the error message.
 RegistrationRequest::Status GetStatusFromError(const std::string& error) {
@@ -75,6 +78,15 @@
   if (base::Contains(error, kTooManyRegistrations)) {
     return RegistrationRequest::TOO_MANY_REGISTRATIONS;
   }
+  if (base::Contains(error, kTooManySubscribers)) {
+    return RegistrationRequest::TOO_MANY_SUBSCRIBERS;
+  }
+  if (base::Contains(error, kInvalidTargetVersion)) {
+    return RegistrationRequest::INVALID_TARGET_VERSION;
+  }
+  if (base::Contains(error, kFisAuthError)) {
+    return RegistrationRequest::FIS_AUTH_ERROR;
+  }
   // Should not be reached, unless the server adds new error types.
   return RegistrationRequest::UNKNOWN_ERROR;
 }
@@ -90,6 +102,8 @@
     case RegistrationRequest::NO_RESPONSE_BODY:
     case RegistrationRequest::RESPONSE_PARSING_FAILED:
     case RegistrationRequest::INTERNAL_SERVER_ERROR:
+    case RegistrationRequest::TOO_MANY_SUBSCRIBERS:
+    case RegistrationRequest::FIS_AUTH_ERROR:
       return true;
     case RegistrationRequest::SUCCESS:
     case RegistrationRequest::INVALID_PARAMETERS:
@@ -97,6 +111,7 @@
     case RegistrationRequest::QUOTA_EXCEEDED:
     case RegistrationRequest::TOO_MANY_REGISTRATIONS:
     case RegistrationRequest::REACHED_MAX_RETRIES:
+    case RegistrationRequest::INVALID_TARGET_VERSION:
       return false;
   }
   return false;
diff --git a/google_apis/gcm/engine/registration_request.h b/google_apis/gcm/engine/registration_request.h
index 267aa89..b6bf767 100644
--- a/google_apis/gcm/engine/registration_request.h
+++ b/google_apis/gcm/engine/registration_request.h
@@ -59,10 +59,14 @@
     INTERNAL_SERVER_ERROR,      // Internal server error during request.
     QUOTA_EXCEEDED,             // Registration quota exceeded.
     TOO_MANY_REGISTRATIONS,     // Max registrations per device exceeded.
+    TOO_MANY_SUBSCRIBERS,       // Max subscribers per sender exceeded.
+    INVALID_TARGET_VERSION,     // Invalid target version.
+    FIS_AUTH_ERROR,             // FIS auth check failed.
+
     // NOTE: always keep this entry at the end. Add new status types only
     // immediately above this line. Make sure to update the corresponding
     // histogram enum accordingly.
-    kMaxValue = TOO_MANY_REGISTRATIONS
+    kMaxValue = FIS_AUTH_ERROR
   };
 
   // Callback completing the registration request.
diff --git a/gpu/command_buffer/client/shared_image_interface.cc b/gpu/command_buffer/client/shared_image_interface.cc
index 69f82dd..fe41e3c 100644
--- a/gpu/command_buffer/client/shared_image_interface.cc
+++ b/gpu/command_buffer/client/shared_image_interface.cc
@@ -34,12 +34,10 @@
 
 // static
 std::unique_ptr<SharedImageInterface::ScopedMapping>
-SharedImageInterface::ScopedMapping::Create(gfx::GpuMemoryBufferHandle handle,
-                                            viz::SharedImageFormat format,
-                                            gfx::Size size,
-                                            gfx::BufferUsage buffer_usage) {
+SharedImageInterface::ScopedMapping::Create(
+    GpuMemoryBufferHandleInfo handle_info) {
   auto scoped_mapping = base::WrapUnique(new ScopedMapping());
-  if (!scoped_mapping->Init(std::move(handle), format, size, buffer_usage)) {
+  if (!scoped_mapping->Init(std::move(handle_info))) {
     LOG(ERROR) << "ScopedMapping init failed.";
     return nullptr;
   }
@@ -47,19 +45,16 @@
 }
 
 bool SharedImageInterface::ScopedMapping::Init(
-    gfx::GpuMemoryBufferHandle handle,
-    viz::SharedImageFormat format,
-    gfx::Size size,
-    gfx::BufferUsage buffer_usage) {
+    GpuMemoryBufferHandleInfo handle_info) {
   GpuMemoryBufferSupport support;
 
   // Only single planar buffer formats are supported currently. Multiplanar will
   // be supported when Multiplanar SharedImages are fully implemented.
-  CHECK(format.is_single_plane());
+  CHECK(handle_info.format.is_single_plane());
   buffer_ = support.CreateGpuMemoryBufferImplFromHandle(
-      std::move(handle), size,
-      viz::SinglePlaneSharedImageFormatToBufferFormat(format), buffer_usage,
-      base::DoNothing());
+      std::move(handle_info.handle), handle_info.size,
+      viz::SinglePlaneSharedImageFormatToBufferFormat(handle_info.format),
+      handle_info.buffer_usage, base::DoNothing());
   if (!buffer_) {
     LOG(ERROR) << "Unable to create GpuMemoruBuffer.";
     return false;
diff --git a/gpu/command_buffer/client/shared_image_interface.h b/gpu/command_buffer/client/shared_image_interface.h
index fcf250f0..f64eea7 100644
--- a/gpu/command_buffer/client/shared_image_interface.h
+++ b/gpu/command_buffer/client/shared_image_interface.h
@@ -14,6 +14,7 @@
 #include "gpu/command_buffer/common/mailbox.h"
 #include "gpu/command_buffer/common/sync_token.h"
 #include "gpu/gpu_export.h"
+#include "gpu/ipc/common/gpu_memory_buffer_handle_info.h"
 #include "gpu/ipc/common/gpu_memory_buffer_impl.h"
 #include "gpu/ipc/common/surface_handle.h"
 #include "third_party/skia/include/core/SkImageInfo.h"
@@ -90,14 +91,8 @@
 
     ScopedMapping();
     static std::unique_ptr<ScopedMapping> Create(
-        gfx::GpuMemoryBufferHandle handle,
-        viz::SharedImageFormat format,
-        gfx::Size size,
-        gfx::BufferUsage buffer_usage);
-    bool Init(gfx::GpuMemoryBufferHandle handle,
-              viz::SharedImageFormat format,
-              gfx::Size size,
-              gfx::BufferUsage buffer_usage);
+        GpuMemoryBufferHandleInfo handle_info);
+    bool Init(GpuMemoryBufferHandleInfo handle_info);
 
     // ScopedMapping is essentially a wrapper around GpuMemoryBuffer for now for
     // simplicity and will be removed later.
diff --git a/gpu/command_buffer/service/shared_image_interface_in_process.cc b/gpu/command_buffer/service/shared_image_interface_in_process.cc
index b64d9b5..024ccde 100644
--- a/gpu/command_buffer/service/shared_image_interface_in_process.cc
+++ b/gpu/command_buffer/service/shared_image_interface_in_process.cc
@@ -424,9 +424,8 @@
     return nullptr;
   }
 
-  auto scoped_mapping = SharedImageInterface::ScopedMapping::Create(
-      std::move(handle_info.handle), handle_info.format, handle_info.size,
-      handle_info.buffer_usage);
+  auto scoped_mapping =
+      SharedImageInterface::ScopedMapping::Create(std::move(handle_info));
 
   if (!scoped_mapping) {
     LOG(ERROR) << "Unable to create ScopedMapping.";
diff --git a/gpu/config/gpu_switches.cc b/gpu/config/gpu_switches.cc
index 0a07ea8f7..ba371bc 100644
--- a/gpu/config/gpu_switches.cc
+++ b/gpu/config/gpu_switches.cc
@@ -144,4 +144,6 @@
 const char kSkiaGraphiteBackendDawnVulkan[] = "dawn-vulkan";
 const char kSkiaGraphiteBackendMetal[] = "metal";
 
+const char kShaderCachePath[] = "shader-cache-path";
+
 }  // namespace switches
diff --git a/gpu/config/gpu_switches.h b/gpu/config/gpu_switches.h
index be4fa7cf..a06b9f86 100644
--- a/gpu/config/gpu_switches.h
+++ b/gpu/config/gpu_switches.h
@@ -50,6 +50,7 @@
 GPU_EXPORT extern const char kSkiaGraphiteBackendDawnSwiftshader[];
 GPU_EXPORT extern const char kSkiaGraphiteBackendDawnVulkan[];
 GPU_EXPORT extern const char kSkiaGraphiteBackendMetal[];
+GPU_EXPORT extern const char kShaderCachePath[];
 
 }  // namespace switches
 
diff --git a/gpu/ipc/client/client_shared_image_interface.cc b/gpu/ipc/client/client_shared_image_interface.cc
index 77ae8ffc2..827ef11 100644
--- a/gpu/ipc/client/client_shared_image_interface.cc
+++ b/gpu/ipc/client/client_shared_image_interface.cc
@@ -223,9 +223,8 @@
     LOG(ERROR) << "Buffer is null.";
     return nullptr;
   }
-  auto scoped_mapping = SharedImageInterface::ScopedMapping::Create(
-      std::move(handle_info.handle), handle_info.format, handle_info.size,
-      handle_info.buffer_usage);
+  auto scoped_mapping =
+      SharedImageInterface::ScopedMapping::Create(std::move(handle_info));
   if (!scoped_mapping) {
     LOG(ERROR) << "Unable to create ScopedMapping.";
     return nullptr;
diff --git a/gpu/ipc/service/built_in_shader_cache_loader.cc b/gpu/ipc/service/built_in_shader_cache_loader.cc
index 4147bbe..1997f03 100644
--- a/gpu/ipc/service/built_in_shader_cache_loader.cc
+++ b/gpu/ipc/service/built_in_shader_cache_loader.cc
@@ -5,11 +5,13 @@
 #include "gpu/ipc/service/built_in_shader_cache_loader.h"
 
 #include "base/apple/foundation_util.h"
+#include "base/command_line.h"
 #include "base/files/file.h"
 #include "base/functional/bind.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/task/thread_pool.h"
 #include "base/time/time.h"
+#include "gpu/config/gpu_switches.h"
 #include "gpu/ipc/service/built_in_shader_cache_writer.h"
 
 namespace gpu {
@@ -126,10 +128,12 @@
   CHECK(!g_loader);
   // Destroyed when finished loading.
   g_loader = new BuiltInShaderCacheLoader;
+  auto path = base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
+      switches::kShaderCachePath);
   base::ThreadPool::PostTask(
       FROM_HERE, {base::TaskPriority::USER_BLOCKING, base::MayBlock()},
       base::BindOnce(&BuiltInShaderCacheLoader::Load,
-                     base::Unretained(g_loader), base::FilePath()));
+                     base::Unretained(g_loader), path));
 }
 
 // static
diff --git a/gpu/ipc/service/built_in_shader_cache_writer.cc b/gpu/ipc/service/built_in_shader_cache_writer.cc
index bb8fa5c..e1c5c22 100644
--- a/gpu/ipc/service/built_in_shader_cache_writer.cc
+++ b/gpu/ipc/service/built_in_shader_cache_writer.cc
@@ -8,6 +8,7 @@
 
 #include "base/command_line.h"
 #include "base/files/file_path.h"
+#include "gpu/config/gpu_switches.h"
 
 namespace gpu {
 
@@ -16,7 +17,7 @@
 // Returns the path to use is no path is supplied.
 base::FilePath GetDefaultPath() {
   auto path = base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
-      "shader-cache-path");
+      switches::kShaderCachePath);
   return path.empty() ? base::FilePath::FromASCII("/tmp/shader") : path;
 }
 
diff --git a/infra/config/generated/testing/variants.pyl b/infra/config/generated/testing/variants.pyl
index dd3a26ba..be54089 100644
--- a/infra/config/generated/testing/variants.pyl
+++ b/infra/config/generated/testing/variants.pyl
@@ -70,16 +70,16 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'identifier': 'Lacros version skew testing ash canary',
-    'description': 'Run with ash-chrome version 119.0.6035.0',
+    'description': 'Run with ash-chrome version 119.0.6037.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v119.0.6035.0',
-          'revision': 'version:119.0.6035.0',
+          'location': 'lacros_version_skew_tests_v119.0.6037.0',
+          'revision': 'version:119.0.6037.0',
         },
       ],
     },
@@ -432,7 +432,7 @@
     'identifier': 'BRYA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'brya',
-      'cros_img': 'brya-release/R119-15626.0.0',
+      'cros_img': 'brya-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -441,7 +441,7 @@
     'identifier': 'BRYA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'brya',
-      'cros_img': 'brya-release/R119-15626.0.0',
+      'cros_img': 'brya-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -474,7 +474,7 @@
     'identifier': 'DEDEDE_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'dedede',
-      'cros_img': 'dedede-release/R119-15626.0.0',
+      'cros_img': 'dedede-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -503,7 +503,7 @@
     'identifier': 'FIZZ_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'fizz',
-      'cros_img': 'fizz-release/R119-15626.0.0',
+      'cros_img': 'fizz-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -536,7 +536,7 @@
     'identifier': 'GUYBRUSH_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'guybrush',
-      'cros_img': 'guybrush-release/R119-15626.0.0',
+      'cros_img': 'guybrush-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -569,7 +569,7 @@
     'identifier': 'PUFF_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'puff',
-      'cros_img': 'puff-release/R119-15626.0.0',
+      'cros_img': 'puff-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -602,7 +602,7 @@
     'identifier': 'EVE_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'eve',
-      'cros_img': 'eve-public/R119-15626.0.0',
+      'cros_img': 'eve-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'dut_pool': 'chromium',
       'public_builder': 'cros_test_platform_public',
@@ -613,7 +613,7 @@
     'identifier': 'HANA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'hana',
-      'cros_img': 'hana-release/R119-15626.0.0',
+      'cros_img': 'hana-release/R119-15629.0.0',
     },
   },
   'CROS_HANA_RELEASE_DEV': {
@@ -641,7 +641,7 @@
     'identifier': 'JACUZZI_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-release/R119-15626.0.0',
+      'cros_img': 'jacuzzi-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -663,7 +663,7 @@
     'identifier': 'JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-release/R119-15626.0.0',
+      'cros_img': 'jacuzzi-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -678,7 +678,7 @@
     'identifier': 'JACUZZI_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-public/R119-15626.0.0',
+      'cros_img': 'jacuzzi-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -686,7 +686,7 @@
     'identifier': 'JACUZZI_CQ_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-public/R119-15626.0.0',
+      'cros_img': 'jacuzzi-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'public_builder': 'cros_test_platform_public',
       'public_builder_bucket': 'testplatform-public',
@@ -696,7 +696,7 @@
     'identifier': 'TROGDOR_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'trogdor',
-      'cros_img': 'trogdor-public/R119-15626.0.0',
+      'cros_img': 'trogdor-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -704,7 +704,7 @@
     'identifier': 'OCTOPUS_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-public/R119-15626.0.0',
+      'cros_img': 'octopus-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -712,7 +712,7 @@
     'identifier': 'OCTOPUS_RELEASE_CHROME_FROM_TLS_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-release/R119-15626.0.0',
+      'cros_img': 'octopus-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -720,7 +720,7 @@
     'identifier': 'OCTOPUS_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-release/R119-15626.0.0',
+      'cros_img': 'octopus-release/R119-15629.0.0',
     },
   },
   'CROS_OCTOPUS_RELEASE_DEV': {
@@ -748,7 +748,7 @@
     'identifier': 'STRONGBAD_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'strongbad',
-      'cros_img': 'strongbad-release/R119-15626.0.0',
+      'cros_img': 'strongbad-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -777,7 +777,7 @@
     'identifier': 'TROGDOR_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'trogdor',
-      'cros_img': 'trogdor-release/R119-15622.0.0',
+      'cros_img': 'trogdor-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -786,7 +786,7 @@
     'skylab': {
       'cros_board': 'volteer',
       'cros_model': 'voxel',
-      'cros_img': 'volteer-public/R119-15626.0.0',
+      'cros_img': 'volteer-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'dut_pool': 'chromium',
       'public_builder': 'cros_test_platform_public',
@@ -797,7 +797,7 @@
     'identifier': 'VOLTEER_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'volteer',
-      'cros_img': 'volteer-release/R119-15626.0.0',
+      'cros_img': 'volteer-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
diff --git a/infra/config/targets/cros-skylab-variants.json b/infra/config/targets/cros-skylab-variants.json
index 3ba5878..9a37966 100644
--- a/infra/config/targets/cros-skylab-variants.json
+++ b/infra/config/targets/cros-skylab-variants.json
@@ -2,8 +2,8 @@
   "CROS_BRYA_RELEASE_ASH_LKGM": {
     "skylab": {
       "cros_board": "brya",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "brya-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "brya-release/R119-15629.0.0",
       "autotest_name": "tast.chrome-from-gcs",
       "dut_pool": "chrome"
     },
@@ -12,8 +12,8 @@
   "CROS_BRYA_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "brya",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "brya-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "brya-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs",
       "dut_pool": "chrome"
     },
@@ -53,8 +53,8 @@
   "CROS_DEDEDE_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "dedede",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "dedede-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "dedede-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs"
     },
     "enabled": true,
@@ -90,8 +90,8 @@
   "CROS_FIZZ_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "fizz",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "fizz-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "fizz-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs",
       "dut_pool": "chrome"
     },
@@ -131,8 +131,8 @@
   "CROS_GUYBRUSH_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "guybrush",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "guybrush-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "guybrush-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs",
       "dut_pool": "chrome"
     },
@@ -172,8 +172,8 @@
   "CROS_PUFF_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "puff",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "puff-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "puff-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs",
       "dut_pool": "chrome"
     },
@@ -213,8 +213,8 @@
   "CROS_EVE_PUBLIC_LKGM": {
     "skylab": {
       "cros_board": "eve",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "eve-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "eve-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive",
       "dut_pool": "chromium",
       "public_builder": "cros_test_platform_public",
@@ -226,8 +226,8 @@
   "CROS_HANA_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "hana",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "hana-release/R119-15626.0.0"
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "hana-release/R119-15629.0.0"
     },
     "enabled": true,
     "identifier": "HANA_RELEASE_LKGM"
@@ -262,8 +262,8 @@
   "CROS_JACUZZI_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "jacuzzi",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "jacuzzi-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "jacuzzi-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs"
     },
     "enabled": true,
@@ -290,8 +290,8 @@
   "CROS_JACUZZI_RELEASE_CHROME_FROM_TLS_ASH_LKGM": {
     "skylab": {
       "cros_board": "jacuzzi",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "jacuzzi-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "jacuzzi-release/R119-15629.0.0",
       "autotest_name": "tast.chrome-from-gcs"
     },
     "identifier": "JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM"
@@ -308,8 +308,8 @@
   "CROS_JACUZZI_PUBLIC_LKGM": {
     "skylab": {
       "cros_board": "jacuzzi",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "jacuzzi-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "jacuzzi-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive"
     },
     "enabled": true,
@@ -318,8 +318,8 @@
   "CROS_JACUZZI_CQ_PUBLIC_LKGM": {
     "skylab": {
       "cros_board": "jacuzzi",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "jacuzzi-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "jacuzzi-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive",
       "public_builder": "cros_test_platform_public",
       "public_builder_bucket": "testplatform-public"
@@ -330,8 +330,8 @@
   "CROS_TROGDOR_PUBLIC_LKGM": {
     "skylab": {
       "cros_board": "trogdor",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "trogdor-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "trogdor-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive"
     },
     "enabled": true,
@@ -340,8 +340,8 @@
   "CROS_OCTOPUS_PUBLIC_LKGM": {
     "skylab": {
       "cros_board": "octopus",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "octopus-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "octopus-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive"
     },
     "enabled": true,
@@ -350,8 +350,8 @@
   "CROS_OCTOPUS_RELEASE_CHROME_FROM_TLS_ASH_LKGM": {
     "skylab": {
       "cros_board": "octopus",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "octopus-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "octopus-release/R119-15629.0.0",
       "autotest_name": "tast.chrome-from-gcs"
     },
     "identifier": "OCTOPUS_RELEASE_CHROME_FROM_TLS_LKGM"
@@ -359,8 +359,8 @@
   "CROS_OCTOPUS_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "octopus",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "octopus-release/R119-15626.0.0"
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "octopus-release/R119-15629.0.0"
     },
     "enabled": true,
     "identifier": "OCTOPUS_RELEASE_LKGM"
@@ -395,8 +395,8 @@
   "CROS_STRONGBAD_RELEASE_LKGM": {
     "skylab": {
       "cros_board": "strongbad",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "strongbad-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "strongbad-release/R119-15629.0.0",
       "autotest_name": "tast.lacros-from-gcs"
     },
     "enabled": true,
@@ -432,8 +432,8 @@
   "CROS_TROGDOR_RELEASE_ASH_LKGM": {
     "skylab": {
       "cros_board": "trogdor",
-      "cros_chrome_version": "119.0.6018.0",
-      "cros_img": "trogdor-release/R119-15622.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "trogdor-release/R119-15629.0.0",
       "autotest_name": "tast.chrome-from-gcs"
     },
     "identifier": "TROGDOR_RELEASE_LKGM"
@@ -442,8 +442,8 @@
     "skylab": {
       "cros_board": "volteer",
       "cros_model": "voxel",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "volteer-public/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "volteer-public/R119-15629.0.0",
       "bucket": "chromiumos-image-archive",
       "dut_pool": "chromium",
       "public_builder": "cros_test_platform_public",
@@ -455,8 +455,8 @@
   "CROS_VOLTEER_RELEASE_ASH_LKGM": {
     "skylab": {
       "cros_board": "volteer",
-      "cros_chrome_version": "119.0.6024.0",
-      "cros_img": "volteer-release/R119-15626.0.0",
+      "cros_chrome_version": "119.0.6034.0",
+      "cros_img": "volteer-release/R119-15629.0.0",
       "autotest_name": "tast.chrome-from-gcs"
     },
     "identifier": "VOLTEER_RELEASE_LKGM"
diff --git a/infra/config/targets/lacros-version-skew-variants.json b/infra/config/targets/lacros-version-skew-variants.json
index 496a1a5b..333693d 100644
--- a/infra/config/targets/lacros-version-skew-variants.json
+++ b/infra/config/targets/lacros-version-skew-variants.json
@@ -1,16 +1,16 @@
 {
   "LACROS_VERSION_SKEW_CANARY": {
     "args": [
-      "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+      "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
     ],
-    "description": "Run with ash-chrome version 119.0.6035.0",
+    "description": "Run with ash-chrome version 119.0.6037.0",
     "identifier": "Lacros version skew testing ash canary",
     "swarming": {
       "cipd_packages": [
         {
           "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-          "location": "lacros_version_skew_tests_v119.0.6035.0",
-          "revision": "version:119.0.6035.0"
+          "location": "lacros_version_skew_tests_v119.0.6037.0",
+          "revision": "version:119.0.6037.0"
         }
       ]
     }
diff --git a/internal b/internal
index e22c0ea..2eb505f 160000
--- a/internal
+++ b/internal
@@ -1 +1 @@
-Subproject commit e22c0ea945eab1778664a95f0f7551c7cb075e80
+Subproject commit 2eb505f90ae72ea13ed5cd2f8c2e3fbe72243748
diff --git a/ios/chrome/browser/browser_state/model/BUILD.gn b/ios/chrome/browser/browser_state/model/BUILD.gn
index f2f9094..94129a4 100644
--- a/ios/chrome/browser/browser_state/model/BUILD.gn
+++ b/ios/chrome/browser/browser_state/model/BUILD.gn
@@ -115,7 +115,7 @@
     "//ios/chrome/browser/screen_time:buildflags",
     "//ios/chrome/browser/search_engines",
     "//ios/chrome/browser/segmentation_platform",
-    "//ios/chrome/browser/send_tab_to_self",
+    "//ios/chrome/browser/send_tab_to_self/model",
     "//ios/chrome/browser/sessions",
     "//ios/chrome/browser/sessions:session_restoration_service_factory",
     "//ios/chrome/browser/shared/model/application_context",
diff --git a/ios/chrome/browser/discover_feed/discover_feed_service_factory.mm b/ios/chrome/browser/discover_feed/discover_feed_service_factory.mm
index fb47c97..a12f1a6 100644
--- a/ios/chrome/browser/discover_feed/discover_feed_service_factory.mm
+++ b/ios/chrome/browser/discover_feed/discover_feed_service_factory.mm
@@ -58,7 +58,8 @@
       AuthenticationServiceFactory::GetForBrowserState(browser_state);
   configuration.identityManager =
       IdentityManagerFactory::GetForBrowserState(browser_state);
-  configuration.metricsRecorder = [[FeedMetricsRecorder alloc] init];
+  configuration.metricsRecorder = [[FeedMetricsRecorder alloc]
+      initWithPrefService:browser_state->GetPrefs()];
   configuration.ssoService = GetApplicationContext()->GetSSOService();
   configuration.templateURLService =
       ios::TemplateURLServiceFactory::GetForBrowserState(browser_state);
diff --git a/ios/chrome/browser/main/BUILD.gn b/ios/chrome/browser/main/BUILD.gn
index 0ba3edb3..db68c692 100644
--- a/ios/chrome/browser/main/BUILD.gn
+++ b/ios/chrome/browser/main/BUILD.gn
@@ -30,7 +30,7 @@
     "//ios/chrome/browser/ntp:features",
     "//ios/chrome/browser/policy",
     "//ios/chrome/browser/reading_list",
-    "//ios/chrome/browser/send_tab_to_self",
+    "//ios/chrome/browser/send_tab_to_self/model",
     "//ios/chrome/browser/sessions",
     "//ios/chrome/browser/sessions:restoration_agent",
     "//ios/chrome/browser/sessions:session_service",
diff --git a/ios/chrome/browser/main/DEPS b/ios/chrome/browser/main/DEPS
index 3afb9b1..6408866 100644
--- a/ios/chrome/browser/main/DEPS
+++ b/ios/chrome/browser/main/DEPS
@@ -18,7 +18,7 @@
     "+ios/chrome/browser/ntp/features.h",
     "+ios/chrome/browser/policy/policy_watcher_browser_agent.h",
     "+ios/chrome/browser/reading_list/reading_list_browser_agent.h",
-    "+ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h",
+    "+ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h",
     "+ios/chrome/browser/sessions/live_tab_context_browser_agent.h",
     "+ios/chrome/browser/sessions/session_restoration_browser_agent.h",
     "+ios/chrome/browser/sessions/session_service_ios.h",
diff --git a/ios/chrome/browser/main/browser_agent_util.mm b/ios/chrome/browser/main/browser_agent_util.mm
index 5f949c1..d9372c0e 100644
--- a/ios/chrome/browser/main/browser_agent_util.mm
+++ b/ios/chrome/browser/main/browser_agent_util.mm
@@ -17,7 +17,7 @@
 #import "ios/chrome/browser/ntp/features.h"
 #import "ios/chrome/browser/policy/policy_watcher_browser_agent.h"
 #import "ios/chrome/browser/reading_list/reading_list_browser_agent.h"
-#import "ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h"
+#import "ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h"
 #import "ios/chrome/browser/sessions/live_tab_context_browser_agent.h"
 #import "ios/chrome/browser/sessions/session_restoration_browser_agent.h"
 #import "ios/chrome/browser/sessions/session_service_ios.h"
diff --git a/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm b/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
index 499150ff..510ec50 100644
--- a/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
+++ b/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
@@ -21,8 +21,8 @@
 #import "components/password_manager/core/browser/password_manager.h"
 #import "components/password_manager/core/browser/password_manager_constants.h"
 #import "components/password_manager/core/browser/password_manager_driver.h"
-#import "components/password_manager/core/browser/password_manager_util.h"
 #import "components/password_manager/core/browser/password_requirements_service.h"
+#import "components/password_manager/core/browser/password_sync_util.h"
 #import "components/password_manager/core/common/password_manager_pref_names.h"
 #import "components/password_manager/ios/password_manager_ios_util.h"
 #import "components/sync/service/sync_service.h"
@@ -91,7 +91,7 @@
 SyncState IOSChromePasswordManagerClient::GetPasswordSyncState() const {
   syncer::SyncService* sync_service =
       SyncServiceFactory::GetForBrowserState(bridge_.browserState);
-  return password_manager_util::GetPasswordSyncState(sync_service);
+  return password_manager::sync_util::GetPasswordSyncState(sync_service);
 }
 
 bool IOSChromePasswordManagerClient::PromptUserToChooseCredentials(
diff --git a/ios/chrome/browser/passwords/password_manager_util_ios.cc b/ios/chrome/browser/passwords/password_manager_util_ios.cc
index c216e6f..e4b402eb 100644
--- a/ios/chrome/browser/passwords/password_manager_util_ios.cc
+++ b/ios/chrome/browser/passwords/password_manager_util_ios.cc
@@ -4,14 +4,15 @@
 
 #import "ios/chrome/browser/passwords/password_manager_util_ios.h"
 
-#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_manager_client.h"
+#include "components/password_manager/core/browser/password_sync_util.h"
 #include "components/sync/service/sync_service.h"
 
 namespace password_manager_util {
 
 bool IsPasswordSyncNormalEncryptionEnabled(
     const syncer::SyncService* sync_service) {
-  return password_manager_util::GetPasswordSyncState(sync_service) ==
+  return password_manager::sync_util::GetPasswordSyncState(sync_service) ==
          password_manager::SyncState::kSyncingNormalEncryption;
 }
 
diff --git a/ios/chrome/browser/reading_list/model/BUILD.gn b/ios/chrome/browser/reading_list/model/BUILD.gn
new file mode 100644
index 0000000..de168c99
--- /dev/null
+++ b/ios/chrome/browser/reading_list/model/BUILD.gn
@@ -0,0 +1,8 @@
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+source_set("model") {
+  sources = [ "reading_list_browser_agent.h" ]
+  public_deps = [ "//ios/chrome/browser/reading_list" ]
+}
diff --git a/ios/chrome/browser/reading_list/model/reading_list_browser_agent.h b/ios/chrome/browser/reading_list/model/reading_list_browser_agent.h
new file mode 100644
index 0000000..da219b7
--- /dev/null
+++ b/ios/chrome/browser/reading_list/model/reading_list_browser_agent.h
@@ -0,0 +1,10 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_BROWSER_READING_LIST_MODEL_READING_LIST_BROWSER_AGENT_H_
+#define IOS_CHROME_BROWSER_READING_LIST_MODEL_READING_LIST_BROWSER_AGENT_H_
+
+#import "ios/chrome/browser/reading_list/reading_list_browser_agent.h"
+
+#endif  // IOS_CHROME_BROWSER_READING_LIST_MODEL_READING_LIST_BROWSER_AGENT_H_
diff --git a/ios/chrome/browser/send_tab_to_self/BUILD.gn b/ios/chrome/browser/send_tab_to_self/model/BUILD.gn
similarity index 96%
rename from ios/chrome/browser/send_tab_to_self/BUILD.gn
rename to ios/chrome/browser/send_tab_to_self/model/BUILD.gn
index e944aaa..5e9b420 100644
--- a/ios/chrome/browser/send_tab_to_self/BUILD.gn
+++ b/ios/chrome/browser/send_tab_to_self/model/BUILD.gn
@@ -2,7 +2,7 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-source_set("send_tab_to_self") {
+source_set("model") {
   sources = [
     "ios_send_tab_to_self_infobar_delegate.h",
     "ios_send_tab_to_self_infobar_delegate.mm",
@@ -40,7 +40,7 @@
   testonly = true
   sources = [ "send_tab_to_self_browser_agent_unittest.mm" ]
   deps = [
-    ":send_tab_to_self",
+    ":model",
     "//base",
     "//base/test:test_support",
     "//components/keyed_service/ios",
diff --git a/ios/chrome/browser/send_tab_to_self/DEPS b/ios/chrome/browser/send_tab_to_self/model/DEPS
similarity index 100%
rename from ios/chrome/browser/send_tab_to_self/DEPS
rename to ios/chrome/browser/send_tab_to_self/model/DEPS
diff --git a/ios/chrome/browser/send_tab_to_self/OWNERS b/ios/chrome/browser/send_tab_to_self/model/OWNERS
similarity index 100%
rename from ios/chrome/browser/send_tab_to_self/OWNERS
rename to ios/chrome/browser/send_tab_to_self/model/OWNERS
diff --git a/ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.h b/ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.h
similarity index 87%
rename from ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.h
rename to ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.h
index 58e9ce10..cbe80f1 100644
--- a/ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.h
+++ b/ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
-#define IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
+#ifndef IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
+#define IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
 
 #include <CoreFoundation/CoreFoundation.h>
 
@@ -35,7 +35,6 @@
   ~IOSSendTabToSelfInfoBarDelegate() override;
 
  private:
-
   // ConfirmInfoBarDelegate:
   InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override;
   int GetButtons() const override;
@@ -63,4 +62,4 @@
 
 }  // namespace send_tab_to_self
 
-#endif  // IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
+#endif  // IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_IOS_SEND_TAB_TO_SELF_INFOBAR_DELEGATE_H_
diff --git a/ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.mm b/ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.mm
similarity index 93%
rename from ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.mm
rename to ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.mm
index 0ca76f1..49c7b579 100644
--- a/ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.mm
+++ b/ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.mm
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#import "ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.h"
+#import "ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.h"
 
 #import <Foundation/Foundation.h>
 
@@ -56,18 +56,21 @@
                   object:nil
                    queue:nil
               usingBlock:^(NSNotification* note) {
-                if (!weakPtr)
+                if (!weakPtr) {
                   return;
+                }
 
                 // Ignore the notification if it was sent by `weakPtr` (i.e. the
                 // instance that responded first to the send to self infobar)
-                if (note.object == weakPtr->registration_)
+                if (note.object == weakPtr->registration_) {
                   return;
+                }
 
                 infobars::InfoBar* infobar = weakPtr->infobar();
                 infobars::InfoBarManager* owner = infobar->owner();
-                if (!owner)
+                if (!owner) {
                   return;
+                }
 
                 owner->RemoveInfoBar(infobar);
               }];
diff --git a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h
similarity index 93%
rename from ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h
rename to ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h
index e87a38d..090d776 100644
--- a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h
+++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h
@@ -2,8 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#ifndef IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
-#define IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
+#ifndef IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
+#define IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
 
 #import <CoreFoundation/CoreFoundation.h>
 
@@ -101,4 +101,4 @@
       model_observation_{this};
 };
 
-#endif  // IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
+#endif  // IOS_CHROME_BROWSER_SEND_TAB_TO_SELF_MODEL_SEND_TAB_TO_SELF_BROWSER_AGENT_H_
diff --git a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.mm b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm
similarity index 96%
rename from ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.mm
rename to ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm
index 1da639ff..eb6029a 100644
--- a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.mm
+++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.mm
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#import "ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h"
+#import "ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h"
 
 #import <memory>
 #import <string>
@@ -22,7 +22,7 @@
 #import "ios/chrome/browser/infobars/infobar_ios.h"
 #import "ios/chrome/browser/infobars/infobar_manager_impl.h"
 #import "ios/chrome/browser/infobars/infobar_utils.h"
-#import "ios/chrome/browser/send_tab_to_self/ios_send_tab_to_self_infobar_delegate.h"
+#import "ios/chrome/browser/send_tab_to_self/model/ios_send_tab_to_self_infobar_delegate.h"
 #import "ios/chrome/browser/shared/model/browser/browser.h"
 #import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
diff --git a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent_unittest.mm b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
similarity index 98%
rename from ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent_unittest.mm
rename to ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
index 2d63c5d7..ccf0777 100644
--- a/ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent_unittest.mm
+++ b/ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent_unittest.mm
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#import "ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h"
+#import "ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h"
 
 #import <memory>
 
diff --git a/ios/chrome/browser/sessions/BUILD.gn b/ios/chrome/browser/sessions/BUILD.gn
index 1fd3203..13767ec2 100644
--- a/ios/chrome/browser/sessions/BUILD.gn
+++ b/ios/chrome/browser/sessions/BUILD.gn
@@ -54,7 +54,6 @@
     "//ios/chrome/browser/web:feature_flags",
     "//ios/chrome/browser/web:page_placeholder",
     "//ios/chrome/browser/web/session_state",
-    "//ios/chrome/browser/web_state_list/web_usage_enabler",
     "//ios/web/public",
     "//ios/web/public:web_state_observer",
     "//ios/web/public/navigation",
@@ -98,6 +97,7 @@
     "//components/sessions",
     "//ios/chrome/browser/sessions/proto",
     "//ios/chrome/browser/shared/model/browser_state",
+    "//ios/chrome/browser/shared/model/url:constants",
     "//ios/chrome/browser/shared/model/web_state_list",
     "//ios/web/public",
     "//ios/web/public/navigation",
@@ -288,7 +288,6 @@
     "//ios/chrome/browser/signin",
     "//ios/chrome/browser/signin:test_support",
     "//ios/chrome/browser/tabs:features",
-    "//ios/chrome/browser/web_state_list/web_usage_enabler",
     "//ios/chrome/test:test_support",
     "//ios/web/common:features",
     "//ios/web/public/navigation",
diff --git a/ios/chrome/browser/sessions/session_restoration_browser_agent.h b/ios/chrome/browser/sessions/session_restoration_browser_agent.h
index cac3b3a..ad1dbe0 100644
--- a/ios/chrome/browser/sessions/session_restoration_browser_agent.h
+++ b/ios/chrome/browser/sessions/session_restoration_browser_agent.h
@@ -25,7 +25,6 @@
 class SessionRestorationObserver;
 @class SessionServiceIOS;
 class WebStateList;
-class WebUsageEnablerBrowserAgent;
 
 // This class is responsible for handling requests of session restoration. It
 // can be observed via SeassonRestorationObserver which it uses to notify
@@ -121,8 +120,6 @@
   // The list of web states to be saved.
   WebStateList* web_state_list_ = nullptr;
 
-  // The web usage enabler for the web state list being restored.
-  WebUsageEnablerBrowserAgent* web_enabler_ = nullptr;
 
   base::ObserverList<SessionRestorationObserver, true> observers_;
 
diff --git a/ios/chrome/browser/sessions/session_restoration_browser_agent.mm b/ios/chrome/browser/sessions/session_restoration_browser_agent.mm
index bdc15dc0..76662ee4 100644
--- a/ios/chrome/browser/sessions/session_restoration_browser_agent.mm
+++ b/ios/chrome/browser/sessions/session_restoration_browser_agent.mm
@@ -31,7 +31,6 @@
 #import "ios/chrome/browser/web/features.h"
 #import "ios/chrome/browser/web/page_placeholder_tab_helper.h"
 #import "ios/chrome/browser/web/session_state/web_session_state_tab_helper.h"
-#import "ios/chrome/browser/web_state_list/web_usage_enabler/web_usage_enabler_browser_agent.h"
 #import "ios/web/public/navigation/navigation_item.h"
 #import "ios/web/public/navigation/navigation_manager.h"
 #import "ios/web/public/session/crw_session_storage.h"
@@ -198,14 +197,6 @@
                                       selectedIndex:selected_index];
 }
 
-// Struct representing a range of tabs.
-struct Range {
-  const int min;
-  const int max;
-
-  int size() const { return max - min; }
-};
-
 }  // namespace
 
 BROWSER_USER_DATA_KEY_IMPL(SessionRestorationBrowserAgent)
@@ -216,7 +207,6 @@
     bool enable_pinned_web_states)
     : session_service_(session_service),
       web_state_list_(browser->GetWebStateList()),
-      web_enabler_(WebUsageEnablerBrowserAgent::FromBrowser(browser)),
       browser_state_(browser->GetBrowserState()),
       session_window_ios_factory_([[SessionWindowIOSFactory alloc]
           initWithWebStateList:web_state_list_]),
@@ -266,86 +256,28 @@
     observer.WillStartSessionRestoration();
   }
 
-  const int old_count = web_state_list_->count();
-  const int old_pinned_tabs_count = web_state_list_->pinned_tabs_count();
-  DCHECK_GE(old_count, 0);
+  // Restore the tabs (except those that would be empty).
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          web_state_list_, FilterEmptyTabs(window), scope,
+          enable_pinned_web_states_,
+          base::BindRepeating(&web::WebState::CreateWithStorageSession,
+                              web::WebState::CreateParams(browser_state_)));
 
-  // Filter all tabs that would be empty before restoration.
-  window = FilterEmptyTabs(window);
-
-  web_state_list_->PerformBatchOperation(
-      base::BindOnce(^(WebStateList* web_state_list) {
-        web::WebState::CreateParams create_params(browser_state_);
-        DeserializeWebStateList(
-            web_state_list, window, scope, enable_pinned_web_states_,
-            base::BindRepeating(&web::WebState::CreateWithStorageSession,
-                                create_params));
-      }));
-
-  DCHECK_GE(web_state_list_->count(), old_count);
-  DCHECK_GE(web_state_list_->pinned_tabs_count(), old_pinned_tabs_count);
-
-  // If the restored tabs include pinned tabs, they would have been restored
-  // after the existing tabs, so they would range from old_pinned_tabs_count
-  // to web_state_list_->pinned_tabs_count().
-  const Range pinned_tabs_range{
-      .min = old_pinned_tabs_count,
-      .max = web_state_list_->pinned_tabs_count(),
-  };
-
-  // The remaining regular tabs would have been restored at the end of the
-  // WebStateList, so from old_count plus the number of pinned tabs restored
-  // to web_state_list_->count().
-  const Range regular_tabs_range{
-      .min = old_count + pinned_tabs_range.size(),
-      .max = web_state_list_->count(),
-  };
-
-  // Check that all the tabs have been restored.
-  DCHECK_LE(pinned_tabs_range.size() + regular_tabs_range.size(),
-            static_cast<int>(window.sessions.count));
-
-  // Collect the list of restored tabs, setup the placeholder and start
-  // fetching the favicon if possible.
-  std::vector<web::WebState*> restored_web_states;
-  for (Range range : {pinned_tabs_range, regular_tabs_range}) {
-    for (int index = range.min; index < range.max; ++index) {
-      web::WebState* web_state = web_state_list_->GetWebStateAt(index);
-      restored_web_states.push_back(web_state);
-
-      const GURL& visible_url = web_state->GetVisibleURL();
-      if (!visible_url.is_valid()) {
-        continue;
-      }
-
-      favicon::WebFaviconDriver::FromWebState(web_state)->FetchFavicon(
-          visible_url, /*is_same_document=*/false);
-
-      if (visible_url != kChromeUINewTabURL) {
-        PagePlaceholderTabHelper::FromWebState(web_state)
-            ->AddPlaceholderForNextNavigation();
-      }
+  // Setup the placeholder and start fetching the favicon for the restored
+  // tabs if necessary.
+  for (web::WebState* web_state : restored_web_states) {
+    const GURL& visible_url = web_state->GetVisibleURL();
+    if (!visible_url.is_valid()) {
+      continue;
     }
-  }
 
-  // Check that all the restored tabs have been accounted for.
-  DCHECK_EQ(pinned_tabs_range.size() + regular_tabs_range.size(),
-            static_cast<int>(restored_web_states.size()));
+    favicon::WebFaviconDriver::FromWebState(web_state)->FetchFavicon(
+        visible_url, /*is_same_document=*/false);
 
-  // If there was only one tab and it was the new tab page, clobber it.
-  if (old_count == 1 && !restored_web_states.empty()) {
-    web::WebState* web_state = web_state_list_->GetWebStateAt(0);
-
-    // An "unrealized" WebState has no pending load. Checking for realization
-    // before accessing the NavigationManager prevents accidental realization
-    // of the WebState.
-    const bool has_pending_load =
-        web_state->IsRealized() &&
-        web_state->GetNavigationManager()->GetPendingItem() != nullptr;
-
-    if (!has_pending_load &&
-        (web_state->GetLastCommittedURL() == kChromeUINewTabURL)) {
-      web_state_list_->CloseWebStateAt(0, WebStateList::CLOSE_USER_ACTION);
+    if (visible_url != kChromeUINewTabURL) {
+      PagePlaceholderTabHelper::FromWebState(web_state)
+          ->AddPlaceholderForNextNavigation();
     }
   }
 
diff --git a/ios/chrome/browser/sessions/session_restoration_browser_agent_unittest.mm b/ios/chrome/browser/sessions/session_restoration_browser_agent_unittest.mm
index 1f1793c..af43b50 100644
--- a/ios/chrome/browser/sessions/session_restoration_browser_agent_unittest.mm
+++ b/ios/chrome/browser/sessions/session_restoration_browser_agent_unittest.mm
@@ -27,7 +27,6 @@
 #import "ios/chrome/browser/signin/authentication_service_factory.h"
 #import "ios/chrome/browser/signin/fake_authentication_service_delegate.h"
 #import "ios/chrome/browser/tabs/features.h"
-#import "ios/chrome/browser/web_state_list/web_usage_enabler/web_usage_enabler_browser_agent.h"
 #import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
 #import "ios/web/public/navigation/navigation_manager.h"
 #import "ios/web/public/navigation/referrer.h"
@@ -172,11 +171,6 @@
     browser_ = std::make_unique<TestBrowser>(
         chrome_browser_state_.get(),
         std::make_unique<BrowserWebStateListDelegate>());
-    // Web usage is disabled during these tests.
-    WebUsageEnablerBrowserAgent::CreateForBrowser(browser_.get());
-    web_usage_enabler_ =
-        WebUsageEnablerBrowserAgent::FromBrowser(browser_.get());
-    web_usage_enabler_->SetWebUsageEnabled(false);
   }
 
   void TearDown() override {
@@ -237,7 +231,6 @@
 
   __strong NSString* session_identifier_ = nil;
   TestSessionService* test_session_service_;
-  WebUsageEnablerBrowserAgent* web_usage_enabler_;
   SessionRestorationBrowserAgent* session_restoration_agent_;
 };
 
diff --git a/ios/chrome/browser/sessions/web_state_list_serialization.h b/ios/chrome/browser/sessions/web_state_list_serialization.h
index 4c4b85d..f702dfa4 100644
--- a/ios/chrome/browser/sessions/web_state_list_serialization.h
+++ b/ios/chrome/browser/sessions/web_state_list_serialization.h
@@ -8,6 +8,7 @@
 #import <Foundation/Foundation.h>
 
 #include <memory>
+#include <vector>
 
 #include "base/functional/callback_forward.h"
 
@@ -52,20 +53,30 @@
 // create the restored WebStates. Use `scope` to limit which WebStates
 // are created. If `enable_pinned_web_states` is false, the tabs are not
 // marked as pinned upon restoration.
-void DeserializeWebStateList(WebStateList* web_state_list,
-                             SessionWindowIOS* session_window,
-                             SessionRestorationScope scope,
-                             bool enable_pinned_web_states,
-                             const WebStateFactory& factory);
+//
+// Returns a vector containing pointer to the restored WebStates. The
+// pointers are still owned by the WebStateList, so they may become
+// invalid as soon as the list is mutated.
+std::vector<web::WebState*> DeserializeWebStateList(
+    WebStateList* web_state_list,
+    SessionWindowIOS* session_window,
+    SessionRestorationScope scope,
+    bool enable_pinned_web_states,
+    const WebStateFactory& factory);
 
 // Restores a `web_state_list` from `storage` using `factory` to create
 // the restored WebStates. Use `scope` to limit which WebStates are created.
 // If `enabled_pinned_web_states` is false, the tabs are not marked as
 // pinned upon restoration.
-void DeserializeWebStateList(WebStateList& web_state_list,
-                             ios::proto::WebStateListStorage storage,
-                             SessionRestorationScope scope,
-                             bool enable_pinned_web_states,
-                             const WebStateFactoryFromProto& factory);
+//
+// Returns a vector containing pointer to the restored WebStates. The
+// pointers are still owned by the WebStateList, so they may become
+// invalid as soon as the list is mutated.
+std::vector<web::WebState*> DeserializeWebStateList(
+    WebStateList* web_state_list,
+    ios::proto::WebStateListStorage storage,
+    SessionRestorationScope scope,
+    bool enable_pinned_web_states,
+    const WebStateFactoryFromProto& factory);
 
 #endif  // IOS_CHROME_BROWSER_SESSIONS_WEB_STATE_LIST_SERIALIZATION_H_
diff --git a/ios/chrome/browser/sessions/web_state_list_serialization.mm b/ios/chrome/browser/sessions/web_state_list_serialization.mm
index 8e87edad..4fcaf85 100644
--- a/ios/chrome/browser/sessions/web_state_list_serialization.mm
+++ b/ios/chrome/browser/sessions/web_state_list_serialization.mm
@@ -19,6 +19,7 @@
 #import "ios/chrome/browser/sessions/proto/storage.pb.h"
 #import "ios/chrome/browser/sessions/session_constants.h"
 #import "ios/chrome/browser/sessions/session_window_ios.h"
+#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
 #import "ios/chrome/browser/shared/model/web_state_list/order_controller.h"
 #import "ios/chrome/browser/shared/model/web_state_list/order_controller_source_from_web_state_list.h"
 #import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
@@ -29,7 +30,6 @@
 #import "ios/web/public/session/crw_session_user_data.h"
 #import "ios/web/public/session/serializable_user_data_manager.h"
 #import "ios/web/public/web_state.h"
-#import "net/base/mac/url_conversions.h"
 
 namespace {
 
@@ -242,6 +242,9 @@
   const int min;
   const int max;
 
+  // Returns the number of items in range.
+  int length() const { return max - min; }
+
   // Returns whether the range contains any item.
   bool empty() const { return min >= max; }
 
@@ -296,10 +299,16 @@
   }
 };
 
-void DeserializeWebStateList(WebStateList& web_state_list,
-                             SessionRestorationScope scope,
-                             bool enable_pinned_web_states,
-                             const Deserializer& deserializer) {
+void DeserializeWebStateListInternal(
+    SessionRestorationScope scope,
+    bool enable_pinned_web_states,
+    const Deserializer& deserializer,
+    std::vector<web::WebState*>* restored_web_states,
+    WebStateList* web_state_list) {
+  DCHECK(web_state_list);
+  DCHECK(restored_web_states);
+  DCHECK(restored_web_states->empty());
+
   int restored_pinned_tabs_count = 0;
   const int restored_tabs_count = deserializer.GetRestoredTabsCount();
   if (enable_pinned_web_states) {
@@ -318,6 +327,27 @@
     return;
   }
 
+  // If there is only one tab, and it is the new tab page, clobber it before
+  // restoring any tabs (this avoid having to search for the tab amongst all
+  // the ones that have been restored).
+  if (web_state_list->count() == 1) {
+    web::WebState* web_state = web_state_list->GetWebStateAt(0);
+
+    // Check for realization before checking for a pending load to prevent
+    // force-realization of the tab. An unrealized WebState cannot have a
+    // load pending anyway.
+    const bool has_pending_load =
+        web_state->IsRealized() &&
+        web_state->GetNavigationManager()->GetPendingItem();
+
+    if (!has_pending_load) {
+      const GURL last_committed_url = web_state->GetLastCommittedURL();
+      if (last_committed_url == kChromeUINewTabURL) {
+        web_state_list->CloseWebStateAt(0, WebStateList::CLOSE_USER_ACTION);
+      }
+    }
+  }
+
   // Instantiate an InsertionHelper object that will help compute the indexes
   // and flags used to insert the WebState in the WebStateList.
   //
@@ -328,8 +358,8 @@
   // to be adjusted to compensate for that.
   const InsertionHelper helper{
       .pinned_tabs_count = restored_pinned_tabs_count,
-      .pinned_offset = web_state_list.pinned_tabs_count() - range.min,
-      .regular_offset = web_state_list.count() - range.min,
+      .pinned_offset = web_state_list->pinned_tabs_count() - range.min,
+      .regular_offset = web_state_list->count() - range.min,
   };
 
   // Get the index of the active item according to storage. If it is in
@@ -342,13 +372,18 @@
   // some WebState may have an opener that is stored after them.
   for (int index = range.min; index < range.max; ++index) {
     std::unique_ptr<web::WebState> web_state = deserializer.RestoreTabAt(index);
-    const int inserted_index = web_state_list.InsertWebState(
+    restored_web_states->push_back(web_state.get());  // Store pointer to item.
+
+    const int inserted_index = web_state_list->InsertWebState(
         helper.insertion_index(index), std::move(web_state),
         helper.insertion_flags(index, index == active_index), WebStateOpener{});
 
     DCHECK_EQ(inserted_index, helper.insertion_index(index));
   }
 
+  // Check that all WebStates have been restored.
+  DCHECK_EQ(range.length(), static_cast<int>(restored_web_states->size()));
+
   // Restore the opener-opened relationship while taking into account that
   // some of the WebState have not been restored due to `scope`, and that
   // the indexes have to be adjusted.
@@ -358,11 +393,10 @@
       continue;
     }
 
-    // Use helper to compute the insertion index of the opener.
-    web::WebState* opener =
-        web_state_list.GetWebStateAt(helper.insertion_index(ref.index));
-
-    web_state_list.SetOpenerOfWebStateAt(
+    // The created WebStates are pushed in order in `restored_web_states`
+    // so the opener will be at index `ref.index - range.min`.
+    web::WebState* opener = (*restored_web_states)[ref.index - range.min];
+    web_state_list->SetOpenerOfWebStateAt(
         helper.insertion_index(index),
         WebStateOpener(opener, ref.navigation_index));
   }
@@ -487,21 +521,29 @@
   storage.set_active_index(active_index);
 }
 
-void DeserializeWebStateList(WebStateList* web_state_list,
-                             SessionWindowIOS* session_window,
-                             SessionRestorationScope scope,
-                             bool enable_pinned_web_states,
-                             const WebStateFactory& factory) {
-  DeserializeWebStateList(
-      *web_state_list, scope, enable_pinned_web_states,
-      DeserializeFromSessionWindow(session_window, factory));
+std::vector<web::WebState*> DeserializeWebStateList(
+    WebStateList* web_state_list,
+    SessionWindowIOS* session_window,
+    SessionRestorationScope scope,
+    bool enable_pinned_web_states,
+    const WebStateFactory& factory) {
+  std::vector<web::WebState*> restored_web_states;
+  web_state_list->PerformBatchOperation(base::BindOnce(
+      &DeserializeWebStateListInternal, scope, enable_pinned_web_states,
+      DeserializeFromSessionWindow(session_window, factory),
+      &restored_web_states));
+  return restored_web_states;
 }
 
-void DeserializeWebStateList(WebStateList& web_state_list,
-                             ios::proto::WebStateListStorage storage,
-                             SessionRestorationScope scope,
-                             bool enable_pinned_web_states,
-                             const WebStateFactoryFromProto& factory) {
-  DeserializeWebStateList(web_state_list, scope, enable_pinned_web_states,
-                          DeserializeFromProto(std::move(storage), factory));
+std::vector<web::WebState*> DeserializeWebStateList(
+    WebStateList* web_state_list,
+    ios::proto::WebStateListStorage storage,
+    SessionRestorationScope scope,
+    bool enable_pinned_web_states,
+    const WebStateFactoryFromProto& factory) {
+  std::vector<web::WebState*> restored_web_states;
+  web_state_list->PerformBatchOperation(base::BindOnce(
+      &DeserializeWebStateListInternal, scope, enable_pinned_web_states,
+      DeserializeFromProto(std::move(storage), factory), &restored_web_states));
+  return restored_web_states;
 }
diff --git a/ios/chrome/browser/sessions/web_state_list_serialization_unittest.mm b/ios/chrome/browser/sessions/web_state_list_serialization_unittest.mm
index 139b64d..205817f3 100644
--- a/ios/chrome/browser/sessions/web_state_list_serialization_unittest.mm
+++ b/ios/chrome/browser/sessions/web_state_list_serialization_unittest.mm
@@ -12,12 +12,14 @@
 #import "ios/chrome/browser/sessions/proto/storage.pb.h"
 #import "ios/chrome/browser/sessions/session_constants.h"
 #import "ios/chrome/browser/sessions/session_window_ios.h"
+#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
 #import "ios/chrome/browser/shared/model/web_state_list/test/fake_web_state_list_delegate.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
 #import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
 #import "ios/web/public/session/crw_session_storage.h"
 #import "ios/web/public/session/crw_session_user_data.h"
 #import "ios/web/public/session/serializable_user_data_manager.h"
+#import "ios/web/public/test/fakes/fake_navigation_manager.h"
 #import "ios/web/public/test/fakes/fake_web_state.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/gtest_mac.h"
@@ -25,21 +27,52 @@
 
 namespace {
 
-// Creates a fake WebState with `cnt` navigation items.
-std::unique_ptr<web::WebState> CreateWebStateWithNavigationItemCount(int cnt) {
+// Creates a fake WebState with `navigation_count` navigation items (all
+// pointing to the same `url`). If `has_pending_load` is true, the last
+// item will be marked as pending.
+std::unique_ptr<web::WebState> CreateWebStateWithNavigations(
+    int navigation_count,
+    bool has_pending_load,
+    const GURL& url) {
+  auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
+  for (int index = 0; index < navigation_count; ++index) {
+    navigation_manager->AddItem(url, ui::PAGE_TRANSITION_TYPED);
+  }
+
+  if (navigation_count > 0) {
+    if (has_pending_load) {
+      const int pending_item_index = navigation_count - 1;
+      navigation_manager->SetPendingItemIndex(pending_item_index);
+      navigation_manager->SetPendingItem(
+          navigation_manager->GetItemAtIndex(pending_item_index));
+    }
+  }
+
   auto web_state = std::make_unique<web::FakeWebState>();
-  web_state->SetNavigationItemCount(cnt);
+  web_state->SetNavigationManager(std::move(navigation_manager));
+  web_state->SetNavigationItemCount(navigation_count);
+  if (navigation_count > 0) {
+    web_state->SetVisibleURL(url);
+  }
+
   return web_state;
 }
 
 // Creates a fake WebState with some navigations.
 std::unique_ptr<web::WebState> CreateWebState() {
-  return CreateWebStateWithNavigationItemCount(1);
+  return CreateWebStateWithNavigations(1, false, GURL(kChromeUIVersionURL));
 }
 
 // Creates a fake WebState with no navigation items.
 std::unique_ptr<web::WebState> CreateWebStateWithNoNavigation() {
-  return CreateWebStateWithNavigationItemCount(0);
+  return CreateWebStateWithNavigations(0, false, GURL());
+}
+
+// Creates a fake WebState on NTP. If `has_pending_load`, then the last
+// item is marked as pending.
+std::unique_ptr<web::WebState> CreateWebStateOnNTP(bool has_pending_load) {
+  return CreateWebStateWithNavigations(1, has_pending_load,
+                                       GURL(kChromeUINewTabURL));
 }
 
 // Creates a fake WebState from `storage`.
@@ -53,6 +86,12 @@
     web::WebStateID web_state_id) {
   return CreateWebState();
 }
+
+// Returns the unique identifier for WebState at `index` in `web_state_list`.
+web::WebStateID IdentifierAt(const WebStateList& web_state_list, int index) {
+  return web_state_list.GetWebStateAt(index)->GetUniqueIdentifier();
+}
+
 }  // namespace
 
 // Comparison operators for testing.
@@ -326,10 +365,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window, SessionRestorationScope::kAll,
-      /* enable_pinned_web_states*/ true,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -387,11 +429,13 @@
       WebStateOpener());
   ASSERT_EQ(1, restored_web_state_list.count());
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window,
-      SessionRestorationScope::kRegularOnly,
-      /* enable_pinned_web_states*/ true,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kRegularOnly,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 3u);
 
   ASSERT_EQ(restored_web_state_list.count(), 4);
   EXPECT_EQ(restored_web_state_list.active_index(), 1);
@@ -442,11 +486,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window,
-      SessionRestorationScope::kPinnedOnly,
-      /* enable_pinned_web_states*/ true,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kPinnedOnly,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 1u);
 
   ASSERT_EQ(restored_web_state_list.count(), 2);
   EXPECT_EQ(restored_web_state_list.active_index(), 1);
@@ -497,10 +543,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window, SessionRestorationScope::kAll,
-      /* enable_pinned_web_states*/ false,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -554,11 +603,13 @@
       WebStateOpener());
   ASSERT_EQ(1, restored_web_state_list.count());
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window,
-      SessionRestorationScope::kRegularOnly,
-      /* enable_pinned_web_states*/ false,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kRegularOnly,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -612,17 +663,188 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(
-      &restored_web_state_list, session_window,
-      SessionRestorationScope::kPinnedOnly,
-      /* enable_pinned_web_states*/ false,
-      base::BindRepeating(&CreateWebStateWithSessionStorage));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kPinnedOnly,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 0u);
 
   ASSERT_EQ(restored_web_state_list.count(), 1);
   EXPECT_EQ(restored_web_state_list.active_index(), 0);
   EXPECT_EQ(restored_web_state_list.pinned_tabs_count(), 0);
 }
 
+// Tests deserializing into a non-empty WebStateList containing a single
+// WebState displaying the NTP and without pending navigation leads to
+// closing the old NTP tab.
+//
+// Objective-C (legacy) variant.
+TEST_F(WebStateListSerializationTest, Deserialize_ObjC_SingleTabNTP) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  SessionWindowIOS* session_window =
+      SerializeWebStateList(&original_web_state_list);
+
+  EXPECT_EQ(session_window.sessions.count, 1u);
+  EXPECT_EQ(session_window.selectedIndex, 0u);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+  EXPECT_NE(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing into a non-empty WebStateList containing a single
+// WebState displaying the NTP and with a pending navigation does not
+// cause the tab to be closed.
+//
+// Objective-C (legacy) variant.
+TEST_F(WebStateListSerializationTest,
+       Deserialize_ObjC_SingleTabNTP_PendingNavigation) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  SessionWindowIOS* session_window =
+      SerializeWebStateList(&original_web_state_list);
+
+  EXPECT_EQ(session_window.sessions.count, 1u);
+  EXPECT_EQ(session_window.selectedIndex, 0u);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ true),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 2);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing an empty session into a non-empty WebStateList
+// containing a single WebState displaying the NTP and without pending
+// does not cause the tab to be closed.
+//
+// Objective-C (legacy) variant.
+TEST_F(WebStateListSerializationTest,
+       Deserialize_ObjC_SingleTabNTP_EmptySession) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+
+  SessionWindowIOS* session_window =
+      SerializeWebStateList(&original_web_state_list);
+
+  EXPECT_EQ(session_window.sessions.count, 0u);
+  EXPECT_EQ(session_window.selectedIndex, static_cast<NSUInteger>(NSNotFound));
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 0u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing into a non-empty WebStateList containing multiple
+// WebStates does not lead to closing those tabs, even if they all display
+// the NTP and have no pending navigation.
+//
+// Objective-C (legacy) variant.
+TEST_F(WebStateListSerializationTest, Deserialize_ObjC__MultipleNTPTabs) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  SessionWindowIOS* session_window =
+      SerializeWebStateList(&original_web_state_list);
+
+  EXPECT_EQ(session_window.sessions.count, 1u);
+  EXPECT_EQ(session_window.selectedIndex, 0u);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  restored_web_state_list.InsertWebState(
+      1, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_NO_FLAGS, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 2);
+
+  // Record the WebStateIDs of the WebStates displaying the NTP.
+  const auto ntp_web_state_id_0 = IdentifierAt(restored_web_state_list, 0);
+  const auto ntp_web_state_id_1 = IdentifierAt(restored_web_state_list, 1);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, session_window,
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithSessionStorage));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 3);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id_0);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 1), ntp_web_state_id_1);
+}
+
 // Tests deserializing into a non-empty WebStateList works when support for
 // pinned tabs is enabled and the scope includes all tabs.
 //
@@ -660,10 +882,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kAll,
-                          /* enable_pinned_web_states*/ true,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -722,10 +947,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kRegularOnly,
-                          /* enable_pinned_web_states*/ true,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kRegularOnly,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 3u);
 
   ASSERT_EQ(restored_web_state_list.count(), 4);
   EXPECT_EQ(restored_web_state_list.active_index(), 1);
@@ -777,10 +1005,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kPinnedOnly,
-                          /* enable_pinned_web_states*/ true,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kPinnedOnly,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 1u);
 
   ASSERT_EQ(restored_web_state_list.count(), 2);
   EXPECT_EQ(restored_web_state_list.active_index(), 1);
@@ -832,10 +1063,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kAll,
-                          /* enable_pinned_web_states*/ false,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -890,10 +1124,13 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kRegularOnly,
-                          /* enable_pinned_web_states*/ false,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kRegularOnly,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 4u);
 
   ASSERT_EQ(restored_web_state_list.count(), 5);
   EXPECT_EQ(restored_web_state_list.active_index(), 2);
@@ -948,12 +1185,188 @@
       WebStateOpener());
   ASSERT_EQ(restored_web_state_list.count(), 1);
 
-  DeserializeWebStateList(restored_web_state_list, std::move(storage),
-                          SessionRestorationScope::kPinnedOnly,
-                          /* enable_pinned_web_states*/ false,
-                          base::BindRepeating(&CreateWebStateWithWebStateID));
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kPinnedOnly,
+          /*enable_pinned_web_states*/ false,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 0u);
 
   ASSERT_EQ(restored_web_state_list.count(), 1);
   EXPECT_EQ(restored_web_state_list.active_index(), 0);
   EXPECT_EQ(restored_web_state_list.pinned_tabs_count(), 0);
 }
+
+// Tests deserializing into a non-empty WebStateList containing a single
+// WebState displaying the NTP and without pending navigation leads to
+// closing the old NTP tab.
+//
+// Protobuf message variant.
+TEST_F(WebStateListSerializationTest, Deserialize_Proto_SingleTabNTP) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  ios::proto::WebStateListStorage storage;
+  SerializeWebStateList(original_web_state_list, storage);
+
+  EXPECT_EQ(storage.items_size(), 1);
+  EXPECT_EQ(storage.active_index(), 0);
+  EXPECT_EQ(storage.pinned_item_count(), 0);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+  EXPECT_NE(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing into a non-empty WebStateList containing a single
+// WebState displaying the NTP and with a pending navigation does not
+// cause the tab to be closed.
+//
+// Protobuf message variant.
+TEST_F(WebStateListSerializationTest,
+       Deserialize_Proto_SingleTabNTP_PendingNavigation) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  ios::proto::WebStateListStorage storage;
+  SerializeWebStateList(original_web_state_list, storage);
+
+  EXPECT_EQ(storage.items_size(), 1);
+  EXPECT_EQ(storage.active_index(), 0);
+  EXPECT_EQ(storage.pinned_item_count(), 0);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ true),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 2);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing an empty session into a non-empty WebStateList
+// containing a single WebState displaying the NTP and without pending
+// does not cause the tab to be closed.
+//
+// Protobuf message variant.
+TEST_F(WebStateListSerializationTest,
+       Deserialize_Proto_SingleTabNTP_EmptySession) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+
+  ios::proto::WebStateListStorage storage;
+  SerializeWebStateList(original_web_state_list, storage);
+
+  EXPECT_EQ(storage.items_size(), 0);
+  EXPECT_EQ(storage.active_index(), -1);
+  EXPECT_EQ(storage.pinned_item_count(), 0);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+
+  // Record the WebStateID of the WebState displaying the NTP.
+  const auto ntp_web_state_id = IdentifierAt(restored_web_state_list, 0);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 0u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 1);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id);
+}
+
+// Tests deserializing into a non-empty WebStateList containing multiple
+// WebStates does not lead to closing those tabs, even if they all display
+// the NTP and have no pending navigation.
+//
+// Protobuf message variant.
+TEST_F(WebStateListSerializationTest, Deserialize_Proto_MultipleNTPTabs) {
+  FakeWebStateListDelegate delegate;
+  WebStateList original_web_state_list(&delegate);
+  original_web_state_list.InsertWebState(
+      0, CreateWebState(), WebStateList::INSERT_ACTIVATE, WebStateOpener());
+
+  ios::proto::WebStateListStorage storage;
+  SerializeWebStateList(original_web_state_list, storage);
+
+  EXPECT_EQ(storage.items_size(), 1);
+  EXPECT_EQ(storage.active_index(), 0);
+  EXPECT_EQ(storage.pinned_item_count(), 0);
+
+  // Create a WebStateList with a single tab displaying NTP.
+  WebStateList restored_web_state_list(&delegate);
+  restored_web_state_list.InsertWebState(
+      0, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_ACTIVATE, WebStateOpener());
+  restored_web_state_list.InsertWebState(
+      1, CreateWebStateOnNTP(/*has_pending_load*/ false),
+      WebStateList::INSERT_NO_FLAGS, WebStateOpener());
+  ASSERT_EQ(restored_web_state_list.count(), 2);
+
+  // Record the WebStateIDs of the WebStates displaying the NTP.
+  const auto ntp_web_state_id_0 = IdentifierAt(restored_web_state_list, 0);
+  const auto ntp_web_state_id_1 = IdentifierAt(restored_web_state_list, 1);
+
+  // Check that after restoration, the old tab displaying the NTP
+  // has been closed.
+  const std::vector<web::WebState*> restored_web_states =
+      DeserializeWebStateList(
+          &restored_web_state_list, std::move(storage),
+          SessionRestorationScope::kAll,
+          /*enable_pinned_web_states*/ true,
+          base::BindRepeating(&CreateWebStateWithWebStateID));
+  EXPECT_EQ(restored_web_states.size(), 1u);
+
+  ASSERT_EQ(restored_web_state_list.count(), 3);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 0), ntp_web_state_id_0);
+  EXPECT_EQ(IdentifierAt(restored_web_state_list, 1), ntp_web_state_id_1);
+}
diff --git a/ios/chrome/browser/settings/sync/utils/sync_fake_server_egtest.mm b/ios/chrome/browser/settings/sync/utils/sync_fake_server_egtest.mm
index dbf3e94d..af88795 100644
--- a/ios/chrome/browser/settings/sync/utils/sync_fake_server_egtest.mm
+++ b/ios/chrome/browser/settings/sync/utils/sync_fake_server_egtest.mm
@@ -71,7 +71,6 @@
   WaitForEntitiesOnFakeServer(0, syncer::AUTOFILL_PROFILE);
   WaitForEntitiesOnFakeServer(0, syncer::BOOKMARKS);
   WaitForEntitiesOnFakeServer(0, syncer::HISTORY);
-  WaitForEntitiesOnFakeServer(0, syncer::TYPED_URLS);
 
   [super tearDown];
 }
@@ -86,7 +85,6 @@
   WaitForEntitiesOnFakeServer(0, syncer::AUTOFILL_PROFILE);
   WaitForEntitiesOnFakeServer(0, syncer::BOOKMARKS);
   WaitForEntitiesOnFakeServer(0, syncer::HISTORY);
-  WaitForEntitiesOnFakeServer(0, syncer::TYPED_URLS);
 }
 
 - (AppLaunchConfiguration)appConfigurationForTestCase {
@@ -364,7 +362,7 @@
                                        timeout:kSyncOperationTimeout];
 }
 
-// Tests that typed url is downloaded from sync server.
+// Tests that history is downloaded from the sync server.
 - (void)testSyncHistoryDownload {
   const GURL mockURL("http://not-a-real-site/");
 
diff --git a/ios/chrome/browser/shared/model/prefs/BUILD.gn b/ios/chrome/browser/shared/model/prefs/BUILD.gn
index 72588a2..5bbc706 100644
--- a/ios/chrome/browser/shared/model/prefs/BUILD.gn
+++ b/ios/chrome/browser/shared/model/prefs/BUILD.gn
@@ -103,6 +103,7 @@
     "//ios/chrome/browser/ui/first_run:field_trial",
     "//ios/chrome/browser/ui/incognito_reauth:incognito_reauth_scene_agent",
     "//ios/chrome/browser/ui/ntp:field_trial",
+    "//ios/chrome/browser/ui/ntp/metrics",
     "//ios/chrome/browser/ui/reading_list:reading_list_constants",
     "//ios/chrome/browser/voice:prefs",
     "//ios/chrome/browser/web",
diff --git a/ios/chrome/browser/shared/model/prefs/browser_prefs.mm b/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
index 2486507..1e69aa0 100644
--- a/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
+++ b/ios/chrome/browser/shared/model/prefs/browser_prefs.mm
@@ -90,6 +90,7 @@
 #import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
 #import "ios/chrome/browser/ui/content_suggestions/safety_check/safety_check_prefs.h"
 #import "ios/chrome/browser/ui/incognito_reauth/incognito_reauth_scene_agent.h"
+#import "ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h"
 #import "ios/chrome/browser/voice/voice_search_prefs_registration.h"
 #import "ios/chrome/browser/web/font_size/font_size_tab_helper.h"
 #import "ios/web/common/features.h"
@@ -541,6 +542,8 @@
 
   registry->RegisterBooleanPref(prefs::kPasswordSharingFlowHasBeenEntered,
                                 false);
+  // Preference related to feed.
+  registry->RegisterTimePref(kActivityBucketLastReportedDateKey, base::Time());
 }
 
 // This method should be periodically pruned of year+ old migrations.
@@ -678,6 +681,19 @@
   prefs->ClearPref(kObsoleteIosSettingsPromoAlreadySeen);
   prefs->ClearPref(kObsoleteIosSettingsSigninPromoDisplayedCount);
   prefs->ClearPref(kPrivacySandboxManuallyControlled);
+
+  // Added 09/2023.
+  // TODO(crbug.com/1486770) To be removed after a few milestones.
+  {
+    NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
+    NSString* key = @(kActivityBucketLastReportedDateKey);
+    NSDate* value = [defaults objectForKey:key];
+    if (value != nil) {
+      [defaults removeObjectForKey:key];
+      prefs->SetTime(kActivityBucketLastReportedDateKey,
+                     base::Time::FromNSDate(value));
+    }
+  }
 }
 
 void MigrateObsoleteUserDefault(void) {
diff --git a/ios/chrome/browser/sync/ios_chrome_sync_client.mm b/ios/chrome/browser/sync/ios_chrome_sync_client.mm
index 28a580c..77d996f9 100644
--- a/ios/chrome/browser/sync/ios_chrome_sync_client.mm
+++ b/ios/chrome/browser/sync/ios_chrome_sync_client.mm
@@ -276,7 +276,6 @@
     case syncer::DEVICE_INFO:
     case syncer::READING_LIST:
     case syncer::SESSIONS:
-    case syncer::TYPED_URLS:
       NOTREACHED();
       return base::WeakPtr<syncer::ModelTypeControllerDelegate>();
 
diff --git a/ios/chrome/browser/sync/sync_service_factory_unittest.cc b/ios/chrome/browser/sync/sync_service_factory_unittest.cc
index e1bb9cc..52971ed4 100644
--- a/ios/chrome/browser/sync/sync_service_factory_unittest.cc
+++ b/ios/chrome/browser/sync/sync_service_factory_unittest.cc
@@ -49,7 +49,7 @@
  protected:
   // Returns the collection of default datatypes.
   syncer::ModelTypeSet DefaultDatatypes() {
-    static_assert(49 == syncer::GetNumModelTypes(),
+    static_assert(48 == syncer::GetNumModelTypes(),
                   "When adding a new type, you probably want to add it here as "
                   "well (assuming it is already enabled).");
 
diff --git a/ios/chrome/browser/ui/browser_view/BUILD.gn b/ios/chrome/browser/ui/browser_view/BUILD.gn
index 6708b864..08216f7 100644
--- a/ios/chrome/browser/ui/browser_view/BUILD.gn
+++ b/ios/chrome/browser/ui/browser_view/BUILD.gn
@@ -90,7 +90,7 @@
     "//ios/chrome/browser/promos_manager:features",
     "//ios/chrome/browser/reading_list",
     "//ios/chrome/browser/segmentation_platform",
-    "//ios/chrome/browser/send_tab_to_self",
+    "//ios/chrome/browser/send_tab_to_self/model",
     "//ios/chrome/browser/sessions",
     "//ios/chrome/browser/sessions:serialisation",
     "//ios/chrome/browser/settings/sync/utils",
diff --git a/ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_egtest.mm b/ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_egtest.mm
index f4c5ab57..627573f3 100644
--- a/ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_egtest.mm
+++ b/ios/chrome/browser/ui/content_suggestions/tab_resumption/tab_resumption_egtest.mm
@@ -205,13 +205,7 @@
 
 // Tests that interacting with the Shortcuts tile works when the tab resumption
 // tile is displayed.
-// TODO(crbug.com/1486969): Test is failing on device. Re-enable the test.
-#if TARGET_OS_SIMULATOR
-#define MAYBE_testInteractWithAnotherTile testInteractWithAnotherTile
-#else
-#define MAYBE_testInteractWithAnotherTile DISABLED_testInteractWithAnotherTile
-#endif
-- (void)MAYBE_testInteractWithAnotherTile {
+- (void)testInteractWithAnotherTile {
   // Check that the tile is not displayed when there is no distant tab.
   WaitUntilTabResumptionTileVisibleOrTimeout(false);
 
@@ -228,21 +222,22 @@
   // Check that the tile is displayed when there is a distant tab.
   WaitUntilTabResumptionTileVisibleOrTimeout(true);
 
-  // Verify Shortcuts module title is visible in Magic Stack.
-  [[EarlGrey
-      selectElementWithMatcher:
-          grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier)]
-      performAction:grey_scrollInDirection(kGREYDirectionRight, 343)];
-  [[EarlGrey selectElementWithMatcher:
-                 grey_accessibilityID(l10n_util::GetNSString(
-                     IDS_IOS_CONTENT_SUGGESTIONS_SHORTCUTS_MODULE_TITLE))]
-      assertWithMatcher:grey_sufficientlyVisible()];
-
-  // Check the RecentTabs shortcut.
-  [[EarlGrey
-      selectElementWithMatcher:chrome_test_util::ButtonWithAccessibilityLabelId(
-                                   IDS_IOS_CONTENT_SUGGESTIONS_RECENT_TABS)]
+  if (![ChromeEarlGrey isIPadIdiom]) {
+    // Rotate iphone device so Magic Stack can be scrollable.
+    [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationLandscapeLeft
+                                  error:nil];
+    [[EarlGrey selectElementWithMatcher:chrome_test_util::NTPCollectionView()]
+        performAction:grey_scrollInDirection(kGREYDirectionDown, 150)];
+  }
+  [[[EarlGrey selectElementWithMatcher:
+                  grey_allOf(chrome_test_util::ButtonWithAccessibilityLabelId(
+                                 IDS_IOS_CONTENT_SUGGESTIONS_RECENT_TABS),
+                             grey_sufficientlyVisible(), nil)]
+         usingSearchAction:grey_scrollInDirection(kGREYDirectionRight, 350)
+      onElementWithMatcher:grey_accessibilityID(
+                               kMagicStackScrollViewAccessibilityIdentifier)]
       performAction:grey_tap()];
+
   [[EarlGrey
       selectElementWithMatcher:chrome_test_util::HeaderWithAccessibilityLabelId(
                                    IDS_IOS_CONTENT_SUGGESTIONS_RECENT_TABS)]
@@ -250,6 +245,11 @@
   [[EarlGrey
       selectElementWithMatcher:chrome_test_util::NavigationBarDoneButton()]
       performAction:grey_tap()];
+
+  if (![ChromeEarlGrey isIPadIdiom]) {
+    // Rotate iphone device back to portrait mode.
+    [EarlGrey rotateDeviceToOrientation:UIDeviceOrientationPortrait error:nil];
+  }
 }
 
 // Tests that the context menu has the correct action and correctly hides the
diff --git a/ios/chrome/browser/ui/ntp/metrics/BUILD.gn b/ios/chrome/browser/ui/ntp/metrics/BUILD.gn
index 20ded87..97508d08 100644
--- a/ios/chrome/browser/ui/ntp/metrics/BUILD.gn
+++ b/ios/chrome/browser/ui/ntp/metrics/BUILD.gn
@@ -23,6 +23,7 @@
     "//components/feed/core/v2/public:common",
     "//components/metrics",
     "//components/ntp_tiles",
+    "//components/prefs",
     "//ios/chrome/browser/discover_feed:constants",
     "//ios/chrome/browser/discover_feed:discover_feed_refresher",
     "//ios/chrome/browser/ntp:features",
@@ -71,6 +72,9 @@
     "//base",
     "//base/test:test_support",
     "//components/feed/core/v2/public:common",
+    "//components/sync_preferences:test_support",
+    "//ios/chrome/browser/shared/model/browser_state:test_support",
+    "//ios/chrome/browser/shared/model/prefs:browser_prefs",
     "//ios/chrome/browser/ui/ntp",
     "//ios/chrome/test:test_support",
     "//ios/web/public/test",
diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h
index 86c8b51..d0eda50 100644
--- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h
+++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.h
@@ -31,9 +31,6 @@
 // The max amount of cards in the Discover Feed.
 extern const int kMaxCardsInFeed;
 
-// The number of days for the Activity Buckets calculations.
-extern const int kRangeForActivityBucketsInDays;
-
 // Stores the time when the user visits an article on the feed.
 extern NSString* const kArticleVisitTimestampKey;
 // Stores the time elapsed on the feed when the user leaves.
@@ -51,7 +48,7 @@
 // Stores the time spent on the feed for a day.
 extern NSString* const kTimeSpentInFeedAggregateKey;
 // Stores the last time the activity bucket was reported.
-extern NSString* const kActivityBucketLastReportedDateKey;
+extern const char kActivityBucketLastReportedDateKey[];
 // Stores the last 28 days of activity bucket reported days.
 extern NSString* const kActivityBucketLastReportedDateArrayKey;
 // Stores the latest activity bucket the user was on.
diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm
index 364b4b1b..7cdba06 100644
--- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm
+++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_constants.mm
@@ -9,7 +9,6 @@
 const int kNonShortClickSeconds = 10;
 const int kMinutesBetweenSessions = 5;
 const int kMaxCardsInFeed = 50;
-const int kRangeForActivityBucketsInDays = 28;
 
 NSString* const kArticleVisitTimestampKey = @"ShortClickInteractionTimestamp";
 NSString* const kLongFeedVisitTimeAggregateKey =
@@ -27,8 +26,8 @@
     @"LastInteractionTimeForGoodVisitsFollowing";
 NSString* const kLastDayTimeInFeedReportedKey = @"LastDayTimeInFeedReported";
 NSString* const kTimeSpentInFeedAggregateKey = @"TimeSpentInFeedAggregate";
-NSString* const kActivityBucketLastReportedDateKey =
-    @"ActivityBucketLastReportedDate";
+const char kActivityBucketLastReportedDateKey[] =
+    "ActivityBucketLastReportedDate";
 NSString* const kActivityBucketLastReportedDateArrayKey =
     @"ActivityBucketLastReportedDateArray";
 NSString* const kActivityBucketKey = @"FeedActivityBucket";
diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.h b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.h
index 989ef19..cf5611f 100644
--- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.h
+++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.h
@@ -17,10 +17,11 @@
 @protocol FeedControlDelegate;
 @protocol NewTabPageFollowDelegate;
 @protocol NewTabPageMetricsDelegate;
+class PrefService;
 
 namespace base {
 class Time;
-}
+}  // namespace base
 
 // Records different metrics for the NTP feeds.
 @interface FeedMetricsRecorder : NSObject <FeedRefreshStateTracker>
@@ -40,6 +41,11 @@
 // Delegate for reporting feed actions to the NTP metrics recorder.
 @property(nonatomic, weak) id<NewTabPageMetricsDelegate> NTPMetricsDelegate;
 
+- (instancetype)initWithPrefService:(PrefService*)prefService
+    NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
 // Records the trigger where a feed refresh is requested.
 + (void)recordFeedRefreshTrigger:(FeedRefreshTrigger)trigger;
 
diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm
index f95acb0..e39934ec 100644
--- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm
+++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder.mm
@@ -10,6 +10,7 @@
 #import "base/metrics/user_metrics.h"
 #import "base/metrics/user_metrics_action.h"
 #import "base/time/time.h"
+#import "components/prefs/pref_service.h"
 #import "ios/chrome/browser/discover_feed/discover_feed_refresher.h"
 #import "ios/chrome/browser/ntp/features.h"
 #import "ios/chrome/browser/ui/ntp/feed_control_delegate.h"
@@ -18,6 +19,13 @@
 #import "ios/chrome/browser/ui/ntp/new_tab_page_follow_delegate.h"
 #import "ios/chrome/browser/ui/ntp/new_tab_page_metrics_delegate.h"
 
+namespace {
+
+// The number of days for the Activity Buckets calculations.
+constexpr base::TimeDelta kRangeForActivityBuckets = base::Days(28);
+
+}  // namespace
+
 using feed::FeedEngagementType;
 using feed::FeedUserActionType;
 
@@ -86,10 +94,22 @@
 // YES if the NTP is visible.
 @property(nonatomic, assign) BOOL isNTPVisible;
 
+// The ChromeBrowserState PrefService.
+@property(nonatomic, assign) PrefService* prefService;
+
 @end
 
 @implementation FeedMetricsRecorder
 
+- (instancetype)initWithPrefService:(PrefService*)prefService {
+  DCHECK(prefService);
+  self = [super init];
+  if (self) {
+    _prefService = prefService;
+  }
+  return self;
+}
+
 #pragma mark - Properties
 
 - (FeedSessionRecorder*)sessionRecorder {
@@ -915,22 +935,21 @@
 
 // Calculates the amount of dates the user has been active for the past 28 days.
 - (void)computeActivityBuckets {
-  NSDate* now = [NSDate date];
+  const base::Time now = base::Time::Now();
   NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
 
-  NSDate* lastActivityBucketReported = base::apple::ObjCCast<NSDate>(
-      [defaults objectForKey:kActivityBucketLastReportedDateKey]);
-  // If the `lastActivityBucketReported` does not exist, set it to now to
+  base::Time lastActivityBucket =
+      self.prefService->GetTime(kActivityBucketLastReportedDateKey);
+  // If the `lastActivityBucket` is not set, set it to now to
   // prevent the first day from logging a metric.
-  if (!lastActivityBucketReported) {
-    lastActivityBucketReported = now;
-    [defaults setObject:lastActivityBucketReported
-                 forKey:kActivityBucketLastReportedDateKey];
+  if (lastActivityBucket == base::Time()) {
+    lastActivityBucket = now;
+    self.prefService->SetTime(kActivityBucketLastReportedDateKey,
+                              lastActivityBucket);
   }
 
-  // Check if the last time the activity was reported is more than 24 hrs ago,
-  // and return for performance.
-  if ([now timeIntervalSinceDate:lastActivityBucketReported] < (24 * 60 * 60)) {
+  // Nothing to do if the activity was reported recently.
+  if ((now - lastActivityBucket) < base::Days(1)) {
     return;
   }
 
@@ -949,10 +968,9 @@
 
   // Check for dates > 28 days and remove older items.
   NSMutableIndexSet* toDelete = [[NSMutableIndexSet alloc] init];
-  for (NSUInteger i = 0; i < lastReportedArray.count; i++) {
-    if ([now timeIntervalSinceDate:[lastReportedArray objectAtIndex:i]] /
-            (24 * 60 * 60) >
-        kRangeForActivityBucketsInDays) {
+  for (NSUInteger i = 0; i < lastReportedArray.count; ++i) {
+    const base::Time date = base::Time::FromNSDate(lastReportedArray[i]);
+    if ((now - date) > kRangeForActivityBuckets) {
       [toDelete addIndex:i];
     } else {
       break;
@@ -992,7 +1010,8 @@
 
   // Activity Buckets Daily Run.
   [self recordActivityBuckets:activityBucket];
-  [defaults setObject:now forKey:kActivityBucketLastReportedDateKey];
+  self.prefService->SetTime(kActivityBucketLastReportedDateKey,
+                            base::Time::Now());
 }
 
 // Records the engagement buckets.
diff --git a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder_unittest.mm b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder_unittest.mm
index d0fe424..87847bd 100644
--- a/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder_unittest.mm
+++ b/ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder_unittest.mm
@@ -9,8 +9,11 @@
 #import "base/test/metrics/histogram_tester.h"
 #import "base/test/metrics/user_action_tester.h"
 #import "components/feed/core/v2/public/common_enums.h"
+#import "components/sync_preferences/testing_pref_service_syncable.h"
+#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
 #import "ios/chrome/browser/ui/ntp/feed_control_delegate.h"
 #import "ios/chrome/browser/ui/ntp/metrics/feed_metrics_recorder+testing.h"
+#import "ios/web/public/test/web_task_environment.h"
 #import "testing/gtest/include/gtest/gtest.h"
 #import "testing/gtest_mac.h"
 #import "testing/platform_test.h"
@@ -23,7 +26,7 @@
 #define EXPECT_ACTION(action, method_call)                 \
   {                                                        \
     EXPECT_EQ(actions_tester_->GetActionCount(action), 0); \
-    [recorder method_call];                                \
+    [recorder_ method_call];                               \
     EXPECT_EQ(actions_tester_->GetActionCount(action), 1); \
   }
 
@@ -32,10 +35,12 @@
 class FeedMetricsRecorderTest : public PlatformTest {
  public:
   FeedMetricsRecorderTest() {
-    recorder = [[FeedMetricsRecorder alloc] init];
+    RegisterBrowserStatePrefs(test_pref_service_.registry());
+    recorder_ =
+        [[FeedMetricsRecorder alloc] initWithPrefService:&test_pref_service_];
     // Mock Delegate to change currently used feed.
-    mockedDelegate = OCMProtocolMock(@protocol(FeedControlDelegate));
-    recorder.feedControlDelegate = mockedDelegate;
+    mocked_delegate_ = OCMProtocolMock(@protocol(FeedControlDelegate));
+    recorder_.feedControlDelegate = mocked_delegate_;
     histogram_tester_.reset(new base::HistogramTester());
     actions_tester_.reset(new base::UserActionTester());
   }
@@ -48,11 +53,12 @@
   const base::TimeDelta kTimeForFeedTimeMetric = base::Minutes(2);
   const base::TimeDelta kOneDay = base::Hours(24);
   void TearDown() override {
-    [recorder resetGoodVisitSession];
+    [recorder_ resetGoodVisitSession];
     PlatformTest::TearDown();
   }
-  FeedMetricsRecorder* recorder;
-  id<FeedControlDelegate> mockedDelegate;
+  sync_preferences::TestingPrefServiceSyncable test_pref_service_;
+  FeedMetricsRecorder* recorder_;
+  id<FeedControlDelegate> mocked_delegate_;
   std::unique_ptr<base::HistogramTester> histogram_tester_;
   std::unique_ptr<base::UserActionTester> actions_tester_;
 };
@@ -65,7 +71,7 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Add URL to Read Later constitutes a Good Visit by itself.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   // There should be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
@@ -78,7 +84,7 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordOpenURLInIncognitoTab];
+  [recorder_ recordOpenURLInIncognitoTab];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -89,7 +95,7 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordNativeContextMenuVisibilityChanged:YES];
+  [recorder_ recordNativeContextMenuVisibilityChanged:YES];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -100,12 +106,12 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Start with a Good Visit.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
   // Adding to Read Later should count as a Good Visit, but we only log one Good
   // Visit per session, so the histogram count should remain at 1.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -117,7 +123,7 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Trigger a non-Good Visit action.
-  [recorder recordHeaderMenuManageTapped];
+  [recorder_ recordHeaderMenuManageTapped];
   // There should not be a Good Visit recorded as the action was not a trigger
   // for a Good Visit.
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
@@ -139,7 +145,7 @@
                                        FeedEngagementType::kGoodVisit, 0);
   // Default feed is Discover
   // Add URL to Read Later constitutes a Good Visit for AllFeeds and Discover.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   // There should be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
@@ -148,12 +154,12 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
 
   // Add URL to Read Later constitutes a Good Visit for AllFeeds (not counted as
   // one has been triggered already this session) and Following. The Discover
   // histogram should still have 1 Good Visit reported.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
@@ -167,8 +173,8 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_GoodTimeInFeed) {
   base::ScopedMockClockOverride mock_clock;
 
-  [recorder recordNTPDidChangeVisibility:YES];
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordNTPDidChangeVisibility:YES];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
 
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
@@ -176,7 +182,7 @@
                      kAddedTimeForMockClock);
   // Calling an arbitrary GV action. This action should not trigger a GV by
   // itself, but cycles the checks for other GV paths.
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -185,14 +191,14 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_ShortClickVisit) {
   base::ScopedMockClockOverride mock_clock;
   // Trigger article click
-  [recorder recordOpenURLInSameTab];
-  [recorder recordNTPDidChangeVisibility:NO];
+  [recorder_ recordOpenURLInSameTab];
+  [recorder_ recordNTPDidChangeVisibility:NO];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   mock_clock.Advance(
       (base::Seconds(kNonShortClickSeconds) + kAddedTimeForMockClock));
   // Coming back to the main feed. There should be a Good Visit.
-  [recorder recordNTPDidChangeVisibility:YES];
+  [recorder_ recordNTPDidChangeVisibility:YES];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -203,22 +209,22 @@
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Trigger Good Visit
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   // Check it's not double logged
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
-  [recorder recordNTPDidChangeVisibility:NO];
+  [recorder_ recordNTPDidChangeVisibility:NO];
 
   // Trigger session expiration by waiting `kMinutesBetweenSessions`
   mock_clock.Advance(
       (base::Minutes(kMinutesBetweenSessions) + kAddedTimeForMockClock));
   // Coming back to the main feed. Session should have been reset so there
   // should be 2 histograms.
-  [recorder recordNTPDidChangeVisibility:YES];
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordNTPDidChangeVisibility:YES];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kAllFeedsEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 2);
 }
@@ -232,7 +238,7 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Add URL to Read Later constitutes a Good Visit by itself.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   // There should be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
@@ -246,7 +252,7 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordOpenURLInIncognitoTab];
+  [recorder_ recordOpenURLInIncognitoTab];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -258,7 +264,7 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordNativeContextMenuVisibilityChanged:YES];
+  [recorder_ recordNativeContextMenuVisibilityChanged:YES];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -270,12 +276,12 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Start with a Good Visit.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
   // Adding to Read Later should count as a Good Visit, but we only log one Good
   // Visit per session, so the histogram count should remain at 1.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -289,7 +295,7 @@
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Trigger a non-Good Visit action.
-  [recorder recordHeaderMenuManageTapped];
+  [recorder_ recordHeaderMenuManageTapped];
   // There should not be a Good Visit recorded as the action was not a trigger
   // for a Good Visit.
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
@@ -301,13 +307,13 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_GoodTimeInFeedDiscover) {
   base::ScopedMockClockOverride mock_clock;
 
-  [recorder recordNTPDidChangeVisibility:YES];
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordNTPDidChangeVisibility:YES];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   mock_clock.Advance(base::Seconds(kGoodVisitTimeInFeedSeconds) +
                      kAddedTimeForMockClock);
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -316,13 +322,13 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_ShortClickVisitDiscover) {
   base::ScopedMockClockOverride mock_clock;
   // Trigger article click
-  [recorder recordOpenURLInSameTab];
+  [recorder_ recordOpenURLInSameTab];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   mock_clock.Advance(
       (base::Seconds(kNonShortClickSeconds) + kAddedTimeForMockClock));
   // Coming back to the main feed. There should be a Good Visit.
-  [recorder recordNTPDidChangeVisibility:YES];
+  [recorder_ recordNTPDidChangeVisibility:YES];
   histogram_tester_->ExpectBucketCount(kDiscoverFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -332,12 +338,12 @@
 // Tests that a Good Visit is recorded when a url is added to Read Later.
 TEST_F(FeedMetricsRecorderTest, GoodExplicitInteraction_Following) {
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // There should not be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Add URL to Read Later constitutes a Good Visit by itself.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   // There should be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
@@ -347,12 +353,12 @@
 // tab.
 TEST_F(FeedMetricsRecorderTest, GoodVisit_OpenInNewIncognitoTab_Following) {
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // There should not be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordOpenURLInIncognitoTab];
+  [recorder_ recordOpenURLInIncognitoTab];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -360,12 +366,12 @@
 // Tests that a Good Visit is recorded when we do a long press on a card.
 TEST_F(FeedMetricsRecorderTest, GoodVisit_LongPress_Following) {
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // There should not be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // After Action, Good Visit should be recorded.
-  [recorder recordNativeContextMenuVisibilityChanged:YES];
+  [recorder_ recordNativeContextMenuVisibilityChanged:YES];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -373,17 +379,17 @@
 // Tests that a Good Visit is only logged once for each Good Visit session.
 TEST_F(FeedMetricsRecorderTest, GoodVisit_OnlyLoggedOncePerVisit_Following) {
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // There should not be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Start with a Good Visit.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
   // Adding to Read Later should count as a Good Visit, but we only log one Good
   // Visit per session, so the histogram count should remain at 1.
-  [recorder recordAddURLToReadLater];
+  [recorder_ recordAddURLToReadLater];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -393,12 +399,12 @@
 TEST_F(FeedMetricsRecorderTest,
        GoodVisit_NonGoodVisitActionTriggered_Following) {
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // There should not be a Good Visit recorded.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   // Trigger a non-Good Visit action.
-  [recorder recordHeaderMenuManageTapped];
+  [recorder_ recordHeaderMenuManageTapped];
   // There should not be a Good Visit recorded as the action was not a trigger
   // for a Good Visit.
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
@@ -410,15 +416,15 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_GoodTimeInFeedFollowing) {
   base::ScopedMockClockOverride mock_clock;
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
 
-  [recorder recordNTPDidChangeVisibility:YES];
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordNTPDidChangeVisibility:YES];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   mock_clock.Advance(base::Seconds(kGoodVisitTimeInFeedSeconds) +
                      kAddedTimeForMockClock);
-  [recorder recordFeedScrolled:kMinScrollForGoodVisitTests];
+  [recorder_ recordFeedScrolled:kMinScrollForGoodVisitTests];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -427,15 +433,15 @@
 TEST_F(FeedMetricsRecorderTest, GoodVisit_ShortClickVisitFollowing) {
   base::ScopedMockClockOverride mock_clock;
   // Change feed to Following.
-  OCMStub([mockedDelegate selectedFeed]).andReturn(FeedTypeFollowing);
+  OCMStub([mocked_delegate_ selectedFeed]).andReturn(FeedTypeFollowing);
   // Trigger article click
-  [recorder recordOpenURLInSameTab];
+  [recorder_ recordOpenURLInSameTab];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 0);
   mock_clock.Advance(
       (base::Seconds(kNonShortClickSeconds) + kAddedTimeForMockClock));
   // Coming back to the main feed. There should be a Good Visit.
-  [recorder recordNTPDidChangeVisibility:YES];
+  [recorder_ recordNTPDidChangeVisibility:YES];
   histogram_tester_->ExpectBucketCount(kFollowingFeedEngagementTypeHistogram,
                                        FeedEngagementType::kGoodVisit, 1);
 }
@@ -446,12 +452,12 @@
 TEST_F(FeedMetricsRecorderTest, TimeSpent_RecordedCorrectly) {
   base::ScopedMockClockOverride mock_clock;
   // Make the feed visible.
-  [recorder recordNTPDidChangeVisibility:YES];
+  [recorder_ recordNTPDidChangeVisibility:YES];
   // Advance clock.
   mock_clock.Advance(kTimeForFeedTimeMetric);
   // Hide feed again.
-  [recorder recordNTPDidChangeVisibility:NO];
-  EXPECT_EQ(kTimeForFeedTimeMetric, recorder.timeSpentInFeed);
+  [recorder_ recordNTPDidChangeVisibility:NO];
+  EXPECT_EQ(kTimeForFeedTimeMetric, recorder_.timeSpentInFeed);
 }
 
 // TODO(crbug.com/1403009) Add test to check if the histogram is recorded
@@ -480,7 +486,7 @@
                 kDiscoverFeedHistogramDeviceOrientationChangedToPortrait),
             0);
   // Change orientation to portrait.
-  [recorder recordDeviceOrientationChanged:UIDeviceOrientationPortrait];
+  [recorder_ recordDeviceOrientationChanged:UIDeviceOrientationPortrait];
   // `kDiscoverFeedHistogramDeviceOrientationChangedToLandscape` should be 0.
   EXPECT_EQ(actions_tester_->GetActionCount(
                 kDiscoverFeedHistogramDeviceOrientationChangedToPortrait),
@@ -489,7 +495,7 @@
                 kDiscoverFeedHistogramDeviceOrientationChangedToLandscape),
             1);
   // Change orientation to Landscape.
-  [recorder recordDeviceOrientationChanged:UIDeviceOrientationLandscapeRight];
+  [recorder_ recordDeviceOrientationChanged:UIDeviceOrientationLandscapeRight];
   // Both actions should be 1.
   EXPECT_EQ(actions_tester_->GetActionCount(
                 kDiscoverFeedHistogramDeviceOrientationChangedToPortrait),
diff --git a/ios/chrome/browser/ui/ntp/new_tab_page_mediator_unittest.mm b/ios/chrome/browser/ui/ntp/new_tab_page_mediator_unittest.mm
index ff69f3d..7fccfdd 100644
--- a/ios/chrome/browser/ui/ntp/new_tab_page_mediator_unittest.mm
+++ b/ios/chrome/browser/ui/ntp/new_tab_page_mediator_unittest.mm
@@ -102,7 +102,9 @@
                discoverFeedService:discover_feed_service];
     header_consumer_ = OCMProtocolMock(@protocol(NewTabPageHeaderConsumer));
     mediator_.headerConsumer = header_consumer_;
-    feed_metrics_recorder_ = [[FeedMetricsRecorder alloc] init];
+    PrefService* prefs = chrome_browser_state_->GetPrefs();
+    feed_metrics_recorder_ =
+        [[FeedMetricsRecorder alloc] initWithPrefService:prefs];
     mediator_.feedMetricsRecorder = feed_metrics_recorder_;
     histogram_tester_.reset(new base::HistogramTester());
   }
diff --git a/ios/chrome/browser/ui/safe_mode/safe_mode_egtest.mm b/ios/chrome/browser/ui/safe_mode/safe_mode_egtest.mm
index b7ba2b1..a1db0a4 100644
--- a/ios/chrome/browser/ui/safe_mode/safe_mode_egtest.mm
+++ b/ios/chrome/browser/ui/safe_mode/safe_mode_egtest.mm
@@ -145,7 +145,7 @@
       LocalizedString(@"IDS_IOS_SAFE_MODE_SENDING_CRASH_REPORT"));
 }
 
-// Tests that a start NTP is shown after 2 crashes.
+// Tests that an NTP is shown after 2 crashes.
 - (void)testPostCrashNTP {
   [SafeModeAppInterface setFailedStartupAttemptCount:0];
   [ChromeEarlGrey closeAllTabsInCurrentMode];
@@ -169,18 +169,7 @@
   [ChromeEarlGrey waitForMainTabCount:3];
   [ChromeEarlGrey waitForIncognitoTabCount:1];
 
-  // Verify Shortcuts module title is visible in Magic Stack.
-  [[EarlGrey selectElementWithMatcher:
-                 grey_accessibilityID(l10n_util::GetNSString(
-                     IDS_IOS_CONTENT_SUGGESTIONS_SHORTCUTS_MODULE_TITLE))]
-      assertWithMatcher:grey_sufficientlyVisible()];
-  [[EarlGrey
-      selectElementWithMatcher:
-          grey_accessibilityID(kMagicStackScrollViewAccessibilityIdentifier)]
-      performAction:grey_swipeFastInDirection(kGREYDirectionLeft)];
-  [[EarlGrey
-      selectElementWithMatcher:grey_accessibilityLabel(l10n_util::GetNSString(
-                                   IDS_IOS_TAB_RESUMPTION_TITLE))]
+  [[EarlGrey selectElementWithMatcher:chrome_test_util::FakeOmnibox()]
       assertWithMatcher:grey_sufficientlyVisible()];
   [SafeModeAppInterface setFailedStartupAttemptCount:0];
 }
diff --git a/ios/chrome/browser/ui/send_tab_to_self/BUILD.gn b/ios/chrome/browser/ui/send_tab_to_self/BUILD.gn
index 1c20f46..159ab44a 100644
--- a/ios/chrome/browser/ui/send_tab_to_self/BUILD.gn
+++ b/ios/chrome/browser/ui/send_tab_to_self/BUILD.gn
@@ -14,7 +14,7 @@
     "//components/signin/public/identity_manager",
     "//ios/chrome/app/strings",
     "//ios/chrome/browser/main",
-    "//ios/chrome/browser/send_tab_to_self",
+    "//ios/chrome/browser/send_tab_to_self/model",
     "//ios/chrome/browser/shared/coordinator/chrome_coordinator",
     "//ios/chrome/browser/shared/model/browser_state",
     "//ios/chrome/browser/shared/model/url:constants",
diff --git a/ios/chrome/browser/ui/send_tab_to_self/send_tab_to_self_coordinator.mm b/ios/chrome/browser/ui/send_tab_to_self/send_tab_to_self_coordinator.mm
index 1f3e621..f48a5d0 100644
--- a/ios/chrome/browser/ui/send_tab_to_self/send_tab_to_self_coordinator.mm
+++ b/ios/chrome/browser/ui/send_tab_to_self/send_tab_to_self_coordinator.mm
@@ -21,7 +21,7 @@
 #import "components/signin/public/base/signin_metrics.h"
 #import "components/sync/service/sync_service.h"
 #import "components/sync/service/sync_service_observer.h"
-#import "ios/chrome/browser/send_tab_to_self/send_tab_to_self_browser_agent.h"
+#import "ios/chrome/browser/send_tab_to_self/model/send_tab_to_self_browser_agent.h"
 #import "ios/chrome/browser/shared/model/browser/browser.h"
 #import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
 #import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
diff --git a/ios/chrome/browser/ui/settings/password/password_manager_egtest.mm b/ios/chrome/browser/ui/settings/password/password_manager_egtest.mm
index 8eb8372..bf0ca75 100644
--- a/ios/chrome/browser/ui/settings/password/password_manager_egtest.mm
+++ b/ios/chrome/browser/ui/settings/password/password_manager_egtest.mm
@@ -742,6 +742,8 @@
             (testDismissPasswordManagerWidgetPromoInstructionsScreen)] ||
       [self isRunningTest:@selector
             (testPasswordManagerWidgetPromoInstructionsDeviceOrientation)]) {
+    config.features_enabled.push_back(
+        password_manager::features::kIOSPasswordAuthOnEntryV2);
     config.additional_args.push_back(
         base::StringPrintf("--enable-features=%s:chosen_feature/"
                            "IPH_iOSPromoPasswordManagerWidget",
diff --git a/ios/chrome/browser/ui/settings/password/passwords_mediator.mm b/ios/chrome/browser/ui/settings/password/passwords_mediator.mm
index ed8591a..fd517eb 100644
--- a/ios/chrome/browser/ui/settings/password/passwords_mediator.mm
+++ b/ios/chrome/browser/ui/settings/password/passwords_mediator.mm
@@ -9,7 +9,7 @@
 #import "components/feature_engagement/public/feature_constants.h"
 #import "components/feature_engagement/public/tracker.h"
 #import "components/password_manager/core/browser/leak_detection_dialog_utils.h"
-#import "components/password_manager/core/browser/password_manager_util.h"
+#import "components/password_manager/core/browser/password_manager_client.h"
 #import "components/password_manager/core/browser/password_sync_util.h"
 #import "components/password_manager/core/common/password_manager_features.h"
 #import "components/sync/service/sync_service_utils.h"
@@ -121,8 +121,8 @@
   _currentState = _passwordCheckManager->GetPasswordCheckState();
   [self updateConsumerPasswordCheckState:_currentState];
   [self.consumer
-      setSavingPasswordsToAccount:password_manager_util::GetPasswordSyncState(
-                                      _syncService) !=
+      setSavingPasswordsToAccount:password_manager::sync_util::
+                                      GetPasswordSyncState(_syncService) !=
                                   password_manager::SyncState::kNotSyncing];
 }
 
@@ -430,8 +430,8 @@
 
 - (void)onSyncStateChanged {
   [self.consumer
-      setSavingPasswordsToAccount:password_manager_util::GetPasswordSyncState(
-                                      _syncService) !=
+      setSavingPasswordsToAccount:password_manager::sync_util::
+                                      GetPasswordSyncState(_syncService) !=
                                   password_manager::SyncState::kNotSyncing];
 }
 
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/BUILD.gn b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/BUILD.gn
index 1c7ddc4..7afcfa6 100644
--- a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/BUILD.gn
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/BUILD.gn
@@ -11,5 +11,9 @@
     "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner@3x.png",
     "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark@2x.png",
     "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark@3x.png",
+    "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@1x.png",
+    "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@2x.png",
+    "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@1x.png",
+    "widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@2x.png",
   ]
 }
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/Contents.json b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/Contents.json
index c0dd507..39b61b9 100644
--- a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/Contents.json
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/Contents.json
@@ -31,6 +31,38 @@
           "value" : "dark"
         }
       ]
+    },
+    {
+      "idiom" : "ipad",
+      "filename" : "widget_promo_instructions_banner_ipad@1x.png",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "ipad",
+      "filename" : "widget_promo_instructions_banner_ipad@2x.png",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "ipad",
+      "filename" : "widget_promo_instructions_banner_dark_ipad@1x.png",
+      "scale" : "1x",
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ]
+    },
+    {
+      "idiom" : "ipad",
+      "filename" : "widget_promo_instructions_banner_dark_ipad@2x.png",
+      "scale" : "2x",
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ]
     }
   ],
   "info" : {
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@1x.png b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@1x.png
new file mode 100644
index 0000000..733d6ce
--- /dev/null
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@1x.png
Binary files differ
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@2x.png b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@2x.png
new file mode 100644
index 0000000..0af212a
--- /dev/null
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_dark_ipad@2x.png
Binary files differ
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@1x.png b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@1x.png
new file mode 100644
index 0000000..5533964
--- /dev/null
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@1x.png
Binary files differ
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@2x.png b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@2x.png
new file mode 100644
index 0000000..9911955
--- /dev/null
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/resources/widget_promo_instructions_banner.imageset/widget_promo_instructions_banner_ipad@2x.png
Binary files differ
diff --git a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_view_controller.mm b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_view_controller.mm
index 0507cb8..ef46001 100644
--- a/ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_view_controller.mm
+++ b/ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_view_controller.mm
@@ -47,10 +47,10 @@
                        constant:32],
     [instructionsViewController.view.bottomAnchor
         constraintEqualToAnchor:self.view.bottomAnchor],
-    [instructionsViewController.view.leadingAnchor
-        constraintEqualToAnchor:self.view.leadingAnchor],
-    [instructionsViewController.view.trailingAnchor
-        constraintEqualToAnchor:self.view.trailingAnchor],
+    [instructionsViewController.view.centerXAnchor
+        constraintEqualToAnchor:self.view.centerXAnchor],
+    [instructionsViewController.view.widthAnchor
+        constraintEqualToAnchor:self.view.widthAnchor],
 
   ]];
 
@@ -89,7 +89,7 @@
 
   instructionsViewController.titleString =
       l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_TITLE);
-  instructionsViewController.titleTextStyle = UIFontTextStyleTitle1;
+  instructionsViewController.titleTextStyle = UIFontTextStyleTitle2;
   instructionsViewController.subtitleString =
       l10n_util::GetNSString(IDS_IOS_WIDGET_PROMO_INSTRUCTIONS_SUBTITLE);
   instructionsViewController.subtitleTextStyle = UIFontTextStyleBody;
@@ -102,14 +102,6 @@
 
   InstructionView* instructionView = [self createInstructionView];
   instructionsViewController.underTitleView = instructionView;
-  [NSLayoutConstraint activateConstraints:@[
-    [instructionView.leadingAnchor
-        constraintEqualToAnchor:instructionsViewController.view.leadingAnchor
-                       constant:24],
-    [instructionView.trailingAnchor
-        constraintEqualToAnchor:instructionsViewController.view.trailingAnchor
-                       constant:-24],
-  ]];
 
   instructionsViewController.view.translatesAutoresizingMaskIntoConstraints =
       NO;
diff --git a/ios/chrome/browser/ui/sharing/activity_services/activities/BUILD.gn b/ios/chrome/browser/ui/sharing/activity_services/activities/BUILD.gn
index 8d79f0e..b3f9791 100644
--- a/ios/chrome/browser/ui/sharing/activity_services/activities/BUILD.gn
+++ b/ios/chrome/browser/ui/sharing/activity_services/activities/BUILD.gn
@@ -28,7 +28,7 @@
     "//components/prefs",
     "//ios/chrome/app/strings",
     "//ios/chrome/browser/reading_list",
-    "//ios/chrome/browser/send_tab_to_self",
+    "//ios/chrome/browser/send_tab_to_self/model",
     "//ios/chrome/browser/shared/public/commands",
     "//ios/chrome/browser/shared/ui/symbols",
     "//ios/chrome/browser/shared/ui/util",
diff --git a/ios/chrome/test/BUILD.gn b/ios/chrome/test/BUILD.gn
index b084f9d..4a376f45 100644
--- a/ios/chrome/test/BUILD.gn
+++ b/ios/chrome/test/BUILD.gn
@@ -256,7 +256,7 @@
     "//ios/chrome/browser/screenshot:unit_tests",
     "//ios/chrome/browser/search_engines:unit_tests",
     "//ios/chrome/browser/segmentation_platform:unit_tests",
-    "//ios/chrome/browser/send_tab_to_self:unit_tests",
+    "//ios/chrome/browser/send_tab_to_self/model:unit_tests",
     "//ios/chrome/browser/sessions:unit_tests",
     "//ios/chrome/browser/settings/sync/utils:unit_tests",
     "//ios/chrome/browser/shared/coordinator/alert:unit_tests",
diff --git a/ios/third_party/earl_grey2/src b/ios/third_party/earl_grey2/src
index 806ee05..bbf8b94 160000
--- a/ios/third_party/earl_grey2/src
+++ b/ios/third_party/earl_grey2/src
@@ -1 +1 @@
-Subproject commit 806ee052db0c7e045f1fd510ea235abed3d746ee
+Subproject commit bbf8b94d59079db24bc49ca3aa03ed860f53a3d0
diff --git a/ios/web_view/internal/passwords/web_view_password_manager_client.mm b/ios/web_view/internal/passwords/web_view_password_manager_client.mm
index add5582..44eea1b 100644
--- a/ios/web_view/internal/passwords/web_view_password_manager_client.mm
+++ b/ios/web_view/internal/passwords/web_view_password_manager_client.mm
@@ -11,7 +11,7 @@
 #include "components/autofill/core/browser/logging/log_manager.h"
 #include "components/keyed_service/core/service_access_type.h"
 #include "components/password_manager/core/browser/password_form.h"
-#include "components/password_manager/core/browser/password_manager_util.h"
+#import "components/password_manager/core/browser/password_sync_util.h"
 #include "components/password_manager/core/common/password_manager_pref_names.h"
 #include "components/password_manager/ios/password_manager_ios_util.h"
 #import "ios/web_view/internal/app/application_context.h"
@@ -104,7 +104,7 @@
 WebViewPasswordManagerClient::~WebViewPasswordManagerClient() = default;
 
 SyncState WebViewPasswordManagerClient::GetPasswordSyncState() const {
-  return password_manager_util::GetPasswordSyncState(sync_service_);
+  return password_manager::sync_util::GetPasswordSyncState(sync_service_);
 }
 
 bool WebViewPasswordManagerClient::PromptUserToChooseCredentials(
diff --git a/ios_internal b/ios_internal
index 5f154f8..e875678 160000
--- a/ios_internal
+++ b/ios_internal
@@ -1 +1 @@
-Subproject commit 5f154f8f111ecd416250348e8ef1c1a68b2582b1
+Subproject commit e875678118f79bdac3c7bd75be7fb79360d77757
diff --git a/media/base/media_switches.cc b/media/base/media_switches.cc
index b0f25ce..d7ff1f4 100644
--- a/media/base/media_switches.cc
+++ b/media/base/media_switches.cc
@@ -638,12 +638,6 @@
              "OpenscreenCastStreamingSession",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
-// Controls whether or not frame drops are included in the video bitrate
-// calculation for the OpenscreenFrameSender backed VideoSender implementation.
-BASE_FEATURE(kOpenscreenVideoBitrateFactorInFrameDrops,
-             "OpenscreenVideoBitrateFactorInFrameDrops",
-             base::FEATURE_ENABLED_BY_DEFAULT);
-
 // Controls whether the Mirroring Service will fetch, analyze, and store
 // information on the quality of the session using RTCP logs.
 BASE_FEATURE(kEnableRtcpReporting,
@@ -1597,6 +1591,12 @@
              "CastStreamingAv1",
              base::FEATURE_DISABLED_BY_DEFAULT);
 
+// Controls whether the new exponential bitrate calculate logic is used, or
+// the legacy linear algorithm.
+BASE_FEATURE(kCastStreamingExponentialVideoBitrateAlgorithm,
+             "CastStreamingExponentialVideoBitrateAlgorithm",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 BASE_FEATURE(kCastStreamingPerformanceOverlay,
              "CastStreamingPerformanceOverlay",
              base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/media/base/media_switches.h b/media/base/media_switches.h
index 5004539..0f0c628 100644
--- a/media/base/media_switches.h
+++ b/media/base/media_switches.h
@@ -177,6 +177,8 @@
 // directly.
 // TODO(https://crbug.com/1453388): Guard Cast Sender flags with !IS_ANDROID.
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kCastStreamingAv1);
+MEDIA_EXPORT BASE_DECLARE_FEATURE(
+    kCastStreamingExponentialVideoBitrateAlgorithm);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kCastStreamingPerformanceOverlay);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kCastStreamingVp9);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kCdmHostVerification);
@@ -279,7 +281,6 @@
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kMemoryPressureBasedSourceBufferGC);
 // TODO(https://crbug.com/1453388): Guard Cast Sender flags with !IS_ANDROID.
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kOpenscreenCastStreamingSession);
-MEDIA_EXPORT BASE_DECLARE_FEATURE(kOpenscreenVideoBitrateFactorInFrameDrops);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseWritePixelsYUV);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseMultiPlaneFormatForHardwareVideo);
 MEDIA_EXPORT BASE_DECLARE_FEATURE(kUseMultiPlaneFormatForSoftwareVideo);
diff --git a/media/capture/mojom/video_capture.mojom b/media/capture/mojom/video_capture.mojom
index 3d92374..80b4857 100644
--- a/media/capture/mojom/video_capture.mojom
+++ b/media/capture/mojom/video_capture.mojom
@@ -81,9 +81,9 @@
   // is no longer going to be used by the Browser/Host.
   OnBufferDestroyed(int32 buffer_id);
 
-  // A frame was dropped early such as by the capture process, i.e.
-  // OnBufferReady() was never called for this frame.
-  OnFrameDroppedEarly(media.mojom.VideoCaptureFrameDropReason reason);
+  // A frame was dropped - OnBufferReady() was never called for this frame. In
+  // other words the frame was dropped before it reached the renderer process.
+  OnFrameDropped(media.mojom.VideoCaptureFrameDropReason reason);
 
   // All subsequent buffers are guaranteed to have this crop-version or higher.
   OnNewCropVersion(uint32 crop_version);
@@ -127,10 +127,6 @@
                         mojo_base.mojom.UnguessableToken session_id)
     => (array<VideoCaptureFormat> formats_in_use);
 
-  // Notifies the host about a frame being dropped.
-  OnFrameDropped(mojo_base.mojom.UnguessableToken device_id,
-                 media.mojom.VideoCaptureFrameDropReason reason);
-
   // Sends a log message to the VideoCaptureHost.
   OnLog(mojo_base.mojom.UnguessableToken device_id, string message);
 };
diff --git a/media/cast/openscreen/remoting_proto_utils.cc b/media/cast/openscreen/remoting_proto_utils.cc
index 3f55bc9..b7043a6 100644
--- a/media/cast/openscreen/remoting_proto_utils.cc
+++ b/media/cast/openscreen/remoting_proto_utils.cc
@@ -102,9 +102,8 @@
 }  // namespace
 
 scoped_refptr<media::DecoderBuffer> ByteArrayToDecoderBuffer(
-    const uint8_t* data,
-    uint32_t size) {
-  base::BigEndianReader reader(data, size);
+    base::span<const uint8_t> data) {
+  base::BigEndianReader reader(data);
   uint8_t payload_version = 0;
   uint16_t proto_size = 0;
   openscreen::cast::DecoderBuffer segment;
diff --git a/media/cast/openscreen/remoting_proto_utils.h b/media/cast/openscreen/remoting_proto_utils.h
index a9ee8d80..bc2afdf 100644
--- a/media/cast/openscreen/remoting_proto_utils.h
+++ b/media/cast/openscreen/remoting_proto_utils.h
@@ -8,7 +8,8 @@
 #include <cstdint>
 #include <vector>
 
-#include "base/memory/ref_counted.h"
+#include "base/containers/span.h"
+#include "base/memory/scoped_refptr.h"
 #include "media/base/audio_decoder_config.h"
 #include "media/base/decoder_buffer.h"
 #include "media/base/demuxer_stream.h"
@@ -50,8 +51,7 @@
 
 // Converts byte array into DecoderBufferSegment.
 scoped_refptr<media::DecoderBuffer> ByteArrayToDecoderBuffer(
-    const uint8_t* data,
-    uint32_t size);
+    base::span<const uint8_t> data);
 
 // Data type conversion between media::AudioDecoderConfig and proto buffer.
 void ConvertAudioDecoderConfigToProto(
diff --git a/media/cast/openscreen/remoting_proto_utils_unittest.cc b/media/cast/openscreen/remoting_proto_utils_unittest.cc
index 589f43e..47979a13 100644
--- a/media/cast/openscreen/remoting_proto_utils_unittest.cc
+++ b/media/cast/openscreen/remoting_proto_utils_unittest.cc
@@ -41,7 +41,7 @@
 
   // 3. To DecoderBuffer
   scoped_refptr<media::DecoderBuffer> output_buffer =
-      ByteArrayToDecoderBuffer(data.data(), data.size());
+      ByteArrayToDecoderBuffer(data);
   DCHECK(output_buffer);
 
   ASSERT_TRUE(output_buffer->end_of_stream());
@@ -79,7 +79,7 @@
 
   // 3. To DecoderBuffer
   scoped_refptr<media::DecoderBuffer> output_buffer =
-      ByteArrayToDecoderBuffer(data.data(), data.size());
+      ByteArrayToDecoderBuffer(data);
   DCHECK(output_buffer);
 
   ASSERT_FALSE(output_buffer->end_of_stream());
diff --git a/media/cast/sender/video_bitrate_suggester.cc b/media/cast/sender/video_bitrate_suggester.cc
index 09ba1d35..d6bb94d8 100644
--- a/media/cast/sender/video_bitrate_suggester.cc
+++ b/media/cast/sender/video_bitrate_suggester.cc
@@ -30,12 +30,6 @@
 VideoBitrateSuggester::~VideoBitrateSuggester() = default;
 
 int VideoBitrateSuggester::GetSuggestedBitrate() {
-  // Skip the more complicated calculation if the feature is not enabled.
-  if (!base::FeatureList::IsEnabled(
-          media::kOpenscreenVideoBitrateFactorInFrameDrops)) {
-    return get_bitrate_cb_.Run();
-  }
-
   // The bitrate retrieved from the callback is based on network usage, however
   // we also need to consider how well this device is handling encoding at
   // this bitrate overall.
@@ -47,17 +41,20 @@
 }
 
 void VideoBitrateSuggester::RecordShouldDropNextFrame(bool should_drop) {
-  // Nothing to do if frame drop logic is disabled.
-  if (!base::FeatureList::IsEnabled(
-          media::kOpenscreenVideoBitrateFactorInFrameDrops)) {
-    return;
-  }
-
   ++number_of_frames_requested_;
   if (should_drop) {
     ++number_of_frames_dropped_;
   }
 
+  if (base::FeatureList::IsEnabled(
+          media::kCastStreamingExponentialVideoBitrateAlgorithm)) {
+    UpdateSuggestionUsingExponentialAlgorithm();
+  } else {
+    UpdateSuggestionUsingLinearAlgorithm();
+  }
+}
+
+void VideoBitrateSuggester::UpdateSuggestionUsingExponentialAlgorithm() {
   // We don't want to change the bitrate too frequently in order to give
   // things time to adjust, so only adjust roughly once a second.
   constexpr int kWindowSize = 30;
@@ -81,4 +78,31 @@
     number_of_frames_dropped_ = 0;
   }
 }
+
+void VideoBitrateSuggester::UpdateSuggestionUsingLinearAlgorithm() {
+  // We don't want to change the bitrate too frequently in order to give
+  // things time to adjust, so only adjust every 100 frames (about 3 seconds
+  // at 30FPS).
+  constexpr int kWindowSize = 100;
+  if (number_of_frames_requested_ == kWindowSize) {
+    constexpr int kBitrateSteps = 8;
+    DCHECK_GE(max_bitrate_, min_bitrate_);
+    const int adjustment = (max_bitrate_ - min_bitrate_) / kBitrateSteps;
+
+    // Generally speaking we shouldn't be dropping any frames, so even one is
+    // a bad sign.
+    if (number_of_frames_dropped_ > 0) {
+      suggested_max_bitrate_ =
+          std::max(min_bitrate_, suggested_max_bitrate_ - adjustment);
+    } else {
+      suggested_max_bitrate_ =
+          std::min(max_bitrate_, suggested_max_bitrate_ + adjustment);
+    }
+
+    // Reset the recorded frame drops to start a new window.
+    number_of_frames_requested_ = 0;
+    number_of_frames_dropped_ = 0;
+  }
+}
+
 }  // namespace media::cast
diff --git a/media/cast/sender/video_bitrate_suggester.h b/media/cast/sender/video_bitrate_suggester.h
index 64d7727c..ba8b995 100644
--- a/media/cast/sender/video_bitrate_suggester.h
+++ b/media/cast/sender/video_bitrate_suggester.h
@@ -25,6 +25,13 @@
   int GetSuggestedBitrate();
 
  private:
+  // NOTE: the exponential algorithm is currently undergoing an experiment
+  // versus the legacy implementation.
+  // TODO(https://issuetracker.google.com/302584587): determine if new algorithm
+  // is more effective.
+  void UpdateSuggestionUsingExponentialAlgorithm();
+  void UpdateSuggestionUsingLinearAlgorithm();
+
   // The method for getting the recommended bitrate.
   FrameSender::GetSuggestedVideoBitrateCB get_bitrate_cb_;
 
diff --git a/media/cast/sender/video_bitrate_suggester_unittest.cc b/media/cast/sender/video_bitrate_suggester_unittest.cc
index ad47306..c36ec0f 100644
--- a/media/cast/sender/video_bitrate_suggester_unittest.cc
+++ b/media/cast/sender/video_bitrate_suggester_unittest.cc
@@ -52,8 +52,6 @@
 
  protected:
   VideoBitrateSuggesterTest() {
-    feature_list_.InitAndEnableFeature(
-        media::kOpenscreenVideoBitrateFactorInFrameDrops);
     video_bitrate_suggester_ = std::make_unique<VideoBitrateSuggester>(
         kVideoConfig,
         base::BindRepeating(&VideoBitrateSuggesterTest::get_suggested_bitrate,
@@ -72,13 +70,26 @@
     return *video_bitrate_suggester_;
   }
 
+  void UseExponentialAlgorithm() {
+    feature_list_.InitAndEnableFeature(
+        media::kCastStreamingExponentialVideoBitrateAlgorithm);
+  }
+
+  void UseLinearAlgorithm() {
+    feature_list_.InitAndDisableFeature(
+        media::kCastStreamingExponentialVideoBitrateAlgorithm);
+  }
+
  private:
   std::unique_ptr<VideoBitrateSuggester> video_bitrate_suggester_;
   base::test::ScopedFeatureList feature_list_;
   int suggested_bitrate_ = 0;
 };
 
-TEST_F(VideoBitrateSuggesterTest, SuggestsBitratesCorrectly) {
+TEST_F(VideoBitrateSuggesterTest,
+       SuggestsBitratesCorrectlyWithExponentialAlgorithm) {
+  UseExponentialAlgorithm();
+
   // We should start with the maximum video bitrate.
   set_suggested_bitrate(5000001);
   EXPECT_EQ(5000000, video_bitrate_suggester().GetSuggestedBitrate());
@@ -130,4 +141,58 @@
   EXPECT_EQ(4998374, video_bitrate_suggester().GetSuggestedBitrate());
 }
 
+TEST_F(VideoBitrateSuggesterTest,
+       SuggestsBitratesCorrectlyWithLinearAlgorithm) {
+  UseLinearAlgorithm();
+
+  // We should start with the maximum video bitrate.
+  set_suggested_bitrate(5000001);
+  EXPECT_EQ(5000000, video_bitrate_suggester().GetSuggestedBitrate());
+
+  // After a period with multiple frame drops, this should go down.
+  RecordShouldDropNextFrame(true);
+  RecordShouldDropNextFrame(true);
+  for (int i = 0; i < 99; ++i) {
+    RecordShouldDropNextFrame(false);
+  }
+
+  // It should now go down.
+  EXPECT_EQ(4412500, video_bitrate_suggester().GetSuggestedBitrate());
+
+  // It should continue to go down to the minimum as long as frames are being
+  // dropped.
+  int last_suggestion = 4412500;
+  for (int i = 0; i < 7; ++i) {
+    RecordShouldDropNextFrame(true);
+    for (int j = 0; j < 99; ++j) {
+      RecordShouldDropNextFrame(false);
+    }
+
+    // It should drop every time.
+    const int suggestion = video_bitrate_suggester().GetSuggestedBitrate();
+    EXPECT_LT(suggestion, last_suggestion);
+    last_suggestion = suggestion;
+  }
+
+  // And then stabilize at the bottom.
+  EXPECT_EQ(300000, video_bitrate_suggester().GetSuggestedBitrate());
+
+  // It should increase once we stop dropping frames.
+  last_suggestion = 300000;
+  for (int i = 0; i < 8; ++i) {
+    for (int j = 0; j < 100; ++j) {
+      RecordShouldDropNextFrame(false);
+    }
+    const int suggestion = video_bitrate_suggester().GetSuggestedBitrate();
+    EXPECT_GT(suggestion, last_suggestion);
+    last_suggestion = suggestion;
+  }
+
+  // And stop at the maximum.
+  EXPECT_EQ(5000000, video_bitrate_suggester().GetSuggestedBitrate());
+
+  // Finally, it should cap at the bitrate suggested by Open Screen.
+  set_suggested_bitrate(4998374);
+  EXPECT_EQ(4998374, video_bitrate_suggester().GetSuggestedBitrate());
+}
 }  // namespace media::cast
diff --git a/media/filters/demuxer_manager.cc b/media/filters/demuxer_manager.cc
index abcac02..c1d191cc 100644
--- a/media/filters/demuxer_manager.cc
+++ b/media/filters/demuxer_manager.cc
@@ -93,27 +93,19 @@
 }
 
 HlsFallbackImplementation SelectHlsFallbackImplementation() {
-#if !BUILDFLAG(IS_ANDROID)
-  // TODO(crbug/1266991): This should return kBuiltinHlsPlayer when we launch
-  // on non-mobile. For now, do not support it.
-  return HlsFallbackImplementation::kNone;
-#elif !BUILDFLAG(ENABLE_HLS_DEMUXER)
-  // Android build supporting only media player.
-  if (base::FeatureList::IsEnabled(kHlsPlayer)) {
-    return HlsFallbackImplementation::kMediaPlayer;
-  }
-  return HlsFallbackImplementation::kNone;
-#else
-  // Android build with both builtin & media player implementations.
-  // Prefer builtin if it is enabled.
+#if BUILDFLAG(ENABLE_HLS_DEMUXER)
   if (base::FeatureList::IsEnabled(kBuiltInHlsPlayer)) {
     return HlsFallbackImplementation::kBuiltinHlsPlayer;
   }
+#endif
+
+#if BUILDFLAG(IS_ANDROID)
   if (base::FeatureList::IsEnabled(kHlsPlayer)) {
     return HlsFallbackImplementation::kMediaPlayer;
   }
-  return HlsFallbackImplementation::kNone;
 #endif
+
+  return HlsFallbackImplementation::kNone;
 }
 
 #endif  // BUILDFLAG(ENABLE_HLS_DEMUXER) || BUILDFLAG(IS_ANDROID)
@@ -229,6 +221,10 @@
   loaded_url_ = std::move(url);
 }
 
+const GURL& DemuxerManager::LoadedUrl() const {
+  return loaded_url_;
+}
+
 #if BUILDFLAG(ENABLE_HLS_DEMUXER) || BUILDFLAG(IS_ANDROID)
 
 void DemuxerManager::PopulateHlsHistograms(bool cryptographic_url) {
@@ -379,6 +375,22 @@
     return DEMUXER_ERROR_COULD_NOT_OPEN;
   }
 
+  // We can only do a universal suspend for posters, unless the flag is enabled.
+  auto suspended_mode = Pipeline::StartType::kSuspendAfterMetadataForAudioOnly;
+  if (has_poster || base::FeatureList::IsEnabled(kPreloadMetadataLazyLoad)) {
+    suspended_mode = Pipeline::StartType::kSuspendAfterMetadata;
+  }
+
+#if BUILDFLAG(ENABLE_HLS_DEMUXER)
+  if (hls_fallback_ == HlsFallbackImplementation::kBuiltinHlsPlayer ||
+      loaded_url_.path_piece().ends_with(".m3u8")) {
+    SetDemuxer(CreateHlsDemuxer());
+    return std::move(on_demuxer_created)
+        .Run(demuxer_.get(), suspended_mode, /*is_streaming=*/false,
+             /*is_static=*/false);
+  }
+#endif  // BUILDFLAG(ENABLE_HLS_DEMUXER)
+
 #if BUILDFLAG(IS_ANDROID)
   const bool media_player_hls =
       hls_fallback_ == HlsFallbackImplementation::kMediaPlayer;
@@ -391,15 +403,6 @@
   }
 #endif
 
-#if BUILDFLAG(ENABLE_HLS_DEMUXER)
-  if (hls_fallback_ == HlsFallbackImplementation::kBuiltinHlsPlayer) {
-    SetDemuxer(CreateHlsDemuxer());
-    return std::move(on_demuxer_created)
-        .Run(demuxer_.get(), Pipeline::StartType::kNormal,
-             /*is_streaming=*/false, /*is_static=*/false);
-  }
-#endif  // BUILDFLAG(ENABLE_HLS_DEMUXER)
-
   // TODO(sandersd): FileSystem objects may also be non-static, but due to our
   // caching layer such situations are broken already. http://crbug.com/593159
   bool is_static = true;
@@ -434,11 +437,6 @@
              is_static);
   }
 
-  // We can only do a universal suspend for posters, unless the flag is enabled.
-  auto suspended_mode = Pipeline::StartType::kSuspendAfterMetadataForAudioOnly;
-  if (has_poster || base::FeatureList::IsEnabled(kPreloadMetadataLazyLoad)) {
-    suspended_mode = Pipeline::StartType::kSuspendAfterMetadata;
-  }
   return std::move(on_demuxer_created)
       .Run(demuxer_.get(), suspended_mode, IsStreaming(), is_static);
 }
diff --git a/media/filters/demuxer_manager.h b/media/filters/demuxer_manager.h
index 7efcdf3..2b1945d9 100644
--- a/media/filters/demuxer_manager.h
+++ b/media/filters/demuxer_manager.h
@@ -128,6 +128,7 @@
 
   void OnPipelineError(PipelineStatus error);
   void SetLoadedUrl(GURL url);
+  const GURL& LoadedUrl() const;
 #if BUILDFLAG(ENABLE_HLS_DEMUXER) || BUILDFLAG(IS_ANDROID)
   void PopulateHlsHistograms(bool cryptographic_url);
   PipelineStatus SelectHlsFallbackMechanism(bool cryptographic_url);
@@ -231,6 +232,7 @@
   // Used for MediaUrlDemuxer when playing HLS content, as well as
   // FFmpegDemuxer in most cases. Also used for creating MemoryDataSource
   // objects.
+  // Note: this may be very large, take care when making copies.
   GURL loaded_url_;
 
   // The data source for creating a demuxer. This should be null when using
diff --git a/media/filters/ffmpeg_demuxer.cc b/media/filters/ffmpeg_demuxer.cc
index 72c42ac..021029e 100644
--- a/media/filters/ffmpeg_demuxer.cc
+++ b/media/filters/ffmpeg_demuxer.cc
@@ -1235,7 +1235,7 @@
     return;
   }
 
-#if BUILDFLAG(IS_ANDROID)
+#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(ENABLE_HLS_DEMUXER)
   if (glue_->detected_hls()) {
     MEDIA_LOG(INFO, media_log_)
         << GetDisplayName() << ": detected HLS manifest";
diff --git a/media/remoting/renderer_controller.cc b/media/remoting/renderer_controller.cc
index 6cdd6dd3..e792cf9e 100644
--- a/media/remoting/renderer_controller.cc
+++ b/media/remoting/renderer_controller.cc
@@ -322,12 +322,12 @@
 }
 
 void RendererController::OnHlsManifestDetected() {
-#if BUILDFLAG(IS_ANDROID)
   is_hls_ = true;
+  // TODO(crbug.com/1266991) Android used to rely solely on MediaPlayer for HLS
+  // playback, but now there is an alternative native player. Should we still
+  // be doing this in all cases? It does work in its current state, on both
+  // android and desktop, but it is not thoroughly tested.
   UpdateRemotePlaybackAvailabilityMonitoringState();
-#else
-  NOTREACHED();
-#endif
 }
 
 void RendererController::UpdateRemotePlaybackAvailabilityMonitoringState() {
diff --git a/media/renderers/win/media_foundation_stream_wrapper.cc b/media/renderers/win/media_foundation_stream_wrapper.cc
index 31545a0a..3c43260 100644
--- a/media/renderers/win/media_foundation_stream_wrapper.cc
+++ b/media/renderers/win/media_foundation_stream_wrapper.cc
@@ -452,6 +452,15 @@
         ReportEncryptionType(buffer);
       }
 
+      if (has_clear_lead_ && !switched_clear_to_encrypted_ &&
+          !buffer->end_of_stream() && buffer->decrypt_config() &&
+          buffer->decrypt_config()->encryption_scheme() !=
+              EncryptionScheme::kUnencrypted) {
+        MEDIA_LOG(INFO, media_log_)
+            << "Stream switched from clear to encrypted buffers.";
+        switched_clear_to_encrypted_ = true;
+      }
+
       // Push |buffer| to process later if needed. Otherwise, process it
       // immediately.
       if (flushed_ || !post_flush_buffers_.empty()) {
@@ -638,6 +647,7 @@
     MEDIA_LOG(INFO, media_log_) << "MediaFoundationStreamWrapper: "
                                 << DemuxerStream::GetTypeName(stream_type_)
                                 << " stream is encrypted with clear lead";
+    has_clear_lead_ = true;
   }
 
   // TODO(xhwang): Report `encryption_type` to `PipelineStatistics` so it's
diff --git a/media/renderers/win/media_foundation_stream_wrapper.h b/media/renderers/win/media_foundation_stream_wrapper.h
index b67e863..9dffd74 100644
--- a/media/renderers/win/media_foundation_stream_wrapper.h
+++ b/media/renderers/win/media_foundation_stream_wrapper.h
@@ -20,6 +20,7 @@
 #include "media/base/decoder_buffer.h"
 #include "media/base/demuxer_stream.h"
 #include "media/base/media_log.h"
+#include "third_party/abseil-cpp/absl/types/optional.h"
 
 namespace media {
 
@@ -160,6 +161,10 @@
 
   int stream_id_;
 
+  bool has_clear_lead_ = false;
+
+  bool switched_clear_to_encrypted_ = false;
+
   // |mf_media_event_queue_| is safe to be called on any thread.
   Microsoft::WRL::ComPtr<IMFMediaEventQueue> mf_media_event_queue_;
   Microsoft::WRL::ComPtr<IMFStreamDescriptor> mf_stream_descriptor_;
diff --git a/mojo/public/cpp/bindings/README.md b/mojo/public/cpp/bindings/README.md
index 200d2f6..7314cf4 100644
--- a/mojo/public/cpp/bindings/README.md
+++ b/mojo/public/cpp/bindings/README.md
@@ -1176,6 +1176,7 @@
 probably never think about while you are coding. It has always been a
 huge pain.
 * Sync calls may lead to deadlocks.
+* Sync web apis are [strongly discouraged](https://www.w3.org/TR/design-principles/#async-by-default).
 
 ### Mojom changes
 
diff --git a/net/base/features.cc b/net/base/features.cc
index 0b9de3e..9fe682d 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -310,6 +310,10 @@
 BASE_FEATURE(kPlatformKeyProbeSHA256,
              "PlatformKeyProbeSHA256",
              base::FEATURE_ENABLED_BY_DEFAULT);
+
+BASE_FEATURE(kEnableGetNetworkConnectivityHintAPI,
+             "EnableGetNetworkConnectivityHintAPI",
+             base::FEATURE_ENABLED_BY_DEFAULT);
 #endif
 
 // Prefetch to follow normal semantics instead of 5-minute rule
diff --git a/net/base/features.h b/net/base/features.h
index 4a8d47c..7eab03e 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -347,6 +347,10 @@
 // Whether to probe for SHA-256 on some legacy platform keys, before assuming
 // the key requires SHA-1. See SSLPlatformKeyWin for details.
 NET_EXPORT BASE_DECLARE_FEATURE(kPlatformKeyProbeSHA256);
+
+// Whether or not to use the GetNetworkConnectivityHint API on modern Windows
+// versions for the Network Change Notifier.
+NET_EXPORT BASE_DECLARE_FEATURE(kEnableGetNetworkConnectivityHintAPI);
 #endif
 
 // Prefetch to follow normal semantics instead of 5-minute rule
diff --git a/net/base/network_change_notifier_win.cc b/net/base/network_change_notifier_win.cc
index 1f128c16..cc0f4c9 100644
--- a/net/base/network_change_notifier_win.cc
+++ b/net/base/network_change_notifier_win.cc
@@ -9,6 +9,7 @@
 
 #include <utility>
 
+#include "base/feature_list.h"
 #include "base/functional/bind.h"
 #include "base/location.h"
 #include "base/logging.h"
@@ -20,6 +21,7 @@
 #include "base/threading/thread.h"
 #include "base/time/time.h"
 #include "base/win/windows_version.h"
+#include "net/base/features.h"
 #include "net/base/winsock_init.h"
 #include "net/base/winsock_util.h"
 
@@ -241,7 +243,9 @@
 // static
 NetworkChangeNotifier::ConnectionType
 NetworkChangeNotifierWin::RecomputeCurrentConnectionType() {
-  if (base::win::GetVersion() >= base::win::Version::WIN10_20H1) {
+  if (base::win::GetVersion() >= base::win::Version::WIN10_20H1 &&
+      base::FeatureList::IsEnabled(
+          features::kEnableGetNetworkConnectivityHintAPI)) {
     return RecomputeCurrentConnectionTypeModern();
   }
 
diff --git a/net/base/registry_controlled_domains/registry_controlled_domain.cc b/net/base/registry_controlled_domains/registry_controlled_domain.cc
index 7fbc97c..f61149f7 100644
--- a/net/base/registry_controlled_domains/registry_controlled_domain.cc
+++ b/net/base/registry_controlled_domains/registry_controlled_domain.cc
@@ -88,7 +88,7 @@
       g_graph, g_graph_length, private_filter == INCLUDE_PRIVATE_REGISTRIES,
       host, &length);
 
-  DCHECK_LE(length, host.size());
+  CHECK_LE(length, host.size());
 
   // No rule found in the registry.
   if (type == kDafsaNotFound) {
@@ -109,8 +109,8 @@
     if (length == host.size())
       return 0;
 
-    DCHECK_LE(length + 2, host.size());
-    DCHECK_EQ('.', host[host.size() - length - 1]);
+    CHECK_LE(length + 2, host.size());
+    CHECK_EQ('.', host[host.size() - length - 1]);
 
     const size_t preceding_dot =
         host.find_last_of('.', host.size() - length - 2);
@@ -140,7 +140,7 @@
     return host.length() - first_dot - 1;
   }
 
-  DCHECK_NE(type, kDafsaNotFound);
+  CHECK_NE(type, kDafsaNotFound);
 
   // If a complete match, then the host is the registry itself, so return 0.
   if (length == host.size())
@@ -179,7 +179,7 @@
 base::StringPiece GetDomainAndRegistryImpl(
     base::StringPiece host,
     PrivateRegistryFilter private_filter) {
-  DCHECK(!host.empty());
+  CHECK(!host.empty());
 
   // Find the length of the registry for this host.
   const size_t registry_length =
@@ -188,12 +188,9 @@
     return base::StringPiece();  // No registry.
   // The "2" in this next line is 1 for the dot, plus a 1-char minimum preceding
   // subcomponent length.
-  DCHECK(host.length() >= 2);
-  if (registry_length > (host.length() - 2)) {
-    NOTREACHED() <<
-        "Host does not have at least one subcomponent before registry!";
-    return base::StringPiece();
-  }
+  CHECK_GE(host.length(), 2u);
+  CHECK_LE(registry_length, host.length() - 2)
+      << "Host does not have at least one subcomponent before registry!";
 
   // Move past the dot preceding the registry, and search for the next previous
   // dot.  Return the host from after that dot, or the whole host when there is
diff --git a/net/dns/system_dns_config_change_notifier.cc b/net/dns/system_dns_config_change_notifier.cc
index 5d71b4d12..ddff4e0 100644
--- a/net/dns/system_dns_config_change_notifier.cc
+++ b/net/dns/system_dns_config_change_notifier.cc
@@ -143,12 +143,6 @@
     }
   }
 
-  void OnConfigChangedForTesting(const DnsConfig& config) {
-    task_runner_->PostTask(
-        FROM_HERE, base::BindOnce(&Core::OnConfigChanged,
-                                  weak_ptr_factory_.GetWeakPtr(), config));
-  }
-
  private:
   void SetAndStartDnsConfigService(
       std::unique_ptr<DnsConfigService> dns_config_service) {
@@ -240,9 +234,4 @@
       std::move(dns_config_service), std::move(done_cb));
 }
 
-void SystemDnsConfigChangeNotifier::OnConfigChangedForTesting(
-    const DnsConfig& config) {
-  core_->OnConfigChangedForTesting(config);  // IN-TEST
-}
-
 }  // namespace net
diff --git a/net/dns/system_dns_config_change_notifier.h b/net/dns/system_dns_config_change_notifier.h
index 51e6b98..d07f721 100644
--- a/net/dns/system_dns_config_change_notifier.h
+++ b/net/dns/system_dns_config_change_notifier.h
@@ -81,8 +81,6 @@
       std::unique_ptr<DnsConfigService> dns_config_service,
       base::OnceClosure done_cb);
 
-  void OnConfigChangedForTesting(const DnsConfig& config);
-
  private:
   class Core;
 
diff --git a/net/http/http_version.h b/net/http/http_version.h
index e91e561..b02ade7 100644
--- a/net/http/http_version.h
+++ b/net/http/http_version.h
@@ -10,39 +10,44 @@
 namespace net {
 
 // Wrapper for an HTTP (major,minor) version pair.
-class HttpVersion {
+// This type is final as the type is copy-constructable and assignable and so
+// there is a risk of slicing if it was subclassed.
+class HttpVersion final {
  public:
   // Default constructor (major=0, minor=0).
-  HttpVersion() : value_(0) { }
+  constexpr HttpVersion() : value_(0) {}
 
   // Build from unsigned major/minor pair.
-  HttpVersion(uint16_t major, uint16_t minor)
+  constexpr HttpVersion(uint16_t major, uint16_t minor)
       : value_(static_cast<uint32_t>(major << 16) | minor) {}
 
+  constexpr HttpVersion(const HttpVersion& rhs) = default;
+  constexpr HttpVersion& operator=(const HttpVersion& rhs) = default;
+
   // Major version number.
-  uint16_t major_value() const { return value_ >> 16; }
+  constexpr uint16_t major_value() const { return value_ >> 16; }
 
   // Minor version number.
-  uint16_t minor_value() const { return value_ & 0xffff; }
+  constexpr uint16_t minor_value() const { return value_ & 0xffff; }
 
   // Overloaded operators:
 
-  bool operator==(const HttpVersion& v) const {
+  constexpr bool operator==(const HttpVersion& v) const {
     return value_ == v.value_;
   }
-  bool operator!=(const HttpVersion& v) const {
+  constexpr bool operator!=(const HttpVersion& v) const {
     return value_ != v.value_;
   }
-  bool operator>(const HttpVersion& v) const {
+  constexpr bool operator>(const HttpVersion& v) const {
     return value_ > v.value_;
   }
-  bool operator>=(const HttpVersion& v) const {
+  constexpr bool operator>=(const HttpVersion& v) const {
     return value_ >= v.value_;
   }
-  bool operator<(const HttpVersion& v) const {
+  constexpr bool operator<(const HttpVersion& v) const {
     return value_ < v.value_;
   }
-  bool operator<=(const HttpVersion& v) const {
+  constexpr bool operator<=(const HttpVersion& v) const {
     return value_ <= v.value_;
   }
 
diff --git a/net/http/transport_security_state.cc b/net/http/transport_security_state.cc
index 698a363..7ea8987 100644
--- a/net/http/transport_security_state.cc
+++ b/net/http/transport_security_state.cc
@@ -500,7 +500,9 @@
            ct::CTPolicyCompliance::CT_POLICY_COMPLIES_VIA_SCTS ||
        policy_compliance == ct::CTPolicyCompliance::CT_POLICY_BUILD_NOT_TIMELY);
 
-  CTRequirementLevel ct_required = CTRequirementLevel::DEFAULT;
+  CTRequirementLevel ct_required = g_ct_required_for_testing
+                                       ? CTRequirementLevel::REQUIRED
+                                       : CTRequirementLevel::NOT_REQUIRED;
   if (require_ct_delegate_) {
     // Allow the delegate to override the CT requirement state.
     ct_required = require_ct_delegate_->IsCTRequiredForHost(
@@ -511,14 +513,7 @@
       return complies ? CT_REQUIREMENTS_MET : CT_REQUIREMENTS_NOT_MET;
     case CTRequirementLevel::NOT_REQUIRED:
       return CT_NOT_REQUIRED;
-    case CTRequirementLevel::DEFAULT:
-      break;
   }
-
-  if (g_ct_required_for_testing) {
-    return complies ? CT_REQUIREMENTS_MET : CT_REQUIREMENTS_NOT_MET;
-  }
-  return CT_NOT_REQUIRED;
 }
 
 void TransportSecurityState::SetDelegate(
diff --git a/net/http/transport_security_state.h b/net/http/transport_security_state.h
index b5b8d80..5430a6af 100644
--- a/net/http/transport_security_state.h
+++ b/net/http/transport_security_state.h
@@ -90,11 +90,6 @@
       // The host is explicitly not required to supply Certificate
       // Transparency information that complies with the CT policy.
       NOT_REQUIRED,
-
-      // The delegate makes no statements, positive or negative, about
-      // requiring the host to supply Certificate Transparency information,
-      // allowing the default behaviour to happen.
-      DEFAULT,
     };
 
     // Called by the TransportSecurityState, allows the Delegate to override
diff --git a/net/http/transport_security_state_unittest.cc b/net/http/transport_security_state_unittest.cc
index 00f652be..5ee5fdf 100644
--- a/net/http/transport_security_state_unittest.cc
+++ b/net/http/transport_security_state_unittest.cc
@@ -1202,36 +1202,6 @@
             cert.get(), SignedCertificateTimestampAndStatusList(),
             ct::CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS));
   }
-
-  // If the Delegate is in the default state, then it should return the same
-  // result as if there was no delegate in the first place.
-  {
-    TransportSecurityState state;
-    const TransportSecurityState::CTRequirementsStatus original_status =
-        state.CheckCTRequirements(
-            HostPortPair("www.example.com", 443), true, hashes, cert.get(),
-            cert.get(), SignedCertificateTimestampAndStatusList(),
-            ct::CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS);
-
-    MockRequireCTDelegate default_require_ct_delegate;
-    EXPECT_CALL(default_require_ct_delegate, IsCTRequiredForHost(_, _, _))
-        .WillRepeatedly(Return(CTRequirementLevel::DEFAULT));
-    state.SetRequireCTDelegate(&default_require_ct_delegate);
-    EXPECT_EQ(
-        original_status,
-        state.CheckCTRequirements(
-            HostPortPair("www.example.com", 443), true, hashes, cert.get(),
-            cert.get(), SignedCertificateTimestampAndStatusList(),
-            ct::CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS));
-
-    state.SetRequireCTDelegate(nullptr);
-    EXPECT_EQ(
-        original_status,
-        state.CheckCTRequirements(
-            HostPortPair("www.example.com", 443), true, hashes, cert.get(),
-            cert.get(), SignedCertificateTimestampAndStatusList(),
-            ct::CTPolicyCompliance::CT_POLICY_NOT_ENOUGH_SCTS));
-  }
 }
 
 enum class CTEmergencyDisableSwitchKind {
diff --git a/net/third_party/quiche/src b/net/third_party/quiche/src
index 7526611..5cdf937 160000
--- a/net/third_party/quiche/src
+++ b/net/third_party/quiche/src
@@ -1 +1 @@
-Subproject commit 75266116f21731354fe44cea6a1aae7eae70441e
+Subproject commit 5cdf937c378cdf08ff55ea9e86cfbf05bec54df2
diff --git a/services/BUILD.gn b/services/BUILD.gn
index 49fbee8..36859bb 100644
--- a/services/BUILD.gn
+++ b/services/BUILD.gn
@@ -42,6 +42,7 @@
       "//services/image_annotation/public/cpp:tests",
       "//services/media_session:tests",
       "//services/media_session/public/cpp:tests",
+      "//services/on_device_model:tests",
       "//services/preferences/tracked:unit_tests",
       "//services/proxy_resolver:tests",
       "//services/resource_coordinator:tests",
diff --git a/services/accessibility/public/mojom/BUILD.gn b/services/accessibility/public/mojom/BUILD.gn
index 791a52c..332e2e4 100644
--- a/services/accessibility/public/mojom/BUILD.gn
+++ b/services/accessibility/public/mojom/BUILD.gn
@@ -51,7 +51,6 @@
       "//skia/public/mojom:mojom",
       "//ui/gfx/geometry/mojom",
     ]
-    webui_module_path = "/"
   }
 
   group("os_accessibility_service_js") {
diff --git a/services/network/network_context.cc b/services/network/network_context.cc
index f47ffb4..a88d817 100644
--- a/services/network/network_context.cc
+++ b/services/network/network_context.cc
@@ -1491,9 +1491,9 @@
   if (!require_ct_delegate_)
     return;
 
-  require_ct_delegate_->UpdateCTPolicies(
-      ct_policy->required_hosts, ct_policy->excluded_hosts,
-      ct_policy->excluded_spkis, ct_policy->excluded_legacy_spkis);
+  require_ct_delegate_->UpdateCTPolicies(ct_policy->excluded_hosts,
+                                         ct_policy->excluded_spkis,
+                                         ct_policy->excluded_legacy_spkis);
 }
 
 int NetworkContext::CheckCTComplianceForSignedExchange(
diff --git a/services/network/public/cpp/BUILD.gn b/services/network/public/cpp/BUILD.gn
index fcada8d..36fef15c 100644
--- a/services/network/public/cpp/BUILD.gn
+++ b/services/network/public/cpp/BUILD.gn
@@ -388,8 +388,6 @@
     "cross_origin_opener_policy_mojom_traits.h",
     "data_element.cc",
     "data_element.h",
-    "http_raw_request_response_info.cc",
-    "http_raw_request_response_info.h",
     "http_request_headers_mojom_traits.cc",
     "http_request_headers_mojom_traits.h",
     "isolation_info_mojom_traits.cc",
@@ -592,9 +590,6 @@
   if (is_linux) {
     sources += [ "network_interface_change_listener_mojom_traits_unittest.cc" ]
   }
-  if (is_android) {
-    sources += [ "system_dns_config_observer_mojom_traits_unittest.cc" ]
-  }
 
   deps = [
     ":cpp",
diff --git a/services/network/public/cpp/http_raw_request_response_info.cc b/services/network/public/cpp/http_raw_request_response_info.cc
deleted file mode 100644
index 0158c9d..0000000
--- a/services/network/public/cpp/http_raw_request_response_info.cc
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2014 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/public/cpp/http_raw_request_response_info.h"
-
-#include "base/memory/scoped_refptr.h"
-
-namespace network {
-
-HttpRawRequestResponseInfo::HttpRawRequestResponseInfo()
-    : http_status_code(0) {}
-
-HttpRawRequestResponseInfo::~HttpRawRequestResponseInfo() {}
-
-scoped_refptr<HttpRawRequestResponseInfo> HttpRawRequestResponseInfo::DeepCopy()
-    const {
-  auto new_info = base::MakeRefCounted<HttpRawRequestResponseInfo>();
-  new_info->http_status_code = http_status_code;
-  new_info->http_status_text = http_status_text;
-  new_info->request_headers = request_headers;
-  new_info->response_headers = response_headers;
-  new_info->request_headers_text = request_headers_text;
-  new_info->response_headers_text = response_headers_text;
-  return new_info;
-}
-
-}  // namespace network
diff --git a/services/network/public/cpp/http_raw_request_response_info.h b/services/network/public/cpp/http_raw_request_response_info.h
deleted file mode 100644
index b86ef1e..0000000
--- a/services/network/public/cpp/http_raw_request_response_info.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2014 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_PUBLIC_CPP_HTTP_RAW_REQUEST_RESPONSE_INFO_H_
-#define SERVICES_NETWORK_PUBLIC_CPP_HTTP_RAW_REQUEST_RESPONSE_INFO_H_
-
-#include <stdint.h>
-
-#include <string>
-#include <vector>
-
-#include "base/component_export.h"
-#include "base/memory/ref_counted.h"
-#include "base/strings/string_split.h"
-
-namespace network {
-
-// Note: when modifying this structure, also update DeepCopy in
-// http_raw_request_response_info.cc.
-struct COMPONENT_EXPORT(NETWORK_CPP_BASE) HttpRawRequestResponseInfo
-    : base::RefCountedThreadSafe<HttpRawRequestResponseInfo> {
-  using HeadersVector = base::StringPairs;
-
-  HttpRawRequestResponseInfo();
-
-  scoped_refptr<HttpRawRequestResponseInfo> DeepCopy() const;
-
-  int32_t http_status_code;
-  std::string http_status_text;  // Not present in HTTP/2
-  HeadersVector request_headers;
-  HeadersVector response_headers;
-  std::string request_headers_text;
-  std::string response_headers_text;
-
- private:
-  friend class base::RefCountedThreadSafe<HttpRawRequestResponseInfo>;
-  ~HttpRawRequestResponseInfo();
-};
-
-}  // namespace network
-
-#endif  // SERVICES_NETWORK_PUBLIC_CPP_HTTP_RAW_REQUEST_RESPONSE_INFO_H_
diff --git a/services/network/public/cpp/system_dns_config_observer_mojom_traits.cc b/services/network/public/cpp/system_dns_config_observer_mojom_traits.cc
deleted file mode 100644
index bdc9ea57..0000000
--- a/services/network/public/cpp/system_dns_config_observer_mojom_traits.cc
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "services/network/public/cpp/system_dns_config_observer_mojom_traits.h"
-
-namespace mojo {
-
-using network::mojom::DnsConfigDataView;
-
-// static
-bool StructTraits<DnsConfigDataView, net::DnsConfig>::Read(
-    DnsConfigDataView data,
-    net::DnsConfig* out) {
-  if (!data.ReadNameservers(&out->nameservers)) {
-    return false;
-  }
-
-  out->dns_over_tls_active = data.dns_over_tls_active();
-
-  if (!data.ReadDnsOverTlsHostname(&out->dns_over_tls_hostname)) {
-    return false;
-  }
-
-  if (!data.ReadSearch(&out->search)) {
-    return false;
-  }
-
-  out->unhandled_options = data.unhandled_options();
-
-  return true;
-}
-
-}  // namespace mojo
diff --git a/services/network/public/cpp/system_dns_config_observer_mojom_traits.h b/services/network/public/cpp/system_dns_config_observer_mojom_traits.h
deleted file mode 100644
index 01498297..0000000
--- a/services/network/public/cpp/system_dns_config_observer_mojom_traits.h
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef SERVICES_NETWORK_PUBLIC_CPP_SYSTEM_DNS_CONFIG_OBSERVER_MOJOM_TRAITS_H_
-#define SERVICES_NETWORK_PUBLIC_CPP_SYSTEM_DNS_CONFIG_OBSERVER_MOJOM_TRAITS_H_
-
-#include <string>
-
-#include "mojo/public/cpp/bindings/struct_traits.h"
-#include "net/dns/dns_config.h"
-#include "services/network/public/mojom/system_dns_config_observer.mojom.h"
-
-namespace mojo {
-
-template <>
-struct StructTraits<network::mojom::DnsConfigDataView, net::DnsConfig> {
-  static const std::vector<net::IPEndPoint>& nameservers(
-      const net::DnsConfig& config) {
-    return config.nameservers;
-  }
-
-  static bool dns_over_tls_active(const net::DnsConfig& config) {
-    return config.dns_over_tls_active;
-  }
-
-  static const std::string& dns_over_tls_hostname(
-      const net::DnsConfig& config) {
-    return config.dns_over_tls_hostname;
-  }
-  static const std::vector<std::string>& search(const net::DnsConfig& config) {
-    return config.search;
-  }
-
-  static bool unhandled_options(const net::DnsConfig& config) {
-    return config.unhandled_options;
-  }
-
-  static bool Read(network::mojom::DnsConfigDataView data, net::DnsConfig* out);
-};
-
-}  // namespace mojo
-
-#endif  // SERVICES_NETWORK_PUBLIC_CPP_SYSTEM_DNS_CONFIG_OBSERVER_MOJOM_TRAITS_H_
diff --git a/services/network/public/cpp/system_dns_config_observer_mojom_traits_unittest.cc b/services/network/public/cpp/system_dns_config_observer_mojom_traits_unittest.cc
deleted file mode 100644
index 9281441..0000000
--- a/services/network/public/cpp/system_dns_config_observer_mojom_traits_unittest.cc
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "services/network/public/cpp/system_dns_config_observer_mojom_traits.h"
-
-#include "mojo/public/cpp/test_support/test_utils.h"
-#include "services/network/public/mojom/system_dns_config_observer.mojom.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace network {
-namespace {
-
-TEST(SystemDnsConfigObserverMojomTraitsTest,
-     SerializeAndDeserializeDefaultValue) {
-  net::DnsConfig original, deserialized;
-  EXPECT_TRUE(mojo::test::SerializeAndDeserialize<mojom::DnsConfig>(
-      original, deserialized));
-
-  EXPECT_EQ(original, deserialized) << "original=" << original.ToDict()
-                                    << "deserialized=" << deserialized.ToDict();
-}
-
-TEST(SystemDnsConfigObserverMojomTraitsTest, SerializeAndDeserializeWithValue) {
-  net::DnsConfig original;
-  original.nameservers = {net::IPEndPoint(net::IPAddress(1, 2, 3, 4), 80)};
-  original.dns_over_tls_active = true;
-  original.dns_over_tls_hostname = "https://example.com/";
-  original.search = {"foo"};
-  original.unhandled_options = true;
-
-  net::DnsConfig deserialized;
-  EXPECT_TRUE(mojo::test::SerializeAndDeserialize<mojom::DnsConfig>(
-      original, deserialized));
-
-  EXPECT_EQ(original, deserialized) << "original=" << original.ToDict()
-                                    << "deserialized=" << deserialized.ToDict();
-}
-
-}  // namespace
-}  // namespace network
diff --git a/services/network/public/mojom/BUILD.gn b/services/network/public/mojom/BUILD.gn
index 311985f7..dd77571 100644
--- a/services/network/public/mojom/BUILD.gn
+++ b/services/network/public/mojom/BUILD.gn
@@ -1398,10 +1398,6 @@
     public_deps += [ "//services/proxy_resolver_win/public/mojom" ]
   }
 
-  if (is_android) {
-    sources += [ "system_dns_config_observer.mojom" ]
-  }
-
   enabled_features = []
 
   # TODO(crbug/598073): When moving the service implementation to
@@ -1578,25 +1574,6 @@
     },
   ]
 
-  if (is_android) {
-    cpp_typemaps += [
-      {
-        types = [
-          {
-            mojom = "network.mojom.DnsConfig"
-            cpp = "::net::DnsConfig"
-          },
-        ]
-        traits_headers = [ "//services/network/public/cpp/system_dns_config_observer_mojom_traits.h" ]
-        traits_sources = [ "//services/network/public/cpp/system_dns_config_observer_mojom_traits.cc" ]
-        traits_public_deps = [
-          "//net",
-          "//services/network/public/cpp:cpp_base",
-        ]
-      },
-    ]
-  }
-
   if (enable_reporting) {
     cpp_typemaps += [
       {
diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom
index 0af727743..d790176 100644
--- a/services/network/public/mojom/network_context.mojom
+++ b/services/network/public/mojom/network_context.mojom
@@ -213,7 +213,6 @@
 
 [EnableIf=is_ct_supported]
 struct CTPolicy {
-  array<string> required_hosts;
   array<string> excluded_hosts;
   array<string> excluded_spkis;
   array<string> excluded_legacy_spkis;
diff --git a/services/network/public/mojom/network_service_test.mojom b/services/network/public/mojom/network_service_test.mojom
index a0bf9dc7..22398e2 100644
--- a/services/network/public/mojom/network_service_test.mojom
+++ b/services/network/public/mojom/network_service_test.mojom
@@ -18,9 +18,6 @@
 import "services/network/public/mojom/ip_endpoint.mojom";
 import "services/network/public/mojom/transferable_socket.mojom";
 
-[EnableIf=is_android]
-import "services/network/public/mojom/system_dns_config_observer.mojom";
-
 // Maps to net::RuleBasedHostResolverProc::Rule::ResolverType.
 //
 // TODO(https://crbug.com/1298106) Deduplicate this enum's definition.
@@ -182,18 +179,6 @@
   [Sync]
   ReplaceSystemDnsConfig() => ();
 
-  // Register a DNS listener `observer` to the network service.
-  // `observer.OnConfigchanged()` is called when SystemDnsConfigChangeNotifier::
-  // Observer::OnSystemDnsConfigChanged() happens in the network service.
-  //
-  // Note that SystemDnsConfigObserver is supposed to notice system DNS change
-  // in the browser UI thread to the network service.
-  // However for content_browser testing, this methods is used to determine the
-  // network service receives a system DNS config from the browser UI thread.
-  [Sync, EnableIf=is_android]
-  AddSystemDnsConfigObserver(
-    pending_remote<SystemDnsConfigObserver> observer) => ();
-
   // Set the DoH configuration to be used during tests.
   [Sync]
   SetTestDohConfig(SecureDnsMode secure_dns_mode,
diff --git a/services/network/public/mojom/system_dns_config_observer.mojom b/services/network/public/mojom/system_dns_config_observer.mojom
deleted file mode 100644
index c779f95..0000000
--- a/services/network/public/mojom/system_dns_config_observer.mojom
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-module network.mojom;
-
-import "services/network/public/mojom/ip_endpoint.mojom";
-
-// DnsConfig stores configuration of the system resolver.
-// Partially type-mapped to net::DnsConfig for DnsConfigServiceAndroid.
-// See DnsConfigServiceAndroid::ConfigReader::WorkItem::DoWork for detail.
-struct DnsConfig {
-  // List of name server addresses.
-  array<IPEndPoint> nameservers;
-
-   // Status of system DNS-over-TLS (DoT).
-  bool dns_over_tls_active;
-  string dns_over_tls_hostname;
-
-  // Suffix search list; used on first lookup when number of dots in given name
-  // is less than `ndots` in net::DnsConfig.
-  array<string> search;
-
-  // True if there are options set in the system configuration that are not yet
-  // supported by DnsClient.
-  bool unhandled_options = false;
-};
-
-// Interface listening for changes to system DNS configuration.
-// TODO(https://crbug.com/1320187): Implement net/dns/dns_config_service_mojo.cc
-interface SystemDnsConfigObserver {
-  // Called on loading new config, including the initial read once the first
-  // valid config has been read.
-  OnConfigChanged(DnsConfig config);
-};
diff --git a/services/on_device_model/BUILD.gn b/services/on_device_model/BUILD.gn
new file mode 100644
index 0000000..d692e08
--- /dev/null
+++ b/services/on_device_model/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+component("on_device_model_service") {
+  sources = [
+    "on_device_model.cc",
+    "on_device_model.h",
+    "on_device_model_service.cc",
+    "on_device_model_service.h",
+  ]
+  deps = [
+    "//base",
+    "//mojo/public/cpp/bindings",
+    "//services/on_device_model/public/mojom",
+  ]
+  defines = [ "IS_ON_DEVICE_MODEL_IMPL" ]
+}
+
+source_set("tests") {
+  testonly = true
+
+  sources = [ "on_device_model_service_unittest.cc" ]
+  deps = [
+    ":on_device_model_service",
+    "//base/test:test_support",
+    "//services/on_device_model/public/mojom",
+    "//testing/gmock",
+    "//testing/gtest",
+  ]
+}
diff --git a/services/on_device_model/OWNERS b/services/on_device_model/OWNERS
new file mode 100644
index 0000000..f23f56f
--- /dev/null
+++ b/services/on_device_model/OWNERS
@@ -0,0 +1,4 @@
+cduvall@chromium.org
+jam@chromium.org
+rockot@google.com
+sky@chromium.org
diff --git a/services/on_device_model/on_device_model.cc b/services/on_device_model/on_device_model.cc
new file mode 100644
index 0000000..8a1dff2
--- /dev/null
+++ b/services/on_device_model/on_device_model.cc
@@ -0,0 +1,26 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/on_device_model/on_device_model.h"
+
+#include "mojo/public/cpp/bindings/remote.h"
+
+namespace on_device_model {
+
+OnDeviceModel::OnDeviceModel(mojom::LoadModelParamsPtr params)
+    : params_(std::move(params)) {}
+
+OnDeviceModel::~OnDeviceModel() = default;
+
+void OnDeviceModel::Execute(
+    const std::string& input,
+    mojo::PendingRemote<mojom::StreamingResponder> response) {
+  mojo::Remote<mojom::StreamingResponder> remote(std::move(response));
+  // TODO(cduvall): Make this work.
+  remote->OnResponse("Model: " + params_->path.MaybeAsASCII());
+  remote->OnResponse("Input: " + input);
+  remote->OnComplete();
+}
+
+}  // namespace on_device_model
diff --git a/services/on_device_model/on_device_model.h b/services/on_device_model/on_device_model.h
new file mode 100644
index 0000000..8e398d78
--- /dev/null
+++ b/services/on_device_model/on_device_model.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_H_
+#define SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_H_
+
+#include "services/on_device_model/public/mojom/on_device_model.mojom.h"
+
+namespace on_device_model {
+
+class OnDeviceModel : public mojom::OnDeviceModel {
+ public:
+  explicit OnDeviceModel(mojom::LoadModelParamsPtr params);
+  ~OnDeviceModel() override;
+
+  OnDeviceModel(const OnDeviceModel&) = delete;
+  OnDeviceModel& operator=(const OnDeviceModel&) = delete;
+
+  void Execute(
+      const std::string& input,
+      mojo::PendingRemote<mojom::StreamingResponder> response) override;
+
+ private:
+  const mojom::LoadModelParamsPtr params_;
+};
+
+}  // namespace on_device_model
+
+#endif  // SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_H_
diff --git a/services/on_device_model/on_device_model_service.cc b/services/on_device_model/on_device_model_service.cc
new file mode 100644
index 0000000..44723c1
--- /dev/null
+++ b/services/on_device_model/on_device_model_service.cc
@@ -0,0 +1,25 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/on_device_model/on_device_model_service.h"
+
+#include "services/on_device_model/on_device_model.h"
+
+namespace on_device_model {
+
+OnDeviceModelService::OnDeviceModelService(
+    mojo::PendingReceiver<mojom::OnDeviceModelService> receiver)
+    : receiver_(this, std::move(receiver)) {}
+
+OnDeviceModelService::~OnDeviceModelService() = default;
+
+void OnDeviceModelService::LoadModel(mojom::LoadModelParamsPtr params,
+                                     LoadModelCallback callback) {
+  mojo::PendingRemote<mojom::OnDeviceModel> remote;
+  model_receivers_.Add(std::make_unique<OnDeviceModel>(std::move(params)),
+                       remote.InitWithNewPipeAndPassReceiver());
+  std::move(callback).Run(mojom::LoadModelResult::NewModel(std::move(remote)));
+}
+
+}  // namespace on_device_model
diff --git a/services/on_device_model/on_device_model_service.h b/services/on_device_model/on_device_model_service.h
new file mode 100644
index 0000000..043b49e
--- /dev/null
+++ b/services/on_device_model/on_device_model_service.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_SERVICE_H_
+#define SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_SERVICE_H_
+
+#include "base/component_export.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/unique_receiver_set.h"
+#include "services/on_device_model/public/mojom/on_device_model.mojom.h"
+
+namespace on_device_model {
+
+class COMPONENT_EXPORT(ON_DEVICE_MODEL) OnDeviceModelService
+    : public mojom::OnDeviceModelService {
+ public:
+  explicit OnDeviceModelService(
+      mojo::PendingReceiver<mojom::OnDeviceModelService> receiver);
+  ~OnDeviceModelService() override;
+
+  OnDeviceModelService(const OnDeviceModelService&) = delete;
+  OnDeviceModelService& operator=(const OnDeviceModelService&) = delete;
+
+  void LoadModel(mojom::LoadModelParamsPtr params,
+                 LoadModelCallback callback) override;
+
+ private:
+  mojo::Receiver<mojom::OnDeviceModelService> receiver_;
+  mojo::UniqueReceiverSet<mojom::OnDeviceModel> model_receivers_;
+};
+
+}  // namespace on_device_model
+
+#endif  // SERVICES_ON_DEVICE_MODEL_ON_DEVICE_MODEL_SERVICE_H_
diff --git a/services/on_device_model/on_device_model_service_unittest.cc b/services/on_device_model/on_device_model_service_unittest.cc
new file mode 100644
index 0000000..568a0ba0
--- /dev/null
+++ b/services/on_device_model/on_device_model_service_unittest.cc
@@ -0,0 +1,92 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/on_device_model/on_device_model_service.h"
+
+#include "base/files/file_path.h"
+#include "base/test/bind.h"
+#include "base/test/task_environment.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace on_device_model {
+namespace {
+
+class ResponseHolder : public mojom::StreamingResponder {
+ public:
+  mojo::PendingRemote<mojom::StreamingResponder> BindRemote() {
+    mojo::PendingRemote<mojom::StreamingResponder> remote;
+    receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
+    return remote;
+  }
+
+  void OnResponse(const std::string& text) override {
+    responses_.push_back(text);
+  }
+
+  void OnComplete() override { run_loop_.Quit(); }
+
+  void WaitForCompletion() { run_loop_.Run(); }
+
+  const std::vector<std::string> responses() const { return responses_; }
+
+ private:
+  base::RunLoop run_loop_;
+  mojo::Receiver<mojom::StreamingResponder> receiver_{this};
+  std::vector<std::string> responses_;
+};
+
+class OnDeviceModelServiceTest : public testing::Test {
+ public:
+  OnDeviceModelServiceTest()
+      : service_impl_(service_.BindNewPipeAndPassReceiver()) {}
+
+  mojo::Remote<mojom::OnDeviceModelService>& service() { return service_; }
+
+  mojo::Remote<mojom::OnDeviceModel> LoadModel(
+      mojom::LoadModelParamsPtr params) {
+    base::RunLoop run_loop;
+    mojo::Remote<mojom::OnDeviceModel> remote;
+    service()->LoadModel(
+        std::move(params),
+        base::BindLambdaForTesting([&](mojom::LoadModelResultPtr result) {
+          remote.Bind(std::move(result->get_model()));
+          run_loop.Quit();
+        }));
+    run_loop.Run();
+    return remote;
+  }
+
+ private:
+  base::test::TaskEnvironment task_environment_;
+  mojo::Remote<mojom::OnDeviceModelService> service_;
+  OnDeviceModelService service_impl_;
+};
+
+TEST_F(OnDeviceModelServiceTest, Responds) {
+  auto model = LoadModel(
+      mojom::LoadModelParams::New(base::FilePath(FILE_PATH_LITERAL("foo"))));
+  {
+    ResponseHolder response;
+    model->Execute("bar", response.BindRemote());
+    response.WaitForCompletion();
+    const auto& responses = response.responses();
+    EXPECT_EQ(responses.size(), 2u);
+    EXPECT_EQ(responses[0], "Model: foo");
+    EXPECT_EQ(responses[1], "Input: bar");
+  }
+  // Try another input on  the same model.
+  {
+    ResponseHolder response;
+    model->Execute("cat", response.BindRemote());
+    response.WaitForCompletion();
+    const auto& responses = response.responses();
+    EXPECT_EQ(responses.size(), 2u);
+    EXPECT_EQ(responses[0], "Model: foo");
+    EXPECT_EQ(responses[1], "Input: cat");
+  }
+}
+
+}  // namespace
+}  // namespace on_device_model
diff --git a/services/on_device_model/public/cpp/BUILD.gn b/services/on_device_model/public/cpp/BUILD.gn
new file mode 100644
index 0000000..89bf7396
--- /dev/null
+++ b/services/on_device_model/public/cpp/BUILD.gn
@@ -0,0 +1,13 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+component("cpp") {
+  output_name = "on_device_model_cpp"
+  sources = [
+    "features.cc",
+    "features.h",
+  ]
+  deps = [ "//base" ]
+  defines = [ "IS_ON_DEVICE_MODEL_CPP_IMPL" ]
+}
diff --git a/services/on_device_model/public/cpp/features.cc b/services/on_device_model/public/cpp/features.cc
new file mode 100644
index 0000000..7dd205e0
--- /dev/null
+++ b/services/on_device_model/public/cpp/features.cc
@@ -0,0 +1,15 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "services/on_device_model/public/cpp/features.h"
+
+#include "base/feature_list.h"
+
+namespace on_device_model::features {
+
+BASE_FEATURE(kOnDeviceModelService,
+             "OnDeviceModelService",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
+}  // namespace on_device_model::features
diff --git a/services/on_device_model/public/cpp/features.h b/services/on_device_model/public/cpp/features.h
new file mode 100644
index 0000000..2e4efff
--- /dev/null
+++ b/services/on_device_model/public/cpp/features.h
@@ -0,0 +1,18 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef SERVICES_ON_DEVICE_MODEL_PUBLIC_CPP_FEATURES_H_
+#define SERVICES_ON_DEVICE_MODEL_PUBLIC_CPP_FEATURES_H_
+
+#include "base/component_export.h"
+#include "base/feature_list.h"
+
+namespace on_device_model::features {
+
+COMPONENT_EXPORT(ON_DEVICE_MODEL_CPP)
+BASE_DECLARE_FEATURE(kOnDeviceModelService);
+
+}  // namespace on_device_model::features
+
+#endif  // SERVICES_ON_DEVICE_MODEL_PUBLIC_CPP_FEATURES_H_
diff --git a/services/on_device_model/public/mojom/BUILD.gn b/services/on_device_model/public/mojom/BUILD.gn
new file mode 100644
index 0000000..99b42995
--- /dev/null
+++ b/services/on_device_model/public/mojom/BUILD.gn
@@ -0,0 +1,15 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/tools/bindings/mojom.gni")
+
+mojom("mojom") {
+  sources = [ "on_device_model.mojom" ]
+  deps = [
+    "//mojo/public/mojom/base",
+    "//sandbox/policy/mojom",
+  ]
+  webui_module_path = "/"
+  use_typescript_sources = true
+}
diff --git a/services/on_device_model/public/mojom/OWNERS b/services/on_device_model/public/mojom/OWNERS
new file mode 100644
index 0000000..08850f4
--- /dev/null
+++ b/services/on_device_model/public/mojom/OWNERS
@@ -0,0 +1,2 @@
+per-file *.mojom=set noparent
+per-file *.mojom=file://ipc/SECURITY_OWNERS
diff --git a/services/on_device_model/public/mojom/on_device_model.mojom b/services/on_device_model/public/mojom/on_device_model.mojom
new file mode 100644
index 0000000..a39d6e1
--- /dev/null
+++ b/services/on_device_model/public/mojom/on_device_model.mojom
@@ -0,0 +1,43 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module on_device_model.mojom;
+
+import "mojo/public/mojom/base/file_path.mojom";
+import "sandbox/policy/mojom/sandbox.mojom";
+
+// Streams a response from a call to execute a model.
+interface StreamingResponder {
+  // This is called N times each time a new chunk of text is available.
+  OnResponse(string text);
+
+  // This is called once when all text for the query has been returned.
+  // OnResponse() will not be called after OnComplete() has been called.
+  OnComplete();
+};
+
+// A loaded model which can be queried.
+interface OnDeviceModel {
+  // Executes model on the given input. The response will be streamed to
+  // |response|.
+  Execute(string input, pending_remote<StreamingResponder> response);
+};
+
+struct LoadModelParams {
+  // The path to the model on disk.
+  mojo_base.mojom.FilePath path;
+};
+
+union LoadModelResult {
+  pending_remote<OnDeviceModel> model;
+  string error;
+};
+
+// A service which allows loading models which are stored on-device.
+[ServiceSandbox=sandbox.mojom.Sandbox.kNoSandbox]
+interface OnDeviceModelService {
+  // Loads a model and returns the model or an error if the model could not be
+  // loaded.
+  LoadModel(LoadModelParams params) => (LoadModelResult result);
+};
diff --git a/styleguide/c++/c++-features.md b/styleguide/c++/c++-features.md
index 1d884364..9872d03 100644
--- a/styleguide/c++/c++-features.md
+++ b/styleguide/c++/c++-features.md
@@ -1624,6 +1624,24 @@
 facilities instead.
 ***
 
+### FixedArray <sup>[banned]</sup>
+
+```c++
+absl::FixedArray<MyObj> objs_;
+```
+
+**Description:** A fixed size array like `std::array`, but with size determined
+at runtime instead of compile time.
+
+**Documentation:**
+[fixed_array.h](https://source.chromium.org/chromium/chromium/src/+/main:third_party/abseil-cpp/absl/container/fixed_array.h)
+
+**Notes:**
+*** promo
+Direct construction is banned due to the risk of UB with uninitialized
+trivially-default-constructible types. Instead use `base/types/fixed_array.h`,
+which is a light-weight wrapper that deletes the problematic constructor.
+
 ### FunctionRef <sup>[banned]</sup>
 
 ```c++
@@ -1846,7 +1864,6 @@
 absl::btree_set
 absl::btree_multimap
 absl::btree_multiset
-absl::FixedArray
 ```
 
 **Description:** Alternatives to STL containers designed to be more efficient
diff --git a/testing/buildbot/chrome.gpu.fyi.json b/testing/buildbot/chrome.gpu.fyi.json
index 889da540..08a0c17 100644
--- a/testing/buildbot/chrome.gpu.fyi.json
+++ b/testing/buildbot/chrome.gpu.fyi.json
@@ -18,7 +18,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating",
         "name": "context_lost_validating_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -46,7 +46,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating",
         "name": "expected_color_pixel_validating_test JACUZZI_RELEASE_LKGM",
         "precommit_args": [
@@ -74,7 +74,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc",
         "name": "gpu_process_launch_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -97,7 +97,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc",
         "name": "hardware_accelerated_feature_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -124,7 +124,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --force_high_performance_gpu",
         "name": "info_collection_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -147,7 +147,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --force_higher_performance_gpu --use-cmd-decoder=validating",
         "name": "mediapipe_validating_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -176,7 +176,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating",
         "name": "pixel_skia_gold_validating_test JACUZZI_RELEASE_LKGM",
         "precommit_args": [
@@ -205,7 +205,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating",
         "name": "screenshot_sync_validating_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -229,7 +229,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc",
         "name": "trace_test JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -253,7 +253,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating",
         "name": "webcodecs_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -279,7 +279,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-gl=angle --use-angle=gles --use-cmd-decoder=passthrough --force_high_performance_gpu",
         "name": "webgl2_conformance_gles_passthrough_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -305,7 +305,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating --force_high_performance_gpu",
         "name": "webgl2_conformance_validating_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
@@ -330,7 +330,7 @@
         ],
         "autotest_name": "chromium_Graphics",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "extra_browser_args": "--log-level=0 --js-flags=--expose-gc --use-cmd-decoder=validating --force_high_performance_gpu",
         "name": "webgl_conformance_validating_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
diff --git a/testing/buildbot/chrome.json b/testing/buildbot/chrome.json
index b74c52ad..3f61c75 100644
--- a/testing/buildbot/chrome.json
+++ b/testing/buildbot/chrome.json
@@ -1177,7 +1177,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "chrome_all_tast_tests BRYA_RELEASE_LKGM",
         "shards": 10,
@@ -1191,7 +1191,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "experiment_percentage": 100,
         "name": "chrome_criticalstaging_tast_tests BRYA_RELEASE_LKGM",
@@ -1206,7 +1206,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "experiment_percentage": 100,
         "name": "chrome_disabled_tast_tests BRYA_RELEASE_LKGM",
@@ -1406,7 +1406,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1419,7 +1419,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_criticalstaging_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 3,
@@ -1433,7 +1433,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_disabled_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 2,
@@ -1461,7 +1461,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R119-15626.0.0",
+        "cros_img": "octopus-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests OCTOPUS_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1569,7 +1569,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "volteer",
-        "cros_img": "volteer-release/R119-15626.0.0",
+        "cros_img": "volteer-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests VOLTEER_RELEASE_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1582,7 +1582,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "volteer",
-        "cros_img": "volteer-release/R119-15626.0.0",
+        "cros_img": "volteer-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_criticalstaging_tast_tests VOLTEER_RELEASE_LKGM",
         "shards": 3,
@@ -1596,7 +1596,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "volteer",
-        "cros_img": "volteer-release/R119-15626.0.0",
+        "cros_img": "volteer-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_disabled_tast_tests VOLTEER_RELEASE_LKGM",
         "shards": 2,
@@ -1676,7 +1676,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "lacros_all_tast_tests BRYA_RELEASE_LKGM",
         "resultdb": {
@@ -1743,7 +1743,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "dedede",
-        "cros_img": "dedede-release/R119-15626.0.0",
+        "cros_img": "dedede-release/R119-15629.0.0",
         "name": "lacros_all_tast_tests DEDEDE_RELEASE_LKGM",
         "resultdb": {
           "enable": true,
@@ -1810,7 +1810,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "fizz",
-        "cros_img": "fizz-release/R119-15626.0.0",
+        "cros_img": "fizz-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "lacros_all_tast_tests FIZZ_RELEASE_LKGM",
         "resultdb": {
@@ -1879,7 +1879,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "guybrush",
-        "cros_img": "guybrush-release/R119-15626.0.0",
+        "cros_img": "guybrush-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "lacros_all_tast_tests GUYBRUSH_RELEASE_LKGM",
         "resultdb": {
@@ -1948,7 +1948,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "puff",
-        "cros_img": "puff-release/R119-15626.0.0",
+        "cros_img": "puff-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "lacros_all_tast_tests PUFF_RELEASE_LKGM",
         "resultdb": {
@@ -2047,7 +2047,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "name": "lacros_all_tast_tests JACUZZI_RELEASE_LKGM",
         "resultdb": {
           "enable": true,
@@ -2112,7 +2112,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "strongbad",
-        "cros_img": "strongbad-release/R119-15626.0.0",
+        "cros_img": "strongbad-release/R119-15629.0.0",
         "name": "lacros_all_tast_tests STRONGBAD_RELEASE_LKGM",
         "resultdb": {
           "enable": true,
diff --git a/testing/buildbot/chromium.chromiumos.json b/testing/buildbot/chromium.chromiumos.json
index ebc14ca..32ee7b4 100644
--- a/testing/buildbot/chromium.chromiumos.json
+++ b/testing/buildbot/chromium.chromiumos.json
@@ -1411,7 +1411,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "volteer",
-        "cros_img": "volteer-public/R119-15626.0.0",
+        "cros_img": "volteer-public/R119-15629.0.0",
         "cros_model": "voxel",
         "dut_pool": "chromium",
         "name": "lacros_all_tast_tests VOLTEER_PUBLIC_LKGM",
@@ -1444,7 +1444,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-public/R119-15626.0.0",
+        "cros_img": "jacuzzi-public/R119-15629.0.0",
         "name": "chromeos_integration_tests JACUZZI_PUBLIC_LKGM",
         "test": "chromeos_integration_tests",
         "test_id_prefix": "ninja://chrome/test:chromeos_integration_tests/",
@@ -1453,7 +1453,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-public/R119-15626.0.0",
+        "cros_img": "jacuzzi-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests JACUZZI_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -1482,7 +1482,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "trogdor",
-        "cros_img": "trogdor-public/R119-15626.0.0",
+        "cros_img": "trogdor-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests TROGDOR_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -5176,9 +5176,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5188,8 +5188,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -5330,9 +5330,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5342,8 +5342,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -5467,9 +5467,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -5479,8 +5479,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.coverage.json b/testing/buildbot/chromium.coverage.json
index 19d2a3b8..e6ba8d11 100644
--- a/testing/buildbot/chromium.coverage.json
+++ b/testing/buildbot/chromium.coverage.json
@@ -25384,9 +25384,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25396,8 +25396,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -25532,9 +25532,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25544,8 +25544,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -25664,9 +25664,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -25676,8 +25676,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index c88ef4f..cc5a39e9 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -41109,7 +41109,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "eve",
-        "cros_img": "eve-public/R119-15626.0.0",
+        "cros_img": "eve-public/R119-15629.0.0",
         "dut_pool": "chromium",
         "name": "lacros_all_tast_tests EVE_PUBLIC_LKGM",
         "public_builder": "cros_test_platform_public",
@@ -41129,7 +41129,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "octopus",
-        "cros_img": "octopus-public/R119-15626.0.0",
+        "cros_img": "octopus-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests OCTOPUS_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -41153,7 +41153,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-public/R119-15626.0.0",
+        "cros_img": "jacuzzi-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests JACUZZI_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -41170,7 +41170,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "trogdor",
-        "cros_img": "trogdor-public/R119-15626.0.0",
+        "cros_img": "trogdor-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests TROGDOR_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -41194,7 +41194,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-public/R119-15626.0.0",
+        "cros_img": "jacuzzi-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests JACUZZI_CQ_PUBLIC_LKGM",
         "public_builder": "cros_test_platform_public",
         "public_builder_bucket": "testplatform-public",
@@ -41213,7 +41213,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-public/R119-15626.0.0",
+        "cros_img": "jacuzzi-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests JACUZZI_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -41230,7 +41230,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "trogdor",
-        "cros_img": "trogdor-public/R119-15626.0.0",
+        "cros_img": "trogdor-public/R119-15629.0.0",
         "name": "lacros_all_tast_tests TROGDOR_PUBLIC_LKGM",
         "resultdb": {
           "enable": true,
@@ -43455,9 +43455,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -43466,8 +43466,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -43603,9 +43603,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -43614,8 +43614,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -43735,9 +43735,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -43746,8 +43746,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -45059,9 +45059,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -45070,8 +45070,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -45207,9 +45207,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -45218,8 +45218,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -45339,9 +45339,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -45350,8 +45350,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -46049,9 +46049,9 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome"
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
         },
@@ -46060,8 +46060,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/chromium.memory.json b/testing/buildbot/chromium.memory.json
index 8886f57..445402f 100644
--- a/testing/buildbot/chromium.memory.json
+++ b/testing/buildbot/chromium.memory.json
@@ -16538,12 +16538,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.interactive_ui_tests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -16553,8 +16553,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -16706,12 +16706,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -16721,8 +16721,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
@@ -16853,12 +16853,12 @@
       {
         "args": [
           "--test-launcher-filter-file=../../testing/buildbot/filters/linux-lacros.lacros_chrome_browsertests.skew.filter",
-          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome",
+          "--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome",
           "--test-launcher-print-test-stdio=always",
           "--combine-ash-logs-on-bots",
           "--asan-symbolize-output"
         ],
-        "description": "Run with ash-chrome version 119.0.6035.0",
+        "description": "Run with ash-chrome version 119.0.6037.0",
         "isolate_profile_data": true,
         "merge": {
           "script": "//testing/merge_scripts/standard_gtest_merge.py"
@@ -16868,8 +16868,8 @@
           "cipd_packages": [
             {
               "cipd_package": "chromium/testing/linux-ash-chromium/x86_64/ash.zip",
-              "location": "lacros_version_skew_tests_v119.0.6035.0",
-              "revision": "version:119.0.6035.0"
+              "location": "lacros_version_skew_tests_v119.0.6037.0",
+              "revision": "version:119.0.6037.0"
             }
           ],
           "dimensions": {
diff --git a/testing/buildbot/filters/win.win-rel-cft.cr23_pixel_browser_tests.filter b/testing/buildbot/filters/win.win-rel-cft.cr23_pixel_browser_tests.filter
index 1634751..7685f8f 100644
--- a/testing/buildbot/filters/win.win-rel-cft.cr23_pixel_browser_tests.filter
+++ b/testing/buildbot/filters/win.win-rel-cft.cr23_pixel_browser_tests.filter
@@ -3,3 +3,7 @@
 -All/PopupViewViewsBrowsertest*
 -InfoBarUiTest.*
 -InteractionTestUtilBrowserTest.CompareScreenshot_WebPage
+
+# https://ci.chromium.org/ui/p/chromium/builders/ci/win-rel-cft/3427/
+-PageInfoBubbleViewCookiesSubpageBrowserTest.*
+-PageInfoBubbleViewDialogBrowserTest.*
diff --git a/testing/buildbot/internal.chromeos.fyi.json b/testing/buildbot/internal.chromeos.fyi.json
index dffaa9e5..1a71a45 100644
--- a/testing/buildbot/internal.chromeos.fyi.json
+++ b/testing/buildbot/internal.chromeos.fyi.json
@@ -1118,7 +1118,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "name": "chrome_all_tast_tests BRYA_RELEASE_LKGM",
         "shards": 10,
@@ -1132,7 +1132,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "experiment_percentage": 100,
         "name": "chrome_criticalstaging_tast_tests BRYA_RELEASE_LKGM",
@@ -1147,7 +1147,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "brya",
-        "cros_img": "brya-release/R119-15626.0.0",
+        "cros_img": "brya-release/R119-15629.0.0",
         "dut_pool": "chrome",
         "experiment_percentage": 100,
         "name": "chrome_disabled_tast_tests BRYA_RELEASE_LKGM",
@@ -1171,7 +1171,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1184,7 +1184,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_criticalstaging_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 3,
@@ -1198,7 +1198,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "jacuzzi",
-        "cros_img": "jacuzzi-release/R119-15626.0.0",
+        "cros_img": "jacuzzi-release/R119-15629.0.0",
         "experiment_percentage": 100,
         "name": "chrome_disabled_tast_tests JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 2,
@@ -1221,7 +1221,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R119-15626.0.0",
+        "cros_img": "octopus-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests OCTOPUS_RELEASE_CHROME_FROM_TLS_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1245,7 +1245,7 @@
       {
         "autotest_name": "tast.chrome-from-gcs",
         "cros_board": "trogdor",
-        "cros_img": "trogdor-release/R119-15622.0.0",
+        "cros_img": "trogdor-release/R119-15629.0.0",
         "name": "chrome_all_tast_tests TROGDOR_RELEASE_LKGM",
         "shards": 10,
         "tast_expr": "STUB_STRING_TO_RUN_TAST_TESTS",
@@ -1284,7 +1284,7 @@
       },
       {
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R119-15626.0.0",
+        "cros_img": "octopus-release/R119-15629.0.0",
         "name": "lacros_fyi_tast_tests OCTOPUS_RELEASE_LKGM",
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
         "test": "lacros_fyi_tast_tests",
@@ -1322,7 +1322,7 @@
       },
       {
         "cros_board": "octopus",
-        "cros_img": "octopus-release/R119-15626.0.0",
+        "cros_img": "octopus-release/R119-15629.0.0",
         "name": "ozone_unittests OCTOPUS_RELEASE_LKGM",
         "test": "ozone_unittests",
         "test_id_prefix": "ninja://ui/ozone:ozone_unittests/",
@@ -1367,7 +1367,7 @@
       },
       {
         "cros_board": "hana",
-        "cros_img": "hana-release/R119-15626.0.0",
+        "cros_img": "hana-release/R119-15629.0.0",
         "name": "lacros_all_tast_tests HANA_RELEASE_LKGM",
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
         "test": "lacros_all_tast_tests",
@@ -1398,7 +1398,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "strongbad",
-        "cros_img": "strongbad-release/R119-15626.0.0",
+        "cros_img": "strongbad-release/R119-15629.0.0",
         "name": "lacros_all_tast_tests STRONGBAD_RELEASE_LKGM",
         "tast_expr": "(\"group:mainline\" && \"dep:lacros\" && !informational)",
         "test": "lacros_all_tast_tests",
@@ -1446,7 +1446,7 @@
       },
       {
         "cros_board": "hana",
-        "cros_img": "hana-release/R119-15626.0.0",
+        "cros_img": "hana-release/R119-15629.0.0",
         "name": "ozone_unittests HANA_RELEASE_LKGM",
         "test": "ozone_unittests",
         "test_id_prefix": "ninja://ui/ozone:ozone_unittests/",
@@ -1474,7 +1474,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "strongbad",
-        "cros_img": "strongbad-release/R119-15626.0.0",
+        "cros_img": "strongbad-release/R119-15629.0.0",
         "name": "ozone_unittests STRONGBAD_RELEASE_LKGM",
         "test": "ozone_unittests",
         "test_id_prefix": "ninja://ui/ozone:ozone_unittests/",
@@ -1519,7 +1519,7 @@
       },
       {
         "cros_board": "hana",
-        "cros_img": "hana-release/R119-15626.0.0",
+        "cros_img": "hana-release/R119-15629.0.0",
         "name": "viz_unittests HANA_RELEASE_LKGM",
         "test": "viz_unittests",
         "test_id_prefix": "ninja://components/viz:viz_unittests/",
@@ -1547,7 +1547,7 @@
       {
         "autotest_name": "tast.lacros-from-gcs",
         "cros_board": "strongbad",
-        "cros_img": "strongbad-release/R119-15626.0.0",
+        "cros_img": "strongbad-release/R119-15629.0.0",
         "name": "viz_unittests STRONGBAD_RELEASE_LKGM",
         "test": "viz_unittests",
         "test_id_prefix": "ninja://components/viz:viz_unittests/",
diff --git a/testing/buildbot/tryserver.chromium.chromiumos.json b/testing/buildbot/tryserver.chromium.chromiumos.json
index c19ab4a2..f1e1e237 100644
--- a/testing/buildbot/tryserver.chromium.chromiumos.json
+++ b/testing/buildbot/tryserver.chromium.chromiumos.json
@@ -9,7 +9,7 @@
       {
         "bucket": "chromiumos-image-archive",
         "cros_board": "volteer",
-        "cros_img": "volteer-public/R119-15626.0.0",
+        "cros_img": "volteer-public/R119-15629.0.0",
         "cros_model": "voxel",
         "dut_pool": "chromium",
         "name": "lacros_all_tast_tests VOLTEER_PUBLIC_LKGM",
diff --git a/testing/buildbot/variants.pyl b/testing/buildbot/variants.pyl
index dd3a26ba..be54089 100644
--- a/testing/buildbot/variants.pyl
+++ b/testing/buildbot/variants.pyl
@@ -70,16 +70,16 @@
   },
   'LACROS_VERSION_SKEW_CANARY': {
     'identifier': 'Lacros version skew testing ash canary',
-    'description': 'Run with ash-chrome version 119.0.6035.0',
+    'description': 'Run with ash-chrome version 119.0.6037.0',
     'args': [
-      '--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6035.0/test_ash_chrome',
+      '--ash-chrome-path-override=../../lacros_version_skew_tests_v119.0.6037.0/test_ash_chrome',
     ],
     'swarming': {
       'cipd_packages': [
         {
           'cipd_package': 'chromium/testing/linux-ash-chromium/x86_64/ash.zip',
-          'location': 'lacros_version_skew_tests_v119.0.6035.0',
-          'revision': 'version:119.0.6035.0',
+          'location': 'lacros_version_skew_tests_v119.0.6037.0',
+          'revision': 'version:119.0.6037.0',
         },
       ],
     },
@@ -432,7 +432,7 @@
     'identifier': 'BRYA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'brya',
-      'cros_img': 'brya-release/R119-15626.0.0',
+      'cros_img': 'brya-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -441,7 +441,7 @@
     'identifier': 'BRYA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'brya',
-      'cros_img': 'brya-release/R119-15626.0.0',
+      'cros_img': 'brya-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -474,7 +474,7 @@
     'identifier': 'DEDEDE_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'dedede',
-      'cros_img': 'dedede-release/R119-15626.0.0',
+      'cros_img': 'dedede-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -503,7 +503,7 @@
     'identifier': 'FIZZ_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'fizz',
-      'cros_img': 'fizz-release/R119-15626.0.0',
+      'cros_img': 'fizz-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -536,7 +536,7 @@
     'identifier': 'GUYBRUSH_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'guybrush',
-      'cros_img': 'guybrush-release/R119-15626.0.0',
+      'cros_img': 'guybrush-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -569,7 +569,7 @@
     'identifier': 'PUFF_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'puff',
-      'cros_img': 'puff-release/R119-15626.0.0',
+      'cros_img': 'puff-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
       'dut_pool': 'chrome',
     },
@@ -602,7 +602,7 @@
     'identifier': 'EVE_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'eve',
-      'cros_img': 'eve-public/R119-15626.0.0',
+      'cros_img': 'eve-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'dut_pool': 'chromium',
       'public_builder': 'cros_test_platform_public',
@@ -613,7 +613,7 @@
     'identifier': 'HANA_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'hana',
-      'cros_img': 'hana-release/R119-15626.0.0',
+      'cros_img': 'hana-release/R119-15629.0.0',
     },
   },
   'CROS_HANA_RELEASE_DEV': {
@@ -641,7 +641,7 @@
     'identifier': 'JACUZZI_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-release/R119-15626.0.0',
+      'cros_img': 'jacuzzi-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -663,7 +663,7 @@
     'identifier': 'JACUZZI_RELEASE_CHROME_FROM_TLS_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-release/R119-15626.0.0',
+      'cros_img': 'jacuzzi-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -678,7 +678,7 @@
     'identifier': 'JACUZZI_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-public/R119-15626.0.0',
+      'cros_img': 'jacuzzi-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -686,7 +686,7 @@
     'identifier': 'JACUZZI_CQ_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'jacuzzi',
-      'cros_img': 'jacuzzi-public/R119-15626.0.0',
+      'cros_img': 'jacuzzi-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'public_builder': 'cros_test_platform_public',
       'public_builder_bucket': 'testplatform-public',
@@ -696,7 +696,7 @@
     'identifier': 'TROGDOR_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'trogdor',
-      'cros_img': 'trogdor-public/R119-15626.0.0',
+      'cros_img': 'trogdor-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -704,7 +704,7 @@
     'identifier': 'OCTOPUS_PUBLIC_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-public/R119-15626.0.0',
+      'cros_img': 'octopus-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
     },
   },
@@ -712,7 +712,7 @@
     'identifier': 'OCTOPUS_RELEASE_CHROME_FROM_TLS_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-release/R119-15626.0.0',
+      'cros_img': 'octopus-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -720,7 +720,7 @@
     'identifier': 'OCTOPUS_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'octopus',
-      'cros_img': 'octopus-release/R119-15626.0.0',
+      'cros_img': 'octopus-release/R119-15629.0.0',
     },
   },
   'CROS_OCTOPUS_RELEASE_DEV': {
@@ -748,7 +748,7 @@
     'identifier': 'STRONGBAD_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'strongbad',
-      'cros_img': 'strongbad-release/R119-15626.0.0',
+      'cros_img': 'strongbad-release/R119-15629.0.0',
       'autotest_name': 'tast.lacros-from-gcs',
     },
   },
@@ -777,7 +777,7 @@
     'identifier': 'TROGDOR_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'trogdor',
-      'cros_img': 'trogdor-release/R119-15622.0.0',
+      'cros_img': 'trogdor-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
@@ -786,7 +786,7 @@
     'skylab': {
       'cros_board': 'volteer',
       'cros_model': 'voxel',
-      'cros_img': 'volteer-public/R119-15626.0.0',
+      'cros_img': 'volteer-public/R119-15629.0.0',
       'bucket': 'chromiumos-image-archive',
       'dut_pool': 'chromium',
       'public_builder': 'cros_test_platform_public',
@@ -797,7 +797,7 @@
     'identifier': 'VOLTEER_RELEASE_LKGM',
     'skylab': {
       'cros_board': 'volteer',
-      'cros_img': 'volteer-release/R119-15626.0.0',
+      'cros_img': 'volteer-release/R119-15629.0.0',
       'autotest_name': 'tast.chrome-from-gcs',
     },
   },
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 05be5f1..90d80ee7 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -2995,6 +2995,25 @@
             ]
         }
     ],
+    "CastStreamingExponentialVideoBitrateAlgorithm": [
+        {
+            "platforms": [
+                "chromeos",
+                "chromeos_lacros",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "CastStreamingExponentialVideoBitrateAlgorithm"
+                    ]
+                }
+            ]
+        }
+    ],
     "CctClientDataHeader": [
         {
             "platforms": [
@@ -3452,6 +3471,21 @@
             ]
         }
     ],
+    "ChromeOSPrintingIppUsb": [
+        {
+            "platforms": [
+                "chromeos"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "IppFirstSetupForUsbPrinters"
+                    ]
+                }
+            ]
+        }
+    ],
     "ChromeOSRawPSIMetrics": [
         {
             "platforms": [
@@ -5943,6 +5977,25 @@
             ]
         }
     ],
+    "EnableDiscountOnNavigationDesktop": [
+        {
+            "platforms": [
+                "chromeos",
+                "chromeos_lacros",
+                "linux",
+                "mac",
+                "windows"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "ShowDiscountOnNavigation"
+                    ]
+                }
+            ]
+        }
+    ],
     "EnableEncryptedReportingClientForUpload": [
         {
             "platforms": [
@@ -6424,7 +6477,7 @@
                     "params": {
                         "pwa-companion-app-id": "kncedjianpafagdchkiinagaaokkhpaa",
                         "pwa-companion-install-uri": "https://mypixelbuds-preprod.corp.google.com/",
-                        "pwa-companion-play-store-uri": ""
+                        "pwa-companion-play-store-uri": "https://play.google.com/store/apps/details?id=com.google.android.apps.wearables.maestro.companion"
                     },
                     "enable_features": [
                         "FastPairPwaCompanion"
@@ -8321,6 +8374,21 @@
             ]
         }
     ],
+    "IOSFeedSyntheticCapabilities": [
+        {
+            "platforms": [
+                "ios"
+            ],
+            "experiments": [
+                {
+                    "name": "Enabled",
+                    "enable_features": [
+                        "EnableFeedSyntheticCapabilities"
+                    ]
+                }
+            ]
+        }
+    ],
     "IOSGhostCards": [
         {
             "platforms": [
@@ -15217,28 +15285,6 @@
             ]
         }
     ],
-    "SendTabToSelfSigninPromo": [
-        {
-            "platforms": [
-                "android",
-                "chromeos",
-                "chromeos_lacros",
-                "fuchsia",
-                "ios",
-                "linux",
-                "mac",
-                "windows"
-            ],
-            "experiments": [
-                {
-                    "name": "Enabled_3",
-                    "enable_features": [
-                        "SendTabToSelfSigninPromo"
-                    ]
-                }
-            ]
-        }
-    ],
     "SendTabToSelfV2": [
         {
             "platforms": [
diff --git a/third_party/angle b/third_party/angle
index 66d3db3b..1cab871 160000
--- a/third_party/angle
+++ b/third_party/angle
@@ -1 +1 @@
-Subproject commit 66d3db3b9fdd1181c6879ca3dfef697ec53e6b43
+Subproject commit 1cab871c220744402b887e73ff86aaf8f7f97fa0
diff --git a/third_party/blink/DEPS b/third_party/blink/DEPS
index 593f221..c6cb058 100644
--- a/third_party/blink/DEPS
+++ b/third_party/blink/DEPS
@@ -6,6 +6,7 @@
     "+base/functional/callback_helpers.h",
     "+base/functional/function_ref.h",
     "+base/memory/raw_ptr.h",
+    "+base/memory/raw_ref.h",
     "+base/memory/raw_ptr_exclusion.h",
     "+base/notreached.h",
     "+base/observer_list.h",
diff --git a/third_party/blink/perf_tests/css/NestingIdentKnownProperty.html b/third_party/blink/perf_tests/css/NestingIdentKnownProperty.html
new file mode 100644
index 0000000..dff3bf0
--- /dev/null
+++ b/third_party/blink/perf_tests/css/NestingIdentKnownProperty.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<script src="../resources/runner.js"></script>
+<script src="./resources/utils.js"></script>
+<script>
+const RULES = 200;
+const DECLARATIONS_PER_RULE = 10;
+
+// This test is the same as NestingIdentNonProperty.html, except that the
+// first ident is a known CSS property, instead of 'not-a-property'.
+function makeStyle() {
+  let rules = [];
+
+  for (let i = 0; i < RULES; i++) {
+    rules.push(`
+      width:is(.a${i}) {
+        ${[...Array(DECLARATIONS_PER_RULE).keys()]
+          .map(x => `--x${x}:a b c d e f g;`).join('\n')}
+      }
+    `);
+  }
+
+  return `
+    div {
+      ${rules.join('\n')}
+    }
+  `
+}
+
+let globalCounter = 0;
+const stylesheetText = makeStyle();
+let stylesheet = new CSSStyleSheet();
+
+PerfTestRunner.measureTime({
+    description: 'Many nested rules that look like width declarations',
+    run: () => {
+      // This is a parsing test: we don't care about style recalc.
+      // We append a rule based on globalCounter to prevent caching
+      // on the stylesheet string.
+      stylesheet.replaceSync(stylesheetText + `\n .b${globalCounter++} {}`);
+    }
+});
+
+</script>
+
diff --git a/third_party/blink/perf_tests/css/NestingIdentLeadingBraces.html b/third_party/blink/perf_tests/css/NestingIdentLeadingBraces.html
new file mode 100644
index 0000000..1a23e91
--- /dev/null
+++ b/third_party/blink/perf_tests/css/NestingIdentLeadingBraces.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<script src="../resources/runner.js"></script>
+<script src="./resources/utils.js"></script>
+<script>
+const DECLARATIONS = 400;
+const TRAILING_TOKENS_PER_DECLARATION = 1000;
+
+// Makes many (invalid) width-declarations where {} appears at the start of
+// the the value, with lots of trailing tokens after.
+function makeStyle() {
+  let rules = [];
+
+  for (let i = 0; i < DECLARATIONS; i++) {
+    rules.push(`width: {}
+      ${[...Array(TRAILING_TOKENS_PER_DECLARATION).keys()]
+          .map(x => `foo${x}`).join(' ')}
+    ;`);
+  }
+
+  return `
+    div {
+      ${rules.join('\n')}
+    }
+  `
+}
+
+let globalCounter = 0;
+const stylesheetText = makeStyle();
+let stylesheet = new CSSStyleSheet();
+
+PerfTestRunner.measureTime({
+    description: 'Many invalid width declarations with braces at the start',
+    run: () => {
+      // This is a parsing test: we don't care about style recalc.
+      // We append a rule based on globalCounter to prevent caching
+      // on the stylesheet string.
+      stylesheet.replaceSync(stylesheetText + `\n .b${globalCounter++} {}`);
+    }
+});
+
+</script>
+
diff --git a/third_party/blink/public/devtools_protocol/browser_protocol.pdl b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
index 6f1bf3f..407a53f 100644
--- a/third_party/blink/public/devtools_protocol/browser_protocol.pdl
+++ b/third_party/blink/public/devtools_protocol/browser_protocol.pdl
@@ -9363,7 +9363,7 @@
   # Ad advertising element inside an interest group.
   type InterestGroupAd extends object
     properties
-      string renderUrl
+      string renderURL
       optional string metadata
 
   # The full details of an interest group.
@@ -9373,10 +9373,10 @@
       string name
       Network.TimeSinceEpoch expirationTime
       string joiningOrigin
-      optional string biddingUrl
-      optional string biddingWasmHelperUrl
-      optional string updateUrl
-      optional string trustedBiddingSignalsUrl
+      optional string biddingLogicURL
+      optional string biddingWasmHelperURL
+      optional string updateURL
+      optional string trustedBiddingSignalsURL
       array of string trustedBiddingSignalsKeys
       optional string userBiddingSignals
       array of InterestGroupAd ads
@@ -11322,7 +11322,6 @@
       LowEndDevice
       InvalidSchemeRedirect
       InvalidSchemeNavigation
-      InProgressNavigation
       NavigationRequestBlockedByCsp
       MainFrameNavigation
       MojoBinderPolicy
diff --git a/third_party/blink/public/platform/modules/video_capture/web_video_capture_impl_manager.h b/third_party/blink/public/platform/modules/video_capture/web_video_capture_impl_manager.h
index 1a877d4..c6d56ec7 100644
--- a/third_party/blink/public/platform/modules/video_capture/web_video_capture_impl_manager.h
+++ b/third_party/blink/public/platform/modules/video_capture/web_video_capture_impl_manager.h
@@ -117,8 +117,6 @@
   void SuspendDevices(const MediaStreamDevices& video_devices, bool suspend);
 
   void OnLog(const media::VideoCaptureSessionId& id, const WebString& message);
-  void OnFrameDropped(const media::VideoCaptureSessionId& id,
-                      media::VideoCaptureFrameDropReason reason);
 
   // Get the feedback callback for the corresponding capture session.
   // Consumers may call the returned callback in any thread to provide
diff --git a/third_party/blink/renderer/bindings/core/v8/active_script_wrappable_creation_key.h b/third_party/blink/renderer/bindings/core/v8/active_script_wrappable_creation_key.h
index ce88b5f..3a0e8c5 100644
--- a/third_party/blink/renderer/bindings/core/v8/active_script_wrappable_creation_key.h
+++ b/third_party/blink/renderer/bindings/core/v8/active_script_wrappable_creation_key.h
@@ -35,6 +35,7 @@
   friend class DedicatedWorker;
   friend class DocumentTransition;
   friend class DOMFileSystem;
+  friend class DOMViewTransition;
   friend class DOMWebSocket;
   friend class EditContext;
   template <typename Traits>
@@ -107,7 +108,6 @@
   friend class SVGImageElement;
   friend class TCPSocket;
   friend class UDPSocket;
-  friend class ViewTransition;
   friend class WakeLockSentinel;
   friend class WebSocketStream;
   friend class WebTransport;
diff --git a/third_party/blink/renderer/bindings/core/v8/v8_compile_hints_producer.cc b/third_party/blink/renderer/bindings/core/v8/v8_compile_hints_producer.cc
index 4e67690..e588fa7 100644
--- a/third_party/blink/renderer/bindings/core/v8/v8_compile_hints_producer.cc
+++ b/third_party/blink/renderer/bindings/core/v8/v8_compile_hints_producer.cc
@@ -26,37 +26,44 @@
     V8CrowdsourcedCompileHintsProducer::data_generated_for_this_process_ =
         false;
 
-V8CrowdsourcedCompileHintsProducer::V8CrowdsourcedCompileHintsProducer(
-    Page* page)
-    : page_(page) {
-  static bool compile_hints_forced =
+namespace {
+bool ShouldThisProcessGenerateData() {
+  bool compile_hints_forced =
       base::FeatureList::IsEnabled(features::kForceProduceCompileHints);
   if (compile_hints_forced) {
-    state_ = State::kCollectingData;
-    return;
+    return true;
   }
 
   // Data collection is only enabled on Windows. TODO(chromium:1406506): enable
   // on more platforms.
 #if BUILDFLAG(IS_WIN)
-  // Call FeatureList::IsEnabled only once.
-  static bool compile_hints_enabled =
+  bool compile_hints_enabled =
       base::FeatureList::IsEnabled(features::kProduceCompileHints2);
   if (!compile_hints_enabled) {
-    return;
+    return false;
   }
 
   // Decide whether we collect the data based on client-side randomization.
   // This is further subject to UKM restrictions: whether the user has enabled
   // the data collection + downsampling. See crbug.com/1483975 .
-  static double data_production_level =
+  double data_production_level =
       features::kProduceCompileHintsDataProductionLevel.Get();
-  if (base::RandDouble() > data_production_level) {
-    return;
-  }
-  state_ = State::kCollectingData;
+  return base::RandDouble() < data_production_level;
+#else
+  return false;
 #endif
 }
+}  // namespace
+
+V8CrowdsourcedCompileHintsProducer::V8CrowdsourcedCompileHintsProducer(
+    Page* page)
+    : page_(page) {
+  // Decide whether to produce the data once per renderer process.
+  static bool should_generate_data = ShouldThisProcessGenerateData();
+  if (should_generate_data && !data_generated_for_this_process_) {
+    state_ = State::kCollectingData;
+  }
+}
 
 void V8CrowdsourcedCompileHintsProducer::RecordScript(
     Frame* frame,
diff --git a/third_party/blink/renderer/bindings/generated_in_core.gni b/third_party/blink/renderer/bindings/generated_in_core.gni
index 13bee5b..f8f1aa0 100644
--- a/third_party/blink/renderer/bindings/generated_in_core.gni
+++ b/third_party/blink/renderer/bindings/generated_in_core.gni
@@ -1275,8 +1275,8 @@
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_readable_stream_default_controller.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_readable_stream_default_reader.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_readable_stream_default_reader.h",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_ready_to_render_event.cc",
-  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_ready_to_render_event.h",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_page_reveal_event.cc",
+  "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_page_reveal_event.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_report.cc",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_report.h",
   "$root_gen_dir/third_party/blink/renderer/bindings/core/v8/v8_report_body.cc",
diff --git a/third_party/blink/renderer/bindings/idl_in_core.gni b/third_party/blink/renderer/bindings/idl_in_core.gni
index 45d0c277..4fe088b 100644
--- a/third_party/blink/renderer/bindings/idl_in_core.gni
+++ b/third_party/blink/renderer/bindings/idl_in_core.gni
@@ -740,7 +740,7 @@
   "//third_party/blink/renderer/core/url_pattern/url_pattern_init.idl",
   "//third_party/blink/renderer/core/url_pattern/url_pattern_options.idl",
   "//third_party/blink/renderer/core/url_pattern/url_pattern_result.idl",
-  "//third_party/blink/renderer/core/view_transition/ready_to_render_event.idl",
+  "//third_party/blink/renderer/core/view_transition/page_reveal_event.idl",
   "//third_party/blink/renderer/core/view_transition/view_transition.idl",
   "//third_party/blink/renderer/core/view_transition/view_transition_callback.idl",
   "//third_party/blink/renderer/core/view_transition/view_transition_supplement.idl",
diff --git a/third_party/blink/renderer/core/animation/animation_test.cc b/third_party/blink/renderer/core/animation/animation_test.cc
index 21f6a14..0a0d986 100644
--- a/third_party/blink/renderer/core/animation/animation_test.cc
+++ b/third_party/blink/renderer/core/animation/animation_test.cc
@@ -1918,8 +1918,7 @@
     </div>
   )HTML");
 
-  auto* scroller =
-      To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller"));
+  auto* scroller = GetLayoutBoxByElementId("scroller");
   if (!RuntimeEnabledFeatures::CompositeScrollAfterPaintEnabled()) {
     ASSERT_TRUE(scroller->UsesCompositedScrolling());
   }
@@ -2281,8 +2280,7 @@
   )HTML");
 
   // Create ScrollTimeline
-  auto* scroller =
-      To<LayoutBoxModelObject>(GetLayoutObjectByElementId("scroller"));
+  auto* scroller = GetLayoutBoxByElementId("scroller");
   PaintLayerScrollableArea* scrollable_area = scroller->GetScrollableArea();
   ASSERT_FALSE(scroller->UsesCompositedScrolling());
   scrollable_area->SetScrollOffset(ScrollOffset(0, 20),
diff --git a/third_party/blink/renderer/core/animation/compositor_animations.cc b/third_party/blink/renderer/core/animation/compositor_animations.cc
index 94b20c1..794a3181 100644
--- a/third_party/blink/renderer/core/animation/compositor_animations.cc
+++ b/third_party/blink/renderer/core/animation/compositor_animations.cc
@@ -1134,30 +1134,18 @@
     return false;
   DCHECK(target->GetDocument().Lifecycle().GetState() >=
          DocumentLifecycle::kPrePaintClean);
-  auto* layout_box_model_object = target->GetLayoutBoxModelObject();
-  if (!layout_box_model_object)
+  if (RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() &&
+      target->GetDocument().Lifecycle().GetState() <
+          DocumentLifecycle::kPaintClean) {
+    // TODO(crbug.com/1434728): This happens when we paint a scroll-driven
+    // animating background.
     return false;
-
-  if (RuntimeEnabledFeatures::CompositeScrollAfterPaintEnabled()) {
-    if (RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() &&
-        target->GetDocument().Lifecycle().GetState() <
-            DocumentLifecycle::kPaintClean) {
-      // TODO(crbug.com/1434728): This happens when we paint a scroll-driven
-      // animating background.
-      return false;
-    }
-    const auto* properties =
-        layout_box_model_object->FirstFragment().PaintProperties();
-    if (!properties || !properties->Scroll()) {
-      return false;
-    }
-    const auto* paint_artifact_compositor =
-        layout_box_model_object->GetFrameView()->GetPaintArtifactCompositor();
-    return paint_artifact_compositor &&
-           paint_artifact_compositor->UsesCompositedScrolling(
-               *properties->Scroll());
   }
-  return layout_box_model_object->UsesCompositedScrolling();
+  auto* layout_box = target->GetLayoutBox();
+  if (!layout_box) {
+    return false;
+  }
+  return layout_box->UsesCompositedScrolling();
 }
 
 CompositorAnimations::FailureReasons
diff --git a/third_party/blink/renderer/core/css/container_query_evaluator_test.cc b/third_party/blink/renderer/core/css/container_query_evaluator_test.cc
index 26002c1..f421f77b 100644
--- a/third_party/blink/renderer/core/css/container_query_evaluator_test.cc
+++ b/third_party/blink/renderer/core/css/container_query_evaluator_test.cc
@@ -82,7 +82,8 @@
             String custom_property_value) {
     CSSTokenizer tokenizer(custom_property_value);
     CSSParserTokenStream stream(tokenizer);
-    CSSTokenizedValue tokenized_value = CSSParserImpl::ConsumeValue(stream);
+    CSSTokenizedValue tokenized_value =
+        CSSParserImpl::ConsumeUnrestrictedPropertyValue(stream);
     const CSSParserContext* context =
         StrictCSSParserContext(SecureContextMode::kSecureContext);
     CSSCustomPropertyDeclaration* value =
diff --git a/third_party/blink/renderer/core/css/font_size_functions.cc b/third_party/blink/renderer/core/css/font_size_functions.cc
index b1383f7..c6291bf 100644
--- a/third_party/blink/renderer/core/css/font_size_functions.cc
+++ b/third_party/blink/renderer/core/css/font_size_functions.cc
@@ -247,11 +247,12 @@
     const FontDescription& font_description) {
   DCHECK(font_data);
   const float computed_size = font_description.ComputedSize();
-  if (!computed_size) {
+  const FontSizeAdjust size_adjust = font_description.SizeAdjust();
+  if (!computed_size ||
+      size_adjust.Value() == FontSizeAdjust::kFontSizeAdjustNone) {
     return absl::nullopt;
   }
 
-  const FontSizeAdjust size_adjust = font_description.SizeAdjust();
   float aspect_value = AspectValue(font_data->GetFontMetrics(),
                                    size_adjust.GetMetric(), computed_size);
   if (!aspect_value) {
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_impl.cc b/third_party/blink/renderer/core/css/parser/css_parser_impl.cc
index ba7344f..70ea0477 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_impl.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_impl.cc
@@ -169,7 +169,7 @@
   StyleRule::RuleType rule_type = RuleTypeForMutableDeclaration(declaration);
   CSSTokenizer tokenizer(string);
   CSSParserTokenStream stream(tokenizer);
-  CSSTokenizedValue tokenized_value = ConsumeValue(stream);
+  CSSTokenizedValue tokenized_value = ConsumeRestrictedPropertyValue(stream);
   parser.ConsumeDeclarationValue(tokenized_value, unresolved_property,
                                  important, rule_type);
   if (parser.parsed_properties_.empty()) {
@@ -188,7 +188,7 @@
   STACK_UNINITIALIZED CSSParserImpl parser(context);
   CSSTokenizer tokenizer(value);
   CSSParserTokenStream stream(tokenizer);
-  CSSTokenizedValue tokenized_value = ConsumeValue(stream);
+  CSSTokenizedValue tokenized_value = ConsumeUnrestrictedPropertyValue(stream);
   parser.ConsumeVariableValue(tokenized_value, property_name, important,
                               is_animation_tainted);
   if (parser.parsed_properties_.empty()) {
@@ -2353,11 +2353,13 @@
   static_assert(static_cast<uint64_t>(AtRuleDescriptorID::Invalid) == 0u);
   static_assert(static_cast<uint64_t>(CSSPropertyID::kInvalid) == 0u);
 
-  if (id) {
-    CSSTokenizedValue tokenized_value = ConsumeValue(stream);
-    important = RemoveImportantAnnotationIfPresent(tokenized_value);
+  stream.ConsumeWhitespace();
 
+  if (id) {
     if (parsing_descriptor) {
+      CSSTokenizedValue tokenized_value =
+          ConsumeUnrestrictedPropertyValue(stream);
+      important = RemoveImportantAnnotationIfPresent(tokenized_value);
       if (important) {
         return false;  // Invalid for descriptors.
       }
@@ -2365,23 +2367,34 @@
       AtRuleDescriptorParser::ParseAtRule(rule_type, atrule_id, tokenized_value,
                                           *context_, parsed_properties_);
     } else {
-      if (important &&
-          (rule_type == StyleRule::kKeyframe || rule_type == StyleRule::kTry)) {
-        return false;
-      }
       const CSSPropertyID unresolved_property = static_cast<CSSPropertyID>(id);
       if (unresolved_property == CSSPropertyID::kVariable) {
         if (rule_type != StyleRule::kStyle &&
             rule_type != StyleRule::kKeyframe) {
           return false;
         }
+        CSSTokenizedValue tokenized_value =
+            ConsumeUnrestrictedPropertyValue(stream);
+        important = RemoveImportantAnnotationIfPresent(tokenized_value);
+        if (important && (rule_type == StyleRule::kKeyframe)) {
+          return false;
+        }
         AtomicString variable_name = lhs.Value().ToAtomicString();
         bool is_animation_tainted = rule_type == StyleRule::kKeyframe;
         ConsumeVariableValue(tokenized_value, variable_name, important,
                              is_animation_tainted);
       } else if (unresolved_property != CSSPropertyID::kInvalid) {
-        ConsumeDeclarationValue(tokenized_value, unresolved_property, important,
-                                rule_type);
+        CSSTokenizedValue tokenized_value =
+            ConsumeRestrictedPropertyValue(stream);
+        important = RemoveImportantAnnotationIfPresent(tokenized_value);
+        if (important && (rule_type == StyleRule::kKeyframe ||
+                          rule_type == StyleRule::kTry)) {
+          return false;
+        }
+        if (stream.AtEnd()) {
+          ConsumeDeclarationValue(tokenized_value, unresolved_property,
+                                  important, rule_type);
+        }
       }
     }
   }
@@ -2389,11 +2402,12 @@
   if (observer_ &&
       (rule_type == StyleRule::kStyle || rule_type == StyleRule::kKeyframe ||
        rule_type == StyleRule::kProperty || rule_type == StyleRule::kTry)) {
-    if (!stream.AtEnd()) {
+    if (!id) {
       // If we skipped the main call to ConsumeValue due to an invalid
       // property/descriptor, the inspector still needs to know the offset
       // where the would-be declaration ends.
-      CSSTokenizedValue tokenized_value = ConsumeValue(stream);
+      CSSTokenizedValue tokenized_value =
+          ConsumeRestrictedPropertyValue(stream);
       important = RemoveImportantAnnotationIfPresent(tokenized_value);
     }
     // The end offset is the offset of the terminating token, which is peeked
@@ -2430,19 +2444,51 @@
                                 context_, parsed_properties_, rule_type);
 }
 
-CSSTokenizedValue CSSParserImpl::ConsumeValue(CSSParserTokenStream& stream) {
+template <typename ConsumeFunction>
+CSSTokenizedValue CSSParserImpl::ConsumeValue(
+    CSSParserTokenStream& stream,
+    ConsumeFunction consume_function) {
   // Consume leading whitespace and comments. This is needed
   // by ConsumeDeclarationValue() / CSSPropertyParser::ParseValue(),
   // and also CSSVariableParser::ParseDeclarationIncludingCSSWide().
   stream.ConsumeWhitespace();
   wtf_size_t value_start_offset = stream.LookAheadOffset();
-  CSSParserTokenRange range = stream.ConsumeUntilPeekedTypeIs<>();
+  CSSParserTokenRange range = consume_function(stream);
   wtf_size_t value_end_offset = stream.LookAheadOffset();
 
   return {range, stream.StringRangeAt(value_start_offset,
                                       value_end_offset - value_start_offset)};
 }
 
+CSSTokenizedValue CSSParserImpl::ConsumeRestrictedPropertyValue(
+    CSSParserTokenStream& stream) {
+  if (!RuntimeEnabledFeatures::CSSNestingIdentEnabled()) {
+    return ConsumeUnrestrictedPropertyValue(stream);
+  }
+
+  if (stream.Peek().GetType() == kLeftBraceToken) {
+    // '{}' must be the whole value, hence we simply consume a component
+    // value from the stream, and consider this the whole value.
+    return ConsumeValue(stream, [](CSSParserTokenStream& stream) {
+      return stream.ConsumeComponentValueIncludingWhitespace();
+    });
+  }
+  // Otherwise, we consume until we're AtEnd() (which in the normal case
+  // means we hit a kSemicolonToken), or until we see kLeftBraceToken.
+  // The latter is a kind of error state, which is dealt with via additional
+  // AtEnd() checks at the call site.
+  return ConsumeValue(stream, [](CSSParserTokenStream& stream) {
+    return stream.ConsumeUntilPeekedTypeIs<kLeftBraceToken>();
+  });
+}
+
+CSSTokenizedValue CSSParserImpl::ConsumeUnrestrictedPropertyValue(
+    CSSParserTokenStream& stream) {
+  return ConsumeValue(stream, [](CSSParserTokenStream& stream) {
+    return stream.ConsumeUntilPeekedTypeIs<>();
+  });
+}
+
 bool CSSParserImpl::RemoveImportantAnnotationIfPresent(
     CSSTokenizedValue& tokenized_value) {
   if (tokenized_value.range.size() == 0) {
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_impl.h b/third_party/blink/renderer/core/css/parser/css_parser_impl.h
index 292dafa..13368f3 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_impl.h
+++ b/third_party/blink/renderer/core/css/parser/css_parser_impl.h
@@ -161,11 +161,19 @@
       wtf_size_t offset,
       const CSSParserContext*);
 
-  // Consumes a value from the remaining tokens in the (possibly bounded)
-  // stream.
+  // A value for a standard property has the following restriction:
+  // it can not contain braces unless it's the whole value [1].
+  // This function makes use of that restriction to early-out of the
+  // streaming tokenizer as soon as possible.
   //
-  // See also CSSParserTokenStream::Boundary.
-  static CSSTokenizedValue ConsumeValue(CSSParserTokenStream&);
+  // [1] https://github.com/w3c/csswg-drafts/issues/9317
+  static CSSTokenizedValue ConsumeRestrictedPropertyValue(
+      CSSParserTokenStream&);
+
+  // Custom properties (as well as descriptors) do not have the restriction
+  // explained above. This function will simply consume until AtEnd.
+  static CSSTokenizedValue ConsumeUnrestrictedPropertyValue(
+      CSSParserTokenStream&);
 
   static bool RemoveImportantAnnotationIfPresent(CSSTokenizedValue&);
 
@@ -283,6 +291,11 @@
                             bool important,
                             bool is_animation_tainted);
 
+  // Consumes tokens from the stream using the provided function, and wraps
+  // the result in a CSSTokenizedValue.
+  template <typename ConsumeFunction>
+  static CSSTokenizedValue ConsumeValue(CSSParserTokenStream&, ConsumeFunction);
+
   static std::unique_ptr<Vector<KeyframeOffset>> ConsumeKeyframeKeyList(
       const CSSParserContext*,
       CSSParserTokenRange);
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_impl_test.cc b/third_party/blink/renderer/core/css/parser/css_parser_impl_test.cc
index 56afd5e..81d2fa2 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_impl_test.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_impl_test.cc
@@ -500,7 +500,8 @@
   for (auto current_case : test_cases) {
     CSSTokenizer tokenizer(current_case.input);
     CSSParserTokenStream stream(tokenizer);
-    CSSTokenizedValue tokenized_value = CSSParserImpl::ConsumeValue(stream);
+    CSSTokenizedValue tokenized_value =
+        CSSParserImpl::ConsumeRestrictedPropertyValue(stream);
     SCOPED_TRACE(current_case.input);
     bool is_important =
         CSSParserImpl::RemoveImportantAnnotationIfPresent(tokenized_value);
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_token_stream.cc b/third_party/blink/renderer/core/css/parser/css_parser_token_stream.cc
index 2f2d5cb..47555c0 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_token_stream.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_token_stream.cc
@@ -59,6 +59,28 @@
   } while (!PeekInternal().IsEOF() && nesting_level);
 }
 
+CSSParserTokenRange CSSParserTokenStream::ConsumeComponentValue() {
+  EnsureLookAhead();
+
+  buffer_.Shrink(0);
+
+  if (AtEnd()) {
+    return CSSParserTokenRange(base::span<CSSParserToken>{});
+  }
+
+  unsigned nesting_level = 0;
+  do {
+    buffer_.push_back(UncheckedConsumeInternal());
+    if (buffer_.back().GetBlockType() == CSSParserToken::kBlockStart) {
+      nesting_level++;
+    } else if (buffer_.back().GetBlockType() == CSSParserToken::kBlockEnd) {
+      nesting_level--;
+    }
+  } while (!PeekInternal().IsEOF() && nesting_level);
+
+  return CSSParserTokenRange(buffer_);
+}
+
 void CSSParserTokenStream::UncheckedSkipToEndOfBlock() {
   DCHECK(HasLookAhead());
 
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_token_stream.h b/third_party/blink/renderer/core/css/parser/css_parser_token_stream.h
index 4febdc6..9dbaa399 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_token_stream.h
+++ b/third_party/blink/renderer/core/css/parser/css_parser_token_stream.h
@@ -260,6 +260,21 @@
     return CSSParserTokenRange(buffer_);
   }
 
+  // https://drafts.csswg.org/css-syntax-3/#consume-a-component-value
+  //
+  // This is similar to ConsumeUntilPeekedTypeIs, in that it returns
+  // a range to an internal buffer that's invalidated on the next call
+  // to either ConsumeComponentValue() or ConsumeUntilPeekedTypeIs(),
+  // but instead of consuming until a specified token type, it just consumes
+  // a single component value and returns the corresponding range.
+  CSSParserTokenRange ConsumeComponentValue();
+
+  CSSParserTokenRange ConsumeComponentValueIncludingWhitespace() {
+    CSSParserTokenRange range = ConsumeComponentValue();
+    ConsumeWhitespace();
+    return range;
+  }
+
   // Restarts
   // ========
   //
diff --git a/third_party/blink/renderer/core/css/parser/css_parser_token_stream_test.cc b/third_party/blink/renderer/core/css/parser/css_parser_token_stream_test.cc
index b04098348..f3d3240 100644
--- a/third_party/blink/renderer/core/css/parser/css_parser_token_stream_test.cc
+++ b/third_party/blink/renderer/core/css/parser/css_parser_token_stream_test.cc
@@ -283,6 +283,125 @@
   EXPECT_TRUE(range.AtEnd());
 }
 
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueEOF) {
+  CSSTokenizer tokenizer(String(""));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_TRUE(range.AtEnd());
+  EXPECT_TRUE(stream.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueToken) {
+  CSSTokenizer tokenizer(String("foo"));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_TRUE(stream.AtEnd());
+  EXPECT_EQ(kIdentToken, range.Consume().GetType());
+  EXPECT_TRUE(range.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueWhitespace) {
+  CSSTokenizer tokenizer(String(" foo"));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_FALSE(stream.AtEnd());
+  EXPECT_EQ(kWhitespaceToken, range.Consume().GetType());
+  EXPECT_TRUE(range.AtEnd());
+
+  EXPECT_EQ(kIdentToken, stream.Consume().GetType());
+  EXPECT_TRUE(stream.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueTrailingWhitespace) {
+  CSSTokenizer tokenizer(String("foo "));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_FALSE(stream.AtEnd());
+  EXPECT_EQ(kIdentToken, range.Consume().GetType());
+  EXPECT_TRUE(range.AtEnd());
+
+  EXPECT_EQ(kWhitespaceToken, stream.Consume().GetType());
+  EXPECT_TRUE(stream.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueBlock) {
+  CSSTokenizer tokenizer(String("{ foo }"));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_TRUE(stream.AtEnd());
+
+  EXPECT_EQ(kLeftBraceToken, range.Consume().GetType());
+  EXPECT_EQ(kWhitespaceToken, range.Consume().GetType());
+  EXPECT_EQ(kIdentToken, range.Consume().GetType());
+  EXPECT_EQ(kWhitespaceToken, range.Consume().GetType());
+  EXPECT_EQ(kRightBraceToken, range.Consume().GetType());
+  EXPECT_TRUE(range.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueMultiBlock) {
+  CSSTokenizer tokenizer(String("{} []"));
+  CSSParserTokenStream stream(tokenizer);
+
+  CSSParserTokenRange range = stream.ConsumeComponentValue();
+  EXPECT_FALSE(stream.AtEnd());
+
+  EXPECT_EQ(kLeftBraceToken, range.Consume().GetType());
+  EXPECT_EQ(kRightBraceToken, range.Consume().GetType());
+  EXPECT_TRUE(range.AtEnd());
+
+  EXPECT_EQ(kWhitespaceToken, stream.Consume().GetType());
+  ASSERT_EQ(kLeftBracketToken, stream.Peek().GetType());
+  { CSSParserTokenStream::BlockGuard guard(stream); }
+  EXPECT_TRUE(stream.AtEnd());
+}
+
+TEST(CSSParserTokenStreamTest, ConsumeComponentValueFunction) {
+  CSSTokenizer tokenizer(String(": foo(42) ;"));
+  CSSParserTokenStream stream(tokenizer);
+
+  {
+    CSSParserTokenRange range = stream.ConsumeComponentValue();
+    EXPECT_FALSE(stream.AtEnd());
+    EXPECT_EQ(kColonToken, range.Consume().GetType());
+    EXPECT_TRUE(range.AtEnd());
+  }
+
+  {
+    CSSParserTokenRange range = stream.ConsumeComponentValue();
+    EXPECT_FALSE(stream.AtEnd());
+    EXPECT_EQ(kWhitespaceToken, range.Consume().GetType());
+    EXPECT_TRUE(range.AtEnd());
+  }
+
+  {
+    CSSParserTokenRange range = stream.ConsumeComponentValue();
+    EXPECT_FALSE(stream.AtEnd());
+    EXPECT_EQ(kFunctionToken, range.Consume().GetType());
+    EXPECT_EQ(kNumberToken, range.Consume().GetType());
+    EXPECT_EQ(kRightParenthesisToken, range.Consume().GetType());
+    EXPECT_TRUE(range.AtEnd());
+  }
+
+  {
+    CSSParserTokenRange range = stream.ConsumeComponentValue();
+    EXPECT_FALSE(stream.AtEnd());
+    EXPECT_EQ(kWhitespaceToken, range.Consume().GetType());
+    EXPECT_TRUE(range.AtEnd());
+  }
+
+  {
+    CSSParserTokenRange range = stream.ConsumeComponentValue();
+    EXPECT_TRUE(stream.AtEnd());
+    EXPECT_EQ(kSemicolonToken, range.Consume().GetType());
+    EXPECT_TRUE(range.AtEnd());
+  }
+}
+
 TEST(CSSParserTokenStreamTest, Boundary) {
   CSSTokenizer tokenizer(String("foo:red;bar:blue;asdf"));
   CSSParserTokenStream stream(tokenizer);
diff --git a/third_party/blink/renderer/core/css/resolver/font_builder.cc b/third_party/blink/renderer/core/css/resolver/font_builder.cc
index 0f3b7c7..2f3a83b1 100644
--- a/third_party/blink/renderer/core/css/resolver/font_builder.cc
+++ b/third_party/blink/renderer/core/css/resolver/font_builder.cc
@@ -374,6 +374,17 @@
     return;
   }
 
+  FontSizeAdjust size_adjust = font_description.SizeAdjust();
+  if (size_adjust.IsFromFont() &&
+      size_adjust.Value() == FontSizeAdjust::kFontSizeAdjustNone) {
+    absl::optional<float> aspect_value = FontSizeFunctions::FontAspectValue(
+        font_data, size_adjust.GetMetric(), font_description.ComputedSize());
+    font_description.SetSizeAdjust(FontSizeAdjust(
+        aspect_value.has_value() ? aspect_value.value()
+                                 : FontSizeAdjust::kFontSizeAdjustNone,
+        size_adjust.GetMetric(), size_adjust.IsFromFont()));
+  }
+
   if (auto adjusted_size = FontSizeFunctions::MetricsMultiplierAdjustedFontSize(
           font_data, font_description)) {
     font_description.SetAdjustedSize(adjusted_size.value());
diff --git a/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc b/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
index 1633bf0..c23da58 100644
--- a/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
+++ b/third_party/blink/renderer/core/css/resolver/style_builder_converter.cc
@@ -743,19 +743,9 @@
     return FontBuilder::InitialSizeAdjust();
   }
 
-  float computed_font_size =
-      state.ParentStyle() ? state.ParentStyle()->ComputedFontSize() : 0;
-  const SimpleFontData* font_data =
-      state.ParentStyle() ? state.ParentStyle()->GetFont().PrimaryFont()
-                          : nullptr;
   if (identifier_value &&
       identifier_value->GetValueID() == CSSValueID::kFromFont) {
-    absl::optional<float> aspect_value = FontSizeFunctions::FontAspectValue(
-        font_data, FontSizeAdjust::Metric::kExHeight, computed_font_size);
-    return FontSizeAdjust(aspect_value.has_value()
-                              ? aspect_value.value()
-                              : FontSizeAdjust::kFontSizeAdjustNone,
-                          true);
+    return FontSizeAdjust(FontSizeAdjust::kFontSizeAdjustNone, true);
   }
 
   if (value.IsPrimitiveValue()) {
@@ -777,12 +767,7 @@
 
   DCHECK(To<CSSIdentifierValue>(pair.Second()).GetValueID() ==
          CSSValueID::kFromFont);
-  absl::optional<float> aspect_value =
-      FontSizeFunctions::FontAspectValue(font_data, metric, computed_font_size);
-  return FontSizeAdjust(aspect_value.has_value()
-                            ? aspect_value.value()
-                            : FontSizeAdjust::kFontSizeAdjustNone,
-                        metric, true);
+  return FontSizeAdjust(FontSizeAdjust::kFontSizeAdjustNone, metric, true);
 }
 
 FontSizeAdjust StyleBuilderConverter::ConvertFontSizeAdjust(
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 0e5b13f1..ae791e5e 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -323,7 +323,7 @@
 #include "third_party/blink/renderer/core/timing/render_blocking_metrics_reporter.h"
 #include "third_party/blink/renderer/core/timing/soft_navigation_heuristics.h"
 #include "third_party/blink/renderer/core/trustedtypes/trusted_html.h"
-#include "third_party/blink/renderer/core/view_transition/ready_to_render_event.h"
+#include "third_party/blink/renderer/core/view_transition/page_reveal_event.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_supplement.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
 #include "third_party/blink/renderer/core/xml/parser/xml_document_parser.h"
@@ -9248,14 +9248,14 @@
   return legacy_dom_mutations_supported_.value();
 }
 
-void Document::EnqueueReadyToRenderEvent() {
-  CHECK(RuntimeEnabledFeatures::ReadyToRenderEventEnabled());
+void Document::EnqueuePageRevealEvent() {
+  CHECK(RuntimeEnabledFeatures::PageRevealEventEnabled());
   CHECK(dom_window_);
 
-  auto* ready_to_render_event = MakeGarbageCollected<ReadyToRenderEvent>();
-  ready_to_render_event->SetTarget(dom_window_);
-  ready_to_render_event->SetCurrentTarget(dom_window_);
-  EnqueueAnimationFrameEvent(ready_to_render_event);
+  auto* page_reveal_event = MakeGarbageCollected<PageRevealEvent>();
+  page_reveal_event->SetTarget(dom_window_);
+  page_reveal_event->SetCurrentTarget(dom_window_);
+  EnqueueAnimationFrameEvent(page_reveal_event);
 }
 
 Resource* Document::GetPendingLinkPreloadForTesting(const KURL& url) {
diff --git a/third_party/blink/renderer/core/dom/document.h b/third_party/blink/renderer/core/dom/document.h
index e0d4283..f4da92a 100644
--- a/third_party/blink/renderer/core/dom/document.h
+++ b/third_party/blink/renderer/core/dom/document.h
@@ -2005,7 +2005,7 @@
 
   bool SupportsLegacyDOMMutations();
 
-  void EnqueueReadyToRenderEvent();
+  void EnqueuePageRevealEvent();
 
  protected:
   void ClearXMLVersion() { xml_version_ = String(); }
diff --git a/third_party/blink/renderer/core/dom/element.cc b/third_party/blink/renderer/core/dom/element.cc
index 6c39e2a..543ee27 100644
--- a/third_party/blink/renderer/core/dom/element.cc
+++ b/third_party/blink/renderer/core/dom/element.cc
@@ -4017,6 +4017,30 @@
   }
 }
 
+TextDirection Element::ParentDirectionality() const {
+  if (!RuntimeEnabledFeatures::CSSPseudoDirEnabled()) {
+    // Do what the code that uses this used to do, until the :dir()
+    // pseudo is enabled.
+    return TextDirection::kLtr;
+  }
+
+  if (const HTMLSlotElement* slot =
+          ToHTMLSlotElementIfSupportsAssignmentOrNull(this)) {
+    return ContainingShadowRoot()->host().CachedDirectionality();
+  }
+
+  Node* parent = parentNode();
+  if (Element* parent_element = DynamicTo<Element>(parent)) {
+    return parent_element->CachedDirectionality();
+  }
+
+  if (ShadowRoot* shadow_root = DynamicTo<ShadowRoot>(parent)) {
+    return shadow_root->host().CachedDirectionality();
+  }
+
+  return TextDirection::kLtr;
+}
+
 void Element::RecomputeDirectionFromParent() {
   // This function recomputes the inherited direction if an element inherits
   // direction from a parent or shadow host.
@@ -4026,21 +4050,8 @@
   // direction change to the descendants that need updating.
   if (GetDocument().HasDirAttribute() &&
       RuntimeEnabledFeatures::CSSPseudoDirEnabled() &&
-      !HTMLElement::ElementAffectsDirectionality(this)) {
-    if (HTMLSlotElement* slot =
-            ToHTMLSlotElementIfSupportsAssignmentOrNull(this)) {
-      SetCachedDirectionality(
-          ContainingShadowRoot()->host().CachedDirectionality());
-    } else {
-      Node* parent = parentNode();
-      if (Element* parent_element = DynamicTo<Element>(parent)) {
-        SetCachedDirectionality(parent_element->CachedDirectionality());
-      } else if (ShadowRoot* shadow_root = DynamicTo<ShadowRoot>(parent)) {
-        SetCachedDirectionality(shadow_root->host().CachedDirectionality());
-      } else {
-        SetCachedDirectionality(TextDirection::kLtr);
-      }
-    }
+      HTMLElement::ElementInheritsDirectionality(this)) {
+    SetCachedDirectionality(ParentDirectionality());
   }
 }
 
diff --git a/third_party/blink/renderer/core/dom/element.h b/third_party/blink/renderer/core/dom/element.h
index e05a0ad..d6008a4e 100644
--- a/third_party/blink/renderer/core/dom/element.h
+++ b/third_party/blink/renderer/core/dom/element.h
@@ -1334,6 +1334,8 @@
   // changes.
   void LangAttributeChanged();
 
+  TextDirection ParentDirectionality() const;
+
  private:
   friend class AXObject;
   struct AffectedByPseudoStateChange;
diff --git a/third_party/blink/renderer/core/dom/node.cc b/third_party/blink/renderer/core/dom/node.cc
index 94d0ade..95d16f8 100644
--- a/third_party/blink/renderer/core/dom/node.cc
+++ b/third_party/blink/renderer/core/dom/node.cc
@@ -3451,11 +3451,50 @@
   return ancestor_tree_scopes;
 }
 
+void Node::SetCachedDirectionality(TextDirection direction) {
+  switch (direction) {
+    case TextDirection::kRtl:
+      SetFlag(kCachedDirectionalityIsRtl);
+      break;
+    case TextDirection::kLtr:
+      ClearFlag(kCachedDirectionalityIsRtl);
+      break;
+  }
+  if (!RuntimeEnabledFeatures::CSSPseudoDirEnabled()) {
+    ClearFlag(kNeedsInheritDirectionalityFromParent);
+  }
+}
+
+bool Node::NeedsInheritDirectionalityFromParent() const {
+  CHECK(!RuntimeEnabledFeatures::CSSPseudoDirEnabled());
+  return GetFlag(kNeedsInheritDirectionalityFromParent);
+}
+
 void Node::SetNeedsInheritDirectionalityFromParent() {
   CHECK(!RuntimeEnabledFeatures::CSSPseudoDirEnabled());
   SetFlag(kNeedsInheritDirectionalityFromParent);
 }
 
+void Node::ClearNeedsInheritDirectionalityFromParent() {
+  CHECK(!RuntimeEnabledFeatures::CSSPseudoDirEnabled());
+  ClearFlag(kNeedsInheritDirectionalityFromParent);
+}
+
+bool Node::DirAutoInheritsFromParent() const {
+  return RuntimeEnabledFeatures::CSSPseudoDirEnabled() &&
+         GetFlag(kDirAutoInheritsFromParent);
+}
+
+void Node::SetDirAutoInheritsFromParent() {
+  CHECK(RuntimeEnabledFeatures::CSSPseudoDirEnabled());
+  return SetFlag(kDirAutoInheritsFromParent);
+}
+
+void Node::ClearDirAutoInheritsFromParent() {
+  CHECK(RuntimeEnabledFeatures::CSSPseudoDirEnabled());
+  return ClearFlag(kDirAutoInheritsFromParent);
+}
+
 void Node::Trace(Visitor* visitor) const {
   visitor->Trace(parent_or_shadow_host_node_);
   visitor->Trace(previous_);
diff --git a/third_party/blink/renderer/core/dom/node.h b/third_party/blink/renderer/core/dom/node.h
index ffc6a58..31fd8d2 100644
--- a/third_party/blink/renderer/core/dom/node.h
+++ b/third_party/blink/renderer/core/dom/node.h
@@ -1001,24 +1001,16 @@
     return (node_flags_ & kCachedDirectionalityIsRtl) ? TextDirection::kRtl
                                                       : TextDirection::kLtr;
   }
-  void SetCachedDirectionality(TextDirection direction) {
-    switch (direction) {
-      case TextDirection::kRtl:
-        SetFlag(kCachedDirectionalityIsRtl);
-        break;
-      case TextDirection::kLtr:
-        ClearFlag(kCachedDirectionalityIsRtl);
-        break;
-    }
-    ClearFlag(kNeedsInheritDirectionalityFromParent);
-  }
-  bool NeedsInheritDirectionalityFromParent() const {
-    return GetFlag(kNeedsInheritDirectionalityFromParent);
-  }
+  void SetCachedDirectionality(TextDirection direction);
+
+  bool NeedsInheritDirectionalityFromParent() const;
   void SetNeedsInheritDirectionalityFromParent();
-  void ClearNeedsInheritDirectionalityFromParent() {
-    ClearFlag(kNeedsInheritDirectionalityFromParent);
-  }
+  void ClearNeedsInheritDirectionalityFromParent();
+
+  bool DirAutoInheritsFromParent() const;
+  void SetDirAutoInheritsFromParent();
+  void ClearDirAutoInheritsFromParent();
+
   void Trace(Visitor*) const override;
 
  private:
@@ -1069,9 +1061,16 @@
 
     kSelfOrAncestorHasDirAutoAttribute = 1 << 28,
     kCachedDirectionalityIsRtl = 1 << 29,
-    // TODO(https://crbug.com/576815): Remove this once new dir=auto handling
-    // ships as part of RuntimeEnabledFeatures::CSSPseudoDirEnabled().
+    // TODO(https://crbug.com/576815): Remove this duplication once new
+    // dir=auto handling ships as part of
+    // RuntimeEnabledFeatures::CSSPseudoDirEnabled().
+    //
+    // This has the same value as the next flag; this one is used only when
+    // !RuntimeEnabledFeatures::CSSPseudoDirEnabled():
     kNeedsInheritDirectionalityFromParent = 1u << 30,
+    // This has the same value as the previous flag; this one is used only when
+    // RuntimeEnabledFeatures::CSSPseudoDirEnabled():
+    kDirAutoInheritsFromParent = 1u << 30,
 
     kDefaultNodeFlags = kIsFinishedParsingChildrenFlag,
 
diff --git a/third_party/blink/renderer/core/events/event_type_names.json5 b/third_party/blink/renderer/core/events/event_type_names.json5
index 2b43a87..4ca2ece9 100644
--- a/third_party/blink/renderer/core/events/event_type_names.json5
+++ b/third_party/blink/renderer/core/events/event_type_names.json5
@@ -252,7 +252,7 @@
     "reading",
     "readingerror",
     "readystatechange",
-    "readytorender",
+    "pagereveal",
     "reflectionchange",
     "rejectionhandled",
     "release",
diff --git a/third_party/blink/renderer/core/exported/web_view_impl.cc b/third_party/blink/renderer/core/exported/web_view_impl.cc
index 84f8612..acb43e9 100644
--- a/third_party/blink/renderer/core/exported/web_view_impl.cc
+++ b/third_party/blink/renderer/core/exported/web_view_impl.cc
@@ -2542,10 +2542,10 @@
   UpdateViewTransitionState(restoring_from_bfcache, storing_in_bfcache,
                             page_restore_params);
 
-  if (RuntimeEnabledFeatures::ReadyToRenderEventEnabled()) {
+  if (RuntimeEnabledFeatures::PageRevealEventEnabled()) {
     if (restoring_from_bfcache) {
       if (auto* main_frame = DynamicTo<LocalFrame>(GetPage()->MainFrame())) {
-        main_frame->GetDocument()->EnqueueReadyToRenderEvent();
+        main_frame->GetDocument()->EnqueuePageRevealEvent();
       }
     }
   }
diff --git a/third_party/blink/renderer/core/exported/web_view_test.cc b/third_party/blink/renderer/core/exported/web_view_test.cc
index ede9d8e..a0a021b 100644
--- a/third_party/blink/renderer/core/exported/web_view_test.cc
+++ b/third_party/blink/renderer/core/exported/web_view_test.cc
@@ -2551,7 +2551,7 @@
       item1->Url(), WebFrameLoadType::kBackForward, item1.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
@@ -2559,7 +2559,7 @@
       item2->Url(), WebFrameLoadType::kBackForward, item2.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
@@ -2567,7 +2567,7 @@
       item1->Url(), WebFrameLoadType::kBackForward, item1.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
@@ -2590,7 +2590,7 @@
       item1->Url(), WebFrameLoadType::kBackForward, item1.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
@@ -2599,7 +2599,7 @@
       item3->Url(), WebFrameLoadType::kBackForward, item3.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
diff --git a/third_party/blink/renderer/core/fetch/body_stream_buffer.cc b/third_party/blink/renderer/core/fetch/body_stream_buffer.cc
index e70ad67..bcebb03 100644
--- a/third_party/blink/renderer/core/fetch/body_stream_buffer.cc
+++ b/third_party/blink/renderer/core/fetch/body_stream_buffer.cc
@@ -335,10 +335,9 @@
 }
 
 ScriptPromise BodyStreamBuffer::Cancel(ScriptState* script_state,
-                                       ScriptValue reason) {
+                                       ScriptValue reason,
+                                       ExceptionState& exception_state) {
   if (underlying_byte_source_) {
-    ExceptionState exception_state(script_state->GetIsolate(),
-                                   ExceptionContextType::kUnknown, "", "");
     ScriptPromise cancel_promise = underlying_byte_source_->Cancel(
         ToV8(reason, script_state->GetContext()->Global(),
              script_state->GetIsolate()),
@@ -351,7 +350,7 @@
     }
   } else {
     CHECK(underlying_source_);
-    return underlying_source_->Cancel(script_state, reason);
+    return underlying_source_->Cancel(script_state, reason, exception_state);
   }
 }
 
diff --git a/third_party/blink/renderer/core/fetch/body_stream_buffer.h b/third_party/blink/renderer/core/fetch/body_stream_buffer.h
index 1243db5..3f58be7 100644
--- a/third_party/blink/renderer/core/fetch/body_stream_buffer.h
+++ b/third_party/blink/renderer/core/fetch/body_stream_buffer.h
@@ -85,7 +85,7 @@
                     ExceptionState&);
   void Tee(BodyStreamBuffer**, BodyStreamBuffer**, ExceptionState&);
 
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason);
+  ScriptPromise Cancel(ScriptState*, ScriptValue reason, ExceptionState&);
 
   // ExecutionContextLifecycleObserver
   void ContextDestroyed() override;
diff --git a/third_party/blink/renderer/core/fetch/body_stream_buffer_test.cc b/third_party/blink/renderer/core/fetch/body_stream_buffer_test.cc
index e687d519..6d3cf00 100644
--- a/third_party/blink/renderer/core/fetch/body_stream_buffer_test.cc
+++ b/third_party/blink/renderer/core/fetch/body_stream_buffer_test.cc
@@ -639,7 +639,7 @@
   ScriptValue reason(scope.GetIsolate(),
                      V8String(scope.GetIsolate(), "reason"));
   EXPECT_FALSE(consumer->IsCancelled());
-  buffer->Cancel(scope.GetScriptState(), reason);
+  buffer->Cancel(scope.GetScriptState(), reason, ASSERT_NO_EXCEPTION);
   EXPECT_TRUE(consumer->IsCancelled());
 }
 
diff --git a/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.cc b/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.cc
index 2b564e30..fb23df7 100644
--- a/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.cc
+++ b/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.cc
@@ -33,7 +33,8 @@
 
 ScriptPromise BodyStreamBufferUnderlyingSource::Cancel(
     ScriptState* script_state,
-    ScriptValue reason) {
+    ScriptValue reason,
+    ExceptionState&) {
   DCHECK_EQ(script_state, script_state_);
   Controller()->Close();
   body_stream_buffer_->CancelConsumer();
diff --git a/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.h b/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.h
index 28395cc..d330ebe 100644
--- a/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.h
+++ b/third_party/blink/renderer/core/fetch/body_stream_buffer_underlying_source.h
@@ -25,7 +25,9 @@
         body_stream_buffer_(body_buffer) {}
 
   ScriptPromise pull(ScriptState*) override;
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason) override;
+  ScriptPromise Cancel(ScriptState*,
+                       ScriptValue reason,
+                       ExceptionState&) override;
 
   ReadableStreamDefaultControllerWithScriptScope* Controller() const {
     return UnderlyingSourceBase::Controller();
diff --git a/third_party/blink/renderer/core/frame/local_dom_window.h b/third_party/blink/renderer/core/frame/local_dom_window.h
index de6fc23..6eeff33 100644
--- a/third_party/blink/renderer/core/frame/local_dom_window.h
+++ b/third_party/blink/renderer/core/frame/local_dom_window.h
@@ -373,7 +373,7 @@
 
   DEFINE_ATTRIBUTE_EVENT_LISTENER(orientationchange, kOrientationchange)
 
-  DEFINE_ATTRIBUTE_EVENT_LISTENER(readytorender, kReadytorender)
+  DEFINE_ATTRIBUTE_EVENT_LISTENER(pagereveal, kPagereveal)
 
   void RegisterEventListenerObserver(EventListenerObserver*);
 
diff --git a/third_party/blink/renderer/core/frame/root_frame_viewport_test.cc b/third_party/blink/renderer/core/frame/root_frame_viewport_test.cc
index cf2b6b36..3b273e5d 100644
--- a/third_party/blink/renderer/core/frame/root_frame_viewport_test.cc
+++ b/third_party/blink/renderer/core/frame/root_frame_viewport_test.cc
@@ -111,6 +111,7 @@
   bool ScrollbarsCanBeActive() const override { return true; }
   bool ShouldPlaceVerticalScrollbarOnLeft() const override { return true; }
   void ScrollControlWasSetNeedsPaintInvalidation() override {}
+  bool UsesCompositedScrolling() const override { NOTREACHED_NORETURN(); }
   bool UserInputScrollable(ScrollbarOrientation orientation) const override {
     return orientation == kHorizontalScrollbar ? user_input_scrollable_x_
                                                : user_input_scrollable_y_;
diff --git a/third_party/blink/renderer/core/frame/web_frame_test.cc b/third_party/blink/renderer/core/frame/web_frame_test.cc
index 6631aaed..68515b3 100644
--- a/third_party/blink/renderer/core/frame/web_frame_test.cc
+++ b/third_party/blink/renderer/core/frame/web_frame_test.cc
@@ -8136,7 +8136,7 @@
       item->Url(), WebFrameLoadType::kBackForward, item.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
diff --git a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
index def81df..e2761c6b 100644
--- a/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
+++ b/third_party/blink/renderer/core/frame/web_local_frame_impl.cc
@@ -2745,7 +2745,7 @@
       is_client_redirect ? ClientRedirectPolicy::kClientRedirect
                          : ClientRedirectPolicy::kNotClientRedirect,
       has_transient_user_activation, initiator_origin.Get(),
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent, is_browser_initiated,
       soft_navigation_heuristics_task_id);
 }
diff --git a/third_party/blink/renderer/core/frame/window.idl b/third_party/blink/renderer/core/frame/window.idl
index b6ae482..7e2e0ffd2 100644
--- a/third_party/blink/renderer/core/frame/window.idl
+++ b/third_party/blink/renderer/core/frame/window.idl
@@ -175,9 +175,9 @@
     //  90 is when rotated counter clockwise.
     [HighEntropy=Direct, MeasureAs=WindowOrientation, RuntimeEnabled=OrientationEvent] readonly attribute long orientation;
 
-    // Event handler attribute for the readytorender event.
+    // Event handler attribute for the pagereveal event.
     // https://drafts.csswg.org/css-view-transitions-2/#reveal-event
-    [RuntimeEnabled=ReadyToRenderEvent] attribute EventHandler onreadytorender;
+    [RuntimeEnabled=PageRevealEvent] attribute EventHandler onpagereveal;
 
     // Accessibility Object Model
     // https://github.com/WICG/aom/blob/HEAD/explainer.md
diff --git a/third_party/blink/renderer/core/html/forms/html_form_element.cc b/third_party/blink/renderer/core/html/forms/html_form_element.cc
index 47b9c60..198ebba 100644
--- a/third_party/blink/renderer/core/html/forms/html_form_element.cc
+++ b/third_party/blink/renderer/core/html/forms/html_form_element.cc
@@ -494,7 +494,6 @@
   DCHECK(form_submission->Method() == FormSubmission::kPostMethod ||
          form_submission->Method() == FormSubmission::kGetMethod);
   DCHECK(form_submission->Data());
-  DCHECK(form_submission->Form());
   if (form_submission->Action().IsEmpty())
     return;
   if (GetExecutionContext()->IsSandboxed(
diff --git a/third_party/blink/renderer/core/html/html_anchor_element.cc b/third_party/blink/renderer/core/html/html_anchor_element.cc
index d1b3c90..6023a58 100644
--- a/third_party/blink/renderer/core/html/html_anchor_element.cc
+++ b/third_party/blink/renderer/core/html/html_anchor_element.cc
@@ -470,6 +470,7 @@
   FrameLoadRequest frame_request(window, request);
   frame_request.SetNavigationPolicy(navigation_policy);
   frame_request.SetClientRedirectReason(ClientNavigationReason::kAnchorClick);
+  frame_request.SetSourceElement(this);
   const AtomicString& target =
       frame_request.CleanNavigationTarget(GetEffectiveTarget());
   if (HasRel(kRelationNoReferrer)) {
@@ -624,6 +625,7 @@
     if (event.isTrusted())
       params->involvement = UserNavigationInvolvement::kActivation;
     params->download_filename = download_attr;
+    params->source_element = this;
     if (window->navigation()->DispatchNavigateEvent(params) !=
         NavigationApi::DispatchResult::kContinue) {
       return;
diff --git a/third_party/blink/renderer/core/html/html_element.cc b/third_party/blink/renderer/core/html/html_element.cc
index b45f6482..4fdeaa4 100644
--- a/third_party/blink/renderer/core/html/html_element.cc
+++ b/third_party/blink/renderer/core/html/html_element.cc
@@ -2437,6 +2437,11 @@
          (input_element && input_element->IsTelephone());
 }
 
+bool HTMLElement::ElementInheritsDirectionality(const Node* node) {
+  return !HTMLElement::ElementAffectsDirectionality(node) ||
+         node->DirAutoInheritsFromParent();
+}
+
 void HTMLElement::ChildrenChanged(const ChildrenChange& change) {
   Element::ChildrenChanged(change);
 
@@ -2449,8 +2454,9 @@
         !RuntimeEnabledFeatures::CSSPseudoDirEnabled()) {
       auto* element = DynamicTo<HTMLElement>(change.sibling_changed);
       if (element && !element->NeedsInheritDirectionalityFromParent() &&
-          !ElementAffectsDirectionality(element))
+          ElementInheritsDirectionality(element)) {
         element->UpdateDirectionalityAndDescendant(CachedDirectionality());
+      }
     }
   }
   if (change.IsChildInsertion()) {
@@ -2639,9 +2645,20 @@
 bool HTMLElement::CalculateAndAdjustAutoDirectionality(Node* stay_within) {
   CHECK(!RuntimeEnabledFeatures::CSSPseudoDirEnabled() || this == stay_within);
   bool is_deferred = false;
-  TextDirection text_direction =
-      ResolveAutoDirectionality<NodeTraversal>(is_deferred, stay_within)
-          .value_or(TextDirection::kLtr);
+  TextDirection text_direction;
+  absl::optional<TextDirection> resolve_result =
+      ResolveAutoDirectionality<NodeTraversal>(is_deferred, stay_within);
+  if (resolve_result) {
+    text_direction = *resolve_result;
+    if (RuntimeEnabledFeatures::CSSPseudoDirEnabled()) {
+      ClearDirAutoInheritsFromParent();
+    }
+  } else {
+    text_direction = ParentDirectionality();
+    if (RuntimeEnabledFeatures::CSSPseudoDirEnabled()) {
+      SetDirAutoInheritsFromParent();
+    }
+  }
   if (CachedDirectionality() != text_direction && !is_deferred) {
     UpdateDirectionalityAndDescendant(text_direction);
 
@@ -2665,12 +2682,12 @@
   Node* stay_within = nullptr;
   if (change.type == ChildrenChangeType::kTextChanged) {
     CHECK(change.old_text);
-    TextDirection old_text_direction =
-        BidiParagraph::BaseDirectionForStringOrLtr(*change.old_text);
+    absl::optional<TextDirection> old_text_direction =
+        BidiParagraph::BaseDirectionForString(*change.old_text);
     auto* character_data = DynamicTo<CharacterData>(change.sibling_changed);
     DCHECK(character_data);
-    TextDirection new_text_direction =
-        BidiParagraph::BaseDirectionForStringOrLtr(character_data->data());
+    absl::optional<TextDirection> new_text_direction =
+        BidiParagraph::BaseDirectionForString(character_data->data());
     if (old_text_direction == new_text_direction)
       return;
     stay_within = change.sibling_changed;
@@ -2680,7 +2697,8 @@
           BidiParagraph::BaseDirectionForString(
               change.sibling_changed->textContent(true));
       if (!new_text_direction ||
-          *new_text_direction == CachedDirectionality()) {
+          (*new_text_direction == CachedDirectionality() &&
+           !DirAutoInheritsFromParent())) {
         return;
       }
     }
@@ -3358,7 +3376,7 @@
     do {
       if (element != this &&
           (ToHTMLSlotElementIfSupportsAssignmentOrNull(element) ||
-           ElementAffectsDirectionality(element) ||
+           !ElementInheritsDirectionality(element) ||
            element->CachedDirectionality() == direction)) {
         element = ElementTraversal::NextSkippingChildren(*element, this);
         continue;
@@ -3372,7 +3390,7 @@
           // TODO(https://crbug.com/576815): This should work for
           // non-HTML elements too.
           if (HTMLElement* child_element = DynamicTo<HTMLElement>(child)) {
-            if (!ElementAffectsDirectionality(child_element) &&
+            if (ElementInheritsDirectionality(child_element) &&
                 child_element->CachedDirectionality() != direction) {
               child_element->UpdateDirectionalityAndDescendant(direction);
             }
@@ -3381,7 +3399,7 @@
         if (shadow_root->HasSlotAssignment()) {
           for (HTMLSlotElement* slot :
                shadow_root->GetSlotAssignment().Slots()) {
-            if (!ElementAffectsDirectionality(slot) &&
+            if (ElementInheritsDirectionality(slot) &&
                 slot->CachedDirectionality() != direction) {
               slot->UpdateDirectionalityAndDescendant(direction);
             }
diff --git a/third_party/blink/renderer/core/html/html_element.h b/third_party/blink/renderer/core/html/html_element.h
index 0d1d8695..c3d4d59 100644
--- a/third_party/blink/renderer/core/html/html_element.h
+++ b/third_party/blink/renderer/core/html/html_element.h
@@ -159,6 +159,7 @@
   bool HasDirectionAuto() const;
 
   static bool ElementAffectsDirectionality(const Node* node);
+  static bool ElementInheritsDirectionality(const Node* node);
 
   virtual bool IsHTMLBodyElement() const { return false; }
   // TODO(crbug.com/1123606): Remove this virtual method once the fenced frame
diff --git a/third_party/blink/renderer/core/layout/build.gni b/third_party/blink/renderer/core/layout/build.gni
index c0e7489d..ef51feea 100644
--- a/third_party/blink/renderer/core/layout/build.gni
+++ b/third_party/blink/renderer/core/layout/build.gni
@@ -361,9 +361,6 @@
   "ng/inline/ng_text_type.h",
   "ng/layout_box_utils.cc",
   "ng/layout_box_utils.h",
-  "ng/layout_ng_block.h",
-  "ng/layout_ng_mixin.cc",
-  "ng/layout_ng_mixin.h",
   "ng/layout_ng_view.cc",
   "ng/layout_ng_view.h",
   "ng/legacy_layout_tree_walking.h",
diff --git a/third_party/blink/renderer/core/layout/layout_box.cc b/third_party/blink/renderer/core/layout/layout_box.cc
index e076c8e..f1ad87a 100644
--- a/third_party/blink/renderer/core/layout/layout_box.cc
+++ b/third_party/blink/renderer/core/layout/layout_box.cc
@@ -110,6 +110,7 @@
 #include "third_party/blink/renderer/platform/geometry/float_rounded_rect.h"
 #include "third_party/blink/renderer/platform/geometry/layout_rect.h"
 #include "third_party/blink/renderer/platform/geometry/length_functions.h"
+#include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
 #include "third_party/blink/renderer/platform/instrumentation/histogram.h"
 #include "third_party/blink/renderer/platform/runtime_enabled_features.h"
 #include "third_party/blink/renderer/platform/text/platform_locale.h"
@@ -4290,6 +4291,19 @@
   return GetScrollableArea()->ScrollsOverflow();
 }
 
+bool LayoutBox::UsesCompositedScrolling() const {
+  NOT_DESTROYED();
+  const auto* properties = FirstFragment().PaintProperties();
+  if (!properties || !properties->Scroll()) {
+    return false;
+  }
+  const auto* paint_artifact_compositor =
+      GetFrameView()->GetPaintArtifactCompositor();
+  return paint_artifact_compositor &&
+         paint_artifact_compositor->UsesCompositedScrolling(
+             *properties->Scroll());
+}
+
 void LayoutBox::OverrideTickmarks(Vector<gfx::Rect> tickmarks) {
   NOT_DESTROYED();
   GetScrollableArea()->SetTickmarksOverride(std::move(tickmarks));
diff --git a/third_party/blink/renderer/core/layout/layout_box.h b/third_party/blink/renderer/core/layout/layout_box.h
index 087f5923..c0fbe5c 100644
--- a/third_party/blink/renderer/core/layout/layout_box.h
+++ b/third_party/blink/renderer/core/layout/layout_box.h
@@ -216,11 +216,13 @@
     return false;
   }
 
-  // Returns whether this object needs a scroll paint property tree node. These
-  // are a requirement for composited scrolling but are also created for
-  // non-composited scrollers.
+  // Returns whether this object needs a scroll paint property tree node.
   bool NeedsScrollNode(CompositingReasons direct_compositing_reasons) const;
 
+  // Returns true if this LayoutBox has a scroll paint property node and the
+  // node is currently composited in cc.
+  bool UsesCompositedScrolling() const;
+
   // Use this with caution! No type checking is done!
   LayoutBox* FirstChildBox() const;
   LayoutBox* FirstInFlowChildBox() const;
diff --git a/third_party/blink/renderer/core/layout/layout_box_model_object.cc b/third_party/blink/renderer/core/layout/layout_box_model_object.cc
index b21e303..48fc701 100644
--- a/third_party/blink/renderer/core/layout/layout_box_model_object.cc
+++ b/third_party/blink/renderer/core/layout/layout_box_model_object.cc
@@ -92,16 +92,6 @@
 LayoutBoxModelObject::LayoutBoxModelObject(ContainerNode* node)
     : LayoutObject(node) {}
 
-bool LayoutBoxModelObject::UsesCompositedScrolling() const {
-  NOT_DESTROYED();
-
-  // TODO(crbug.com/1414885): We may need to redefine this function for
-  // CompositeScrollAfterPaint.
-  const auto* properties = FirstFragment().PaintProperties();
-  return properties && properties->ScrollTranslation() &&
-         properties->ScrollTranslation()->HasDirectCompositingReasons();
-}
-
 LayoutBoxModelObject::~LayoutBoxModelObject() = default;
 
 void LayoutBoxModelObject::WillBeDestroyed() {
diff --git a/third_party/blink/renderer/core/layout/layout_box_model_object.h b/third_party/blink/renderer/core/layout/layout_box_model_object.h
index de4c867f..eb7f360a 100644
--- a/third_party/blink/renderer/core/layout/layout_box_model_object.h
+++ b/third_party/blink/renderer/core/layout/layout_box_model_object.h
@@ -178,8 +178,6 @@
   // it. If there are no filters, it returns its argument.
   PhysicalRect ApplyFiltersToRect(const PhysicalRect&) const;
 
-  bool UsesCompositedScrolling() const;
-
   // These return the CSS computed padding values.
   LayoutUnit ComputedCSSPaddingTop() const {
     NOT_DESTROYED();
diff --git a/third_party/blink/renderer/core/layout/layout_frame_set.cc b/third_party/blink/renderer/core/layout/layout_frame_set.cc
index 21b32cb..e3bda82 100644
--- a/third_party/blink/renderer/core/layout/layout_frame_set.cc
+++ b/third_party/blink/renderer/core/layout/layout_frame_set.cc
@@ -10,7 +10,7 @@
 
 namespace blink {
 
-LayoutFrameSet::LayoutFrameSet(Element* element) : LayoutNGBlock(element) {
+LayoutFrameSet::LayoutFrameSet(Element* element) : LayoutBlock(element) {
   DCHECK(IsA<HTMLFrameSetElement>(element));
 }
 
@@ -21,7 +21,7 @@
 
 bool LayoutFrameSet::IsOfType(LayoutObjectType type) const {
   NOT_DESTROYED();
-  return type == kLayoutObjectFrameSet || LayoutNGBlock::IsOfType(type);
+  return type == kLayoutObjectFrameSet || LayoutBlock::IsOfType(type);
 }
 
 bool LayoutFrameSet::IsChildAllowed(LayoutObject* child,
@@ -32,12 +32,12 @@
 
 void LayoutFrameSet::AddChild(LayoutObject* new_child,
                               LayoutObject* before_child) {
-  LayoutNGBlock::AddChild(new_child, before_child);
+  LayoutBlock::AddChild(new_child, before_child);
   To<HTMLFrameSetElement>(GetNode())->DirtyEdgeInfoAndFullPaintInvalidation();
 }
 
 void LayoutFrameSet::RemoveChild(LayoutObject* child) {
-  LayoutNGBlock::RemoveChild(child);
+  LayoutBlock::RemoveChild(child);
   if (DocumentBeingDestroyed()) {
     return;
   }
diff --git a/third_party/blink/renderer/core/layout/layout_frame_set.h b/third_party/blink/renderer/core/layout/layout_frame_set.h
index 6bf4c1d..55f0b90 100644
--- a/third_party/blink/renderer/core/layout/layout_frame_set.h
+++ b/third_party/blink/renderer/core/layout/layout_frame_set.h
@@ -5,11 +5,11 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_LAYOUT_FRAME_SET_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_LAYOUT_FRAME_SET_H_
 
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 
 namespace blink {
 
-class LayoutFrameSet final : public LayoutNGBlock {
+class LayoutFrameSet final : public LayoutBlock {
  public:
   explicit LayoutFrameSet(Element*);
 
diff --git a/third_party/blink/renderer/core/layout/layout_shift_tracker_test.cc b/third_party/blink/renderer/core/layout/layout_shift_tracker_test.cc
index a42ddf75..4bcd6f24 100644
--- a/third_party/blink/renderer/core/layout/layout_shift_tracker_test.cc
+++ b/third_party/blink/renderer/core/layout/layout_shift_tracker_test.cc
@@ -330,7 +330,7 @@
       item1->Url(), WebFrameLoadType::kBackForward, item1.Get(),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent, is_browser_initiated,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
 
diff --git a/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.cc b/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.cc
index 54ad9f1..c870f89 100644
--- a/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.cc
+++ b/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.cc
@@ -17,7 +17,7 @@
 namespace blink {
 
 LayoutNGFlexibleBox::LayoutNGFlexibleBox(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {}
+    : LayoutBlock(element) {}
 
 bool LayoutNGFlexibleBox::HasTopOverflow() const {
   const auto& style = StyleRef();
@@ -73,7 +73,7 @@
     // optgroups.
     return object->GetNode() == &select->InnerElement();
   }
-  return LayoutNGMixin<LayoutBlock>::IsChildAllowed(object, style);
+  return LayoutBlock::IsChildAllowed(object, style);
 }
 
 void LayoutNGFlexibleBox::SetNeedsLayoutForDevtools() {
diff --git a/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.h b/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.h
index 9458278..e5625c1c 100644
--- a/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.h
+++ b/third_party/blink/renderer/core/layout/ng/flex/layout_ng_flexible_box.h
@@ -6,8 +6,8 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_FLEX_LAYOUT_NG_FLEXIBLE_BOX_H_
 
 #include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 #include "third_party/blink/renderer/core/layout/ng/flex/ng_flex_data.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
 
 namespace blink {
 
@@ -17,7 +17,7 @@
 // anywhere, because neither paint nor ancestor layout needs it. So the NG flex
 // layout algorithm will fill one of these in when devtools requests it.
 
-class CORE_EXPORT LayoutNGFlexibleBox : public LayoutNGBlock {
+class CORE_EXPORT LayoutNGFlexibleBox : public LayoutBlock {
  public:
   explicit LayoutNGFlexibleBox(Element*);
 
@@ -46,8 +46,7 @@
 
   bool IsOfType(LayoutObjectType type) const override {
     NOT_DESTROYED();
-    return type == kLayoutObjectNGFlexibleBox ||
-           LayoutNGMixin<LayoutBlock>::IsOfType(type);
+    return type == kLayoutObjectNGFlexibleBox || LayoutBlock::IsOfType(type);
   }
 };
 
diff --git a/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.cc b/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.cc
index 2b92023..b80371c 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.cc
+++ b/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.cc
@@ -8,8 +8,7 @@
 
 namespace blink {
 
-LayoutNGGrid::LayoutNGGrid(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {}
+LayoutNGGrid::LayoutNGGrid(Element* element) : LayoutBlock(element) {}
 
 void LayoutNGGrid::AddChild(LayoutObject* new_child,
                             LayoutObject* before_child) {
diff --git a/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.h b/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.h
index 0813e2e..2ba20f1 100644
--- a/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.h
+++ b/third_party/blink/renderer/core/layout/ng/grid/layout_ng_grid.h
@@ -6,13 +6,12 @@
 #define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_GRID_LAYOUT_NG_GRID_H_
 
 #include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 #include "third_party/blink/renderer/core/layout/ng/grid/ng_grid_data.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h"
 
 namespace blink {
 
-class CORE_EXPORT LayoutNGGrid : public LayoutNGBlock {
+class CORE_EXPORT LayoutNGGrid : public LayoutBlock {
  public:
   explicit LayoutNGGrid(Element*);
 
@@ -45,8 +44,7 @@
  protected:
   bool IsOfType(LayoutObjectType type) const override {
     NOT_DESTROYED();
-    return type == kLayoutObjectNGGrid ||
-           LayoutNGMixin<LayoutBlock>::IsOfType(type);
+    return type == kLayoutObjectNGGrid || LayoutBlock::IsOfType(type);
   }
 
  private:
diff --git a/third_party/blink/renderer/core/layout/ng/layout_ng_block.h b/third_party/blink/renderer/core/layout/ng/layout_ng_block.h
deleted file mode 100644
index 844f59ab..0000000
--- a/third_party/blink/renderer/core/layout/ng/layout_ng_block.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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.
-
-#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_BLOCK_H_
-#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_BLOCK_H_
-
-#include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/layout/layout_block.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h"
-
-namespace blink {
-
-extern template class CORE_EXTERN_TEMPLATE_EXPORT LayoutNGMixin<LayoutBlock>;
-
-using LayoutNGBlock = LayoutNGMixin<LayoutBlock>;
-
-}  // namespace blink
-
-#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_BLOCK_H_
diff --git a/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.cc b/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.cc
index ab6ff13f..e66059e 100644
--- a/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.cc
+++ b/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.cc
@@ -13,19 +13,18 @@
 namespace blink {
 
 LayoutNGBlockFlow::LayoutNGBlockFlow(ContainerNode* node)
-    : LayoutNGMixin<LayoutBlockFlow>(node) {}
+    : LayoutBlockFlow(node) {}
 
 LayoutNGBlockFlow::~LayoutNGBlockFlow() = default;
 
 void LayoutNGBlockFlow::Trace(Visitor* visitor) const {
   visitor->Trace(ng_inline_node_data_);
-  LayoutNGMixin<LayoutBlockFlow>::Trace(visitor);
+  LayoutBlockFlow::Trace(visitor);
 }
 
 bool LayoutNGBlockFlow::IsOfType(LayoutObjectType type) const {
   NOT_DESTROYED();
-  return type == kLayoutObjectNGBlockFlow ||
-         LayoutNGMixin<LayoutBlockFlow>::IsOfType(type);
+  return type == kLayoutObjectNGBlockFlow || LayoutBlockFlow::IsOfType(type);
 }
 
 void LayoutNGBlockFlow::StyleDidChange(StyleDifference diff,
diff --git a/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h b/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h
index f21481c..d2864aa 100644
--- a/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h
+++ b/third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h
@@ -7,18 +7,14 @@
 
 #include "third_party/blink/renderer/core/core_export.h"
 #include "third_party/blink/renderer/core/layout/layout_block_flow.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
 
 namespace blink {
 
 enum class NGBaselineAlgorithmType;
 struct NGInlineNodeData;
 
-extern template class CORE_EXTERN_TEMPLATE_EXPORT
-    LayoutNGMixin<LayoutBlockFlow>;
-
 // This overrides the default layout block algorithm to use Layout NG.
-class CORE_EXPORT LayoutNGBlockFlow : public LayoutNGMixin<LayoutBlockFlow> {
+class CORE_EXPORT LayoutNGBlockFlow : public LayoutBlockFlow {
  public:
   explicit LayoutNGBlockFlow(ContainerNode*);
   ~LayoutNGBlockFlow() override;
diff --git a/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.cc b/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.cc
deleted file mode 100644
index 42fd9092..0000000
--- a/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.cc
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2017 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h"
-
-#include <memory>
-#include <utility>
-
-#include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_box_utils.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_out_of_flow_layout_part.h"
-#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h"
-#include "third_party/blink/renderer/core/layout/ng/table/layout_ng_table_caption.h"
-#include "third_party/blink/renderer/core/layout/svg/layout_svg_text.h"
-#include "third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.h"
-#include "third_party/blink/renderer/core/paint/paint_layer.h"
-#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
-
-namespace blink {
-
-template <typename Base>
-LayoutNGMixin<Base>::LayoutNGMixin(ContainerNode* node) : Base(node) {
-  Base::CheckIsNotDestroyed();
-  static_assert(
-      std::is_base_of<LayoutBlock, Base>::value,
-      "Base class of LayoutNGMixin must be LayoutBlock or derived class.");
-}
-
-template <typename Base>
-LayoutNGMixin<Base>::~LayoutNGMixin() = default;
-
-template class CORE_TEMPLATE_EXPORT LayoutNGMixin<LayoutBlock>;
-template class CORE_TEMPLATE_EXPORT LayoutNGMixin<LayoutBlockFlow>;
-
-}  // namespace blink
diff --git a/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h b/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h
deleted file mode 100644
index 8492475..0000000
--- a/third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2017 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_MIXIN_H_
-#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_MIXIN_H_
-
-#include <type_traits>
-
-#include "third_party/blink/renderer/core/layout/layout_block.h"
-
-namespace blink {
-
-// This mixin holds code shared between LayoutNG subclasses of
-// LayoutBlock.
-//
-// If you'd like to make a LayoutNGFoo class inheriting from
-// LayoutNGMixin<LayoutBar>, you need to do:
-//  * Add the following to the header for LayoutNGFoo.
-//     extern template class CORE_EXTERN_TEMPLATE_EXPORT
-//         LayoutNGMixin<LayoutBar>;
-//  * Add |#include "header for LayoutNGFoo"| to layout_ng_mixin.cc.
-//    It's the header for LayoutNGFoo, not for LayoutBar. The purpose is to
-//    include the above |extern template| declaration.
-//  * Add |template class CORE_TEMPLATE_EXPORT LayoutNGMixin<LayoutBar>;| to
-//    layout_ng_mixin.cc.
-template <typename Base>
-class LayoutNGMixin : public Base {
- public:
-  explicit LayoutNGMixin(ContainerNode*);
-  ~LayoutNGMixin() override;
-};
-
-}  // namespace blink
-
-#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_LAYOUT_NG_MIXIN_H_
diff --git a/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.cc b/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.cc
index c298da4..28ccab7d 100644
--- a/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.cc
+++ b/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.cc
@@ -11,14 +11,13 @@
 namespace blink {
 
 LayoutNGMathMLBlock::LayoutNGMathMLBlock(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {
-}
+    : LayoutBlock(element) {}
 
 bool LayoutNGMathMLBlock::IsOfType(LayoutObjectType type) const {
   return type == kLayoutObjectMathML ||
          (type == kLayoutObjectMathMLRoot && GetNode() &&
           GetNode()->HasTagName(mathml_names::kMathTag)) ||
-         LayoutNGMixin<LayoutBlock>::IsOfType(type);
+         LayoutBlock::IsOfType(type);
 }
 
 bool LayoutNGMathMLBlock::IsChildAllowed(LayoutObject* child,
@@ -29,12 +28,12 @@
 bool LayoutNGMathMLBlock::CanHaveChildren() const {
   if (GetNode() && GetNode()->HasTagName(mathml_names::kMspaceTag))
     return false;
-  return LayoutNGMixin<LayoutBlock>::CanHaveChildren();
+  return LayoutBlock::CanHaveChildren();
 }
 
 void LayoutNGMathMLBlock::StyleDidChange(StyleDifference diff,
                                          const ComputedStyle* old_style) {
-  LayoutNGMixin<LayoutBlock>::StyleDidChange(diff, old_style);
+  LayoutBlock::StyleDidChange(diff, old_style);
   if (!old_style)
     return;
   if (IsA<MathMLUnderOverElement>(GetNode()) &&
diff --git a/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.h b/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.h
index 94d202e..c383b4f1 100644
--- a/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.h
+++ b/third_party/blink/renderer/core/layout/ng/mathml/layout_ng_mathml_block.h
@@ -5,11 +5,11 @@
 #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_MATHML_LAYOUT_NG_MATHML_BLOCK_H_
 #define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_MATHML_LAYOUT_NG_MATHML_BLOCK_H_
 
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 
 namespace blink {
 
-class LayoutNGMathMLBlock : public LayoutNGBlock {
+class LayoutNGMathMLBlock : public LayoutBlock {
  public:
   explicit LayoutNGMathMLBlock(Element*);
 
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.cc b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.cc
index 3235559..a21bb2bc 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.cc
@@ -37,14 +37,13 @@
 
 }  // namespace
 
-LayoutNGTable::LayoutNGTable(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {}
+LayoutNGTable::LayoutNGTable(Element* element) : LayoutBlock(element) {}
 
 LayoutNGTable::~LayoutNGTable() = default;
 
 void LayoutNGTable::Trace(Visitor* visitor) const {
   visitor->Trace(cached_table_borders_);
-  LayoutNGBlock::Trace(visitor);
+  LayoutBlock::Trace(visitor);
 }
 
 LayoutNGTable* LayoutNGTable::CreateAnonymousWithParent(
@@ -316,7 +315,7 @@
 void LayoutNGTable::RemoveChild(LayoutObject* child) {
   NOT_DESTROYED();
   TableGridStructureChanged();
-  LayoutNGMixin<LayoutBlock>::RemoveChild(child);
+  LayoutBlock::RemoveChild(child);
 }
 
 void LayoutNGTable::StyleDidChange(StyleDifference diff,
@@ -334,7 +333,7 @@
     if (borders_changed || collapse_changed)
       GridBordersChanged();
   }
-  LayoutNGMixin<LayoutBlock>::StyleDidChange(diff, old_style);
+  LayoutBlock::StyleDidChange(diff, old_style);
 }
 
 LayoutBox* LayoutNGTable::CreateAnonymousBoxWithSameTypeAs(
@@ -361,8 +360,8 @@
       clip_rect.size.height = LayoutUnit(infinite_rect.height());
     }
   } else {
-    clip_rect = LayoutNGMixin<LayoutBlock>::OverflowClipRect(
-        location, overlay_scrollbar_clip_behavior);
+    clip_rect = LayoutBlock::OverflowClipRect(location,
+                                              overlay_scrollbar_clip_behavior);
   }
   // TODO(1142929)
   // We cannot handle table hidden overflow with captions correctly.
@@ -402,7 +401,7 @@
         .ConvertToPhysical(Style()->GetWritingDirection())
         .left;
   }
-  return LayoutNGMixin<LayoutBlock>::BorderLeft();
+  return LayoutBlock::BorderLeft();
 }
 
 LayoutUnit LayoutNGTable::BorderRight() const {
@@ -414,7 +413,7 @@
         .ConvertToPhysical(Style()->GetWritingDirection())
         .right;
   }
-  return LayoutNGMixin<LayoutBlock>::BorderRight();
+  return LayoutBlock::BorderRight();
 }
 
 LayoutUnit LayoutNGTable::BorderTop() const {
@@ -426,7 +425,7 @@
         .ConvertToPhysical(Style()->GetWritingDirection())
         .top;
   }
-  return LayoutNGMixin<LayoutBlock>::BorderTop();
+  return LayoutBlock::BorderTop();
 }
 
 LayoutUnit LayoutNGTable::BorderBottom() const {
@@ -438,35 +437,35 @@
         .ConvertToPhysical(Style()->GetWritingDirection())
         .bottom;
   }
-  return LayoutNGMixin<LayoutBlock>::BorderBottom();
+  return LayoutBlock::BorderBottom();
 }
 
 LayoutUnit LayoutNGTable::PaddingTop() const {
   NOT_DESTROYED();
   if (ShouldCollapseBorders())
     return LayoutUnit();
-  return LayoutNGMixin<LayoutBlock>::PaddingTop();
+  return LayoutBlock::PaddingTop();
 }
 
 LayoutUnit LayoutNGTable::PaddingBottom() const {
   NOT_DESTROYED();
   if (ShouldCollapseBorders())
     return LayoutUnit();
-  return LayoutNGMixin<LayoutBlock>::PaddingBottom();
+  return LayoutBlock::PaddingBottom();
 }
 
 LayoutUnit LayoutNGTable::PaddingLeft() const {
   NOT_DESTROYED();
   if (ShouldCollapseBorders())
     return LayoutUnit();
-  return LayoutNGMixin<LayoutBlock>::PaddingLeft();
+  return LayoutBlock::PaddingLeft();
 }
 
 LayoutUnit LayoutNGTable::PaddingRight() const {
   NOT_DESTROYED();
   if (ShouldCollapseBorders())
     return LayoutUnit();
-  return LayoutNGMixin<LayoutBlock>::PaddingRight();
+  return LayoutBlock::PaddingRight();
 }
 
 // Effective column index is index of columns with mergeable
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.h b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.h
index 9a9f9c3d..4e33095ce 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.h
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table.h
@@ -8,7 +8,7 @@
 #include "base/dcheck_is_on.h"
 #include "base/notreached.h"
 #include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 #include "third_party/blink/renderer/core/layout/ng/table/ng_table_layout_algorithm_types.h"
 
 namespace blink {
@@ -96,7 +96,7 @@
 // The validation state is a IsTableColumnsConstraintsDirty flag
 // on LayoutObject. They are invalidated inside
 // LayoutObject::SetNeeds*Layout.
-class CORE_EXPORT LayoutNGTable : public LayoutNGBlock {
+class CORE_EXPORT LayoutNGTable : public LayoutBlock {
  public:
   explicit LayoutNGTable(Element*);
   ~LayoutNGTable() override;
@@ -213,8 +213,7 @@
  protected:
   bool IsOfType(LayoutObjectType type) const override {
     NOT_DESTROYED();
-    return type == kLayoutObjectTable ||
-           LayoutNGMixin<LayoutBlock>::IsOfType(type);
+    return type == kLayoutObjectTable || LayoutBlock::IsOfType(type);
   }
 
   // Table paints background specially.
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.cc b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.cc
index 92e380e..ab3478d 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.cc
@@ -13,8 +13,7 @@
 
 namespace blink {
 
-LayoutNGTableRow::LayoutNGTableRow(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {}
+LayoutNGTableRow::LayoutNGTableRow(Element* element) : LayoutBlock(element) {}
 
 LayoutNGTableRow* LayoutNGTableRow::CreateAnonymousWithParent(
     const LayoutObject& parent) {
@@ -112,21 +111,21 @@
     before_child = SplitAnonymousBoxesAroundChild(before_child);
 
   DCHECK(!before_child || before_child->IsTableCell());
-  LayoutNGMixin<LayoutBlock>::AddChild(child, before_child);
+  LayoutBlock::AddChild(child, before_child);
 }
 
 void LayoutNGTableRow::RemoveChild(LayoutObject* child) {
   NOT_DESTROYED();
   if (LayoutNGTable* table = Table())
     table->TableGridStructureChanged();
-  LayoutNGMixin<LayoutBlock>::RemoveChild(child);
+  LayoutBlock::RemoveChild(child);
 }
 
 void LayoutNGTableRow::WillBeRemovedFromTree() {
   NOT_DESTROYED();
   if (LayoutNGTable* table = Table())
     table->TableGridStructureChanged();
-  LayoutNGMixin<LayoutBlock>::WillBeRemovedFromTree();
+  LayoutBlock::WillBeRemovedFromTree();
 }
 
 void LayoutNGTableRow::StyleDidChange(StyleDifference diff,
@@ -139,7 +138,7 @@
       table->GridBordersChanged();
     }
   }
-  LayoutNGMixin<LayoutBlock>::StyleDidChange(diff, old_style);
+  LayoutBlock::StyleDidChange(diff, old_style);
 }
 
 LayoutBox* LayoutNGTableRow::CreateAnonymousBoxWithSameTypeAs(
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.h b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.h
index 6c847d2..23bcae7 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.h
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_row.h
@@ -8,7 +8,7 @@
 #include "base/dcheck_is_on.h"
 #include "base/notreached.h"
 #include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 
 namespace blink {
 
@@ -17,7 +17,7 @@
 class LayoutNGTable;
 
 // Every child of LayoutNGTableRow must be LayoutNGTableCell.
-class CORE_EXPORT LayoutNGTableRow : public LayoutNGBlock {
+class CORE_EXPORT LayoutNGTableRow : public LayoutBlock {
  public:
   explicit LayoutNGTableRow(Element*);
 
@@ -91,8 +91,7 @@
  protected:
   bool IsOfType(LayoutObjectType type) const override {
     NOT_DESTROYED();
-    return type == kLayoutObjectTableRow ||
-           LayoutNGMixin<LayoutBlock>::IsOfType(type);
+    return type == kLayoutObjectTableRow || LayoutBlock::IsOfType(type);
   }
 
   // Table section paints background specially.
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.cc b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.cc
index 124d3be..a727e30 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.cc
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.cc
@@ -12,7 +12,7 @@
 namespace blink {
 
 LayoutNGTableSection::LayoutNGTableSection(Element* element)
-    : LayoutNGMixin<LayoutBlock>(element) {}
+    : LayoutBlock(element) {}
 
 LayoutNGTableSection* LayoutNGTableSection::CreateAnonymousWithParent(
     const LayoutObject& parent) {
@@ -92,21 +92,21 @@
   if (before_child && before_child->Parent() != this)
     before_child = SplitAnonymousBoxesAroundChild(before_child);
 
-  LayoutNGMixin<LayoutBlock>::AddChild(child, before_child);
+  LayoutBlock::AddChild(child, before_child);
 }
 
 void LayoutNGTableSection::RemoveChild(LayoutObject* child) {
   NOT_DESTROYED();
   if (LayoutNGTable* table = Table())
     table->TableGridStructureChanged();
-  LayoutNGMixin<LayoutBlock>::RemoveChild(child);
+  LayoutBlock::RemoveChild(child);
 }
 
 void LayoutNGTableSection::WillBeRemovedFromTree() {
   NOT_DESTROYED();
   if (LayoutNGTable* table = Table())
     table->TableGridStructureChanged();
-  LayoutNGMixin<LayoutBlock>::WillBeRemovedFromTree();
+  LayoutBlock::WillBeRemovedFromTree();
 }
 
 void LayoutNGTableSection::StyleDidChange(StyleDifference diff,
@@ -119,7 +119,7 @@
       table->GridBordersChanged();
     }
   }
-  LayoutNGMixin<LayoutBlock>::StyleDidChange(diff, old_style);
+  LayoutBlock::StyleDidChange(diff, old_style);
 }
 
 LayoutBox* LayoutNGTableSection::CreateAnonymousBoxWithSameTypeAs(
diff --git a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.h b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.h
index 66016c0..2dbfe98 100644
--- a/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.h
+++ b/third_party/blink/renderer/core/layout/ng/table/layout_ng_table_section.h
@@ -7,7 +7,7 @@
 
 #include "base/notreached.h"
 #include "third_party/blink/renderer/core/core_export.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_block.h"
+#include "third_party/blink/renderer/core/layout/layout_block.h"
 
 namespace blink {
 
@@ -16,7 +16,7 @@
 
 // NOTE:
 // Every child of LayoutNGTableSection must be LayoutNGTableRow.
-class CORE_EXPORT LayoutNGTableSection : public LayoutNGBlock {
+class CORE_EXPORT LayoutNGTableSection : public LayoutBlock {
  public:
   explicit LayoutNGTableSection(Element*);
 
@@ -83,8 +83,7 @@
  protected:
   bool IsOfType(LayoutObjectType type) const override {
     NOT_DESTROYED();
-    return type == kLayoutObjectTableSection ||
-           LayoutNGMixin<LayoutBlock>::IsOfType(type);
+    return type == kLayoutObjectTableSection || LayoutBlock::IsOfType(type);
   }
 
   // Table section paints background specially.
diff --git a/third_party/blink/renderer/core/loader/document_loader.cc b/third_party/blink/renderer/core/loader/document_loader.cc
index 61e8984b..de9a6d9 100644
--- a/third_party/blink/renderer/core/loader/document_loader.cc
+++ b/third_party/blink/renderer/core/loader/document_loader.cc
@@ -1404,6 +1404,7 @@
     bool has_transient_user_activation,
     const SecurityOrigin* initiator_origin,
     bool is_synchronously_committed,
+    Element* source_element,
     mojom::blink::TriggeringEventInfo triggering_event_info,
     bool is_browser_initiated,
     absl::optional<scheduler::TaskAttributionId>
@@ -1470,6 +1471,7 @@
                mojom::blink::TriggeringEventInfo::kFromTrustedEvent) {
       params->involvement = UserNavigationInvolvement::kActivation;
     }
+    params->source_element = source_element;
     params->destination_item = history_item;
     params->is_browser_initiated = is_browser_initiated;
     params->is_synchronously_committed_same_document =
@@ -1931,8 +1933,8 @@
   // render opportunity after activation) since the event is fired as part of
   // updating the rendering which is suppressed until the prerender is
   // activated.
-  if (RuntimeEnabledFeatures::ReadyToRenderEventEnabled()) {
-    document->EnqueueReadyToRenderEvent();
+  if (RuntimeEnabledFeatures::PageRevealEventEnabled()) {
+    document->EnqueuePageRevealEvent();
   }
 }
 
diff --git a/third_party/blink/renderer/core/loader/document_loader.h b/third_party/blink/renderer/core/loader/document_loader.h
index 0949d8d..f6b1dc8 100644
--- a/third_party/blink/renderer/core/loader/document_loader.h
+++ b/third_party/blink/renderer/core/loader/document_loader.h
@@ -103,6 +103,7 @@
 class CodeCacheHost;
 class Document;
 class DocumentParser;
+class Element;
 class Frame;
 class FrameLoader;
 class HistoryItem;
@@ -300,6 +301,7 @@
       bool has_transient_user_activation,
       const SecurityOrigin* initiator_origin,
       bool is_synchronously_committed,
+      Element* source_element,
       mojom::blink::TriggeringEventInfo,
       bool is_browser_initiated,
       absl::optional<scheduler::TaskAttributionId>
diff --git a/third_party/blink/renderer/core/loader/form_submission.cc b/third_party/blink/renderer/core/loader/form_submission.cc
index 5a6fff8..1c6c5c8 100644
--- a/third_party/blink/renderer/core/loader/form_submission.cc
+++ b/third_party/blink/renderer/core/loader/form_submission.cc
@@ -154,7 +154,7 @@
     const KURL& action,
     const AtomicString& target,
     const AtomicString& content_type,
-    HTMLFormElement* form,
+    Element* submitter,
     scoped_refptr<EncodedFormData> data,
     const Event* event,
     NavigationPolicy navigation_policy,
@@ -172,7 +172,7 @@
       action_(action),
       target_(target),
       content_type_(content_type),
-      form_(form),
+      submitter_(submitter),
       form_data_(std::move(data)),
       navigation_policy_(navigation_policy),
       triggering_event_info_(triggering_event_info),
@@ -324,7 +324,11 @@
                                  *resource_request);
   frame_request.SetNavigationPolicy(NavigationPolicyFromEvent(event));
   frame_request.SetClientRedirectReason(reason);
-  frame_request.SetForm(form);
+  if (submit_button) {
+    frame_request.SetSourceElement(submit_button);
+  } else {
+    frame_request.SetSourceElement(form);
+  }
   frame_request.SetTriggeringEventInfo(triggering_event_info);
   AtomicString target_or_base_target = frame_request.CleanNavigationTarget(
       copied_attributes.Target().empty() ? document.BaseTarget()
@@ -365,8 +369,8 @@
 
   return MakeGarbageCollected<FormSubmission>(
       copied_attributes.Method(), action_url, target_or_base_target,
-      encoding_type, form, std::move(form_data), event,
-      frame_request.GetNavigationPolicy(), triggering_event_info, reason,
+      encoding_type, frame_request.GetSourceElement(), std::move(form_data),
+      event, frame_request.GetNavigationPolicy(), triggering_event_info, reason,
       std::move(resource_request), target_frame, load_type,
       form->GetDocument().domWindow(),
       form->GetDocument().GetFrame()->GetLocalFrameToken(),
@@ -378,7 +382,7 @@
 }
 
 void FormSubmission::Trace(Visitor* visitor) const {
-  visitor->Trace(form_);
+  visitor->Trace(submitter_);
   visitor->Trace(target_frame_);
   visitor->Trace(origin_window_);
 }
@@ -387,7 +391,7 @@
   FrameLoadRequest frame_request(origin_window_.Get(), *resource_request_);
   frame_request.SetNavigationPolicy(navigation_policy_);
   frame_request.SetClientRedirectReason(reason_);
-  frame_request.SetForm(form_);
+  frame_request.SetSourceElement(submitter_);
   frame_request.SetTriggeringEventInfo(triggering_event_info_);
   frame_request.SetInitiatorFrameToken(initiator_frame_token_);
   frame_request.SetInitiatorPolicyContainerKeepAliveHandle(
diff --git a/third_party/blink/renderer/core/loader/form_submission.h b/third_party/blink/renderer/core/loader/form_submission.h
index 52cdb5d..bebb9f9 100644
--- a/third_party/blink/renderer/core/loader/form_submission.h
+++ b/third_party/blink/renderer/core/loader/form_submission.h
@@ -44,6 +44,7 @@
 
 namespace blink {
 
+class Element;
 class EncodedFormData;
 class Event;
 class Frame;
@@ -109,7 +110,7 @@
       const KURL& action,
       const AtomicString& target,
       const AtomicString& content_type,
-      HTMLFormElement*,
+      Element* submitter,
       scoped_refptr<EncodedFormData>,
       const Event*,
       NavigationPolicy navigation_policy,
@@ -134,7 +135,6 @@
 
   SubmitMethod Method() const { return method_; }
   const KURL& Action() const { return action_; }
-  HTMLFormElement* Form() const { return form_.Get(); }
   EncodedFormData* Data() const { return form_data_.get(); }
 
   const String& Result() const { return result_; }
@@ -147,7 +147,7 @@
   KURL action_;
   AtomicString target_;
   AtomicString content_type_;
-  Member<HTMLFormElement> form_;
+  Member<Element> submitter_;
   scoped_refptr<EncodedFormData> form_data_;
   NavigationPolicy navigation_policy_;
   mojom::blink::TriggeringEventInfo triggering_event_info_;
diff --git a/third_party/blink/renderer/core/loader/frame_load_request.cc b/third_party/blink/renderer/core/loader/frame_load_request.cc
index c08c6fe..3bba7ed 100644
--- a/third_party/blink/renderer/core/loader/frame_load_request.cc
+++ b/third_party/blink/renderer/core/loader/frame_load_request.cc
@@ -13,6 +13,7 @@
 #include "third_party/blink/renderer/core/events/current_input_event.h"
 #include "third_party/blink/renderer/core/fileapi/public_url_manager.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
+#include "third_party/blink/renderer/core/html/forms/html_form_element.h"
 #include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h"
 #include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
 #include "third_party/blink/renderer/platform/loader/fetch/resource_request.h"
@@ -121,6 +122,16 @@
     const ResourceRequestHead& resource_request_head)
     : FrameLoadRequest(origin_window, ResourceRequest(resource_request_head)) {}
 
+HTMLFormElement* FrameLoadRequest::Form() const {
+  if (IsA<HTMLFormElement>(source_element_)) {
+    return To<HTMLFormElement>(source_element_);
+  }
+  if (IsA<HTMLFormControlElement>(source_element_)) {
+    return To<HTMLFormControlElement>(source_element_)->formOwner();
+  }
+  return nullptr;
+}
+
 bool FrameLoadRequest::CanDisplay(const KURL& url) const {
   DCHECK(!origin_window_ || origin_window_->GetSecurityOrigin() ==
                                 resource_request_.RequestorOrigin());
diff --git a/third_party/blink/renderer/core/loader/frame_load_request.h b/third_party/blink/renderer/core/loader/frame_load_request.h
index 7f39a879..c56a97e 100644
--- a/third_party/blink/renderer/core/loader/frame_load_request.h
+++ b/third_party/blink/renderer/core/loader/frame_load_request.h
@@ -51,6 +51,7 @@
 
 namespace blink {
 
+class Element;
 class HTMLFormElement;
 class LocalDOMWindow;
 class KURL;
@@ -118,8 +119,9 @@
     source_location_ = std::move(source_location);
   }
 
-  HTMLFormElement* Form() const { return form_; }
-  void SetForm(HTMLFormElement* form) { form_ = form; }
+  HTMLFormElement* Form() const;
+  Element* GetSourceElement() const { return source_element_; }
+  void SetSourceElement(Element* element) { source_element_ = element; }
 
   ShouldSendReferrer GetShouldSendReferrer() const {
     return should_send_referrer_;
@@ -229,7 +231,7 @@
   NavigationPolicy navigation_policy_ = kNavigationPolicyCurrentTab;
   mojom::blink::TriggeringEventInfo triggering_event_info_ =
       mojom::blink::TriggeringEventInfo::kNotFromEvent;
-  HTMLFormElement* form_ = nullptr;
+  Element* source_element_ = nullptr;
   ShouldSendReferrer should_send_referrer_;
   scoped_refptr<const DOMWrapperWorld> world_;
   scoped_refptr<base::RefCountedData<mojo::Remote<mojom::blink::BlobURLToken>>>
diff --git a/third_party/blink/renderer/core/loader/frame_loader.cc b/third_party/blink/renderer/core/loader/frame_loader.cc
index aa7882e0..9a4244f6 100644
--- a/third_party/blink/renderer/core/loader/frame_loader.cc
+++ b/third_party/blink/renderer/core/loader/frame_loader.cc
@@ -702,8 +702,8 @@
                                       frame_load_type,
                                       IsOnInitialEmptyDocument()),
         resource_request.HasUserGesture(), origin_window->GetSecurityOrigin(),
-        /*is_synchronously_committed=*/true, request.GetTriggeringEventInfo(),
-        /*is_browser_initiated=*/false,
+        /*is_synchronously_committed=*/true, request.GetSourceElement(),
+        request.GetTriggeringEventInfo(), /*is_browser_initiated=*/false,
         /*soft_navigation_heuristics_task_id=*/absl::nullopt);
     return;
   }
@@ -785,7 +785,7 @@
                              frame_->DomWindow()->GetSecurityOrigin()))) {
     auto* params = MakeGarbageCollected<NavigateEventDispatchParams>(
         url, NavigateEventType::kCrossDocument, frame_load_type);
-    params->form = request.Form();
+    params->source_element = request.GetSourceElement();
     if (request.GetTriggeringEventInfo() ==
         mojom::blink::TriggeringEventInfo::kFromTrustedEvent) {
       params->involvement = UserNavigationInvolvement::kActivation;
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event.cc b/third_party/blink/renderer/core/navigation_api/navigate_event.cc
index b148e013..72a0535 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event.cc
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event.cc
@@ -70,7 +70,8 @@
                 ? init->info()
                 : ScriptValue(context->GetIsolate(),
                               v8::Undefined(context->GetIsolate()))),
-      has_ua_visual_transition_(init->hasUAVisualTransition()) {
+      has_ua_visual_transition_(init->hasUAVisualTransition()),
+      source_element_(init->sourceElement()) {
   CHECK(IsA<LocalDOMWindow>(context));
   CHECK(!controller_ || controller_->signal() == signal_);
 }
@@ -501,6 +502,7 @@
   visitor->Trace(signal_);
   visitor->Trace(form_data_);
   visitor->Trace(info_);
+  visitor->Trace(source_element_);
   visitor->Trace(navigation_action_promises_list_);
   visitor->Trace(navigation_action_handlers_list_);
 }
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event.h b/third_party/blink/renderer/core/navigation_api/navigate_event.h
index 12f89b0d..c260a9c 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event.h
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event.h
@@ -67,6 +67,7 @@
   String downloadRequest() const { return download_request_; }
   ScriptValue info() const { return info_; }
   bool hasUAVisualTransition() const { return has_ua_visual_transition_; }
+  Element* sourceElement() const { return source_element_; }
   void intercept(NavigationInterceptOptions*, ExceptionState&);
   void commit(ExceptionState&);
 
@@ -117,6 +118,7 @@
   String download_request_;
   ScriptValue info_;
   bool has_ua_visual_transition_ = false;
+  Member<Element> source_element_;
   absl::optional<V8NavigationFocusReset> focus_reset_behavior_ = absl::nullopt;
   absl::optional<V8NavigationScrollBehavior> scroll_behavior_ = absl::nullopt;
   absl::optional<V8NavigationCommitBehavior> commit_behavior_ = absl::nullopt;
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event.idl b/third_party/blink/renderer/core/navigation_api/navigate_event.idl
index 3cadeb7..6fb37c3 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event.idl
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event.idl
@@ -19,6 +19,7 @@
   readonly attribute DOMString? downloadRequest;
   readonly attribute any info;
   [RuntimeEnabled=HasUAVisualTransition] readonly attribute boolean hasUAVisualTransition;
+  [RuntimeEnabled=NavigateEventSourceElement] readonly attribute Element? sourceElement;
 
   [RaisesException] void intercept(optional NavigationInterceptOptions options = {});
   [RaisesException, RuntimeEnabled=NavigateEventCommitBehavior] void commit();
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.cc b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.cc
index 95e2bac..4b6bbc1 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.cc
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.cc
@@ -22,7 +22,7 @@
 NavigateEventDispatchParams::~NavigateEventDispatchParams() = default;
 
 void NavigateEventDispatchParams::Trace(Visitor* visitor) const {
-  visitor->Trace(form);
+  visitor->Trace(source_element);
   visitor->Trace(destination_item);
 }
 
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h
index c1d1864c..90708ed 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event_dispatch_params.h
@@ -15,7 +15,7 @@
 
 namespace blink {
 
-class HTMLFormElement;
+class Element;
 class HistoryItem;
 class SerializedScriptValue;
 
@@ -34,7 +34,7 @@
   const NavigateEventType event_type;
   const WebFrameLoadType frame_load_type;
   UserNavigationInvolvement involvement = UserNavigationInvolvement::kNone;
-  Member<HTMLFormElement> form;
+  Member<Element> source_element;
   scoped_refptr<SerializedScriptValue> state_object;
   Member<HistoryItem> destination_item;
   bool is_browser_initiated = false;
diff --git a/third_party/blink/renderer/core/navigation_api/navigate_event_init.idl b/third_party/blink/renderer/core/navigation_api/navigate_event_init.idl
index b190fda..3d894dc 100644
--- a/third_party/blink/renderer/core/navigation_api/navigate_event_init.idl
+++ b/third_party/blink/renderer/core/navigation_api/navigate_event_init.idl
@@ -22,4 +22,5 @@
   DOMString? downloadRequest = null;
   any info;
   boolean hasUAVisualTransition = false;
+  Element? sourceElement = null;
 };
diff --git a/third_party/blink/renderer/core/navigation_api/navigation_api.cc b/third_party/blink/renderer/core/navigation_api/navigation_api.cc
index fe4313d..53fd70b1 100644
--- a/third_party/blink/renderer/core/navigation_api/navigation_api.cc
+++ b/third_party/blink/renderer/core/navigation_api/navigation_api.cc
@@ -746,8 +746,18 @@
 
   init->setUserInitiated(params->involvement !=
                          UserNavigationInvolvement::kNone);
-  if (params->form && params->form->Method() == FormSubmission::kPostMethod) {
-    init->setFormData(FormData::Create(params->form, ASSERT_NO_EXCEPTION));
+  if (params->source_element) {
+    HTMLFormElement* form =
+        DynamicTo<HTMLFormElement>(params->source_element.Get());
+    if (!form) {
+      if (auto* control =
+              DynamicTo<HTMLFormControlElement>(params->source_element.Get())) {
+        form = control->formOwner();
+      }
+    }
+    if (form && form->Method() == FormSubmission::kPostMethod) {
+      init->setFormData(FormData::Create(form, ASSERT_NO_EXCEPTION));
+    }
   }
   if (ongoing_api_method_tracker_) {
     init->setInfo(ongoing_api_method_tracker_->GetInfo());
@@ -755,6 +765,10 @@
   auto* controller = AbortController::Create(script_state);
   init->setSignal(controller->signal());
   init->setDownloadRequest(params->download_filename);
+  if (params->source_element &&
+      params->source_element->GetExecutionContext() == window_) {
+    init->setSourceElement(params->source_element);
+  }
   // This unique_ptr needs to be in the function's scope, to maintain the
   // SoftNavigationEventScope until the event handler runs.
   std::unique_ptr<SoftNavigationEventScope> soft_navigation_scope;
diff --git a/third_party/blink/renderer/core/navigation_api/navigation_api_test.cc b/third_party/blink/renderer/core/navigation_api/navigation_api_test.cc
index fb9fa50..b0e63e0 100644
--- a/third_party/blink/renderer/core/navigation_api/navigation_api_test.cc
+++ b/third_party/blink/renderer/core/navigation_api/navigation_api_test.cc
@@ -92,7 +92,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result1, mojom::blink::CommitResult::Ok);
@@ -105,7 +105,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result2, mojom::blink::CommitResult::Aborted);
@@ -116,7 +116,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result3, mojom::blink::CommitResult::Ok);
@@ -152,7 +152,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       /*has_transient_user_activation=*/false, /*initiator_origin=*/nullptr,
-      /*is_synchronously_committed=*/false,
+      /*is_synchronously_committed=*/false, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       /*is_browser_initiated=*/true,
       /*soft_navigation_heuristics_task_id=*/absl::nullopt);
@@ -185,7 +185,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result1, mojom::blink::CommitResult::Ok);
@@ -198,7 +198,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result2, mojom::blink::CommitResult::Aborted);
@@ -209,7 +209,7 @@
       url, WebFrameLoadType::kBackForward, MakeHistoryItemFor(url, key),
       ClientRedirectPolicy::kNotClientRedirect,
       false /* has_transient_user_activation */, nullptr /* initiator_origin */,
-      false /* is_synchronously_committed */,
+      false /* is_synchronously_committed */, /*source_element=*/nullptr,
       mojom::blink::TriggeringEventInfo::kNotFromEvent,
       true /* is_browser_initiated */, absl::nullopt);
   EXPECT_EQ(result3, mojom::blink::CommitResult::Ok);
diff --git a/third_party/blink/renderer/core/page/page_animator.cc b/third_party/blink/renderer/core/page/page_animator.cc
index 222dcb2..8054c55 100644
--- a/third_party/blink/renderer/core/page/page_animator.cc
+++ b/third_party/blink/renderer/core/page/page_animator.cc
@@ -145,14 +145,14 @@
 
   // TODO(bokan): Requires an update to "update the rendering" steps in HTML.
   run_for_all_active_controllers_with_timing([&](wtf_size_t i) {
-    if (RuntimeEnabledFeatures::ReadyToRenderEventEnabled()) {
+    if (RuntimeEnabledFeatures::PageRevealEventEnabled()) {
       active_controllers[i]->DispatchEvents([](const Event* event) {
-        return event->type() == event_type_names::kReadytorender;
+        return event->type() == event_type_names::kPagereveal;
       });
     }
 
     if (RuntimeEnabledFeatures::ViewTransitionOnNavigationEnabled()) {
-      CHECK(RuntimeEnabledFeatures::ReadyToRenderEventEnabled());
+      CHECK(RuntimeEnabledFeatures::PageRevealEventEnabled());
       if (const LocalDOMWindow* window = active_controllers[i]->GetWindow()) {
         CHECK(window->document());
         if (ViewTransition* transition =
diff --git a/third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.cc b/third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.cc
index acc174c..fcd2a6f 100644
--- a/third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.cc
+++ b/third_party/blink/renderer/core/paint/ng/ng_box_fragment_painter.cc
@@ -20,7 +20,6 @@
 #include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h"
 #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h"
 #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h"
-#include "third_party/blink/renderer/core/layout/ng/layout_ng_mixin.h"
 #include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h"
 #include "third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h"
 #include "third_party/blink/renderer/core/layout/ng/ng_outline_utils.h"
diff --git a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
index 785a3dcd..2b74486 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.cc
@@ -528,13 +528,7 @@
 
   if (IsExplicitScrollType(scroll_type) ||
       scroll_type == mojom::blink::ScrollType::kScrollStart) {
-    // We don't need to show scrollbars for kCompositor scrolls unless the
-    // scrollbar is non-composited (!NeedsCompositorScrolling). See
-    // PaintLayerScrollableArea::ShouldDirectlyCompositeScrollbar.
-    if (scroll_type != mojom::blink::ScrollType::kCompositor ||
-        !NeedsCompositedScrolling()) {
-      ShowNonMacOverlayScrollbars();
-    }
+    ShowNonMacOverlayScrollbars();
     GetScrollAnchor()->Clear();
   }
   if (ContentCaptureManager* manager = frame_view->GetFrame()
diff --git a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h
index 2e472f5a..e7f43a9 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h
+++ b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h
@@ -428,15 +428,12 @@
   // Rectangle encompassing the scroll corner and resizer rect.
   gfx::Rect ScrollCornerAndResizerRect() const;
 
-  // The difference between this function and NeedsCompositedScrolling() is
-  // that this function returns the composited scrolling status based on paint
-  // properties which are updated based on the latter.
+  // Returns true if the scroll node is currently composited in cc.
   bool UsesCompositedScrolling() const override;
 
   // In CompositeScrollAfterPaint, NeedsCompositedScrolling() is false if
   // composited scrolling will be determined after paint.
-  // TODO(crbug.com/1414885): We may need to redefine these functions for
-  // CompositeScrollAfterPaint.
+  // TODO(crbug.com/1414885): Remove these functions.
   void UpdateNeedsCompositedScrolling(
       bool force_prefer_compositing_to_lcd_text);
   bool NeedsCompositedScrolling() const { return needs_composited_scrolling_; }
diff --git a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area_test.cc b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area_test.cc
index ad6803c2..92c87079 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_scrollable_area_test.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_scrollable_area_test.cc
@@ -1170,10 +1170,9 @@
 
   EXPECT_TRUE(scrollable_area->ScrollbarsHiddenIfOverlay());
 
-  // This will be false because
-  // cc::MainThreadScrollingReason::kNotOpaqueForTextAndLCDText. See
-  // PaintLayerScrollableArea::ComputeNeedsCompositedScrollingInternal.
-  EXPECT_FALSE(scrollable_area->NeedsCompositedScrolling());
+  // This is false because we prefer LCD-text by default and the scroller
+  // doesn't have an opaque background to preserve LCD-text if composited.
+  EXPECT_FALSE(scrollable_area->UsesCompositedScrolling());
 
   scrollable_area->SetScrollOffset(ScrollOffset(0, 20),
                                    mojom::blink::ScrollType::kCompositor);
diff --git a/third_party/blink/renderer/core/scroll/scroll_animator_test.cc b/third_party/blink/renderer/core/scroll/scroll_animator_test.cc
index 611be86..d03877f 100644
--- a/third_party/blink/renderer/core/scroll/scroll_animator_test.cc
+++ b/third_party/blink/renderer/core/scroll/scroll_animator_test.cc
@@ -88,6 +88,7 @@
   MOCK_METHOD0(ScheduleAnimation, bool());
   MOCK_CONST_METHOD0(UsedColorSchemeScrollbars, mojom::blink::ColorScheme());
 
+  bool UsesCompositedScrolling() const override { NOTREACHED_NORETURN(); }
   bool UserInputScrollable(ScrollbarOrientation) const override { return true; }
   bool ShouldPlaceVerticalScrollbarOnLeft() const override { return false; }
   gfx::Vector2d ScrollOffsetInt() const override { return gfx::Vector2d(); }
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.cc b/third_party/blink/renderer/core/scroll/scrollable_area.cc
index f3cfe1a..55b02515 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.cc
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.cc
@@ -997,6 +997,9 @@
 
   // Don't do this for composited scrollbars. These scrollbars are handled
   // by separate code in cc::ScrollbarAnimationController.
+  // TODO(crbug.com/1229864): We may want to always composite overlay
+  // scrollbars to avoid the bug and the duplicated code for composited and
+  // non-composited overlay scrollbars.
   if (UsesCompositedScrolling())
     return;
 
diff --git a/third_party/blink/renderer/core/scroll/scrollable_area.h b/third_party/blink/renderer/core/scroll/scrollable_area.h
index 495ef04..c7e5d5c 100644
--- a/third_party/blink/renderer/core/scroll/scrollable_area.h
+++ b/third_party/blink/renderer/core/scroll/scrollable_area.h
@@ -411,10 +411,7 @@
   virtual void RegisterForAnimation() {}
   virtual void DeregisterForAnimation() {}
 
-  virtual bool UsesCompositedScrolling() const {
-    NOTREACHED();
-    return false;
-  }
+  virtual bool UsesCompositedScrolling() const = 0;
   virtual bool ShouldScrollOnMainThread() const { return false; }
 
   // Overlay scrollbars can "fade-out" when inactive. This value should only be
diff --git a/third_party/blink/renderer/core/streams/test_underlying_source.h b/third_party/blink/renderer/core/streams/test_underlying_source.h
index 2a17d99..5ff131ff 100644
--- a/third_party/blink/renderer/core/streams/test_underlying_source.h
+++ b/third_party/blink/renderer/core/streams/test_underlying_source.h
@@ -31,7 +31,9 @@
     is_start_called_ = true;
     return ScriptPromise::CastUndefined(script_state);
   }
-  ScriptPromise Cancel(ScriptState* script_state, ScriptValue reason) override {
+  ScriptPromise Cancel(ScriptState* script_state,
+                       ScriptValue reason,
+                       ExceptionState&) override {
     DCHECK(!is_cancelled_);
     DCHECK(!is_cancelled_with_undefined_);
     DCHECK(!is_cancelled_with_null_);
diff --git a/third_party/blink/renderer/core/streams/transferable_streams.cc b/third_party/blink/renderer/core/streams/transferable_streams.cc
index 5efef52..48441a9 100644
--- a/third_party/blink/renderer/core/streams/transferable_streams.cc
+++ b/third_party/blink/renderer/core/streams/transferable_streams.cc
@@ -900,12 +900,12 @@
     return promise->GetScriptPromise(script_state);
   }
 
-  ScriptPromise Cancel(ScriptState* script_state, ScriptValue reason) override {
+  ScriptPromise Cancel(ScriptState* script_state,
+                       ScriptValue reason,
+                       ExceptionState& exception_state) override {
     if (has_finished_reading_stream1_) {
-      return source2_->Cancel(script_state, reason);
+      return source2_->Cancel(script_state, reason, exception_state);
     }
-    ExceptionState exception_state(script_state->GetIsolate(),
-                                   ExceptionContextType::kUnknown, "", "");
     ScriptPromise cancel_promise1 =
         reader_for_stream1_->cancel(script_state, reason, exception_state);
     if (exception_state.HadException()) {
diff --git a/third_party/blink/renderer/core/streams/transferable_streams_test.cc b/third_party/blink/renderer/core/streams/transferable_streams_test.cc
index 97bf9b1..f4fe6b6 100644
--- a/third_party/blink/renderer/core/streams/transferable_streams_test.cc
+++ b/third_party/blink/renderer/core/streams/transferable_streams_test.cc
@@ -78,7 +78,9 @@
     ++index_;
     return ScriptPromise::CastUndefined(script_state);
   }
-  ScriptPromise Cancel(ScriptState* script_state, ScriptValue reason) override {
+  ScriptPromise Cancel(ScriptState* script_state,
+                       ScriptValue reason,
+                       ExceptionState&) override {
     cancelled_ = true;
     cancel_reason_ = reason;
     return ScriptPromise::CastUndefined(script_state);
diff --git a/third_party/blink/renderer/core/streams/underlying_source_base.cc b/third_party/blink/renderer/core/streams/underlying_source_base.cc
index fb5d0f55..f059a9e 100644
--- a/third_party/blink/renderer/core/streams/underlying_source_base.cc
+++ b/third_party/blink/renderer/core/streams/underlying_source_base.cc
@@ -34,21 +34,27 @@
   return ScriptPromise::CastUndefined(script_state);
 }
 
-ScriptPromise UnderlyingSourceBase::cancelWrapper(ScriptState* script_state) {
+ScriptPromise UnderlyingSourceBase::cancelWrapper(
+    ScriptState* script_state,
+    ExceptionState& exception_state) {
   v8::Isolate* isolate = script_state->GetIsolate();
   return cancelWrapper(script_state,
-                       ScriptValue(isolate, v8::Undefined(isolate)));
+                       ScriptValue(isolate, v8::Undefined(isolate)),
+                       exception_state);
 }
 
-ScriptPromise UnderlyingSourceBase::cancelWrapper(ScriptState* script_state,
-                                                  ScriptValue reason) {
+ScriptPromise UnderlyingSourceBase::cancelWrapper(
+    ScriptState* script_state,
+    ScriptValue reason,
+    ExceptionState& exception_state) {
   DCHECK(controller_);  // startWrapper() must have been called
   controller_->Deactivate();
-  return Cancel(script_state, reason);
+  return Cancel(script_state, reason, exception_state);
 }
 
 ScriptPromise UnderlyingSourceBase::Cancel(ScriptState* script_state,
-                                           ScriptValue reason) {
+                                           ScriptValue reason,
+                                           ExceptionState&) {
   return ScriptPromise::CastUndefined(script_state);
 }
 
diff --git a/third_party/blink/renderer/core/streams/underlying_source_base.h b/third_party/blink/renderer/core/streams/underlying_source_base.h
index 8e44407..7c2bcf7a 100644
--- a/third_party/blink/renderer/core/streams/underlying_source_base.h
+++ b/third_party/blink/renderer/core/streams/underlying_source_base.h
@@ -34,9 +34,13 @@
 
   virtual ScriptPromise pull(ScriptState*);
 
-  ScriptPromise cancelWrapper(ScriptState*);
-  ScriptPromise cancelWrapper(ScriptState*, ScriptValue reason);
-  virtual ScriptPromise Cancel(ScriptState*, ScriptValue reason);
+  ScriptPromise cancelWrapper(ScriptState*, ExceptionState&);
+  ScriptPromise cancelWrapper(ScriptState*,
+                              ScriptValue reason,
+                              ExceptionState&);
+  virtual ScriptPromise Cancel(ScriptState*,
+                               ScriptValue reason,
+                               ExceptionState&);
 
   ScriptValue type(ScriptState*) const;
 
diff --git a/third_party/blink/renderer/core/streams/underlying_source_base.idl b/third_party/blink/renderer/core/streams/underlying_source_base.idl
index 337733c..f515abc 100644
--- a/third_party/blink/renderer/core/streams/underlying_source_base.idl
+++ b/third_party/blink/renderer/core/streams/underlying_source_base.idl
@@ -13,7 +13,7 @@
 interface UnderlyingSourceBase {
     [CallWith=ScriptState, ImplementedAs=startWrapper] Promise<void> start(ReadableStreamDefaultController stream);
     [CallWith=ScriptState] Promise<void> pull();
-    [CallWith=ScriptState, ImplementedAs=cancelWrapper] Promise<void> cancel(optional any reason);
+    [CallWith=ScriptState, RaisesException, ImplementedAs=cancelWrapper] Promise<void> cancel(optional any reason);
 
     // This only exists to prevent Object.prototype.type being accessed.
     [CallWith=ScriptState] readonly attribute any type;
diff --git a/third_party/blink/renderer/core/svg/svg_a_element.cc b/third_party/blink/renderer/core/svg/svg_a_element.cc
index a6d25453..2ff155f 100644
--- a/third_party/blink/renderer/core/svg/svg_a_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_a_element.cc
@@ -141,6 +141,7 @@
       frame_request.SetNavigationPolicy(NavigationPolicyFromEvent(&event));
       frame_request.SetClientRedirectReason(
           ClientNavigationReason::kAnchorClick);
+      frame_request.SetSourceElement(this);
       frame_request.SetTriggeringEventInfo(
           event.isTrusted()
               ? mojom::blink::TriggeringEventInfo::kFromTrustedEvent
diff --git a/third_party/blink/renderer/core/view_transition/build.gni b/third_party/blink/renderer/core/view_transition/build.gni
index bf9dd769..7e1dbcfb 100644
--- a/third_party/blink/renderer/core/view_transition/build.gni
+++ b/third_party/blink/renderer/core/view_transition/build.gni
@@ -3,8 +3,10 @@
 # found in the LICENSE file.
 
 blink_core_sources_view_transition = [
-  "ready_to_render_event.cc",
-  "ready_to_render_event.h",
+  "dom_view_transition.cc",
+  "dom_view_transition.h",
+  "page_reveal_event.cc",
+  "page_reveal_event.h",
   "view_transition.cc",
   "view_transition.h",
   "view_transition_content_element.cc",
diff --git a/third_party/blink/renderer/core/view_transition/dom_view_transition.cc b/third_party/blink/renderer/core/view_transition/dom_view_transition.cc
new file mode 100644
index 0000000..130025b
--- /dev/null
+++ b/third_party/blink/renderer/core/view_transition/dom_view_transition.cc
@@ -0,0 +1,276 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/view_transition/dom_view_transition.h"
+
+#include "third_party/blink/renderer/bindings/core/v8/capture_source_location.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_property.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/dom/dom_exception.h"
+#include "third_party/blink/renderer/core/events/error_event.h"
+#include "third_party/blink/renderer/core/execution_context/agent.h"
+#include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
+
+namespace blink {
+
+namespace {
+
+const char kAbortedMessage[] = "Transition was skipped";
+const char kInvalidStateMessage[] =
+    "Transition was aborted because of invalid state";
+const char kTimeoutMessage[] =
+    "Transition was aborted because of timeout in DOM update";
+
+}  // namespace
+
+DOMViewTransition::DOMViewTransition(
+    ExecutionContext& execution_context,
+    ViewTransition& view_transition,
+    ScriptState& script_state,
+    V8ViewTransitionCallback* update_dom_callback)
+    : ActiveScriptWrappable<DOMViewTransition>({}),
+      execution_context_(&execution_context),
+      view_transition_{&view_transition},
+      script_state_(&script_state),
+      update_dom_callback_(update_dom_callback),
+      finished_promise_property_(
+          MakeGarbageCollected<PromiseProperty>(execution_context_)),
+      ready_promise_property_(
+          MakeGarbageCollected<PromiseProperty>(execution_context_)),
+      dom_updated_promise_property_(
+          MakeGarbageCollected<PromiseProperty>(execution_context_)) {
+  CHECK(execution_context_->GetAgent());
+}
+
+DOMViewTransition::~DOMViewTransition() = default;
+
+void DOMViewTransition::skipTransition() {
+  view_transition_->SkipTransition();
+}
+
+ScriptPromise DOMViewTransition::finished() const {
+  return finished_promise_property_->Promise(script_state_->World());
+}
+
+ScriptPromise DOMViewTransition::ready() const {
+  return ready_promise_property_->Promise(script_state_->World());
+}
+
+ScriptPromise DOMViewTransition::updateCallbackDone() const {
+  return dom_updated_promise_property_->Promise(script_state_->World());
+}
+
+void DOMViewTransition::DidSkipTransition(
+    ViewTransition::PromiseResponse response) {
+  CHECK_NE(response, ViewTransition::PromiseResponse::kResolve);
+
+  // If the ready promise has not yet been resolved, reject it.
+  if (ready_promise_property_->GetState() == PromiseProperty::State::kPending) {
+    AtMicrotask(response, ready_promise_property_);
+  }
+
+  // If we haven't run the dom change callback yet, schedule a task to do so.
+  // The finished promise will propagate the result of the updateCallbackDone
+  // promise when this callback runs.
+  if (!dom_callback_result_) {
+    execution_context_->GetTaskRunner(TaskType::kMiscPlatformAPI)
+        ->PostTask(
+            FROM_HERE,
+            WTF::BindOnce(
+                base::IgnoreResult(&DOMViewTransition::InvokeDOMChangeCallback),
+                WrapPersistent(this)));
+  } else if (dom_callback_result_ != DOMCallbackResult::kRunning) {
+    // If the DOM callback finished and there was a failure then the finished
+    // promise should have been rejected with updateCallbackDone.
+    if (dom_callback_result_ == DOMCallbackResult::kFailed) {
+      CHECK_EQ(finished_promise_property_->GetState(),
+               PromiseProperty::State::kRejected);
+    } else {
+      CHECK_EQ(*dom_callback_result_, DOMCallbackResult::kFinished);
+      // But if the callback was successful, we need to resolve the finished
+      // promise while skipping the transition.
+      AtMicrotask(ViewTransition::PromiseResponse::kResolve,
+                  finished_promise_property_);
+    }
+  }
+}
+
+void DOMViewTransition::NotifyDOMCallbackFinished(bool success,
+                                                  ScriptValue value) {
+  CHECK_EQ(*dom_callback_result_, DOMCallbackResult::kRunning);
+  // Handle all promises which depend on this callback.
+  if (success) {
+    dom_updated_promise_property_->ResolveWithUndefined();
+
+    // If we're already at the terminal state, the transition was skipped before
+    // the callback finished. Also handle the finish promise.
+    if (view_transition_->IsDone()) {
+      finished_promise_property_->ResolveWithUndefined();
+    }
+  } else {
+    dom_updated_promise_property_->Reject(value);
+
+    // The ready promise rejects with the value of updateCallbackDone callback
+    // if it's skipped because of an error in the callback.
+    if (!view_transition_->IsDone()) {
+      ready_promise_property_->Reject(value);
+    }
+
+    // If the domUpdate callback fails the transition is skipped. The finish
+    // promise should mirror the result of updateCallbackDone.
+    finished_promise_property_->Reject(value);
+  }
+
+  dom_callback_result_ =
+      success ? DOMCallbackResult::kFinished : DOMCallbackResult::kFailed;
+  view_transition_->NotifyDOMCallbackFinished(success);
+}
+
+void DOMViewTransition::DidStartAnimating() {
+  AtMicrotask(ViewTransition::PromiseResponse::kResolve,
+              ready_promise_property_);
+}
+
+void DOMViewTransition::DidFinishAnimating() {
+  AtMicrotask(ViewTransition::PromiseResponse::kResolve,
+              finished_promise_property_);
+}
+
+DOMViewTransition::DOMCallbackResult
+DOMViewTransition::InvokeDOMChangeCallback() {
+  CHECK(!dom_callback_result_) << "UpdateDOM callback invoked multiple times.";
+
+  if (!update_dom_callback_) {
+    // TODO(bokan): The no-callback case doesn't need to be special, it should
+    // go through the NotifyDOMCallbackFinished flow.
+    dom_callback_result_ = DOMCallbackResult::kFinished;
+    AtMicrotask(ViewTransition::PromiseResponse::kResolve,
+                dom_updated_promise_property_);
+
+    // If we're already at the terminal state, the dom update callback was
+    // scheduled to run after the transition was skipped.
+    if (view_transition_->IsDone()) {
+      AtMicrotask(ViewTransition::PromiseResponse::kResolve,
+                  finished_promise_property_);
+    }
+    return *dom_callback_result_;
+  }
+
+  v8::Maybe<ScriptPromise> result = update_dom_callback_->Invoke(nullptr);
+
+  // TODO(vmpstr): Should this be a DCHECK?
+  if (result.IsNothing()) {
+    dom_callback_result_ = DOMCallbackResult::kFailed;
+    AtMicrotask(ViewTransition::PromiseResponse::kRejectAbort,
+                dom_updated_promise_property_);
+    AtMicrotask(ViewTransition::PromiseResponse::kRejectAbort,
+                finished_promise_property_);
+    return *dom_callback_result_;
+  }
+
+  dom_callback_result_ = DOMCallbackResult::kRunning;
+
+  ScriptState::Scope scope(script_state_);
+
+  result.ToChecked().Then(
+      MakeGarbageCollected<ScriptFunction>(
+          script_state_,
+          MakeGarbageCollected<DOMChangeFinishedCallback>(*this, true)),
+      MakeGarbageCollected<ScriptFunction>(
+          script_state_,
+          MakeGarbageCollected<DOMChangeFinishedCallback>(*this, false)));
+
+  return *dom_callback_result_;
+}
+
+bool DOMViewTransition::HasPendingActivity() const {
+  return !view_transition_->IsDone();
+}
+
+void DOMViewTransition::Trace(Visitor* visitor) const {
+  visitor->Trace(execution_context_);
+  visitor->Trace(view_transition_);
+  visitor->Trace(script_state_);
+  visitor->Trace(update_dom_callback_);
+  visitor->Trace(finished_promise_property_);
+  visitor->Trace(ready_promise_property_);
+  visitor->Trace(dom_updated_promise_property_);
+
+  ScriptWrappable::Trace(visitor);
+}
+
+void DOMViewTransition::AtMicrotask(ViewTransition::PromiseResponse response,
+                                    PromiseProperty* property) {
+  execution_context_->GetAgent()->event_loop()->EnqueueMicrotask(
+      WTF::BindOnce(&DOMViewTransition::HandlePromise, WrapPersistent(this),
+                    response, WrapPersistent(property)));
+}
+
+void DOMViewTransition::HandlePromise(ViewTransition::PromiseResponse response,
+                                      PromiseProperty* property) {
+  DCHECK_EQ(property->GetState(), PromiseProperty::State::kPending);
+  if (!script_state_->ContextIsValid()) {
+    return;
+  }
+
+  switch (response) {
+    case ViewTransition::PromiseResponse::kResolve:
+      property->ResolveWithUndefined();
+      break;
+    case ViewTransition::PromiseResponse::kRejectAbort: {
+      ScriptState::Scope scope(script_state_);
+      auto value = ScriptValue::From(
+          script_state_, MakeGarbageCollected<DOMException>(
+                             DOMExceptionCode::kAbortError, kAbortedMessage));
+      property->Reject(value);
+      break;
+    }
+    case ViewTransition::PromiseResponse::kRejectInvalidState: {
+      ScriptState::Scope scope(script_state_);
+      auto value = ScriptValue::From(
+          script_state_,
+          MakeGarbageCollected<DOMException>(
+              DOMExceptionCode::kInvalidStateError, kInvalidStateMessage));
+      property->Reject(value);
+      break;
+    }
+    case ViewTransition::PromiseResponse::kRejectTimeout: {
+      ScriptState::Scope scope(script_state_);
+      auto value = ScriptValue::From(
+          script_state_, MakeGarbageCollected<DOMException>(
+                             DOMExceptionCode::kTimeoutError, kTimeoutMessage));
+      property->Reject(value);
+      break;
+    }
+  }
+}
+
+// DOMChangeFinishedCallback implementation.
+DOMViewTransition::DOMChangeFinishedCallback::DOMChangeFinishedCallback(
+    DOMViewTransition& dom_view_transition,
+    bool success)
+    : dom_view_transition_(&dom_view_transition), success_(success) {}
+
+DOMViewTransition::DOMChangeFinishedCallback::~DOMChangeFinishedCallback() =
+    default;
+
+ScriptValue DOMViewTransition::DOMChangeFinishedCallback::Call(
+    ScriptState* script_state,
+    ScriptValue value) {
+  dom_view_transition_->NotifyDOMCallbackFinished(success_, std::move(value));
+  return ScriptValue();
+}
+
+void DOMViewTransition::DOMChangeFinishedCallback::Trace(
+    Visitor* visitor) const {
+  ScriptFunction::Callable::Trace(visitor);
+  visitor->Trace(dom_view_transition_);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/view_transition/dom_view_transition.h b/third_party/blink/renderer/core/view_transition/dom_view_transition.h
new file mode 100644
index 0000000..0dbe4a4
--- /dev/null
+++ b/third_party/blink/renderer/core/view_transition/dom_view_transition.h
@@ -0,0 +1,120 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_DOM_VIEW_TRANSITION_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_DOM_VIEW_TRANSITION_H_
+
+#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_property.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_view_transition_callback.h"
+#include "third_party/blink/renderer/core/core_export.h"
+#include "third_party/blink/renderer/core/view_transition/view_transition.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
+#include "third_party/blink/renderer/platform/heap/member.h"
+
+namespace blink {
+
+class ExecutionContext;
+class ScriptState;
+class ViewTransition;
+
+// This class handles script interaction for the ViewTransition object. It
+// implements the ViewTransition IDL interface.
+class CORE_EXPORT DOMViewTransition
+    : public ScriptWrappable,
+      public ActiveScriptWrappable<DOMViewTransition> {
+  DEFINE_WRAPPERTYPEINFO();
+
+  using PromiseProperty =
+      ScriptPromiseProperty<ToV8UndefinedGenerator, ScriptValue>;
+
+ public:
+  explicit DOMViewTransition(ExecutionContext&,
+                             ViewTransition&,
+                             ScriptState&,
+                             V8ViewTransitionCallback*);
+
+  ~DOMViewTransition() override;
+
+  // IDL implementation. Refer to view_transition.idl for additional comments.
+  void skipTransition();
+  ScriptPromise finished() const;
+  ScriptPromise ready() const;
+  ScriptPromise updateCallbackDone() const;
+
+  // Called from ViewTransition when the transition is skipped/aborted for any
+  // reason.
+  void DidSkipTransition(ViewTransition::PromiseResponse);
+
+  // Called just after the associated ViewTransition advances into the
+  // kAnimating state but before any animation frames have been produced.
+  void DidStartAnimating();
+  // Called just before the associated ViewTransition advances from kAnimating
+  // to kFinished state but before any finalization has run.
+  void DidFinishAnimating();
+
+  // Returns the result of invoking the callback.
+  // kFailed: Indicates that there was a failure in running the callback and the
+  //          transition should be skipped.
+  // kFinished: Indicates that there was no callback to run so we can move to
+  //            finished state synchronously.
+  // kRunning: Indicates that the callback is in running state. Note that even
+  //           if the callback is synchronous, the notification that it has
+  //           finished running is async.
+  enum class DOMCallbackResult { kFailed, kFinished, kRunning };
+  DOMCallbackResult InvokeDOMChangeCallback();
+
+  // ActiveScriptWrappable functionality.
+  // TODO(bokan): `this` doesn't actually need to be ActiveScriptWrappable but
+  // is used to `view_transition_` alive in the face of the Viz callback. Could
+  // ViewTransition more explicitly manage its lifetime?
+  bool HasPendingActivity() const override;
+  ExecutionContext* GetExecutionContext() const { return execution_context_; }
+
+  ViewTransition* GetViewTransitionForTest() { return view_transition_; }
+
+  void Trace(Visitor* visitor) const override;
+
+ private:
+  void AtMicrotask(ViewTransition::PromiseResponse response,
+                   PromiseProperty* resolver);
+  void HandlePromise(ViewTransition::PromiseResponse response,
+                     PromiseProperty* property);
+
+  void NotifyDOMCallbackFinished(bool success, ScriptValue value);
+
+  // Invoked when ViewTransitionCallback finishes running.
+  class DOMChangeFinishedCallback : public ScriptFunction::Callable {
+   public:
+    explicit DOMChangeFinishedCallback(DOMViewTransition&, bool success);
+    ~DOMChangeFinishedCallback() override;
+
+    ScriptValue Call(ScriptState*, ScriptValue) override;
+    void Trace(Visitor*) const override;
+
+   private:
+    Member<DOMViewTransition> dom_view_transition_;
+    const bool success_;
+  };
+
+  Member<ExecutionContext> execution_context_;
+
+  Member<ViewTransition> view_transition_;
+
+  Member<ScriptState> script_state_;
+
+  Member<V8ViewTransitionCallback> update_dom_callback_;
+  Member<PromiseProperty> finished_promise_property_;
+  Member<PromiseProperty> ready_promise_property_;
+  Member<PromiseProperty> dom_updated_promise_property_;
+
+  // The result of running the `update_dom_callback_`. This is set from
+  // InvokeDOMChangeCallback and is empty until then.
+  absl::optional<DOMCallbackResult> dom_callback_result_;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_DOM_VIEW_TRANSITION_H_
diff --git a/third_party/blink/renderer/core/view_transition/page_reveal_event.cc b/third_party/blink/renderer/core/view_transition/page_reveal_event.cc
new file mode 100644
index 0000000..9dfa8af
--- /dev/null
+++ b/third_party/blink/renderer/core/view_transition/page_reveal_event.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/view_transition/page_reveal_event.h"
+
+#include "third_party/blink/renderer/core/event_interface_names.h"
+#include "third_party/blink/renderer/core/event_type_names.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+
+namespace blink {
+
+PageRevealEvent::PageRevealEvent()
+    : Event(event_type_names::kPagereveal, Bubbles::kNo, Cancelable::kNo) {
+  CHECK(RuntimeEnabledFeatures::PageRevealEventEnabled());
+}
+
+PageRevealEvent::~PageRevealEvent() = default;
+
+const AtomicString& PageRevealEvent::InterfaceName() const {
+  return event_interface_names::kPageRevealEvent;
+}
+
+void PageRevealEvent::Trace(Visitor* visitor) const {
+  Event::Trace(visitor);
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/core/view_transition/page_reveal_event.h b/third_party/blink/renderer/core/view_transition/page_reveal_event.h
new file mode 100644
index 0000000..dffac9c0
--- /dev/null
+++ b/third_party/blink/renderer/core/view_transition/page_reveal_event.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_PAGE_REVEAL_EVENT_H_
+#define THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_PAGE_REVEAL_EVENT_H_
+
+#include "third_party/blink/renderer/core/dom/events/event.h"
+
+namespace blink {
+
+// Implementation for the pagereveal event. Fired before the first
+// rendering update after a Document is activated (loaded, restored from
+// BFCache, prerender activated).
+// TODO(bokan): Update spec link once it's settled.
+// https://drafts.csswg.org/css-view-transitions-2/#reveal-event
+class PageRevealEvent final : public Event {
+  DEFINE_WRAPPERTYPEINFO();
+
+ public:
+  PageRevealEvent();
+  ~PageRevealEvent() override;
+
+  const AtomicString& InterfaceName() const override;
+
+  void Trace(Visitor*) const override;
+};
+
+}  // namespace blink
+
+#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_PAGE_REVEAL_EVENT_H_
diff --git a/third_party/blink/renderer/core/view_transition/ready_to_render_event.idl b/third_party/blink/renderer/core/view_transition/page_reveal_event.idl
similarity index 79%
rename from third_party/blink/renderer/core/view_transition/ready_to_render_event.idl
rename to third_party/blink/renderer/core/view_transition/page_reveal_event.idl
index 638efd6c0..e0848e0 100644
--- a/third_party/blink/renderer/core/view_transition/ready_to_render_event.idl
+++ b/third_party/blink/renderer/core/view_transition/page_reveal_event.idl
@@ -6,7 +6,7 @@
 
 [
   Exposed=Window,
-  RuntimeEnabled=ReadyToRenderEvent
-] interface ReadyToRenderEvent : Event {
+  RuntimeEnabled=PageRevealEvent
+] interface PageRevealEvent : Event {
   // TODO(crbug.com/1466250): Expose viewTransition property.
 };
diff --git a/third_party/blink/renderer/core/view_transition/ready_to_render_event.cc b/third_party/blink/renderer/core/view_transition/ready_to_render_event.cc
deleted file mode 100644
index 873e6041..0000000
--- a/third_party/blink/renderer/core/view_transition/ready_to_render_event.cc
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "third_party/blink/renderer/core/view_transition/ready_to_render_event.h"
-
-#include "third_party/blink/renderer/core/event_interface_names.h"
-#include "third_party/blink/renderer/core/event_type_names.h"
-#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
-
-namespace blink {
-
-ReadyToRenderEvent::ReadyToRenderEvent()
-    : Event(event_type_names::kReadytorender, Bubbles::kNo, Cancelable::kNo) {
-  CHECK(RuntimeEnabledFeatures::ReadyToRenderEventEnabled());
-}
-
-ReadyToRenderEvent::~ReadyToRenderEvent() = default;
-
-const AtomicString& ReadyToRenderEvent::InterfaceName() const {
-  return event_interface_names::kReadyToRenderEvent;
-}
-
-void ReadyToRenderEvent::Trace(Visitor* visitor) const {
-  Event::Trace(visitor);
-}
-
-}  // namespace blink
diff --git a/third_party/blink/renderer/core/view_transition/ready_to_render_event.h b/third_party/blink/renderer/core/view_transition/ready_to_render_event.h
deleted file mode 100644
index f1a4976..0000000
--- a/third_party/blink/renderer/core/view_transition/ready_to_render_event.h
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2023 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_READY_TO_RENDER_EVENT_H_
-#define THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_READY_TO_RENDER_EVENT_H_
-
-#include "third_party/blink/renderer/core/dom/events/event.h"
-
-namespace blink {
-
-// Implementation for the readytorender event. Fired before the first
-// rendering update after a Document is activated (loaded, restored from
-// BFCache, prerender activated).
-// TODO(bokan): Update spec link once it's settled.
-// https://drafts.csswg.org/css-view-transitions-2/#reveal-event
-class ReadyToRenderEvent final : public Event {
-  DEFINE_WRAPPERTYPEINFO();
-
- public:
-  ReadyToRenderEvent();
-  ~ReadyToRenderEvent() override;
-
-  const AtomicString& InterfaceName() const override;
-
-  void Trace(Visitor*) const override;
-};
-
-}  // namespace blink
-
-#endif  // THIRD_PARTY_BLINK_RENDERER_CORE_VIEW_TRANSITION_READY_TO_RENDER_EVENT_H_
diff --git a/third_party/blink/renderer/core/view_transition/view_transition.cc b/third_party/blink/renderer/core/view_transition/view_transition.cc
index dcc56c81..ea16092 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition.cc
+++ b/third_party/blink/renderer/core/view_transition/view_transition.cc
@@ -10,20 +10,11 @@
 #include "cc/trees/layer_tree_host.h"
 #include "cc/trees/paint_holding_reason.h"
 #include "third_party/blink/public/platform/web_content_settings_client.h"
-#include "third_party/blink/renderer/bindings/core/v8/capture_source_location.h"
-#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
-#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
-#include "third_party/blink/renderer/bindings/core/v8/script_promise_property.h"
-#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
-#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
 #include "third_party/blink/renderer/core/css/css_rule.h"
 #include "third_party/blink/renderer/core/css/style_change_reason.h"
 #include "third_party/blink/renderer/core/dom/document.h"
-#include "third_party/blink/renderer/core/dom/dom_exception.h"
 #include "third_party/blink/renderer/core/dom/dom_node_ids.h"
 #include "third_party/blink/renderer/core/dom/pseudo_element.h"
-#include "third_party/blink/renderer/core/events/error_event.h"
-#include "third_party/blink/renderer/core/execution_context/agent.h"
 #include "third_party/blink/renderer/core/frame/local_dom_window.h"
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
@@ -33,13 +24,12 @@
 #include "third_party/blink/renderer/core/page/chrome_client.h"
 #include "third_party/blink/renderer/core/page/page.h"
 #include "third_party/blink/renderer/core/paint/paint_layer.h"
-#include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/core/view_transition/dom_view_transition.h"
 #include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
 #include "third_party/blink/renderer/platform/graphics/compositor_element_id.h"
 #include "third_party/blink/renderer/platform/graphics/paint/clip_paint_property_node.h"
 #include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
-#include "third_party/blink/renderer/platform/scheduler/public/event_loop.h"
 #include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
 #include "third_party/blink/renderer/platform/wtf/hash_map.h"
 #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
@@ -49,11 +39,6 @@
 namespace blink {
 namespace {
 
-const char kAbortedMessage[] = "Transition was skipped";
-const char kInvalidStateMessage[] =
-    "Transition was aborted because of invalid state";
-const char kTimeoutMessage[] =
-    "Transition was aborted because of timeout in DOM update";
 uint32_t NextDocumentTag() {
   static uint32_t next_document_tag = 1u;
   return next_document_tag++;
@@ -61,89 +46,6 @@
 
 }  // namespace
 
-// DOMChangeFinishedCallback implementation.
-ViewTransition::DOMChangeFinishedCallback::DOMChangeFinishedCallback(
-    ViewTransition* transition,
-    bool success)
-    : transition_(transition), success_(success) {
-  DCHECK(transition_);
-  DCHECK(transition_->script_bound_state_);
-}
-
-ViewTransition::DOMChangeFinishedCallback::~DOMChangeFinishedCallback() =
-    default;
-
-ScriptValue ViewTransition::DOMChangeFinishedCallback::Call(
-    ScriptState* script_state,
-    ScriptValue value) {
-  transition_->NotifyDOMCallbackFinished(success_, std::move(value));
-  return ScriptValue();
-}
-
-void ViewTransition::DOMChangeFinishedCallback::Trace(Visitor* visitor) const {
-  ScriptFunction::Callable::Trace(visitor);
-  visitor->Trace(transition_);
-}
-
-ViewTransition::ScriptBoundState::ScriptBoundState(
-    ExecutionContext* context,
-    ScriptState* state,
-    V8ViewTransitionCallback* callback)
-    : script_state(state),
-      update_dom_callback(callback),
-      dom_updated_promise_property(
-          MakeGarbageCollected<PromiseProperty>(context)),
-      ready_promise_property(MakeGarbageCollected<PromiseProperty>(context)),
-      finished_promise_property(
-          MakeGarbageCollected<PromiseProperty>(context)) {}
-
-void ViewTransition::ScriptBoundState::Trace(Visitor* visitor) const {
-  visitor->Trace(script_state);
-  visitor->Trace(update_dom_callback);
-  visitor->Trace(dom_updated_promise_property);
-  visitor->Trace(ready_promise_property);
-  visitor->Trace(finished_promise_property);
-}
-
-void ViewTransition::ScriptBoundState::HandlePromise(
-    Response response,
-    PromiseProperty* property) {
-  DCHECK_EQ(property->GetState(), PromiseProperty::State::kPending);
-  if (!script_state->ContextIsValid())
-    return;
-
-  switch (response) {
-    case Response::kResolve:
-      property->ResolveWithUndefined();
-      break;
-    case Response::kRejectAbort: {
-      ScriptState::Scope scope(script_state);
-      auto value = ScriptValue::From(
-          script_state, MakeGarbageCollected<DOMException>(
-                            DOMExceptionCode::kAbortError, kAbortedMessage));
-      property->Reject(value);
-      break;
-    }
-    case Response::kRejectInvalidState: {
-      ScriptState::Scope scope(script_state);
-      auto value = ScriptValue::From(
-          script_state,
-          MakeGarbageCollected<DOMException>(
-              DOMExceptionCode::kInvalidStateError, kInvalidStateMessage));
-      property->Reject(value);
-      break;
-    }
-    case Response::kRejectTimeout: {
-      ScriptState::Scope scope(script_state);
-      auto value = ScriptValue::From(
-          script_state, MakeGarbageCollected<DOMException>(
-                            DOMExceptionCode::kTimeoutError, kTimeoutMessage));
-      property->Reject(value);
-      break;
-    }
-  }
-}
-
 ViewTransition::ScopedPauseRendering::ScopedPauseRendering(
     const Document& document) {
   if (!document.GetFrame()->IsLocalRoot())
@@ -211,6 +113,7 @@
     ScriptState* script_state,
     V8ViewTransitionCallback* callback,
     Delegate* delegate) {
+  CHECK(document->GetExecutionContext());
   return MakeGarbageCollected<ViewTransition>(document, script_state, callback,
                                               delegate);
 }
@@ -219,18 +122,18 @@
                                ScriptState* script_state,
                                V8ViewTransitionCallback* update_dom_callback,
                                Delegate* delegate)
-    : ActiveScriptWrappable<ViewTransition>({}),
-      ExecutionContextLifecycleObserver(document->GetExecutionContext()),
+    : ExecutionContextLifecycleObserver(document->GetExecutionContext()),
       creation_type_(CreationType::kScript),
       document_(document),
       delegate_(delegate),
       document_tag_(NextDocumentTag()),
-      script_bound_state_(
-          MakeGarbageCollected<ScriptBoundState>(GetExecutionContext(),
-                                                 script_state,
-                                                 update_dom_callback)),
       style_tracker_(
-          MakeGarbageCollected<ViewTransitionStyleTracker>(*document_)) {
+          MakeGarbageCollected<ViewTransitionStyleTracker>(*document_)),
+      script_delegate_(MakeGarbageCollected<DOMViewTransition>(
+          *document->GetExecutionContext(),
+          *this,
+          *script_state,
+          update_dom_callback)) {
   ProcessCurrentState();
 }
 
@@ -246,8 +149,7 @@
 ViewTransition::ViewTransition(Document* document,
                                ViewTransitionStateCallback callback,
                                Delegate* delegate)
-    : ActiveScriptWrappable<ViewTransition>({}),
-      ExecutionContextLifecycleObserver(document->GetExecutionContext()),
+    : ExecutionContextLifecycleObserver(document->GetExecutionContext()),
       creation_type_(CreationType::kForSnapshot),
       document_(document),
       delegate_(delegate),
@@ -273,8 +175,7 @@
 ViewTransition::ViewTransition(Document* document,
                                ViewTransitionState transition_state,
                                Delegate* delegate)
-    : ActiveScriptWrappable<ViewTransition>({}),
-      ExecutionContextLifecycleObserver(document->GetExecutionContext()),
+    : ExecutionContextLifecycleObserver(document->GetExecutionContext()),
       creation_type_(CreationType::kFromSnapshot),
       document_(document),
       delegate_(delegate),
@@ -290,54 +191,20 @@
   ProcessCurrentState();
 }
 
-void ViewTransition::skipTransition() {
-  SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
-}
-
-void ViewTransition::SkipTransitionInternal(
-    ScriptBoundState::Response response) {
-  DCHECK_NE(response, ScriptBoundState::Response::kResolve);
+void ViewTransition::SkipTransition(PromiseResponse response) {
+  DCHECK_NE(response, PromiseResponse::kResolve);
   if (IsTerminalState(state_))
     return;
 
   // Cleanup logic which is tied to ViewTransition objects created using the
-  // script API. If |context_destroyed_| is false the Document is being torn
-  // down and the script specific callbacks don't need to be dispatched.
-  if (!context_destroyed_ && creation_type_ == CreationType::kScript) {
-    DCHECK(script_bound_state_);
-
-    // If the ready promise has not yet been resolved, reject it.
-    if (script_bound_state_->ready_promise_property->GetState() ==
-        PromiseProperty::State::kPending) {
-      AtMicrotask(response, script_bound_state_->ready_promise_property);
-    }
-
-    // If we haven't run the dom change callback yet, schedule a task to do so.
-    // The finished promise will propagate the result of the updateCallbackDone
-    // promise when this callback runs.
-    if (static_cast<int>(state_) <
-        static_cast<int>(State::kDOMCallbackRunning)) {
-      DCHECK(!dom_callback_succeeded_);
-      document_->GetTaskRunner(TaskType::kMiscPlatformAPI)
-          ->PostTask(
-              FROM_HERE,
-              WTF::BindOnce(
-                  base::IgnoreResult(&ViewTransition::InvokeDOMChangeCallback),
-                  WrapPersistent(this)));
-    } else if (static_cast<int>(state_) >=
-               static_cast<int>(State::kDOMCallbackFinished)) {
-      // If the DOM callback finished and there was a failure then the finished
-      // promise should have been rejected with updateCallbackDone.
-      if (!dom_callback_succeeded_) {
-        DCHECK_EQ(script_bound_state_->finished_promise_property->GetState(),
-                  PromiseProperty::State::kRejected);
-      } else {
-        // But if the callback was successful, we need to resolve the finished
-        // promise while skipping the transition.
-        AtMicrotask(ScriptBoundState::Response::kResolve,
-                    script_bound_state_->finished_promise_property);
-      }
-    }
+  // script API. script_delegate_ is cleared when the Document is being torn
+  // down and script specific callbacks don't need to be dispatched in that
+  // case.
+  if (script_delegate_ && creation_type_ == CreationType::kScript) {
+    // TODO(bokan): This should be called for navigation created VT as
+    // well.
+    DCHECK(script_delegate_);
+    script_delegate_->DidSkipTransition(response);
   }
 
   // If we already started processing the transition (i.e. we're beyond capture
@@ -368,24 +235,6 @@
   AdvanceTo(State::kAborted);
 }
 
-ScriptPromise ViewTransition::finished() const {
-  DCHECK(script_bound_state_);
-  return script_bound_state_->finished_promise_property->Promise(
-      script_bound_state_->script_state->World());
-}
-
-ScriptPromise ViewTransition::ready() const {
-  DCHECK(script_bound_state_);
-  return script_bound_state_->ready_promise_property->Promise(
-      script_bound_state_->script_state->World());
-}
-
-ScriptPromise ViewTransition::updateCallbackDone() const {
-  DCHECK(script_bound_state_);
-  return script_bound_state_->dom_updated_promise_property->Promise(
-      script_bound_state_->script_state->World());
-}
-
 bool ViewTransition::AdvanceTo(State state) {
   DCHECK(CanAdvanceTo(state)) << "Current state " << static_cast<int>(state_)
                               << " new state " << static_cast<int>(state);
@@ -536,8 +385,7 @@
       // Capture request pending -- create the request
       case State::kCaptureRequestPending:
         if (!style_tracker_->Capture()) {
-          SkipTransitionInternal(
-              ScriptBoundState::Response::kRejectInvalidState);
+          SkipTransition(PromiseResponse::kRejectInvalidState);
           break;
         }
 
@@ -587,9 +435,10 @@
 
         // The following logic is only executed for ViewTransition objects
         // created by the script API.
-        DCHECK(script_bound_state_);
-
-        DOMCallbackResult result = InvokeDOMChangeCallback();
+        CHECK_EQ(creation_type_, CreationType::kScript);
+        CHECK(script_delegate_);
+        DOMViewTransition::DOMCallbackResult result =
+            script_delegate_->InvokeDOMChangeCallback();
 
         // Since invoking the callback could yield (at least when devtools
         // breakpoint is hit, but maybe in other situations), we could have
@@ -600,18 +449,16 @@
         }
 
         switch (result) {
-          case DOMCallbackResult::kFinished:
+          case DOMViewTransition::DOMCallbackResult::kFinished:
             process_next_state = AdvanceTo(State::kDOMCallbackFinished);
             DCHECK(process_next_state);
-            DCHECK(dom_callback_succeeded_);
             break;
-          case DOMCallbackResult::kFailed:
+          case DOMViewTransition::DOMCallbackResult::kFailed:
             process_next_state = AdvanceTo(State::kDOMCallbackFinished);
             DCHECK(process_next_state);
-            DCHECK(!dom_callback_succeeded_);
-            SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
+            SkipTransition(PromiseResponse::kRejectAbort);
             break;
-          case DOMCallbackResult::kRunning:
+          case DOMViewTransition::DOMCallbackResult::kRunning:
             process_next_state = AdvanceTo(State::kDOMCallbackRunning);
             DCHECK(process_next_state);
             break;
@@ -638,10 +485,6 @@
         }
 
         ResumeRendering();
-        if (!dom_callback_succeeded_) {
-          SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
-          break;
-        }
         process_next_state = AdvanceTo(State::kAnimateTagDiscovery);
         DCHECK(process_next_state);
         break;
@@ -659,8 +502,7 @@
 
       case State::kAnimateRequestPending:
         if (!style_tracker_->Start()) {
-          SkipTransitionInternal(
-              ScriptBoundState::Response::kRejectInvalidState);
+          SkipTransition(PromiseResponse::kRejectInvalidState);
           break;
         }
 
@@ -672,8 +514,10 @@
 
         DCHECK(!in_main_lifecycle_update_);
         if (creation_type_ == CreationType::kScript) {
-          AtMicrotask(ScriptBoundState::Response::kResolve,
-                      script_bound_state_->ready_promise_property);
+          // TODO(bokan): This should be called for navigation created VT as
+          // well.
+          CHECK(script_delegate_);
+          script_delegate_->DidStartAnimating();
         }
         break;
 
@@ -693,8 +537,10 @@
         style_tracker_->StartFinished();
 
         if (creation_type_ == CreationType::kScript) {
-          AtMicrotask(ScriptBoundState::Response::kResolve,
-                      script_bound_state_->finished_promise_property);
+          // TODO(bokan): This should be called for navigation created VT as
+          // well.
+          CHECK(script_delegate_);
+          script_delegate_->DidFinishAnimating();
         }
 
         delegate_->AddPendingRequest(ViewTransitionRequest::CreateRelease(
@@ -718,10 +564,8 @@
 void ViewTransition::Trace(Visitor* visitor) const {
   visitor->Trace(document_);
   visitor->Trace(style_tracker_);
-  visitor->Trace(script_bound_state_);
+  visitor->Trace(script_delegate_);
 
-  ScriptWrappable::Trace(visitor);
-  ActiveScriptWrappable::Trace(visitor);
   ExecutionContextLifecycleObserver::Trace(visitor);
 }
 
@@ -733,58 +577,14 @@
   return style_tracker_->MatchForOnlyChild(pseudo_id, view_transition_name);
 }
 
-ViewTransition::DOMCallbackResult ViewTransition::InvokeDOMChangeCallback() {
-  DCHECK(script_bound_state_);
-
-  if (!script_bound_state_->update_dom_callback) {
-    dom_callback_succeeded_ = true;
-    AtMicrotask(ScriptBoundState::Response::kResolve,
-                script_bound_state_->dom_updated_promise_property);
-
-    // If we're already at the terminal state, the dom update callback was
-    // scheduled to run after the transition was skipped.
-    if (IsDone()) {
-      AtMicrotask(ScriptBoundState::Response::kResolve,
-                  script_bound_state_->finished_promise_property);
-    }
-    return DOMCallbackResult::kFinished;
-  }
-
-  v8::Maybe<ScriptPromise> result =
-      script_bound_state_->update_dom_callback->Invoke(nullptr);
-
-  // TODO(vmpstr): Should this be a DCHECK?
-  if (result.IsNothing()) {
-    dom_callback_succeeded_ = false;
-    AtMicrotask(ScriptBoundState::Response::kRejectAbort,
-                script_bound_state_->dom_updated_promise_property);
-    AtMicrotask(ScriptBoundState::Response::kRejectAbort,
-                script_bound_state_->finished_promise_property);
-    return DOMCallbackResult::kFailed;
-  }
-
-  ScriptState::Scope scope(script_bound_state_->script_state);
-
-  result.ToChecked().Then(
-      MakeGarbageCollected<ScriptFunction>(
-          script_bound_state_->script_state,
-          MakeGarbageCollected<DOMChangeFinishedCallback>(this, true)),
-      MakeGarbageCollected<ScriptFunction>(
-          script_bound_state_->script_state,
-          MakeGarbageCollected<DOMChangeFinishedCallback>(this, false)));
-  return DOMCallbackResult::kRunning;
-}
-
 void ViewTransition::ContextDestroyed() {
   TRACE_EVENT0("blink", "ViewTransition::ContextDestroyed");
 
-  // TODO(khushalsagar): This needs to be called for pages entering BFCache.
-  context_destroyed_ = true;
-  SkipTransitionInternal(ScriptBoundState::Response::kRejectAbort);
-}
+  // Don't try to interact with script after the Document starts shutdown.
+  script_delegate_.Clear();
 
-bool ViewTransition::HasPendingActivity() const {
-  return !IsTerminalState(state_);
+  // TODO(khushalsagar): This needs to be called for pages entering BFCache.
+  SkipTransition(PromiseResponse::kRejectAbort);
 }
 
 void ViewTransition::NotifyCaptureFinished() {
@@ -797,36 +597,19 @@
   ProcessCurrentState();
 }
 
-void ViewTransition::NotifyDOMCallbackFinished(bool success,
-                                               ScriptValue value) {
-  // Handle all promises which depend on this callback.
-  if (success) {
-    script_bound_state_->dom_updated_promise_property->ResolveWithUndefined();
-
-    // If we're already at the terminal state, the transition was skipped before
-    // the callback finished. Also handle the finish promise.
-    if (IsDone())
-      script_bound_state_->finished_promise_property->ResolveWithUndefined();
-  } else {
-    script_bound_state_->dom_updated_promise_property->Reject(value);
-
-    // The ready promise rejects with the value of updateCallbackDone callback
-    // if it's skipped because of an error in the callback.
-    if (!IsDone())
-      script_bound_state_->ready_promise_property->Reject(value);
-
-    // If the domUpdate callback fails the transition is skipped. The finish
-    // promise should mirror the result of updateCallbackDone.
-    script_bound_state_->finished_promise_property->Reject(value);
-  }
-
-  dom_callback_succeeded_ = success;
+void ViewTransition::NotifyDOMCallbackFinished(bool success) {
   if (IsTerminalState(state_))
     return;
 
   bool process_next_state = AdvanceTo(State::kDOMCallbackFinished);
   DCHECK(process_next_state);
+  if (!success) {
+    SkipTransition(PromiseResponse::kRejectAbort);
+  }
   ProcessCurrentState();
+
+  // Succeed or fail, rendering must be resumed after this.
+  CHECK(!rendering_paused_scope_);
 }
 
 bool ViewTransition::NeedsViewTransitionEffectNode(
@@ -957,7 +740,7 @@
 
   if (state_ == State::kAnimating && style_tracker_ &&
       !style_tracker_->RunPostPrePaintSteps()) {
-    SkipTransitionInternal(ScriptBoundState::Response::kRejectInvalidState);
+    SkipTransition(PromiseResponse::kRejectInvalidState);
   }
 }
 
@@ -975,7 +758,7 @@
   if (style_tracker_ &&
       document_->Lifecycle().GetState() >= DocumentLifecycle::kPrePaintClean &&
       !style_tracker_->RunPostPrePaintSteps()) {
-    SkipTransitionInternal(ScriptBoundState::Response::kRejectInvalidState);
+    SkipTransition(PromiseResponse::kRejectInvalidState);
   }
 }
 
@@ -1055,7 +838,7 @@
     return;
 
   ResumeRendering();
-  SkipTransitionInternal(ScriptBoundState::Response::kRejectTimeout);
+  SkipTransition(PromiseResponse::kRejectTimeout);
   AdvanceTo(State::kTimedOut);
 }
 
@@ -1068,14 +851,6 @@
   rendering_paused_scope_.reset();
 }
 
-void ViewTransition::AtMicrotask(ScriptBoundState::Response response,
-                                 PromiseProperty* property) {
-  document_->GetAgent().event_loop()->EnqueueMicrotask(
-      WTF::BindOnce(&ViewTransition::ScriptBoundState::HandlePromise,
-                    WrapPersistent(script_bound_state_.Get()), response,
-                    WrapPersistent(property)));
-}
-
 void ViewTransition::ActivateFromSnapshot() {
   if (state_ != State::kWaitForRenderBlock)
     return;
diff --git a/third_party/blink/renderer/core/view_transition/view_transition.h b/third_party/blink/renderer/core/view_transition/view_transition.h
index 4144c7f..765d2e225 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition.h
+++ b/third_party/blink/renderer/core/view_transition/view_transition.h
@@ -35,18 +35,14 @@
 namespace blink {
 
 class Document;
+class DOMViewTransition;
 class Element;
 class LayoutObject;
 class PseudoElement;
-class ScriptPromise;
-class ScriptState;
 
-class CORE_EXPORT ViewTransition : public ScriptWrappable,
-                                   public ActiveScriptWrappable<ViewTransition>,
+class CORE_EXPORT ViewTransition : public GarbageCollected<ViewTransition>,
                                    public ExecutionContextLifecycleObserver,
                                    public ChromeClient::CommitObserver {
-  DEFINE_WRAPPERTYPEINFO();
-
  public:
   class Delegate {
    public:
@@ -84,12 +80,7 @@
   ViewTransition(Document*, ViewTransitionStateCallback, Delegate*);
   ViewTransition(Document*, ViewTransitionState, Delegate*);
 
-  // IDL implementation. Refer to view_transition.idl for additional
-  // comments.
-  void skipTransition();
-  ScriptPromise finished() const;
-  ScriptPromise ready() const;
-  ScriptPromise updateCallbackDone() const;
+  DOMViewTransition* GetScriptDelegate() { return script_delegate_; }
 
   // GC functionality.
   void Trace(Visitor* visitor) const override;
@@ -102,9 +93,6 @@
   // ExecutionContextLifecycleObserver implementation.
   void ContextDestroyed() override;
 
-  // ActiveScriptWrappable functionality.
-  bool HasPendingActivity() const override;
-
   // Returns true if this object needs to create an EffectNode for its element
   // transition.
   bool NeedsViewTransitionEffectNode(const LayoutObject& object) const;
@@ -225,13 +213,24 @@
   // block concept, has up to date style.
   void UpdateSnapshotContainingBlockStyle();
 
+  // Indicates how the promise should be handled.
+  enum class PromiseResponse {
+    kResolve,
+    kRejectAbort,
+    kRejectInvalidState,
+    kRejectTimeout
+  };
+  void SkipTransition(PromiseResponse response = PromiseResponse::kRejectAbort);
+
+  // Dispatched when the promise returned from the author's update callback has
+  // resolved and start phase of the animation can be initiated. Note: this is
+  // called only if a callback is provided.
+  void NotifyDOMCallbackFinished(bool success);
+
  private:
   friend class ViewTransitionTest;
   friend class AXViewTransitionTest;
 
-  using PromiseProperty =
-      ScriptPromiseProperty<ToV8UndefinedGenerator, ScriptValue>;
-
   // Tracks how the ViewTransition object was created.
   enum class CreationType {
     // Created via the document.startViewTransition() script API.
@@ -279,30 +278,6 @@
   };
   static const char* StateToString(State state);
 
-  // State which is created only when ViewTransition is accessed from
-  // script.
-  struct ScriptBoundState : public GarbageCollected<ScriptBoundState> {
-    ScriptBoundState(ExecutionContext* context,
-                     ScriptState*,
-                     V8ViewTransitionCallback*);
-
-    // Indicates how the promise should be handled.
-    enum class Response {
-      kResolve,
-      kRejectAbort,
-      kRejectInvalidState,
-      kRejectTimeout
-    };
-    void HandlePromise(Response response, PromiseProperty* property);
-    void Trace(Visitor* visitor) const;
-
-    Member<ScriptState> script_state;
-    Member<V8ViewTransitionCallback> update_dom_callback;
-    Member<PromiseProperty> dom_updated_promise_property;
-    Member<PromiseProperty> ready_promise_property;
-    Member<PromiseProperty> finished_promise_property;
-  };
-
   // Advance to the new state. This returns true if the state should be
   // processed immediately.
   bool AdvanceTo(State state);
@@ -320,27 +295,8 @@
 
   void ProcessCurrentState();
 
-  // Invoked when ViewTransitionCallback finishes running.
-  class DOMChangeFinishedCallback : public ScriptFunction::Callable {
-   public:
-    explicit DOMChangeFinishedCallback(ViewTransition* transition,
-                                       bool success);
-    ~DOMChangeFinishedCallback() override;
-
-    ScriptValue Call(ScriptState*, ScriptValue) override;
-    void Trace(Visitor* visitor) const override;
-
-   private:
-    Member<ViewTransition> transition_;
-    const bool success_;
-  };
-
   void NotifyCaptureFinished();
 
-  // Dispatched when the ViewTransitionCallback has finished executing and
-  // start phase of the animation can be initiated.
-  void NotifyDOMCallbackFinished(bool success, ScriptValue value);
-
   // Used to defer visual updates between transition prepare dispatching and
   // transition start to allow the page to set up the final scene
   // asynchronously.
@@ -348,21 +304,6 @@
   void OnRenderingPausedTimeout();
   void ResumeRendering();
 
-  // Returns the result of invoking the callback.
-  // kFailed: Indicates that there was a failure in running the callback and the
-  //          transition should be skipped.
-  // kFinished: Indicates that there was no callback to run so we can move to
-  //            finished state synchronously.
-  // kRunning: Indicates that the callback is in running state. Note that even
-  //           if the callback is synchronous, the notification that it has
-  //           finished running is async.
-  enum class DOMCallbackResult { kFailed, kFinished, kRunning };
-  DOMCallbackResult InvokeDOMChangeCallback();
-
-  void AtMicrotask(ScriptBoundState::Response response,
-                   PromiseProperty* resolver);
-  void SkipTransitionInternal(ScriptBoundState::Response response);
-
   State state_ = State::kInitial;
   const CreationType creation_type_;
 
@@ -374,8 +315,6 @@
   // belongs. It's unique among other local documents.
   uint32_t document_tag_ = 0u;
 
-  Member<ScriptBoundState> script_bound_state_;
-
   Member<ViewTransitionStyleTracker> style_tracker_;
 
   // Manages pausing rendering of the Document between capture and updateDOM
@@ -399,6 +338,11 @@
 
   ViewTransitionStateCallback transition_state_callback_;
 
+  // This is the object that implements the IDL interface exposed to script. It
+  // is non-null only when ViewTransition is created from the
+  // `startViewTransition` script API.
+  Member<DOMViewTransition> script_delegate_;
+
   bool in_main_lifecycle_update_ = false;
   bool dom_callback_succeeded_ = false;
   bool first_animating_frame_ = true;
diff --git a/third_party/blink/renderer/core/view_transition/view_transition.idl b/third_party/blink/renderer/core/view_transition/view_transition.idl
index 33cc34e6..dea2e56 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition.idl
+++ b/third_party/blink/renderer/core/view_transition/view_transition.idl
@@ -7,6 +7,7 @@
 [
     ActiveScriptWrappable,
     Exposed=Window,
+    ImplementedAs=DOMViewTransition,
     RuntimeEnabled=ViewTransition
 ] interface ViewTransition {
   // Skips the transition. This will cause the UpdateDOMCallback to run if it
diff --git a/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc b/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
index 76c87d43..04c864d4 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
+++ b/third_party/blink/renderer/core/view_transition/view_transition_supplement.cc
@@ -11,6 +11,7 @@
 #include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/frame/local_frame_view.h"
 #include "third_party/blink/renderer/core/style/computed_style.h"
+#include "third_party/blink/renderer/core/view_transition/dom_view_transition.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
@@ -53,7 +54,7 @@
     if (!transition)
       return;
 
-    transition->skipTransition();
+    transition->SkipTransition();
     DCHECK(!ViewTransitionUtils::GetTransition(*document));
   });
 }
@@ -81,7 +82,7 @@
 }
 
 // static
-ViewTransition* ViewTransitionSupplement::startViewTransition(
+DOMViewTransition* ViewTransitionSupplement::startViewTransition(
     ScriptState* script_state,
     Document& document,
     V8ViewTransitionCallback* callback,
@@ -101,7 +102,7 @@
                                      exception_state);
 }
 
-ViewTransition* ViewTransitionSupplement::StartTransition(
+DOMViewTransition* ViewTransitionSupplement::StartTransition(
     ScriptState* script_state,
     Document& document,
     V8ViewTransitionCallback* callback,
@@ -112,9 +113,10 @@
     return nullptr;
 
   if (transition_)
-    transition_->skipTransition();
+    transition_->SkipTransition();
+
   DCHECK(!transition_)
-      << "skipTransition() should finish existing |transition_|";
+      << "SkipTransition() should finish existing |transition_|";
 
   // We need to be connected to a view to have a transition. We also need a
   // document element, since that's the originating element for the pseudo tree.
@@ -129,10 +131,10 @@
   // transition in a child frame.
   if (HasActiveTransitionInAncestorFrame(document.GetFrame())) {
     auto skipped_transition = transition_;
-    skipped_transition->skipTransition();
+    skipped_transition->SkipTransition();
 
     DCHECK(!transition_);
-    return skipped_transition;
+    return skipped_transition->GetScriptDelegate();
   }
 
   // Skip transitions in all frames associated with this widget. We can only
@@ -140,7 +142,7 @@
   SkipTransitionInAllLocalFrames(document.GetFrame());
   DCHECK(transition_);
 
-  return transition_;
+  return transition_->GetScriptDelegate();
 }
 
 void ViewTransitionSupplement::SetCrossDocumentOptIn(
@@ -161,9 +163,9 @@
   if (cross_document_opt_in_ ==
           mojom::blink::ViewTransitionSameOriginOptIn::kDisabled &&
       transition_ && !transition_->IsCreatedViaScriptAPI()) {
-    transition_->skipTransition();
+    transition_->SkipTransition();
     DCHECK(!transition_)
-        << "skipTransition() should finish existing |transition_|";
+        << "SkipTransition() should finish existing |transition_|";
   }
 }
 
@@ -182,10 +184,10 @@
   if (transition_) {
     // We should skip a transition if one exists, regardless of how it was
     // created, since navigation transition takes precedence.
-    transition_->skipTransition();
+    transition_->SkipTransition();
   }
   DCHECK(!transition_)
-      << "skipTransition() should finish existing |transition_|";
+      << "SkipTransition() should finish existing |transition_|";
   transition_ = ViewTransition::CreateForSnapshotForNavigation(
       &document, std::move(callback), this);
 }
@@ -203,7 +205,7 @@
 void ViewTransitionSupplement::AbortTransition(Document& document) {
   auto* supplement = FromIfExists(document);
   if (supplement && supplement->transition_) {
-    supplement->transition_->skipTransition();
+    supplement->transition_->SkipTransition();
     DCHECK(!supplement->transition_);
   }
 }
@@ -305,9 +307,9 @@
   }
 
   // Since we don't have an opt-in, skip a navigation transition if it exists.
-  transition_->skipTransition();
+  transition_->SkipTransition();
   DCHECK(!transition_)
-      << "skipTransition() should finish existing |transition_|";
+      << "SkipTransition() should finish existing |transition_|";
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/view_transition/view_transition_supplement.h b/third_party/blink/renderer/core/view_transition/view_transition_supplement.h
index 69e9be7..d36456e 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition_supplement.h
+++ b/third_party/blink/renderer/core/view_transition/view_transition_supplement.h
@@ -14,7 +14,7 @@
 #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
 
 namespace blink {
-class ViewTransition;
+class DOMViewTransition;
 class V8ViewTransitionCallback;
 
 class CORE_EXPORT ViewTransitionSupplement
@@ -30,10 +30,11 @@
 
   // Creates and starts a same-document ViewTransition initiated using the
   // script API.
-  static ViewTransition* startViewTransition(ScriptState*,
-                                             Document&,
-                                             V8ViewTransitionCallback* callback,
-                                             ExceptionState&);
+  static DOMViewTransition* startViewTransition(
+      ScriptState*,
+      Document&,
+      V8ViewTransitionCallback* callback,
+      ExceptionState&);
 
   // Creates a ViewTransition to cache the state of a Document before a
   // navigation. The cached state is provided to the caller using the
@@ -78,10 +79,10 @@
   void WillInsertBody();
 
  private:
-  ViewTransition* StartTransition(ScriptState* script_state,
-                                  Document& document,
-                                  V8ViewTransitionCallback* callback,
-                                  ExceptionState& exception_state);
+  DOMViewTransition* StartTransition(ScriptState* script_state,
+                                     Document& document,
+                                     V8ViewTransitionCallback* callback,
+                                     ExceptionState& exception_state);
   void StartTransition(Document& document,
                        ViewTransition::ViewTransitionStateCallback callback);
   void StartTransition(Document& document,
diff --git a/third_party/blink/renderer/core/view_transition/view_transition_test.cc b/third_party/blink/renderer/core/view_transition/view_transition_test.cc
index bc74874..16922dd 100644
--- a/third_party/blink/renderer/core/view_transition/view_transition_test.cc
+++ b/third_party/blink/renderer/core/view_transition/view_transition_test.cc
@@ -32,6 +32,7 @@
 #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
 #include "third_party/blink/renderer/core/testing/mock_function_scope.h"
 #include "third_party/blink/renderer/core/timing/layout_shift.h"
+#include "third_party/blink/renderer/core/view_transition/dom_view_transition.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_supplement.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
@@ -120,14 +121,14 @@
 
   using State = ViewTransition::State;
 
-  State GetState(ViewTransition* transition) const {
-    return transition->state_;
+  State GetState(DOMViewTransition* transition) const {
+    return transition->GetViewTransitionForTest()->state_;
   }
 
   void FinishTransition() {
     auto* transition = ViewTransitionUtils::GetTransition(GetDocument());
     if (transition)
-      transition->skipTransition();
+      transition->SkipTransition();
   }
 
   bool ShouldCompositeForViewTransition(Element* e) {
@@ -846,7 +847,7 @@
       v8::Function::New(v8_scope.GetContext(), start_setup_lambda, {})
           .ToLocalChecked();
 
-  ViewTransition* transition = ViewTransitionSupplement::startViewTransition(
+  auto* transition = ViewTransitionSupplement::startViewTransition(
       script_state, GetDocument(),
       V8ViewTransitionCallback::Create(start_setup_callback), exception_state);
 
@@ -855,7 +856,8 @@
 
   // The snapshot rect should not have been shrunk by the virtual keyboard, even
   // though it shrinks the WebView.
-  EXPECT_EQ(transition->GetSnapshotRootSize(), original_size);
+  EXPECT_EQ(transition->GetViewTransitionForTest()->GetSnapshotRootSize(),
+            original_size);
 
   // The height of the ::view-transition should come from the snapshot root
   // rect.
@@ -881,7 +883,8 @@
       ->SetVirtualKeyboardResizeHeightForTesting(0);
 
   // The snapshot rect should remain the same size.
-  EXPECT_EQ(transition->GetSnapshotRootSize(), original_size);
+  EXPECT_EQ(transition->GetViewTransitionForTest()->GetSnapshotRootSize(),
+            original_size);
 
   // The start phase should generate pseudo elements for rendering new live
   // content.
@@ -910,7 +913,7 @@
       v8::Function::New(v8_scope.GetContext(), start_setup_lambda, {})
           .ToLocalChecked();
 
-  ViewTransition* transition = ViewTransitionSupplement::startViewTransition(
+  DOMViewTransition* transition = ViewTransitionSupplement::startViewTransition(
       script_state, *document,
       V8ViewTransitionCallback::Create(start_setup_callback), exception_state);
   ASSERT_FALSE(transition);
@@ -944,8 +947,9 @@
   ASSERT_FALSE(exception_state.HadException());
 
   EXPECT_TRUE(GetDocument().GetLayoutView()->NeedsPaintPropertyUpdate());
-  EXPECT_TRUE(transition->NeedsViewTransitionEffectNode(
-      *GetDocument().GetLayoutView()));
+  EXPECT_TRUE(
+      transition->GetViewTransitionForTest()->NeedsViewTransitionEffectNode(
+          *GetDocument().GetLayoutView()));
 }
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object.cc b/third_party/blink/renderer/modules/accessibility/ax_object.cc
index 7726b40c..e780a0b 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_object.cc
@@ -5655,8 +5655,12 @@
   if (!element) {
     for (AXObject* parent = ParentObject(); parent;
          parent = parent->ParentObject()) {
-      if (parent) {
-        return parent->GetElement();
+      // It's possible to have a parent without a node here if the parent is a
+      // pseudo element descendant. Since we're looking for the nearest element,
+      // keep going up the ancestor chain until we find a parent that has one.
+      element = parent->GetElement();
+      if (element) {
+        return element;
       }
     }
   }
diff --git a/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc b/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
index 3f22072..56dd542 100644
--- a/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
+++ b/third_party/blink/renderer/modules/accessibility/ax_object_cache_test.cc
@@ -13,8 +13,10 @@
 #include "third_party/blink/renderer/core/dom/pseudo_element.h"
 #include "third_party/blink/renderer/core/frame/frame_test_helpers.h"
 #include "third_party/blink/renderer/core/testing/mock_function_scope.h"
+#include "third_party/blink/renderer/core/view_transition/dom_view_transition.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition_supplement.h"
+#include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
 #include "third_party/blink/renderer/modules/accessibility/ax_object.h"
 #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
 #include "third_party/blink/renderer/modules/accessibility/testing/accessibility_test.h"
@@ -170,8 +172,8 @@
 
   using State = ViewTransition::State;
 
-  State GetState(ViewTransition* transition) const {
-    return transition->state_;
+  State GetState(DOMViewTransition* transition) const {
+    return transition->GetViewTransitionForTest()->state_;
   }
 
  protected:
diff --git a/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.cc b/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.cc
index f5f25e3..0cdfc36 100644
--- a/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.cc
+++ b/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.cc
@@ -125,7 +125,8 @@
 template <typename NativeFrameType>
 ScriptPromise FrameQueueUnderlyingSource<NativeFrameType>::Cancel(
     ScriptState* script_state,
-    ScriptValue reason) {
+    ScriptValue reason,
+    ExceptionState&) {
   DCHECK(realm_task_runner_->RunsTasksInCurrentSequence());
   Close();
   return ScriptPromise::CastUndefined(script_state);
diff --git a/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.h b/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.h
index e9803f4..24df3e4c 100644
--- a/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.h
+++ b/third_party/blink/renderer/modules/breakout_box/frame_queue_underlying_source.h
@@ -44,7 +44,9 @@
   // UnderlyingSourceBase
   ScriptPromise pull(ScriptState*) override;
   ScriptPromise Start(ScriptState*) override;
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason) override;
+  ScriptPromise Cancel(ScriptState*,
+                       ScriptValue reason,
+                       ExceptionState&) override;
 
   // ScriptWrappable interface
   bool HasPendingActivity() const final;
diff --git a/third_party/blink/renderer/modules/direct_sockets/stream_wrapper.cc b/third_party/blink/renderer/modules/direct_sockets/stream_wrapper.cc
index 81dea56..6272205 100644
--- a/third_party/blink/renderer/modules/direct_sockets/stream_wrapper.cc
+++ b/third_party/blink/renderer/modules/direct_sockets/stream_wrapper.cc
@@ -42,7 +42,9 @@
     return ScriptPromise::CastUndefined(script_state);
   }
 
-  ScriptPromise Cancel(ScriptState* script_state, ScriptValue reason) override {
+  ScriptPromise Cancel(ScriptState* script_state,
+                       ScriptValue reason,
+                       ExceptionState&) override {
     readable_stream_wrapper_->CloseStream();
     return ScriptPromise::CastUndefined(script_state);
   }
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture.cc b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
index 012160d4..bb6e8aa5 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture.cc
@@ -501,8 +501,8 @@
 
 bool TrackIsInactive(const MediaStreamTrack& track) {
   // Spec instructs to return an exception if the Track's readyState() is not
-  // "live". Also reject if the track is disabled or muted.
-  return track.readyState() != "live" || !track.enabled() || track.muted();
+  // "live". Also reject if the track is disabled.
+  return track.readyState() != "live" || !track.enabled();
 }
 
 BackgroundBlurMode ParseBackgroundBlur(bool blink_mode) {
diff --git a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
index 23ca12a..77288be 100644
--- a/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
+++ b/third_party/blink/renderer/modules/imagecapture/image_capture_test.cc
@@ -4,10 +4,12 @@
 
 #include "third_party/blink/renderer/modules/imagecapture/image_capture.h"
 
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/web/web_heap.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_function.h"
 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
 #include "third_party/blink/renderer/bindings/core/v8/v8_union_string_stringsequence.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_constrain_boolean_parameters.h"
@@ -21,10 +23,15 @@
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_constraindomstringparameters_string_stringsequence.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_constraindoublerange_double.h"
 #include "third_party/blink/renderer/bindings/modules/v8/v8_union_constrainpoint2dparameters_point2dsequence.h"
+#include "third_party/blink/renderer/core/frame/local_frame.h"
 #include "third_party/blink/renderer/core/page/page.h"
 #include "third_party/blink/renderer/modules/imagecapture/image_capture.h"
+#include "third_party/blink/renderer/modules/mediastream/media_stream_video_capturer_source.h"
+#include "third_party/blink/renderer/modules/mediastream/media_stream_video_track.h"
 #include "third_party/blink/renderer/modules/mediastream/mock_media_stream_track.h"
+#include "third_party/blink/renderer/modules/mediastream/mock_video_capturer_source.h"
 #include "third_party/blink/renderer/platform/bindings/script_state.h"
+#include "third_party/blink/renderer/platform/testing/io_task_runner_testing_platform_support.h"
 
 namespace blink {
 
@@ -37,6 +44,12 @@
 using PopulatePanTiltZoom =
     base::StrongAlias<class PopulatePanTiltZoomZoomTag, bool>;
 
+using media::VideoFrame;
+using testing::_;
+using testing::Invoke;
+using testing::NiceMock;
+using testing::Return;
+
 constexpr double kExposureCompensationDelta = 1;
 constexpr double kExposureTimeDelta = 2;
 constexpr double kColorTemperatureDelta = 3;
@@ -584,19 +597,90 @@
           ConstraintCreator::Create(all_capabilities->backgroundBlur()[0])));
 }
 
+class MockMediaStreamComponent
+    : public GarbageCollected<MockMediaStreamComponent>,
+      public MediaStreamComponent {
+ public:
+  virtual ~MockMediaStreamComponent() = default;
+  MOCK_CONST_METHOD0(Clone, MediaStreamComponent*());
+  MOCK_CONST_METHOD0(Source, MediaStreamSource*());
+  MOCK_CONST_METHOD0(Id, String());
+  MOCK_CONST_METHOD0(UniqueId, int());
+  MOCK_CONST_METHOD0(GetSourceType, MediaStreamSource::StreamType());
+  MOCK_CONST_METHOD0(GetSourceName, const String&());
+  MOCK_CONST_METHOD0(GetReadyState, MediaStreamSource::ReadyState());
+  MOCK_CONST_METHOD0(Remote, bool());
+  MOCK_CONST_METHOD0(Enabled, bool());
+  MOCK_METHOD1(SetEnabled, void(bool));
+  MOCK_METHOD0(ContentHint, WebMediaStreamTrack::ContentHintType());
+  MOCK_METHOD1(SetContentHint, void(WebMediaStreamTrack::ContentHintType));
+  MOCK_CONST_METHOD0(GetPlatformTrack, MediaStreamTrackPlatform*());
+  MOCK_METHOD1(SetPlatformTrack,
+               void(std::unique_ptr<MediaStreamTrackPlatform>));
+  MOCK_METHOD1(GetSettings, void(MediaStreamTrackPlatform::Settings&));
+  MOCK_METHOD0(GetCaptureHandle, MediaStreamTrackPlatform::CaptureHandle());
+  MOCK_METHOD0(CreationFrame, WebLocalFrame*());
+  MOCK_METHOD1(SetCreationFrame, void(WebLocalFrame*));
+  MOCK_METHOD1(AddSourceObserver, void(MediaStreamSource::Observer*));
+  MOCK_METHOD1(AddSink, void(WebMediaStreamAudioSink*));
+  MOCK_METHOD4(AddSink,
+               void(WebMediaStreamSink*,
+                    const VideoCaptureDeliverFrameCB&,
+                    MediaStreamVideoSink::IsSecure,
+                    MediaStreamVideoSink::UsesAlpha));
+  MOCK_CONST_METHOD0(ToString, String());
+};
+
 }  // namespace
 
 class ImageCaptureTest : public testing::Test {
  public:
+  ImageCaptureTest()
+      : component_(MakeGarbageCollected<MockMediaStreamComponent>()),
+        track_(MakeGarbageCollected<MockMediaStreamTrack>()),
+        image_capture_(MakeGarbageCollected<ImageCapture>(
+            /*execution_context=*/nullptr,
+            track_,
+            /*pan_tilt_zoom_allowed=*/true,
+            base::DoNothing())) {
+    track_->SetComponent(component_);
+  }
+
   void TearDown() override { WebHeap::CollectAllGarbageForTesting(); }
 
- protected:
-  ImageCapture* CreateImageCapture(bool pan_tilt_zoom_allowed = true) const {
-    constexpr ExecutionContext* execution_context = nullptr;
-    MediaStreamTrack* track = MakeGarbageCollected<MockMediaStreamTrack>();
-    return MakeGarbageCollected<ImageCapture>(
-        execution_context, track, pan_tilt_zoom_allowed, base::DoNothing());
+  void SetupTrackMocks(V8TestingScope& scope) {
+    source_ = std::make_unique<MediaStreamVideoCapturerSource>(
+        scope.GetFrame().GetTaskRunner(TaskType::kInternalMediaRealTime),
+        &scope.GetFrame(),
+        MediaStreamVideoCapturerSource::SourceStoppedCallback(),
+        std::make_unique<NiceMock<MockVideoCapturerSource>>());
+    platform_track_ = std::make_unique<MediaStreamVideoTrack>(
+        source_.get(), WebPlatformMediaStreamSource::ConstraintsOnceCallback(),
+        /*enabled=*/true);
+    EXPECT_CALL(*component_, GetPlatformTrack)
+        .WillRepeatedly(Return(platform_track_.get()));
+    EXPECT_CALL(*component_, GetSourceType)
+        .WillRepeatedly(Return(MediaStreamSource::kTypeVideo));
+
+    EXPECT_CALL(*component_, AddSink(_, _, _, _))
+        .WillOnce(Invoke([&](WebMediaStreamSink* sink,
+                             const VideoCaptureDeliverFrameCB& callback,
+                             MediaStreamVideoSink::IsSecure is_secure,
+                             MediaStreamVideoSink::UsesAlpha uses_alpha) {
+          platform_track_->AddSink(sink, callback, is_secure, uses_alpha);
+          callback.Run(VideoFrame::CreateBlackFrame(gfx::Size(1, 1)),
+                       /*scaled_video_frames=*/{},
+                       /*estimated_capture_time=*/base::TimeTicks());
+        }));
   }
+
+ protected:
+  Persistent<MockMediaStreamComponent> component_;
+  Persistent<MockMediaStreamTrack> track_;
+  Persistent<ImageCapture> image_capture_;
+  ScopedTestingPlatformSupport<IOTaskRunnerTestingPlatformSupport> platform_;
+  std::unique_ptr<MediaStreamVideoCapturerSource> source_;
+  std::unique_ptr<MediaStreamVideoTrack> platform_track_;
 };
 
 class ImageCaptureConstraintTest : public ImageCaptureTest {
@@ -665,7 +749,6 @@
     // Otherwise `CheckMinValues` does not really check anything.
     DCHECK_GT(all_capabilities_->focusDistance()->min() + kFocusDistanceDelta,
               default_settings_->focusDistance());
-    image_capture_ = CreateImageCapture();
   }
 
  protected:
@@ -682,7 +765,6 @@
   Persistent<MediaTrackCapabilities> all_capabilities_;
   Persistent<MediaTrackCapabilities> all_non_capabilities_;
   Persistent<MediaTrackSettings> default_settings_;
-  Persistent<ImageCapture> image_capture_;
 };
 
 TEST_F(ImageCaptureConstraintTest, ApplyBasicBareValueConstraints) {
@@ -1482,4 +1564,58 @@
   EXPECT_EQ(capture_error->Name(), "SecurityError");
 }
 
+TEST_F(ImageCaptureTest, GrabFrameOfLiveTrackIsFulfilled) {
+  V8TestingScope scope;
+  SetupTrackMocks(scope);
+  track_->SetReadyState("live");
+  track_->setEnabled(true);
+  track_->SetMuted(false);
+
+  ScriptPromise result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsFulfilled());
+}
+
+TEST_F(ImageCaptureTest, GrabFrameOfMutedTrackIsFulfilled) {
+  V8TestingScope scope;
+  SetupTrackMocks(scope);
+  track_->SetReadyState("live");
+  track_->setEnabled(true);
+  track_->SetMuted(true);
+
+  ScriptPromise result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsFulfilled());
+}
+
+TEST_F(ImageCaptureTest, GrabFrameOfEndedTrackRejects) {
+  V8TestingScope scope;
+  track_->SetReadyState("ended");
+  track_->setEnabled(true);
+  track_->SetMuted(false);
+
+  ScriptPromise result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsRejected());
+}
+
+TEST_F(ImageCaptureTest, GrabFrameOfDisabledTrackRejects) {
+  V8TestingScope scope;
+  track_->SetReadyState("live");
+  track_->setEnabled(false);
+  track_->SetMuted(false);
+
+  ScriptPromise result = image_capture_->grabFrame(scope.GetScriptState());
+
+  ScriptPromiseTester tester(scope.GetScriptState(), result);
+  tester.WaitUntilSettled();
+  EXPECT_TRUE(tester.IsRejected());
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.cc b/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.cc
index ecbeef8..17bf212 100644
--- a/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.cc
+++ b/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.cc
@@ -91,12 +91,6 @@
     std::move(stop_capture_cb_).Run();
 }
 
-void LocalVideoCapturerSource::OnFrameDropped(
-    media::VideoCaptureFrameDropReason reason) {
-  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  manager_->OnFrameDropped(session_id_, reason);
-}
-
 void LocalVideoCapturerSource::OnLog(const std::string& message) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   manager_->OnLog(session_id_, WebString::FromUTF8(message));
diff --git a/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.h b/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.h
index c06ffee..bee1050 100644
--- a/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.h
+++ b/third_party/blink/renderer/modules/mediastream/local_video_capturer_source.h
@@ -62,7 +62,6 @@
   void MaybeSuspend() override;
   void Resume() override;
   void StopCapture() override;
-  void OnFrameDropped(media::VideoCaptureFrameDropReason reason) override;
   void OnLog(const std::string& message) override;
   media::VideoCaptureFeedbackCB GetFeedbackCallback() const override;
 
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.cc b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.cc
index 6854cc2a..111bce0 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_frame_delegate.cc
@@ -92,17 +92,8 @@
 
 absl::optional<uint64_t> RTCEncodedAudioFrameDelegate::AbsCaptureTime() const {
   base::AutoLock lock(lock_);
-  if (webrtc_frame_ &&
-      webrtc_frame_->GetDirection() ==
-          webrtc::TransformableFrameInterface::Direction::kReceiver) {
-    webrtc::TransformableAudioFrameInterface* incoming_audio_frame =
-        static_cast<webrtc::TransformableAudioFrameInterface*>(
-            webrtc_frame_.get());
-
-    return incoming_audio_frame->AbsoluteCaptureTimestamp();
-  }
-
-  return absl::nullopt;
+  return webrtc_frame_ ? webrtc_frame_->AbsoluteCaptureTimestamp()
+                       : absl::nullopt;
 }
 
 std::unique_ptr<webrtc::TransformableAudioFrameInterface>
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.cc b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.cc
index b5a2f71b..f59e102 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.cc
@@ -44,7 +44,8 @@
 }
 
 ScriptPromise RTCEncodedAudioUnderlyingSource::Cancel(ScriptState* script_state,
-                                                      ScriptValue reason) {
+                                                      ScriptValue reason,
+                                                      ExceptionState&) {
   DCHECK(task_runner_->BelongsToCurrentThread());
   if (disconnect_callback_)
     std::move(disconnect_callback_).Run();
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.h b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.h
index 50e2f2f..c67f83b 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_audio_underlying_source.h
@@ -27,7 +27,9 @@
 
   // UnderlyingSourceBase
   ScriptPromise pull(ScriptState*) override;
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason) override;
+  ScriptPromise Cancel(ScriptState*,
+                       ScriptValue reason,
+                       ExceptionState&) override;
 
   void OnFrameFromSource(
       std::unique_ptr<webrtc::TransformableAudioFrameInterface>);
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.cc b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.cc
index 54ca7d1..2a637ab 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.cc
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.cc
@@ -44,7 +44,8 @@
 }
 
 ScriptPromise RTCEncodedVideoUnderlyingSource::Cancel(ScriptState* script_state,
-                                                      ScriptValue reason) {
+                                                      ScriptValue reason,
+                                                      ExceptionState&) {
   DCHECK(task_runner_->BelongsToCurrentThread());
   if (disconnect_callback_)
     std::move(disconnect_callback_).Run();
diff --git a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.h b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.h
index e3f15e8..2017bb5 100644
--- a/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.h
+++ b/third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_source.h
@@ -27,7 +27,9 @@
 
   // UnderlyingSourceBase
   ScriptPromise pull(ScriptState*) override;
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason) override;
+  ScriptPromise Cancel(ScriptState*,
+                       ScriptValue reason,
+                       ExceptionState&) override;
 
   void OnFrameFromSource(
       std::unique_ptr<webrtc::TransformableVideoFrameInterface>);
diff --git a/third_party/blink/renderer/modules/webgpu/dawn_enum_conversions.cc b/third_party/blink/renderer/modules/webgpu/dawn_enum_conversions.cc
index 1cf4d956..b40d099d 100644
--- a/third_party/blink/renderer/modules/webgpu/dawn_enum_conversions.cc
+++ b/third_party/blink/renderer/modules/webgpu/dawn_enum_conversions.cc
@@ -842,6 +842,8 @@
       return WGPUVertexFormat_Sint32x3;
     case V8GPUVertexFormat::Enum::kSint32X4:
       return WGPUVertexFormat_Sint32x4;
+    case V8GPUVertexFormat::Enum::kUnorm1010102:
+      return WGPUVertexFormat_Unorm10_10_10_2;
   }
 }
 
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_adapter.cc b/third_party/blink/renderer/modules/webgpu/gpu_adapter.cc
index 120ac6e..91cfe92 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_adapter.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_adapter.cc
@@ -31,6 +31,8 @@
   switch (f) {
     case WGPUFeatureName_Depth32FloatStencil8:
       return V8GPUFeatureName::Enum::kDepth32FloatStencil8;
+    case WGPUFeatureName_TimestampQuery:
+      return V8GPUFeatureName::Enum::kTimestampQuery;
     case WGPUFeatureName_TimestampQueryInsidePasses:
       return V8GPUFeatureName::Enum::kTimestampQueryInsidePasses;
     case WGPUFeatureName_PipelineStatisticsQuery:
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_command_encoder.cc b/third_party/blink/renderer/modules/webgpu/gpu_command_encoder.cc
index 1a4717ad..a2d10ed5 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_command_encoder.cc
+++ b/third_party/blink/renderer/modules/webgpu/gpu_command_encoder.cc
@@ -51,6 +51,45 @@
 
 namespace {
 
+// Dawn represents `undefined` as the special uint32_t value
+// WGPU_QUERY_SET_INDEX_UNDEFINED (0xFFFF'FFFF). Blink must make sure that an
+// actual value of 0xFFFF'FFFF coming in from JS is not treated as
+// WGPU_QUERY_SET_INDEX_UNDEFINED, so it injects an error in that case.
+template <typename GPUTimestampWrites, typename WGPUTimestampWrites>
+const char* ValidateAndConvertTimestampWrites(
+    const GPUTimestampWrites* webgpu_desc,
+    WGPUTimestampWrites* dawn_desc) {
+  DCHECK(webgpu_desc);
+  DCHECK(webgpu_desc->querySet());
+
+  uint32_t beginningOfPassWriteIndex = 0;
+  if (webgpu_desc->hasBeginningOfPassWriteIndex()) {
+    beginningOfPassWriteIndex = webgpu_desc->beginningOfPassWriteIndex();
+    if (beginningOfPassWriteIndex == WGPU_QUERY_SET_INDEX_UNDEFINED) {
+      return "beginningOfPassWriteIndex is too large";
+    }
+  } else {
+    beginningOfPassWriteIndex = WGPU_QUERY_SET_INDEX_UNDEFINED;
+  }
+
+  uint32_t endOfPassWriteIndex = 0;
+  if (webgpu_desc->hasEndOfPassWriteIndex()) {
+    endOfPassWriteIndex = webgpu_desc->endOfPassWriteIndex();
+    if (endOfPassWriteIndex == WGPU_QUERY_SET_INDEX_UNDEFINED) {
+      return "endOfPassWriteIndex is too large";
+    }
+  } else {
+    endOfPassWriteIndex = WGPU_QUERY_SET_INDEX_UNDEFINED;
+  }
+
+  *dawn_desc = {};
+  dawn_desc->querySet = webgpu_desc->querySet()->GetHandle();
+  dawn_desc->beginningOfPassWriteIndex = beginningOfPassWriteIndex;
+  dawn_desc->endOfPassWriteIndex = endOfPassWriteIndex;
+
+  return nullptr;
+}
+
 WGPURenderPassDepthStencilAttachment AsDawnType(
     GPUDevice* device,
     const GPURenderPassDepthStencilAttachment* webgpu_desc) {
@@ -175,8 +214,17 @@
     dawn_desc.occlusionQuerySet = AsDawnType(descriptor->occlusionQuerySet());
   }
 
+  WGPURenderPassTimestampWrites timestampWrites = {};
   if (descriptor->hasTimestampWrites()) {
-    // TODO(dawn:1800): Re-enable timestamp queries.
+    GPURenderPassTimestampWrites* timestamp_writes =
+        descriptor->timestampWrites();
+    const char* error =
+        ValidateAndConvertTimestampWrites(timestamp_writes, &timestampWrites);
+    if (error) {
+      GetProcs().commandEncoderInjectValidationError(GetHandle(), error);
+    } else {
+      dawn_desc.timestampWrites = &timestampWrites;
+    }
   }
 
   WGPURenderPassDescriptorMaxDrawCount max_draw_count = {};
@@ -205,8 +253,17 @@
     dawn_desc.label = label.c_str();
   }
 
+  WGPUComputePassTimestampWrites timestampWrites = {};
   if (descriptor->hasTimestampWrites()) {
-    // TODO(dawn:1800): Re-enable timestamp queries.
+    GPUComputePassTimestampWrites* timestamp_writes =
+        descriptor->timestampWrites();
+    const char* error =
+        ValidateAndConvertTimestampWrites(timestamp_writes, &timestampWrites);
+    if (error) {
+      GetProcs().commandEncoderInjectValidationError(GetHandle(), error);
+    } else {
+      dawn_desc.timestampWrites = &timestampWrites;
+    }
   }
 
   GPUComputePassEncoder* encoder = MakeGarbageCollected<GPUComputePassEncoder>(
diff --git a/third_party/blink/renderer/modules/webgpu/gpu_vertex_attribute.idl b/third_party/blink/renderer/modules/webgpu/gpu_vertex_attribute.idl
index 61e5ff4..32b35c4 100644
--- a/third_party/blink/renderer/modules/webgpu/gpu_vertex_attribute.idl
+++ b/third_party/blink/renderer/modules/webgpu/gpu_vertex_attribute.idl
@@ -41,4 +41,5 @@
     "sint32x2",
     "sint32x3",
     "sint32x4",
+    "unorm10-10-10-2",
 };
diff --git a/third_party/blink/renderer/modules/websockets/websocket_stream.cc b/third_party/blink/renderer/modules/websockets/websocket_stream.cc
index 8fe62bc..99e94793 100644
--- a/third_party/blink/renderer/modules/websockets/websocket_stream.cc
+++ b/third_party/blink/renderer/modules/websockets/websocket_stream.cc
@@ -43,7 +43,9 @@
 
   // UnderlyingSourceBase implementation.
   ScriptPromise pull(ScriptState*) override;
-  ScriptPromise Cancel(ScriptState*, ScriptValue reason) override;
+  ScriptPromise Cancel(ScriptState*,
+                       ScriptValue reason,
+                       ExceptionState&) override;
 
   // API for WebSocketStream.
   void DidReceiveTextMessage(const String&);
@@ -124,10 +126,11 @@
 
 ScriptPromise WebSocketStream::UnderlyingSource::Cancel(
     ScriptState* script_state,
-    ScriptValue reason) {
+    ScriptValue reason,
+    ExceptionState& exception_state) {
   DVLOG(1) << "WebSocketStream::UnderlyingSource " << this << " Cancel()";
   closed_ = true;
-  creator_->CloseMaybeWithReason(reason);
+  creator_->CloseMaybeWithReason(reason, exception_state);
   return ScriptPromise::CastUndefined(script_state);
 }
 
@@ -214,7 +217,7 @@
     ExceptionState& exception_state) {
   DVLOG(1) << "WebSocketStream::UnderlyingSink " << this << " close()";
   closed_ = true;
-  creator_->CloseWithUnspecifiedCode();
+  creator_->CloseWithUnspecifiedCode(exception_state);
   DCHECK(!close_resolver_);
   close_resolver_ = MakeGarbageCollected<ScriptPromiseResolver>(
       script_state, exception_state.GetContext());
@@ -228,7 +231,7 @@
   DVLOG(1) << "WebSocketStream::UnderlyingSink " << this << " abort()";
 
   closed_ = true;
-  creator_->CloseMaybeWithReason(reason);
+  creator_->CloseMaybeWithReason(reason, exception_state);
   return ScriptPromise::CastUndefined(script_state);
 }
 
@@ -666,12 +669,9 @@
 
 // If |maybe_reason| contains a valid code and reason, then closes with it,
 // otherwise closes with unspecified code and reason.
-void WebSocketStream::CloseMaybeWithReason(ScriptValue maybe_reason) {
+void WebSocketStream::CloseMaybeWithReason(ScriptValue maybe_reason,
+                                           ExceptionState& exception_state) {
   DVLOG(1) << "WebSocketStream " << this << " CloseMaybeWithReason()";
-
-  // Exceptions thrown here are ignored.
-  ExceptionState exception_state(script_state_->GetIsolate(),
-                                 ExceptionContextType::kUnknown, "", "");
   WebSocketCloseInfo* info = NativeValueTraits<WebSocketCloseInfo>::NativeValue(
       script_state_->GetIsolate(), maybe_reason.V8Value(), exception_state);
   if (!exception_state.HadException() && info->hasCode()) {
@@ -684,14 +684,12 @@
   if (exception_state.HadException()) {
     exception_state.ClearException();
   }
-  CloseWithUnspecifiedCode();
+  CloseWithUnspecifiedCode(exception_state);
 }
 
-void WebSocketStream::CloseWithUnspecifiedCode() {
+void WebSocketStream::CloseWithUnspecifiedCode(
+    ExceptionState& exception_state) {
   DVLOG(1) << "WebSocketStream " << this << " CloseWithUnspecifiedCode()";
-
-  ExceptionState exception_state(script_state_->GetIsolate(),
-                                 ExceptionContextType::kUnknown, "", "");
   CloseInternal(WebSocketChannel::kCloseEventCodeNotSpecified, String(),
                 exception_state);
   DCHECK(!exception_state.HadException());
diff --git a/third_party/blink/renderer/modules/websockets/websocket_stream.h b/third_party/blink/renderer/modules/websockets/websocket_stream.h
index 22315515..97f5cd4a 100644
--- a/third_party/blink/renderer/modules/websockets/websocket_stream.h
+++ b/third_party/blink/renderer/modules/websockets/websocket_stream.h
@@ -102,9 +102,9 @@
   // Closes the connection. If |maybe_reason| is an object with a valid "code"
   // property and optionally a valid "reason" property, will use them as the
   // code and reason, otherwise will close with unspecified close.
-  void CloseMaybeWithReason(ScriptValue maybe_reason);
+  void CloseMaybeWithReason(ScriptValue maybe_reason, ExceptionState&);
 
-  void CloseWithUnspecifiedCode();
+  void CloseWithUnspecifiedCode(ExceptionState&);
   void CloseInternal(int code,
                      const String& reason,
                      ExceptionState& exception_state);
diff --git a/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager.cc b/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager.cc
index 9138f60..abbfb15 100644
--- a/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager.cc
+++ b/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager.cc
@@ -272,18 +272,6 @@
   }
 }
 
-void WebVideoCaptureImplManager::OnFrameDropped(
-    const media::VideoCaptureSessionId& id,
-    media::VideoCaptureFrameDropReason reason) {
-  DCHECK(render_main_task_runner_->BelongsToCurrentThread());
-  const auto it = base::ranges::find(devices_, id, &DeviceEntry::session_id);
-  if (it == devices_.end())
-    return;
-  Platform::Current()->GetIOTaskRunner()->PostTask(
-      FROM_HERE, base::BindOnce(&VideoCaptureImpl::OnFrameDropped,
-                                it->impl->GetWeakPtr(), reason));
-}
-
 void WebVideoCaptureImplManager::OnLog(const media::VideoCaptureSessionId& id,
                                        const WebString& message) {
   DCHECK(render_main_task_runner_->BelongsToCurrentThread());
diff --git a/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager_test.cc b/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager_test.cc
index f74e8a5..210ec22 100644
--- a/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager_test.cc
+++ b/third_party/blink/renderer/platform/exported/video_capture/web_video_capture_impl_manager_test.cc
@@ -105,9 +105,7 @@
     NOTREACHED();
   }
 
-  MOCK_METHOD2(OnFrameDropped,
-               void(const base::UnguessableToken&,
-                    media::VideoCaptureFrameDropReason));
+  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason));
   MOCK_METHOD2(OnLog, void(const base::UnguessableToken&, const String&));
 
   PauseResumeCallback* const pause_callback_;
diff --git a/third_party/blink/renderer/platform/fonts/font_size_adjust.h b/third_party/blink/renderer/platform/fonts/font_size_adjust.h
index 5e88dcb..bdad218 100644
--- a/third_party/blink/renderer/platform/fonts/font_size_adjust.h
+++ b/third_party/blink/renderer/platform/fonts/font_size_adjust.h
@@ -28,7 +28,9 @@
 
   static constexpr float kFontSizeAdjustNone = -1;
 
-  explicit operator bool() const { return value_ != kFontSizeAdjustNone; }
+  explicit operator bool() const {
+    return value_ != kFontSizeAdjustNone || is_from_font_;
+  }
   bool operator==(const FontSizeAdjust& other) const {
     return value_ == other.Value() && metric_ == other.GetMetric() &&
            is_from_font_ == other.IsFromFont();
diff --git a/third_party/blink/renderer/platform/fonts/shaping/han_kerning.cc b/third_party/blink/renderer/platform/fonts/shaping/han_kerning.cc
index cf8f97d9..af905b9 100644
--- a/third_party/blink/renderer/platform/fonts/shaping/han_kerning.cc
+++ b/third_party/blink/renderer/platform/fonts/shaping/han_kerning.cc
@@ -16,12 +16,6 @@
 
 namespace {
 
-inline bool IsCjkSymbolsAndPunctuationOrEastAsianFullwidth(UChar ch) {
-  return (ch >= 0x3000 && ch <= 0x303F) ||
-         static_cast<UEastAsianWidth>(u_getIntPropertyValue(
-             ch, UCHAR_EAST_ASIAN_WIDTH)) == UEastAsianWidth::U_EA_FULLWIDTH;
-}
-
 // Get `CharType` from the glyph bounding box.
 HanKerning::CharType GetType(const SkRect& bound,
                              float em,
@@ -87,14 +81,15 @@
     case kKatakanaMiddleDot:          // U+30FB
       return CharType::kMiddle;
   }
-  const auto gc = static_cast<UCharCategory>(u_charType(ch));
-  if (gc == UCharCategory::U_START_PUNCTUATION &&
-      IsCjkSymbolsAndPunctuationOrEastAsianFullwidth(ch)) {
-    return CharType::kOpen;
-  }
-  if (gc == UCharCategory::U_END_PUNCTUATION &&
-      IsCjkSymbolsAndPunctuationOrEastAsianFullwidth(ch)) {
-    return CharType::kClose;
+  if (Character::IsBlockCjkSymbolsAndPunctuation(ch) ||
+      Character::IsEastAsianWidthFullwidth(ch)) {
+    const auto gc = static_cast<UCharCategory>(u_charType(ch));
+    if (gc == UCharCategory::U_START_PUNCTUATION) {
+      return CharType::kOpen;
+    }
+    if (gc == UCharCategory::U_END_PUNCTUATION) {
+      return CharType::kClose;
+    }
   }
   return CharType::kOther;
 }
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
index 639608c3..f0a900f 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.cc
@@ -1095,7 +1095,6 @@
 
 bool PaintArtifactCompositor::UsesCompositedScrolling(
     const ScrollPaintPropertyNode& scroll) const {
-  DCHECK(RuntimeEnabledFeatures::CompositeScrollAfterPaintEnabled());
   CHECK(root_layer_);
   if (!root_layer_->layer_tree_host()) {
     return false;
diff --git a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
index 0a875ac..a9d5b2e 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/property_tree_manager.cc
@@ -246,12 +246,10 @@
 bool PropertyTreeManager::UsesCompositedScrolling(
     const cc::LayerTreeHost& host,
     const ScrollPaintPropertyNode& scroll) {
-  DCHECK(RuntimeEnabledFeatures::CompositeScrollAfterPaintEnabled());
   const auto* property_trees = host.property_trees();
   const auto* cc_scroll = property_trees->scroll_tree().Node(
       scroll.CcNodeId(property_trees->sequence_number()));
-  DCHECK(cc_scroll);
-  return cc_scroll->is_composited;
+  return cc_scroll && cc_scroll->is_composited;
 }
 
 void PropertyTreeManager::SetupRootTransformNode() {
diff --git a/third_party/blink/renderer/platform/media/BUILD.gn b/third_party/blink/renderer/platform/media/BUILD.gn
index 9556112..7062313 100644
--- a/third_party/blink/renderer/platform/media/BUILD.gn
+++ b/third_party/blink/renderer/platform/media/BUILD.gn
@@ -149,6 +149,10 @@
     "//third_party/blink/renderer/platform:test_support",
   ]
 
+  if (enable_hls_demuxer) {
+    sources += [ "hls_data_source_provider_impl_unittest.cc" ]
+  }
+
   if (media_use_ffmpeg || !is_android) {
     sources += [
       "buffered_data_source_host_impl_unittest.cc",
diff --git a/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.cc b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.cc
index d1b1abfe..77e509a4 100644
--- a/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.cc
+++ b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.cc
@@ -135,6 +135,25 @@
       tick_clock);
 }
 
+HlsDataSourceProviderImpl::~HlsDataSourceProviderImpl() {
+  DCHECK(main_task_runner_->BelongsToCurrentThread());
+  // MultiBufferDataSource relies on a weak pointer reference to
+  // `buffered_data_source_host_`, so any instance of MBDS _MUST_ be aborted
+  // before `buffered_data_source_host_` is deleted.
+  for (auto* const data_source : GetActiveDataSources()) {
+    data_source->Abort();
+  }
+}
+
+void HlsDataSourceProviderImpl::RequestDataSourceInternal(
+    std::unique_ptr<MultiBufferDataSource> data_source,
+    RequestCb callback) {
+  auto* mb_data_source_ptr = data_source.get();
+  mb_data_source_ptr->Initialize(base::BindOnce(
+      &Self::DataSourceInitialized, weak_factory_.GetWeakPtr(),
+      std::move(data_source), absl::nullopt, std::move(callback)));
+}
+
 void HlsDataSourceProviderImpl::RequestDataSource(
     GURL uri,
     absl::optional<media::hls::types::ByteRange> range,
@@ -150,11 +169,7 @@
       buffered_data_source_host_.get(),
       base::BindRepeating(&Self::NotifyDownloading, weak_factory_.GetWeakPtr(),
                           uri.spec()));
-
-  auto* mb_data_source_ptr = mb_data_source.get();
-  mb_data_source_ptr->Initialize(
-      base::BindOnce(&Self::DataSourceInitialized, weak_factory_.GetWeakPtr(),
-                     std::move(mb_data_source), range, std::move(callback)));
+  RequestDataSourceInternal(std::move(mb_data_source), std::move(callback));
 }
 
 const std::deque<MultiBufferDataSource*>&
diff --git a/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.h b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.h
index 2c503ec4..ec73430 100644
--- a/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.h
+++ b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl.h
@@ -13,6 +13,7 @@
 #include "base/types/pass_key.h"
 #include "media/base/media_log.h"
 #include "media/filters/hls_data_source_provider.h"
+#include "third_party/blink/renderer/platform/platform_export.h"
 
 namespace blink {
 
@@ -21,10 +22,12 @@
 class MultiBufferDataSource;
 class HlsDataSourceImpl;
 
-class HlsDataSourceProviderImpl : public media::HlsDataSourceProvider {
+class PLATFORM_EXPORT HlsDataSourceProviderImpl
+    : public media::HlsDataSourceProvider {
   using Self = HlsDataSourceProviderImpl;
 
  public:
+  ~HlsDataSourceProviderImpl() override;
   HlsDataSourceProviderImpl(
       media::MediaLog* media_log,
       UrlIndex* url_index,
@@ -32,6 +35,12 @@
       scoped_refptr<base::SequencedTaskRunner> media_task_runner,
       const base::TickClock* tick_clock);
 
+  void RequestMockDataSourceForTesting(
+      std::unique_ptr<MultiBufferDataSource> mock_ds,
+      RequestCb callback) {
+    RequestDataSourceInternal(std::move(mock_ds), std::move(callback));
+  }
+
   // `media::DataSourceProvider` implementation
   void RequestDataSource(GURL uri,
                          absl::optional<media::hls::types::ByteRange> range,
@@ -48,6 +57,10 @@
                                  std::unique_ptr<MultiBufferDataSource>);
 
  private:
+  void RequestDataSourceInternal(
+      std::unique_ptr<MultiBufferDataSource> data_source,
+      RequestCb callback);
+
   void NotifyDataSourceProgress();
 
   void NotifyDownloading(const std::string& uri, bool is_downloading);
diff --git a/third_party/blink/renderer/platform/media/hls_data_source_provider_impl_unittest.cc b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl_unittest.cc
new file mode 100644
index 0000000..289e332
--- /dev/null
+++ b/third_party/blink/renderer/platform/media/hls_data_source_provider_impl_unittest.cc
@@ -0,0 +1,190 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/memory/scoped_refptr.h"
+#include "base/run_loop.h"
+#include "base/test/gmock_callback_support.h"
+#include "base/test/simple_test_tick_clock.h"
+#include "base/test/task_environment.h"
+#include "media/base/mock_media_log.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/platform/media/buffered_data_source_host_impl.h"
+#include "third_party/blink/renderer/platform/media/hls_data_source_provider_impl.h"
+#include "third_party/blink/renderer/platform/media/multi_buffer_data_source.h"
+
+namespace blink {
+
+using base::test::RunOnceCallback;
+using testing::_;
+using testing::AtLeast;
+using testing::ByMove;
+using testing::DoAll;
+using testing::Eq;
+using testing::Invoke;
+using testing::NiceMock;
+using testing::NotNull;
+using testing::Ref;
+using testing::Return;
+using testing::SaveArg;
+using testing::SetArgPointee;
+using testing::StrictMock;
+
+namespace {
+
+class TestUrlIndex : public UrlIndex {
+ public:
+  explicit TestUrlIndex(scoped_refptr<base::SingleThreadTaskRunner> task_runner)
+      : UrlIndex(nullptr, task_runner) {}
+
+  scoped_refptr<UrlData> NewUrlData(const GURL& url,
+                                    UrlData::CorsMode cors_mode) override {
+    NOTREACHED();
+    return nullptr;
+  }
+};
+
+class TestUrlData : public UrlData {
+ public:
+  TestUrlData(const GURL& url,
+              UrlIndex* url_index,
+              scoped_refptr<base::SingleThreadTaskRunner> task_runner)
+      : UrlData(url, UrlData::CORS_UNSPECIFIED, url_index, task_runner) {}
+
+  ResourceMultiBuffer* multibuffer() override { return nullptr; }
+};
+
+class MockBufferedDataSourceHost : public BufferedDataSourceHost {
+ public:
+  MockBufferedDataSourceHost() = default;
+  MockBufferedDataSourceHost(const MockBufferedDataSourceHost&) = delete;
+  MockBufferedDataSourceHost& operator=(const MockBufferedDataSourceHost&) =
+      delete;
+  ~MockBufferedDataSourceHost() override = default;
+
+  MOCK_METHOD1(SetTotalBytes, void(int64_t total_bytes));
+  MOCK_METHOD2(AddBufferedByteRange, void(int64_t start, int64_t end));
+};
+
+class MockMultiBufferDataSource : public MultiBufferDataSource {
+ public:
+  MockMultiBufferDataSource(
+      const scoped_refptr<base::SingleThreadTaskRunner>& task_runner,
+      scoped_refptr<UrlData> url_data,
+      media::MediaLog* media_log,
+      BufferedDataSourceHost* host,
+      DownloadingCB downloading_cb)
+      : MultiBufferDataSource(std::move(task_runner),
+                              std::move(url_data),
+                              media_log,
+                              host,
+                              std::move(downloading_cb)) {}
+
+  void Initialize(InitializeCB init_cb) override {
+    InitializeCalled();
+    std::move(init_cb).Run(true);
+  }
+
+  MOCK_METHOD(void, InitializeCalled, (), ());
+  MOCK_METHOD(void, Abort, (), (override));
+  MOCK_METHOD(void,
+              Read,
+              (int64_t, int, uint8_t*, media::DataSource::ReadCB),
+              (override));
+};
+
+}  // namespace
+
+class HlsDataSourceProviderImplUnittest : public testing::Test {
+ public:
+  HlsDataSourceProviderImplUnittest()
+      : media_log_(std::make_unique<NiceMock<media::MockMediaLog>>()),
+        tick_clock_(std::make_unique<base::SimpleTestTickClock>()),
+        mock_host_(std::make_unique<MockBufferedDataSourceHost>()) {
+    url_index_ = std::make_unique<TestUrlIndex>(
+        task_environment_.GetMainThreadTaskRunner());
+  }
+
+  void SetUpDSP() {
+    impl_ = std::make_unique<HlsDataSourceProviderImpl>(
+        media_log_.get(), url_index_.get(),
+        task_environment_.GetMainThreadTaskRunner(),
+        task_environment_.GetMainThreadTaskRunner(), tick_clock_.get());
+  }
+
+  ~HlsDataSourceProviderImplUnittest() override {
+    data_source_.reset();
+    // URL data needs to be freed before the url index, because UrlData keeps
+    // a rawptr to UrlIndex.
+    task_environment_.RunUntilIdle();
+    url_index_.reset();
+  }
+
+  media::HlsDataSourceProvider::RequestCb StoreDSP() {
+    return base::BindOnce(&HlsDataSourceProviderImplUnittest::StoreDSPImpl,
+                          base::Unretained(this));
+  }
+
+  void StoreDSPImpl(std::unique_ptr<media::HlsDataSource> data_source) {
+    ASSERT_EQ(data_source_, nullptr);
+    data_source_ = std::move(data_source);
+  }
+
+  std::unique_ptr<MockMultiBufferDataSource> MakeMockDataSource() {
+    return std::make_unique<MockMultiBufferDataSource>(
+        task_environment_.GetMainThreadTaskRunner(),
+        NewUrlData(GURL("https://example.com")), media_log_.get(),
+        mock_host_.get(),
+        base::BindRepeating(&HlsDataSourceProviderImplUnittest::Downloading,
+                            base::Unretained(this)));
+  }
+
+  void Downloading(bool) {}
+
+  scoped_refptr<UrlData> NewUrlData(const GURL& url) {
+    return new TestUrlData(url, url_index_.get(),
+                           task_environment_.GetMainThreadTaskRunner());
+  }
+
+ protected:
+  base::test::TaskEnvironment task_environment_;
+  std::unique_ptr<media::MediaLog> media_log_;
+  std::unique_ptr<base::TickClock> tick_clock_;
+  std::unique_ptr<MockBufferedDataSourceHost> mock_host_;
+  std::unique_ptr<TestUrlIndex> url_index_;
+  std::unique_ptr<HlsDataSourceProviderImpl> impl_;
+  std::unique_ptr<media::HlsDataSource> data_source_;
+};
+
+TEST_F(HlsDataSourceProviderImplUnittest, TestMultibuffersCreateReadAbort) {
+  SetUpDSP();
+  std::unique_ptr<MockMultiBufferDataSource> mock_ds = MakeMockDataSource();
+
+  EXPECT_CALL(*mock_ds, InitializeCalled());
+  EXPECT_CALL(*mock_ds, Read(0, 50, nullptr, _));
+  EXPECT_CALL(*mock_ds, Abort());
+
+  impl_->RequestMockDataSourceForTesting(std::move(mock_ds), StoreDSP());
+  task_environment_.RunUntilIdle();
+
+  ASSERT_NE(data_source_, nullptr);
+  data_source_->Read(
+      0, 50, nullptr,
+      base::BindOnce([](media::HlsDataSource::ReadStatus::Or<size_t>) {
+        // This callback should never be executed because mock_ds::Read is
+        // never replied to.
+        FAIL() << "This Read() should never complete.";
+      }));
+
+  task_environment_.RunUntilIdle();
+  // Resetting impl triggers the abort.
+  impl_.reset();
+  task_environment_.RunUntilIdle();
+}
+
+}  // namespace blink
diff --git a/third_party/blink/renderer/platform/media/multi_buffer_data_source.h b/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
index 29ca8ef..8d6408c 100644
--- a/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
+++ b/third_party/blink/renderer/platform/media/multi_buffer_data_source.h
@@ -68,7 +68,7 @@
   //
   // Method called on the render thread.
   using InitializeCB = base::OnceCallback<void(bool)>;
-  void Initialize(InitializeCB init_cb);
+  virtual void Initialize(InitializeCB init_cb);
 
   // Adjusts the buffering algorithm based on the given preload value.
   void SetPreload(media::DataSource::Preload preload) override;
diff --git a/third_party/blink/renderer/platform/media/web_media_player_impl.cc b/third_party/blink/renderer/platform/media/web_media_player_impl.cc
index b81ba608..8df21ad2 100644
--- a/third_party/blink/renderer/platform/media/web_media_player_impl.cc
+++ b/third_party/blink/renderer/platform/media/web_media_player_impl.cc
@@ -816,16 +816,18 @@
 #endif  // BUILDFLAG(IS_ANDROID)
 
   // Note: `url` may be very large, take care when making copies.
-  loaded_url_ = GURL(url);
+  demuxer_manager_->SetLoadedUrl(GURL(url));
   load_type_ = load_type;
 
-  ReportMetrics(load_type, loaded_url_, *frame_, media_log_.get());
+  ReportMetrics(load_type, demuxer_manager_->LoadedUrl(), *frame_,
+                media_log_.get());
 
   // Set subresource URL for crash reporting; will be truncated to 256 bytes.
   static base::debug::CrashKeyString* subresource_url =
       base::debug::AllocateCrashKeyString("subresource_url",
                                           base::debug::CrashKeySize::Size256);
-  base::debug::SetCrashKeyString(subresource_url, loaded_url_.spec());
+  base::debug::SetCrashKeyString(subresource_url,
+                                 demuxer_manager_->LoadedUrl().spec());
 
   SetNetworkState(WebMediaPlayer::kNetworkStateLoading);
   SetReadyState(WebMediaPlayer::kReadyStateHaveNothing);
@@ -841,15 +843,17 @@
 
   media_metrics_provider_->Initialize(
       load_type == kLoadTypeMediaSource,
-      load_type == kLoadTypeURL ? GetMediaURLScheme(loaded_url_)
-                                : media::mojom::MediaURLScheme::kUnknown,
+      load_type == kLoadTypeURL
+          ? GetMediaURLScheme(demuxer_manager_->LoadedUrl())
+          : media::mojom::MediaURLScheme::kUnknown,
       media::mojom::MediaStreamType::kNone);
 
   // If a demuxer override was specified or a Media Source pipeline will be
   // used, the pipeline can start immediately.
   if (demuxer_manager_->HasDemuxerOverride() ||
       load_type == kLoadTypeMediaSource ||
-      loaded_url_.SchemeIs(media::remoting::kRemotingScheme)) {
+      demuxer_manager_->LoadedUrl().SchemeIs(
+          media::remoting::kRemotingScheme)) {
     StartPipeline();
     return;
   }
@@ -857,14 +861,14 @@
   // Short circuit the more complex loading path for data:// URLs. Sending
   // them through the network based loading path just wastes memory and causes
   // worse performance since reads become asynchronous.
-  if (loaded_url_.SchemeIs(url::kDataScheme)) {
+  if (demuxer_manager_->LoadedUrl().SchemeIs(url::kDataScheme)) {
     std::string mime_type, charset, data;
-    if (!net::DataURL::Parse(loaded_url_, &mime_type, &charset, &data) ||
+    if (!net::DataURL::Parse(demuxer_manager_->LoadedUrl(), &mime_type,
+                             &charset, &data) ||
         data.empty()) {
       return MemoryDataSourceInitialized(false, 0);
     }
     size_t data_size = data.size();
-    demuxer_manager_->SetLoadedUrl(loaded_url_);
     demuxer_manager_->SetDataSource(
         std::make_unique<media::MemoryDataSource>(std::move(data)));
     MemoryDataSourceInitialized(true, data_size);
@@ -1791,9 +1795,6 @@
   DCHECK(main_task_runner_->BelongsToCurrentThread());
   DCHECK(pipeline_controller_);
   pipeline_controller_->Stop();
-  // Note: Does not consider the full redirect chain, which could contain
-  // undetected mixed content.
-  demuxer_manager_->SetLoadedUrl(loaded_url_);
 
   // delete the thread dumper on the media thread.
   media_task_runner_->DeleteSoon(FROM_HERE,
@@ -1807,7 +1808,7 @@
 }
 
 void WebMediaPlayerImpl::UpdateLoadedUrl(const GURL& url) {
-  loaded_url_ = url;
+  demuxer_manager_->SetLoadedUrl(url);
 }
 
 void WebMediaPlayerImpl::DemuxerRequestsSeek(base::TimeDelta seek_time) {
@@ -2601,8 +2602,8 @@
 void WebMediaPlayerImpl::MemoryDataSourceInitialized(bool success,
                                                      size_t data_size) {
   if (success) {
-    // Replace `loaded_url_` with an empty data:// URL since it may be large.
-    loaded_url_ = GURL("data:,");
+    // Replace the loaded url with an empty data:// URL since it may be large.
+    demuxer_manager_->SetLoadedUrl(GURL("data:,"));
 
     // Mark all the data as buffered.
     buffered_data_source_host_->SetTotalBytes(data_size);
@@ -3398,7 +3399,8 @@
 void WebMediaPlayerImpl::UpdateRemotePlaybackCompatibility(bool is_compatible) {
   DCHECK(main_task_runner_->BelongsToCurrentThread());
 
-  client_->RemotePlaybackCompatibilityChanged(loaded_url_, is_compatible);
+  client_->RemotePlaybackCompatibilityChanged(demuxer_manager_->LoadedUrl(),
+                                              is_compatible);
 }
 
 void WebMediaPlayerImpl::ForceStaleStateForTesting(ReadyState target_state) {
diff --git a/third_party/blink/renderer/platform/media/web_media_player_impl.h b/third_party/blink/renderer/platform/media/web_media_player_impl.h
index 59da81a..db5d3ee 100644
--- a/third_party/blink/renderer/platform/media/web_media_player_impl.h
+++ b/third_party/blink/renderer/platform/media/web_media_player_impl.h
@@ -925,12 +925,6 @@
   // unimportant.
   bool suppress_destruction_errors_ = false;
 
-  // TODO(dalecurtis): The following comment is inaccurate as this value is also
-  // used for, for example, data URLs.
-  // Used for HLS playback and in certain fallback paths (e.g. on older devices
-  // that can't support the unified media pipeline).
-  GURL loaded_url_ ALLOW_DISCOURAGED_TYPE("Avoids conversion in media code");
-
   // NOTE: |using_media_player_renderer_| is set based on the usage of a
   // MediaResource::Type::URL in StartPipeline(). This works because
   // MediaPlayerRendererClientFactory is the only factory that uses
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index b641e27..b2e9ffa 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -2460,6 +2460,10 @@
       status: "experimental",
     },
     {
+      name: "NavigateEventSourceElement",
+      status: "experimental",
+    },
+    {
       name: "NavigationId",
       status: "experimental",
       origin_trial_feature_name: "SoftNavigationHeuristics",
@@ -2771,6 +2775,11 @@
       base_feature: "none",
     },
     {
+      name: "PageRevealEvent",
+      status: "experimental",
+      implied_by: ["ViewTransitionOnNavigation"],
+    },
+    {
       name: "PaintFlexGridSortedByOrder",
       status: "stable",
     },
@@ -3059,11 +3068,6 @@
       name: "ReadableStreamTeeCloneForBranch2",
       status: "stable",
     },
-    {
-      name: "ReadyToRenderEvent",
-      status: "experimental",
-      implied_by: ["ViewTransitionOnNavigation"],
-    },
     // If enabled, the Accept-Language header will be reduced.
     {
       name: "ReduceAcceptLanguage",
diff --git a/third_party/blink/renderer/platform/text/character.h b/third_party/blink/renderer/platform/text/character.h
index 779b141f..86a705d 100644
--- a/third_party/blink/renderer/platform/text/character.h
+++ b/third_party/blink/renderer/platform/text/character.h
@@ -55,6 +55,22 @@
     return character >= lower_bound && character <= upper_bound;
   }
 
+  // Commonly used Unicode Blocks in CSS specs.
+  // https://www.unicode.org/Public/UNIDATA/Blocks.txt
+  static bool IsBlockCjkSymbolsAndPunctuation(UChar32 ch) {
+    return IsInRange(ch, 0x3000, 0x303F);
+  }
+  static bool IsBlockHalfwidthAndFullwidthForms(UChar32 ch) {
+    return IsInRange(ch, 0xFF00, 0xFFEF);
+  }
+
+  // East Asian Width: https://unicode.org/reports/tr11/
+  static UEastAsianWidth EastAsianWidth(UChar32 ch) {
+    return static_cast<UEastAsianWidth>(
+        u_getIntPropertyValue(ch, UCHAR_EAST_ASIAN_WIDTH));
+  }
+  static bool IsEastAsianWidthFullwidth(UChar32 ch);
+
   static inline bool IsUnicodeVariationSelector(UChar32 character) {
     // http://www.unicode.org/Public/UCD/latest/ucd/StandardizedVariants.html
     return IsInRange(character, 0x180B,
@@ -220,6 +236,15 @@
   static bool IsHangulSlow(UChar32);
 };
 
+inline bool Character::IsEastAsianWidthFullwidth(UChar32 ch) {
+  // All EAW=F characters are in the "Halfwidth and Fullwidth forms" block,
+  // except U+3000 IDEOGRAPHIC SPACE.
+  // https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp?a=[:ea=F:]
+  return ch == kIdeographicSpaceCharacter ||
+         (IsBlockHalfwidthAndFullwidthForms(ch) &&
+          EastAsianWidth(ch) == UEastAsianWidth::U_EA_FULLWIDTH);
+}
+
 }  // namespace blink
 
 #endif  // THIRD_PARTY_BLINK_RENDERER_PLATFORM_TEXT_CHARACTER_H_
diff --git a/third_party/blink/renderer/platform/text/character_test.cc b/third_party/blink/renderer/platform/text/character_test.cc
index daac8c7c..34b7fc6 100644
--- a/third_party/blink/renderer/platform/text/character_test.cc
+++ b/third_party/blink/renderer/platform/text/character_test.cc
@@ -23,11 +23,23 @@
                                      << " is not a CJKIdeographOrSymbol.";
 }
 
-TEST(CharacterTest, HammerEmojiVsCJKIdeographOrSymbol) {
-  for (UChar32 test_char = 0; test_char < kMaxCodepoint; test_char++) {
-    if (Character::IsEmojiEmojiDefault(test_char)) {
-      EXPECT_TRUE(IsCJKIdeographOrSymbolWithMessage(test_char));
+// Test Unicode-derived functions work as intended.
+// These functions may need to be adjusted if Unicode changes.
+TEST(CharacterTest, Derived) {
+  for (UChar32 ch = 0; ch < kMaxCodepoint; ++ch) {
+    if (Character::IsEmojiEmojiDefault(ch)) {
+      EXPECT_TRUE(IsCJKIdeographOrSymbolWithMessage(ch));
     }
+
+    const UBlockCode block = ublock_getCode(ch);
+    EXPECT_EQ(Character::IsBlockCjkSymbolsAndPunctuation(ch),
+              block == UBLOCK_CJK_SYMBOLS_AND_PUNCTUATION);
+    EXPECT_EQ(Character::IsBlockHalfwidthAndFullwidthForms(ch),
+              block == UBLOCK_HALFWIDTH_AND_FULLWIDTH_FORMS);
+
+    const UEastAsianWidth eaw = Character::EastAsianWidth(ch);
+    EXPECT_EQ(Character::IsEastAsianWidthFullwidth(ch),
+              eaw == UEastAsianWidth::U_EA_FULLWIDTH);
   }
 }
 
diff --git a/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc b/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc
index a77b6cf..67e53578 100644
--- a/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc
+++ b/third_party/blink/renderer/platform/video_capture/video_capture_impl.cc
@@ -867,11 +867,6 @@
                      weak_factory_.GetWeakPtr(), std::move(callback)));
 }
 
-void VideoCaptureImpl::OnFrameDropped(
-    media::VideoCaptureFrameDropReason reason) {
-  GetVideoCaptureHost()->OnFrameDropped(device_id_, reason);
-}
-
 void VideoCaptureImpl::OnLog(const String& message) {
   GetVideoCaptureHost()->OnLog(device_id_, message);
 }
@@ -1165,7 +1160,7 @@
   }
 }
 
-void VideoCaptureImpl::OnFrameDroppedEarly(
+void VideoCaptureImpl::OnFrameDropped(
     media::VideoCaptureFrameDropReason reason) {
   DCHECK_CALLED_ON_VALID_THREAD(io_thread_checker_);
   for (const auto& client : clients_) {
diff --git a/third_party/blink/renderer/platform/video_capture/video_capture_impl.h b/third_party/blink/renderer/platform/video_capture/video_capture_impl.h
index 0687407..fa3f796 100644
--- a/third_party/blink/renderer/platform/video_capture/video_capture_impl.h
+++ b/third_party/blink/renderer/platform/video_capture/video_capture_impl.h
@@ -107,7 +107,6 @@
   // |callback| will be invoked with the results.
   void GetDeviceFormatsInUse(VideoCaptureDeviceFormatsCallback callback);
 
-  void OnFrameDropped(media::VideoCaptureFrameDropReason reason);
   void OnLog(const String& message);
 
   const media::VideoCaptureSessionId& session_id() const { return session_id_; }
@@ -129,7 +128,7 @@
       media::mojom::blink::ReadyBufferPtr buffer,
       Vector<media::mojom::blink::ReadyBufferPtr> scaled_buffers) override;
   void OnBufferDestroyed(int32_t buffer_id) override;
-  void OnFrameDroppedEarly(media::VideoCaptureFrameDropReason reason) override;
+  void OnFrameDropped(media::VideoCaptureFrameDropReason reason) override;
   void OnNewCropVersion(uint32_t crop_version) override;
 
   void ProcessFeedback(const media::VideoCaptureFeedback& feedback);
diff --git a/third_party/blink/renderer/platform/video_capture/video_capture_impl_test.cc b/third_party/blink/renderer/platform/video_capture/video_capture_impl_test.cc
index 92cb8c8..ec4bb7e2 100644
--- a/third_party/blink/renderer/platform/video_capture/video_capture_impl_test.cc
+++ b/third_party/blink/renderer/platform/video_capture/video_capture_impl_test.cc
@@ -191,7 +191,7 @@
                void(scoped_refptr<media::VideoFrame>,
                     std::vector<scoped_refptr<media::VideoFrame>>,
                     base::TimeTicks));
-  MOCK_METHOD1(OnFrameDroppedEarly, void(media::VideoCaptureFrameDropReason));
+  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason));
   MOCK_METHOD1(OnStateUpdate, void(VideoCaptureState));
   MOCK_METHOD1(OnDeviceFormatsInUse,
                void(const Vector<media::VideoCaptureFormat>&));
@@ -204,7 +204,7 @@
     const auto frame_ready_callback = WTF::BindRepeating(
         &VideoCaptureImplTest::OnFrameReady, base::Unretained(this));
     const auto frame_dropped_callback = WTF::BindRepeating(
-        &VideoCaptureImplTest::OnFrameDroppedEarly, base::Unretained(this));
+        &VideoCaptureImplTest::OnFrameDropped, base::Unretained(this));
 
     video_capture_impl_->StartCapture(
         client_id, params, state_update_callback, frame_ready_callback,
@@ -628,13 +628,13 @@
   testing_io_thread.Stop();
 }
 
-TEST_F(VideoCaptureImplTest, OnFrameDroppedEarly) {
+TEST_F(VideoCaptureImplTest, OnFrameDropped) {
   EXPECT_CALL(mock_video_capture_host_, DoStart(_, session_id_, params_small_));
-  EXPECT_CALL(*this, OnFrameDroppedEarly(_));
+  EXPECT_CALL(*this, OnFrameDropped(_));
   EXPECT_CALL(mock_video_capture_host_, Stop(_));
 
   StartCapture(0, params_small_);
-  video_capture_impl_->OnFrameDroppedEarly(
+  video_capture_impl_->OnFrameDropped(
       media::VideoCaptureFrameDropReason::kBufferPoolMaxBufferCountExceeded);
   StopCapture(0);
 }
diff --git a/third_party/blink/renderer/platform/video_capture/video_capturer_source.h b/third_party/blink/renderer/platform/video_capture/video_capturer_source.h
index db5965d..faaec73 100644
--- a/third_party/blink/renderer/platform/video_capture/video_capturer_source.h
+++ b/third_party/blink/renderer/platform/video_capture/video_capturer_source.h
@@ -107,9 +107,6 @@
   // use refcounted or weak references in |new_frame_callback|.
   virtual void StopCapture() = 0;
 
-  // Indicates to the source that a frame has been dropped.
-  virtual void OnFrameDropped(media::VideoCaptureFrameDropReason reason) {}
-
   // Hints to the source that if it has an alpha channel, that alpha channel
   // will be ignored and can be discarded.
   virtual void SetCanDiscardAlpha(bool can_discard_alpha) {}
diff --git a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
index 3cd1d76..2087117 100644
--- a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
+++ b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
@@ -87,7 +87,6 @@
 # ====== New tests from wpt-importer added here ======
 crbug.com/626703 external/wpt/clipboard-apis/async-navigator-clipboard-write-multiple.tentative.https.sub.html [ Timeout ]
 crbug.com/626703 external/wpt/preload/modulepreload-as.html [ Failure Timeout ]
-crbug.com/626703 virtual/fenced-frame-mparch/wpt_internal/fenced_frame/unfenced-top.https.html [ Timeout ]
 crbug.com/626703 external/wpt/url/url-setters-a-area.window.html?exclude=(file|javascript|mailto) [ Crash Failure ]
 crbug.com/626703 external/wpt/url/a-element-origin-xhtml.xhtml [ Crash Failure ]
 crbug.com/626703 external/wpt/fullscreen/api/document-exit-fullscreen-nested-in-iframe.html [ Timeout ]
@@ -151,3 +150,5 @@
 crbug.com/1456636 [ Linux ] virtual/view-transition-wide-gamut/view-transition/parent-transition-cancels-child.html [ Failure ]  # Reftest image failure
 crbug.com/1456636 [ Linux ] virtual/view-transition-mpa-serialization/view-transition/parent-transition-cancels-child.html [ Failure ]  # Reftest image failure
 crbug.com/1456636 [ Linux ] virtual/view-transition/view-transition/parent-transition-cancels-child.html [ Failure ]  # Reftest image failure
+
+crbug.com/1487775 virtual/fenced-frame-mparch/wpt_internal/fenced_frame/unfenced-top.https.html [ Failure Timeout ]
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 24fbfd40..49f7a34 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -2853,7 +2853,6 @@
 crbug.com/626703 [ Mac12 ] external/wpt/IndexedDB/idbobjectstore_getAllKeys.any.html [ Timeout ]
 crbug.com/626703 [ Mac13 ] external/wpt/IndexedDB/idbobjectstore_getAllKeys.any.html [ Timeout ]
 crbug.com/626703 [ Mac13 Release ] external/wpt/background-fetch/fetch.https.window.html [ Timeout ]
-crbug.com/626703 external/wpt/css/css-fonts/font-size-adjust-014.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-highlight-api/painting/custom-highlight-painting-prioritization-003.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-page/layers-002-print.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-page/layers-003-print.html [ Failure ]
@@ -6757,3 +6756,10 @@
 # Gardener 2023-09-28
 crbug.com/1487363 [ Linux ] inspector-protocol/overlay/overlay-with-emulation-scale.js [ Failure ]
 crbug.com/1486131 [ Linux ] external/wpt/html/browsers/history/the-history-interface/005.html [ Failure Pass ]
+
+# Gardener 2023-09-29
+crbug.com/1481694 [ Win11 ] accessibility/selection-change-notification-on-selection-removed.html [ Failure ]
+crbug.com/1486616 [ Linux ] fast/sub-pixel/sub-pixel-composited-layers.html [ Failure Pass ]
+crbug.com/1487880 [ Linux ] external/wpt/html/semantics/interactive-elements/the-details-element/toggleEvent.html [ Failure Pass ]
+crbug.com/1487886 [ Linux ] external/wpt/long-animation-frame/tentative/loaf-trusted-types.html [ Timeout Pass ]
+crbug.com/1487899 [ Linux ] fast/canvas/canvas-toBlob-jpeg-medium-quality.html [ Failure Pass ]
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-connection.window.js.ini b/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-connection.window.js.ini
deleted file mode 100644
index 2e5b648c..0000000
--- a/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-connection.window.js.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[back-forward-cache-open-connection.window.html]
-  [Testing BFCache support for page with open IndexedDB connection, and eviction behavior when receiving versionchange event.]
-    expected:
-      if (product == "content_shell") and (os == "mac"): [FAIL, PRECONDITION_FAILED]
-      PRECONDITION_FAILED
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-transaction.window.js.ini b/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-transaction.window.js.ini
deleted file mode 100644
index 6f0150a..0000000
--- a/third_party/blink/web_tests/external/wpt/IndexedDB/back-forward-cache-open-transaction.window.js.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[back-forward-cache-open-transaction.window.html]
-  [BFCache support test for page with open IndexedDB transaction]
-    expected: PRECONDITION_FAILED
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest-not-in-list.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest-not-in-list.json
index c66903c..b6d8a55 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest-not-in-list.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest-not-in-list.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token.py"
+  "id_assertion_endpoint": "token.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest.py b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest.py
index 8a6172ce..5a6166f 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest.py
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest.py
@@ -13,6 +13,7 @@
   "accounts_endpoint": "accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
   "id_assertion_endpoint": "token.py",
-  "revocation_endpoint": "revoke.py"
+  "revocation_endpoint": "revoke.py",
+  "signin_url": "signin.html"
 }
 """
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_accounts.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_accounts.json
index 590704cf..5aec07f4 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_accounts.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_accounts.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token.py"
+  "id_assertion_endpoint": "token.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_token.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_token.json
index 190420736..84800884 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_token.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_redirect_token.json
@@ -2,5 +2,6 @@
   "accounts_endpoint": "accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
   "id_assertion_endpoint": "/common/redirect.py?location=/credential-management/support/fedcm/token.py&status=308",
-  "revocation_endpoint": "revoke.py"
+  "revocation_endpoint": "revoke.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_account_auto_selected_flag.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_account_auto_selected_flag.json
index 39d2b1e..20ee0bf4 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_account_auto_selected_flag.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_account_auto_selected_flag.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "two_accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token_with_account_auto_selected_flag.py"
+  "id_assertion_endpoint": "token_with_account_auto_selected_flag.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_no_accounts.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_no_accounts.json
index ad3f295..3667eae 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_no_accounts.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_no_accounts.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "no_accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token_with_account_id.py"
+  "id_assertion_endpoint": "token_with_account_id.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_single_account.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_single_account.json
index 15a657c..d1eb008 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_single_account.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_single_account.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "single_account.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token_with_account_id.py"
+  "id_assertion_endpoint": "token_with_account_id.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_two_accounts.json b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_two_accounts.json
index 932fb85d..2a93b2f 100644
--- a/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_two_accounts.json
+++ b/third_party/blink/web_tests/external/wpt/credential-management/support/fedcm/manifest_with_two_accounts.json
@@ -1,5 +1,6 @@
 {
   "accounts_endpoint": "two_accounts.py",
   "client_metadata_endpoint": "client_metadata.py",
-  "id_assertion_endpoint": "token_with_account_id.py"
+  "id_assertion_endpoint": "token_with_account_id.py",
+  "signin_url": "signin.html"
 }
diff --git a/third_party/blink/web_tests/external/wpt/css/css-fonts/font-size-adjust-014.html.ini b/third_party/blink/web_tests/external/wpt/css/css-fonts/font-size-adjust-014.html.ini
deleted file mode 100644
index 5972a8c..0000000
--- a/third_party/blink/web_tests/external/wpt/css/css-fonts/font-size-adjust-014.html.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[font-size-adjust-014.html]
-  expected: FAIL
diff --git a/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN-ref.html b/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN-ref.html
index 496d699d..0d938b2 100644
--- a/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN-ref.html
+++ b/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN-ref.html
@@ -10,7 +10,7 @@
     <meta name="assert" content="
       When dir='auto', the direction is set according to the first strong character
       of the text, ignoring neutrals and numbers.
-      If there is no strong character, as in this test, the direction defaults to LTR." />
+      If there is no strong character, as in this test, the direction defaults to the parent." />
     <style>
       input, textarea {
         font-size:1em;
@@ -35,7 +35,7 @@
         <p dir="ltr">@123!</p>
       </div>
       <div dir="rtl">
-        <p dir="ltr">@123!</p>
+        <p dir="rtl">@123!</p>
       </div>
     </div>
     <div class="ref">
@@ -43,7 +43,7 @@
         <p dir="ltr">@123!</p>
       </div>
       <div dir="rtl">
-        <p dir="ltr">@123!</p>
+        <p dir="rtl">@123!</p>
       </div>
     </div>
   </body>
diff --git a/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN.html b/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN.html
index 5d948d34..467b4d0 100644
--- a/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN.html
+++ b/third_party/blink/web_tests/external/wpt/html/dom/elements/global-attributes/dir_auto-N-EN.html
@@ -11,7 +11,7 @@
     <meta name="assert" content="
       When dir='auto', the direction is set according to the first strong character
       of the text, ignoring neutrals and numbers.
-      If there is no strong character, as in this test, the direction defaults to LTR." />
+      If there is no strong character, as in this test, the direction defaults to the parent." />
     <style>
       input, textarea {
         font-size:1em;
@@ -44,7 +44,7 @@
         <p dir="ltr">@123!</p>
       </div>
       <div dir="rtl">
-        <p dir="ltr">@123!</p>
+        <p dir="rtl">@123!</p>
       </div>
     </div>
   </body>
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/event-constructor.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/event-constructor.html
index a668730e..863681ce 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/event-constructor.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/event-constructor.html
@@ -18,7 +18,8 @@
       signal: (new AbortController()).signal,
       formData: null,
       downloadRequest: null,
-      info: null
+      info: null,
+      sourceElement: null
     });
   });
 }, "destination is required");
@@ -35,7 +36,8 @@
         hashChange: false,
         formData: null,
         downloadRequest: null,
-        info: null
+        info: null,
+        sourceElement: null
       });
     });
   });
@@ -50,6 +52,7 @@
     const signal = (new AbortController()).signal;
     const downloadRequest = "abc";
     const hasUAVisualTransition = true;
+    const sourceElement = document.createElement("a");
 
     const event = new NavigateEvent("navigate", {
       navigationType: "replace",
@@ -61,7 +64,8 @@
       formData,
       downloadRequest,
       info,
-      hasUAVisualTransition
+      hasUAVisualTransition,
+      sourceElement
     });
 
     assert_equals(event.navigationType, "replace");
@@ -74,6 +78,7 @@
     assert_equals(event.downloadRequest, downloadRequest);
     assert_equals(event.info, info);
     assert_equals(event.hasUAVisualTransition, hasUAVisualTransition);
+    assert_equals(event.sourceElement, sourceElement);
   });
   history.pushState(2, null, "#2");
 }, "all properties are reflected back");
@@ -93,6 +98,7 @@
     assert_equals(event.formData, null);
     assert_equals(event.downloadRequest, null);
     assert_equals(event.info, undefined);
+    assert_equals(event.sourceElement, null);
   });
   history.pushState(3, null, "#3");
 }, "defaults are as expected");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-cross-origin.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-cross-origin.html
index ee09924..62e5adb 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-cross-origin.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-cross-origin.html
@@ -17,6 +17,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
     e.preventDefault();
   });
   a.click();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
index b9506984..6918142 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download-userInitiated.html
@@ -21,6 +21,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
     e.preventDefault();
     t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
   });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download.html
index 05fb0ecf..d04245ec 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-download.html
@@ -25,6 +25,7 @@
       assert_equals(e.destination.key, "");
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
+      assert_equals(e.sourceElement, a);
       e.preventDefault();
     });
     a.click();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-fragment.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-fragment.html
index 51221eb..6443cce 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-fragment.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-fragment.html
@@ -17,6 +17,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
     e.preventDefault();
     t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
   });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
index 68f5bf0..5a6dce7e 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-same-origin-cross-document.html
@@ -18,6 +18,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
     e.preventDefault();
   });
   a.click();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-userInitiated.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-userInitiated.html
index 39192c9..bb76e7a 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-userInitiated.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-userInitiated.html
@@ -20,6 +20,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
     e.preventDefault();
     t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
   });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-with-target.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-with-target.html
index 6407b963..e4b897d 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-with-target.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-anchor-with-target.html
@@ -23,6 +23,7 @@
     assert_equals(e.destination.key, "");
     assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
     });
     a.click();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-back-forward.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
index c8b1043..869fc16 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-back-forward.html
@@ -19,6 +19,7 @@
       assert_equals(e.destination.key, "");
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
+      assert_equals(e.sourceElement, null);
     });
     navigation.back();
   }), 0);
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-navigate.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-navigate.html
index 5dac40d..d19a168 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-navigate.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-navigate.html
@@ -16,6 +16,7 @@
       assert_equals(e.destination.key, "");
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
+      assert_equals(e.sourceElement, null);
     });
     navigation.navigate("#foo", { state: navState });
   }, 0);
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-reload.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-reload.html
index a180e08..ac6528c 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-reload.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-destination-getState-reload.html
@@ -16,6 +16,7 @@
       assert_equals(e.destination.key, "");
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
+      assert_equals(e.sourceElement, null);
       e.intercept();
     });
     navigation.updateCurrentEntry({ state: navState });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-get.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-get.html
index 87a102dd..7a71a1c 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-get.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-get.html
@@ -21,6 +21,7 @@
 
     // Because it's a GET, not a POST
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, form);
   });
   window.onload = t.step_func(() => form.submit());
 }, "<form> submission with GET method fires navigate event but with formData null");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-reload.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-reload.html
index f18a11e..2171690 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-reload.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-reload.html
@@ -12,6 +12,7 @@
     iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
       assert_equals(e.navigationType, "push");
       assert_not_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
 
       iframe.onload = t.step_func(() => {
         iframe.contentWindow.navigation.onnavigate = t.step_func_done(e => {
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-requestSubmit.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-requestSubmit.html
new file mode 100644
index 0000000..38dd9c7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-requestSubmit.html
@@ -0,0 +1,35 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<form id="form" method="post" action="">
+<input type="submit" id="submit1">
+<input type="image" id="submit2">
+<button type="submit" id="submit3">
+</form>
+<script>
+promise_test(async () => {
+  await new Promise(r => window.onload = r);
+
+  let sourceElement;
+  navigation.onnavigate = e => {
+    e.preventDefault();
+    sourceElement = e.sourceElement;
+  };
+
+  form.requestSubmit(submit1);
+  await new Promise(r => navigation.onnavigateerror = r);
+  assert_equals(sourceElement, submit1);
+
+  form.requestSubmit(submit2);
+  await new Promise(r => navigation.onnavigateerror = r);
+  assert_equals(sourceElement, submit2);
+
+  form.requestSubmit(submit3);
+  await new Promise(r => navigation.onnavigateerror = r);
+  assert_equals(sourceElement, submit3);
+
+  form.requestSubmit();
+  await new Promise(r => navigation.onnavigateerror = r);
+  assert_equals(sourceElement, form);
+}, "<form> requestSubmit() sets sourceElement");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-traverse.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-traverse.html
index d673537..b9665c805 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-traverse.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-traverse.html
@@ -12,6 +12,7 @@
     iframe.contentWindow.navigation.onnavigate = t.step_func(e => {
       assert_equals(e.navigationType, "push");
       assert_not_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
 
       iframe.onload = t.step_func(() => {
         // Avoid the replace behavior that occurs if you navigate during the load handler
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-userInitiated.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-userInitiated.html
index 40c5905..246e028 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-userInitiated.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-userInitiated.html
@@ -24,6 +24,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_not_equals(e.formData, null);
+    assert_equals(e.sourceElement, submit);
   });
   window.onload = t.step_func(() => test_driver.click(submit));
 }, "<form> submission fires navigate event");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-with-target.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-with-target.html
index f6fe05c..4b1a8ce9 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-with-target.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form-with-target.html
@@ -22,6 +22,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_not_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
     });
     form.submit();
   });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form.html
index c57d72c..653ef3b 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-form.html
@@ -19,6 +19,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_not_equals(e.formData, null);
+    assert_equals(e.sourceElement, form);
   });
   window.onload = t.step_func(() => form.submit());
 }, "<form> submission fires navigate event");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-fragment.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-fragment.html
index 57a30c8..ffa9665d6 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-fragment.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-fragment.html
@@ -23,6 +23,7 @@
       assert_equals(e.destination.id, target_id);
         assert_equals(e.destination.index, 0);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
     });
 
     history.back();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-pushState.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-pushState.html
index bf2e6e4..a22eb1b 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-pushState.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-after-pushState.html
@@ -23,6 +23,7 @@
       assert_equals(e.destination.id, target_id);
       assert_equals(e.destination.index, 0);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
     });
 
     history.back();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-cross-document.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-cross-document.html
index cd7be6e9..29c626a 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-cross-document.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-back-cross-document.html
@@ -23,6 +23,7 @@
         assert_equals(e.destination.index, 0);
         assert_equals(e.formData, null);
         assert_equals(e.info, undefined);
+        assert_equals(e.sourceElement, null);
       });
       assert_true(i.contentWindow.navigation.canGoBack);
       i.contentWindow.history.back();
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-go-0.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-go-0.html
index b1f4142..5508928e 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-go-0.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-go-0.html
@@ -18,6 +18,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
     });
 
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-pushState.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-pushState.html
index 266309a..5079b210 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-pushState.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-pushState.html
@@ -17,6 +17,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, null);
     e.preventDefault();
     t.step_timeout(t.step_func_done(() => {
       assert_equals(location.hash, "");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-replaceState.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-replaceState.html
index ea6d3df..444116a 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-replaceState.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-history-replaceState.html
@@ -17,6 +17,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, null);
     e.preventDefault();
     t.step_timeout(t.step_func_done(() => {
       assert_equals(location.hash, "");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-iframe-location.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-iframe-location.html
index 25d5147..1bd7918 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-iframe-location.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-iframe-location.html
@@ -21,6 +21,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
       t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
     });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-location.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-location.html
index a4d0c607..a0ed4dc 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-location.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-location.html
@@ -17,6 +17,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, null);
   });
   location.href = "#1";
 }, "location API fires navigate event");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-meta-refresh.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-meta-refresh.html
index 9fa59b2..d4d5b1c8 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-meta-refresh.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-meta-refresh.html
@@ -20,6 +20,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
     });
   });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-cross-document.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
index 2e1adbe..0840515 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-cross-document.html
@@ -25,6 +25,7 @@
         assert_equals(e.destination.index, 0);
         assert_equals(e.formData, null);
         assert_equals(e.info, "hi");
+        assert_equals(e.sourceElement, null);
       });
       i.contentWindow.onbeforeunload = () => beforeunload_called = true;
       assert_true(i.contentWindow.navigation.canGoBack);
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
index cebd2f36..42c694e2 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document-in-iframe.html
@@ -27,6 +27,7 @@
     assert_equals(e.destination.index, 0);
     assert_equals(e.formData, null);
     assert_equals(e.info, "hi");
+    assert_equals(e.sourceElement, null);
   }
   await i.contentWindow.navigation.back({ info: "hi" }).finished;
 }, "navigate event for navigation.back() - same-document in an iframe");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document.html
index e587569..e675dcdf 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-back-same-document.html
@@ -24,6 +24,7 @@
         assert_equals(e.formData, null);
         assert_equals(e.info, "hi");
         assert_not_equals(e.hasUAVisualTransition, undefined);
+        assert_equals(e.sourceElement, null);
       });
       assert_true(navigation.canGoBack);
       navigation.back({ info: "hi" });
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-navigate.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-navigate.html
index ffc8ea8..c56af1b 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-navigate.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-navigation-navigate.html
@@ -16,6 +16,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, null);
   });
   navigation.navigate("#foo");
 }, "navigate event for navigation.navigate()");
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-svg-anchor-fragment.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-svg-anchor-fragment.html
new file mode 100644
index 0000000..c6d210cc
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-svg-anchor-fragment.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<svg><a id="a" href="#1"></a></svg>
+<script>
+async_test(t => {
+  navigation.onnavigate = t.step_func(e => {
+    assert_equals(e.navigationType, "push");
+    assert_true(e.cancelable);
+    assert_true(e.canIntercept);
+    assert_false(e.userInitiated);
+    assert_true(e.hashChange);
+    assert_equals(e.downloadRequest, null);
+    assert_equals(e.formData, null);
+    assert_equals(new URL(e.destination.url).hash, "#1");
+    assert_true(e.destination.sameDocument);
+    assert_equals(e.destination.key, "");
+    assert_equals(e.destination.id, "");
+    assert_equals(e.destination.index, -1);
+    assert_equals(e.sourceElement, document.getElementById("a"));
+    e.preventDefault();
+    t.step_timeout(t.step_func_done(() => assert_equals(location.hash, "")), 0);
+  });
+  document.getElementById("a").dispatchEvent(new MouseEvent('click'));
+}, "<svg:a> click fires navigate event");
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-to-srcdoc.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-to-srcdoc.html
index 8bbb66a..ea5c3f9 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-to-srcdoc.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-to-srcdoc.html
@@ -21,6 +21,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
 
       // Make sure it doesn't navigate anyway.
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open-self.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open-self.html
index a6e443f..4e569b6d 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open-self.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open-self.html
@@ -16,6 +16,7 @@
     assert_equals(e.destination.id, "");
     assert_equals(e.destination.index, -1);
     assert_equals(e.formData, null);
+    assert_equals(e.sourceElement, null);
     e.preventDefault();
   });
   window.onload = t.step_func(() => window.open("#1", "_self"));
diff --git a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open.html b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open.html
index 1fe2402..42828308 100644
--- a/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open.html
+++ b/third_party/blink/web_tests/external/wpt/navigation-api/navigate-event/navigate-window-open.html
@@ -21,6 +21,7 @@
       assert_equals(e.destination.id, "");
       assert_equals(e.destination.index, -1);
       assert_equals(e.formData, null);
+      assert_equals(e.sourceElement, null);
       e.preventDefault();
     });
 
diff --git a/third_party/blink/web_tests/external/wpt/resources/chromium/webxr-test.js b/third_party/blink/web_tests/external/wpt/resources/chromium/webxr-test.js
index e927f82..aba34479 100644
--- a/third_party/blink/web_tests/external/wpt/resources/chromium/webxr-test.js
+++ b/third_party/blink/web_tests/external/wpt/resources/chromium/webxr-test.js
@@ -196,7 +196,7 @@
 
       // If there were no successful results, returns a null session.
       return {
-        result: {failureReason: vrMojom.RequestSessionError.NO_RUNTIME_FOUND}
+        result: {failureReason: xrSessionMojom.RequestSessionError.NO_RUNTIME_FOUND}
       };
     });
   }
diff --git a/third_party/blink/web_tests/external/wpt/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html b/third_party/blink/web_tests/external/wpt/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html
index 715cd36..83cf642 100644
--- a/third_party/blink/web_tests/external/wpt/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html
+++ b/third_party/blink/web_tests/external/wpt/webrtc-encoded-transform/RTCEncodedAudioFrame-clone.https.html
@@ -37,6 +37,7 @@
       const original = result.value;
       let clone = structuredClone(original);
       assert_equals(original.timestamp, clone.timestamp);
+      assert_equals(original.getMetadata().absCaptureTime, clone.getMetadata().absCaptureTime);
       assert_array_equals(Array.from(original.data), Array.from(clone.data));
       await writer2.write(clone);
       resolve();
diff --git a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-navigated-expected.txt b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-navigated-expected.txt
index c8b0494..bf4059ef 100644
--- a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-navigated-expected.txt
+++ b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-navigated-expected.txt
@@ -172,6 +172,7 @@
 PASS oldChildWindow.ononline is newChildWindow.ononline
 PASS oldChildWindow.onoverscroll is newChildWindow.onoverscroll
 PASS oldChildWindow.onpagehide is newChildWindow.onpagehide
+PASS oldChildWindow.onpagereveal is newChildWindow.onpagereveal
 PASS oldChildWindow.onpageshow is newChildWindow.onpageshow
 PASS oldChildWindow.onpause is newChildWindow.onpause
 PASS oldChildWindow.onplay is newChildWindow.onplay
@@ -188,7 +189,6 @@
 PASS oldChildWindow.onpopstate is newChildWindow.onpopstate
 PASS oldChildWindow.onprogress is newChildWindow.onprogress
 PASS oldChildWindow.onratechange is newChildWindow.onratechange
-PASS oldChildWindow.onreadytorender is newChildWindow.onreadytorender
 PASS oldChildWindow.onrejectionhandled is newChildWindow.onrejectionhandled
 PASS oldChildWindow.onreset is newChildWindow.onreset
 PASS oldChildWindow.onresize is newChildWindow.onresize
diff --git a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-and-gced-expected.txt b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-and-gced-expected.txt
index 5a39016..9e663cd6 100644
--- a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-and-gced-expected.txt
+++ b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-and-gced-expected.txt
@@ -124,6 +124,7 @@
 PASS childWindow.ononline is null
 PASS childWindow.onoverscroll is null
 PASS childWindow.onpagehide is null
+PASS childWindow.onpagereveal is null
 PASS childWindow.onpageshow is null
 PASS childWindow.onpause is null
 PASS childWindow.onplay is null
@@ -140,7 +141,6 @@
 PASS childWindow.onpopstate is null
 PASS childWindow.onprogress is null
 PASS childWindow.onratechange is null
-PASS childWindow.onreadytorender is null
 PASS childWindow.onrejectionhandled is null
 PASS childWindow.onreset is null
 PASS childWindow.onresize is null
diff --git a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-expected.txt b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-expected.txt
index c02874b..1ff67c0c 100644
--- a/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-expected.txt
+++ b/third_party/blink/web_tests/fast/dom/Window/property-access-on-cached-window-after-frame-removed-expected.txt
@@ -124,6 +124,7 @@
 PASS childWindow.ononline is null
 PASS childWindow.onoverscroll is null
 PASS childWindow.onpagehide is null
+PASS childWindow.onpagereveal is null
 PASS childWindow.onpageshow is null
 PASS childWindow.onpause is null
 PASS childWindow.onplay is null
@@ -140,7 +141,6 @@
 PASS childWindow.onpopstate is null
 PASS childWindow.onprogress is null
 PASS childWindow.onratechange is null
-PASS childWindow.onreadytorender is null
 PASS childWindow.onrejectionhandled is null
 PASS childWindow.onreset is null
 PASS childWindow.onresize is null
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/main-flamechart-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/main-flamechart-a11y-test.js
index bf40dec..8fffff2 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/main-flamechart-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/main-flamechart-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - main flamechart.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/network-flamechart-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/network-flamechart-a11y-test.js
index b0a94a104..a5a776fa 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/network-flamechart-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/network-flamechart-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - network flamechart.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-right-toolbar-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-right-toolbar-a11y-test.js
index f18086e..bb13a1d 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-right-toolbar-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-right-toolbar-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - panel right toolbar.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-toolbar-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-toolbar-a11y-test.js
index a864c17..2449b0a 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-toolbar-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/panel-toolbar-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - panel toolbar.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/settings-pane-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/settings-pane-a11y-test.js
index 53c82639..58e7cffb 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/settings-pane-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/settings-pane-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - settings pane.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/timeline-overview-container-a11y-test.js b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/timeline-overview-container-a11y-test.js
index e9fa4c2f..108c4dc 100644
--- a/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/timeline-overview-container-a11y-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/a11y-axe-core/performance/timeline-overview-container-a11y-test.js
@@ -9,7 +9,6 @@
 (async function() {
   TestRunner.addResult('Testing a11y in performance panel - timeline overview container.');
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.runPerfTraceWithReload();
diff --git a/third_party/blink/web_tests/http/tests/devtools/bindings/inline-styles-binding.js b/third_party/blink/web_tests/http/tests/devtools/bindings/inline-styles-binding.js
index e01935c0..6896b6464 100644
--- a/third_party/blink/web_tests/http/tests/devtools/bindings/inline-styles-binding.js
+++ b/third_party/blink/web_tests/http/tests/devtools/bindings/inline-styles-binding.js
@@ -7,6 +7,7 @@
 
 import * as SDK from 'devtools/core/sdk/sdk.js';
 import * as BindingsModule from 'devtools/models/bindings/bindings.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Editing inline styles should play nice with inline scripts.\n`);
@@ -47,7 +48,7 @@
     TestRunner.addResult('Adding rule' + i)
     await TestRunner.cssModel.addRule(styleSheetId, `.new-rule {
   --new: true;
-}`, TextUtils.TextRange.createFromLocation(0, 0));
+}`, TextUtils.TextRange.TextRange.createFromLocation(0, 0));
     await TestRunner.waitForPendingLiveLocationUpdates();
     printLocationUpdates();
     i++;
diff --git a/third_party/blink/web_tests/http/tests/devtools/changes/changes-sidebar.js b/third_party/blink/web_tests/http/tests/devtools/changes/changes-sidebar.js
index 4287622..65bf4ed 100644
--- a/third_party/blink/web_tests/http/tests/devtools/changes/changes-sidebar.js
+++ b/third_party/blink/web_tests/http/tests/devtools/changes/changes-sidebar.js
@@ -6,6 +6,7 @@
 
 import * as Common from 'devtools/core/common/common.js';
 import * as BindingsModule from 'devtools/models/bindings/bindings.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Tests that the changes sidebar contains the changed uisourcecodes.\n`);
@@ -68,6 +69,6 @@
 
   function addUISourceCode(url, content) {
     return project.addContentProvider(
-        url, TextUtils.StaticContentProvider.fromString(url, Common.ResourceType.resourceTypes.Stylesheet, content));
+        url, TextUtils.StaticContentProvider.StaticContentProvider.fromString(url, Common.ResourceType.resourceTypes.Stylesheet, content));
   }
 })();
diff --git a/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer-expected.txt b/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer-expected.txt
index 9e4ea67..c2825b9d 100644
--- a/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer-expected.txt
+++ b/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer-expected.txt
@@ -1,4 +1,4 @@
-Test TextUtils.BalancedJSONTokenizer.
+Test TextUtils.TextUtils.BalancedJSONTokenizer.
 
 
 Running: testMatchQuotes
diff --git a/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer.js b/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer.js
index cd79676..a6abb18 100644
--- a/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer.js
+++ b/third_party/blink/web_tests/http/tests/devtools/components/json-balanced-tokenizer.js
@@ -4,11 +4,13 @@
 
 import {TestRunner} from 'test_runner';
 
-(async function() {
-  TestRunner.addResult(`Test TextUtils.BalancedJSONTokenizer.\n`);
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
-  const BalancedJSONTokenizer = TextUtils.BalancedJSONTokenizer ||
-      TextUtils.TextUtils.BalancedJSONTokenizer;
+(async function() {
+  TestRunner.addResult(`Test TextUtils.TextUtils.BalancedJSONTokenizer.\n`);
+
+  const BalancedJSONTokenizer = TextUtils.TextUtils.BalancedJSONTokenizer ||
+      TextUtils.TextUtils.Utils.BalancedJSONTokenizer;
 
   TestRunner.runTestSuite([
     function testMatchQuotes(next) {
diff --git a/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes-expected.txt b/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes-expected.txt
index 6865744..53c2ada 100644
--- a/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes-expected.txt
+++ b/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes-expected.txt
@@ -1,4 +1,4 @@
-Tests TextUtils.TextUtils.splitStringByRegexes.
+Tests TextUtils.TextUtils.Utils.splitStringByRegexes.
 
 
 Running: testSimple
diff --git a/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes.js b/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes.js
index cf7247c1..885067c4 100644
--- a/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes.js
+++ b/third_party/blink/web_tests/http/tests/devtools/components/split-string-by-regexes.js
@@ -4,49 +4,51 @@
 
 import {TestRunner} from 'test_runner';
 
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
+
 (async function() {
-  TestRunner.addResult(`Tests TextUtils.TextUtils.splitStringByRegexes.\n`);
+  TestRunner.addResult(`Tests TextUtils.TextUtils.Utils.splitStringByRegexes.\n`);
 
 
   TestRunner.runTestSuite([
     function testSimple(next) {
       var regexes = [/hello/g, /[0-9]+/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('hello123hello123', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('hello123hello123', regexes);
       dumpResults(results);
       next();
     },
 
     function testMatchAtStart(next) {
       var regexes = [/yes/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('yes thank you', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('yes thank you', regexes);
       dumpResults(results);
       next();
     },
 
     function testMatchAtEnd(next) {
       var regexes = [/you/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('yes thank you', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('yes thank you', regexes);
       dumpResults(results);
       next();
     },
 
     function testAvoidInnerMatch(next) {
       var regexes = [/url\("red\.com"\)/g, /red/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('image: url("red.com")', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('image: url("red.com")', regexes);
       dumpResults(results);
       next();
     },
 
     function testNoMatch(next) {
       var regexes = [/something/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('nothing', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('nothing', regexes);
       dumpResults(results);
       next();
     },
 
     function testNoMatches(next) {
       var regexes = [/something/g, /123/g, /abc/g];
-      var results = TextUtils.TextUtils.splitStringByRegexes('nothing', regexes);
+      var results = TextUtils.TextUtils.Utils.splitStringByRegexes('nothing', regexes);
       dumpResults(results);
       next();
     },
@@ -54,7 +56,7 @@
     function testComplex(next) {
       var regexes = [/\(([^)]+)\)/g, /okay/g, /ka/g];
       var results =
-          TextUtils.TextUtils.splitStringByRegexes('Start. (okay) kit-kat okay (kale) ka( ) okay. End', regexes);
+          TextUtils.TextUtils.Utils.splitStringByRegexes('Start. (okay) kit-kat okay (kale) ka( ) okay. End', regexes);
       dumpResults(results);
       next();
     }
diff --git a/third_party/blink/web_tests/http/tests/devtools/components/utilities-highlight-results.js b/third_party/blink/web_tests/http/tests/devtools/components/utilities-highlight-results.js
index e17f72c..b22eebd 100644
--- a/third_party/blink/web_tests/http/tests/devtools/components/utilities-highlight-results.js
+++ b/third_party/blink/web_tests/http/tests/devtools/components/utilities-highlight-results.js
@@ -5,6 +5,7 @@
 import {TestRunner} from 'test_runner';
 
 import * as UIModule from 'devtools/ui/legacy/legacy.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Tests how utilities functions highlight text and then revert/re-apply highlighting changes.\n`);
@@ -58,7 +59,7 @@
   }
 
   function range(offset, length) {
-    return new TextUtils.SourceRange(offset, length);
+    return new TextUtils.TextRange.SourceRange(offset, length);
   }
 
   performTestForElement(textElement(['function']), [range(0, 8)]);  // Highlight whole text node.
diff --git a/third_party/blink/web_tests/http/tests/devtools/console/console-correct-suggestions.js b/third_party/blink/web_tests/http/tests/devtools/console/console-correct-suggestions.js
index e745245f..b14dc35 100644
--- a/third_party/blink/web_tests/http/tests/devtools/console/console-correct-suggestions.js
+++ b/third_party/blink/web_tests/http/tests/devtools/console/console-correct-suggestions.js
@@ -5,6 +5,8 @@
 import {TestRunner} from 'test_runner';
 import {ConsoleTestRunner} from 'console_test_runner';
 
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
+
 (async function() {
   TestRunner.addResult(`Tests that console correctly finds suggestions in complicated cases.\n`);
 
@@ -61,7 +63,7 @@
       cursorPosition = Infinity;
 
     consoleEditor.setText(text.replace('|', ''));
-    consoleEditor.setSelection(TextUtils.TextRange.createFromLocation(0, cursorPosition));
+    consoleEditor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(0, cursorPosition));
     consoleEditor.autocompleteController.autocomplete(force);
     var message =
         'Checking \'' + text.replace('\n', '\\n').replace('\r', '\\r') + '\'';
diff --git a/third_party/blink/web_tests/http/tests/devtools/editor/text-editor-enter-behaviour.js b/third_party/blink/web_tests/http/tests/devtools/editor/text-editor-enter-behaviour.js
index 788ee90..e03263a 100644
--- a/third_party/blink/web_tests/http/tests/devtools/editor/text-editor-enter-behaviour.js
+++ b/third_party/blink/web_tests/http/tests/devtools/editor/text-editor-enter-behaviour.js
@@ -5,6 +5,8 @@
 import {TestRunner} from 'test_runner';
 import {SourcesTestRunner} from 'sources_test_runner';
 
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
+
 (async function() {
   TestRunner.addResult(`This test checks text editor enter behaviour.\n`);
   await TestRunner.showPanel('sources');
@@ -39,57 +41,57 @@
     function testEnterInTheLineEnd(next) {
       textEditor.setText(testFunction.toString());
       var line = textEditor.line(2);
-      textEditor.setSelection(TextUtils.TextRange.createFromLocation(2, line.length));
+      textEditor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(2, line.length));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterAfterOpenCurlyBrace(next) {
       textEditor.setText(testFunction.toString());
       var line = textEditor.line(1);
-      textEditor.setSelection(TextUtils.TextRange.createFromLocation(1, line.length));
+      textEditor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(1, line.length));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterInTheMiddleOfLine(next) {
       textEditor.setText(testFunction.toString());
       var line = textEditor.line(2);
-      textEditor.setSelection(TextUtils.TextRange.createFromLocation(2, line.length / 2));
+      textEditor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(2, line.length / 2));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterInTheBeginningOfTheLine(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(TextUtils.TextRange.createFromLocation(2, 0));
+      textEditor.setSelection(TextUtils.TextRange.TextRange.createFromLocation(2, 0));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterWithTheSelection(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(new TextUtils.TextRange(2, 2, 2, 4));
+      textEditor.setSelection(new TextUtils.TextRange.TextRange(2, 2, 2, 4));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterWithReversedSelection(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(new TextUtils.TextRange(2, 4, 2, 2));
+      textEditor.setSelection(new TextUtils.TextRange.TextRange(2, 4, 2, 2));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterWithTheMultiLineSelection(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(new TextUtils.TextRange(2, 0, 8, 4));
+      textEditor.setSelection(new TextUtils.TextRange.TextRange(2, 0, 8, 4));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterWithFullLineSelection(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(new TextUtils.TextRange(2, 0, 3, 0));
+      textEditor.setSelection(new TextUtils.TextRange.TextRange(2, 0, 3, 0));
       hitEnterDumpTextAndNext(next);
     },
 
     function testEnterBeforeOpenBrace(next) {
       textEditor.setText(testFunction.toString());
-      textEditor.setSelection(new TextUtils.TextRange(8, 0, 8, 0));
+      textEditor.setSelection(new TextUtils.TextRange.TextRange(8, 0, 8, 0));
       hitEnterDumpTextAndNext(next);
     },
 
diff --git a/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-inline-element-style-changes-should-not-force-style-recalc.js b/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-inline-element-style-changes-should-not-force-style-recalc.js
index 3898f95..87e9b173 100644
--- a/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-inline-element-style-changes-should-not-force-style-recalc.js
+++ b/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-inline-element-style-changes-should-not-force-style-recalc.js
@@ -11,7 +11,6 @@
 (async function() {
   TestRunner.addResult(
       `Tests that inspector doesn't force styles recalc on operations with inline element styles that result in no changes.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.showPanel('elements');
   await TestRunner.loadHTML(`
diff --git a/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-should-not-force-sync-style-recalc.js b/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-should-not-force-sync-style-recalc.js
index 7be7dc45..c7c344fc 100644
--- a/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-should-not-force-sync-style-recalc.js
+++ b/third_party/blink/web_tests/http/tests/devtools/elements/styles-4/styles-should-not-force-sync-style-recalc.js
@@ -10,7 +10,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests that inspector doesn't force sync layout on operations with CSSOM.Bug 315885.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.showPanel('elements');
   await TestRunner.loadHTML(`
diff --git a/third_party/blink/web_tests/http/tests/devtools/elements/styles/original-content-provider.js b/third_party/blink/web_tests/http/tests/devtools/elements/styles/original-content-provider.js
index 275bd543..a16d1f0 100644
--- a/third_party/blink/web_tests/http/tests/devtools/elements/styles/original-content-provider.js
+++ b/third_party/blink/web_tests/http/tests/devtools/elements/styles/original-content-provider.js
@@ -6,6 +6,7 @@
 import {ElementsTestRunner} from 'elements_test_runner';
 
 import * as SDK from 'devtools/core/sdk/sdk.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(
@@ -49,35 +50,35 @@
   TestRunner.runTestSuite([
     function testSetStyle(next) {
       var header = headers.find(header => header.sourceURL.endsWith('set-style.css'));
-      TestRunner.cssModel.setStyleText(header.id, new TextUtils.TextRange(1, 5, 1, 18), 'EDITED: EDITED', true)
+      TestRunner.cssModel.setStyleText(header.id, new TextUtils.TextRange.TextRange(1, 5, 1, 18), 'EDITED: EDITED', true)
           .then(success => onEdit(header, success))
           .then(next);
     },
 
     function testSetSelector(next) {
       var header = headers.find(header => header.sourceURL.endsWith('set-selector.css'));
-      TestRunner.cssModel.setSelectorText(header.id, new TextUtils.TextRange(1, 0, 1, 3), 'EDITED')
+      TestRunner.cssModel.setSelectorText(header.id, new TextUtils.TextRange.TextRange(1, 0, 1, 3), 'EDITED')
           .then(success => onEdit(header, success))
           .then(next);
     },
 
     function testSetMedia(next) {
       var header = headers.find(header => header.sourceURL.endsWith('set-media.css'));
-      TestRunner.cssModel.setMediaText(header.id, new TextUtils.TextRange(1, 7, 1, 12), 'EDITED')
+      TestRunner.cssModel.setMediaText(header.id, new TextUtils.TextRange.TextRange(1, 7, 1, 12), 'EDITED')
           .then(success => onEdit(header, success))
           .then(next);
     },
 
     function testSetKeyframeKey(next) {
       var header = headers.find(header => header.sourceURL.endsWith('set-keyframe-key.css'));
-      TestRunner.cssModel.setKeyframeKey(header.id, new TextUtils.TextRange(1, 23, 1, 27), 'from')
+      TestRunner.cssModel.setKeyframeKey(header.id, new TextUtils.TextRange.TextRange(1, 23, 1, 27), 'from')
           .then(success => onEdit(header, success))
           .then(next);
     },
 
     function testAddRule(next) {
       var header = headers.find(header => header.sourceURL.endsWith('add-rule.css'));
-      TestRunner.cssModel.addRule(header.id, 'EDITED {}\n', new TextUtils.TextRange(1, 0, 1, 0))
+      TestRunner.cssModel.addRule(header.id, 'EDITED {}\n', new TextUtils.TextRange.TextRange(1, 0, 1, 0))
           .then(success => onEdit(header, success))
           .then(next);
     },
diff --git a/third_party/blink/web_tests/http/tests/devtools/extensions/extensions-timeline-api.js b/third_party/blink/web_tests/http/tests/devtools/extensions/extensions-timeline-api.js
index 702eb90..6220840 100644
--- a/third_party/blink/web_tests/http/tests/devtools/extensions/extensions-timeline-api.js
+++ b/third_party/blink/web_tests/http/tests/devtools/extensions/extensions-timeline-api.js
@@ -9,7 +9,6 @@
 import * as TimelineModule from 'devtools/panels/timeline/timeline.js';
 
 (async function() {
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   TestRunner.enableTimelineExtensionAndStart = function(callback) {
diff --git a/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask-expected.txt b/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask-expected.txt
index a72fdfe9..1456e59d 100644
--- a/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask-expected.txt
+++ b/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask-expected.txt
@@ -37,5 +37,5 @@
     startTime : <number>
     type : "Layout"
 }
-Text details for Layout: test://evaluations/0/forced-layout-in-microtask.js:24:36
+Text details for Layout: test://evaluations/0/forced-layout-in-microtask.js:23:36
 
diff --git a/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask.js b/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask.js
index 25183b1..c593daa 100644
--- a/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask.js
+++ b/third_party/blink/web_tests/http/tests/devtools/forced-layout-in-microtask.js
@@ -7,7 +7,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests that Layout record has correct locations of layout being invalidated and forced.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
       <style>
diff --git a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/cross-origin-test.js b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/cross-origin-test.js
index 167f9b8..5807f7f 100644
--- a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/cross-origin-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/cross-origin-test.js
@@ -7,7 +7,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests V8 code cache for javascript resources\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   // Clear browser cache to avoid any existing entries for the fetched
diff --git a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-module-test.js b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-module-test.js
index fcc1f03..519aa79 100644
--- a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-module-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-module-test.js
@@ -11,7 +11,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests V8 code cache for javascript resources\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   // Clear browser cache to avoid any existing entries for the fetched
diff --git a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-test.js b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-test.js
index 623ba1d..20f39d91 100644
--- a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/same-origin-test.js
@@ -13,7 +13,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests V8 code cache for javascript resources\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   // Clear browser cache to avoid any existing entries for the fetched
diff --git a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/stale-revalidation-test.js b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/stale-revalidation-test.js
index b069f7f..338030a2 100644
--- a/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/stale-revalidation-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/isolated-code-cache/stale-revalidation-test.js
@@ -16,7 +16,6 @@
   // The main purpose of the test is to demonstrate that after producing the
   // code cache on the 2nd load, it gets cleared on disk by subsequent loads of
   // the subresource, even if the response instructs to reuse the old resource.
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   // Clear browser cache to avoid any existing entries for the fetched scripts
diff --git a/third_party/blink/web_tests/http/tests/devtools/oopif/oopif-performance-cpu-profiles.js b/third_party/blink/web_tests/http/tests/devtools/oopif/oopif-performance-cpu-profiles.js
index 03fe218..967bc04 100644
--- a/third_party/blink/web_tests/http/tests/devtools/oopif/oopif-performance-cpu-profiles.js
+++ b/third_party/blink/web_tests/http/tests/devtools/oopif/oopif-performance-cpu-profiles.js
@@ -8,7 +8,6 @@
 (async function() {
   TestRunner.addResult(`Test CPU profiles are recorded for OOPIFs.\n`);
 
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   await PerformanceTestRunner.startTimeline();
diff --git a/third_party/blink/web_tests/http/tests/devtools/search/search-in-static.js b/third_party/blink/web_tests/http/tests/devtools/search/search-in-static.js
index fc9b2823..a98cd68 100644
--- a/third_party/blink/web_tests/http/tests/devtools/search/search-in-static.js
+++ b/third_party/blink/web_tests/http/tests/devtools/search/search-in-static.js
@@ -8,6 +8,7 @@
 
 import * as Common from 'devtools/core/common/common.js';
 import * as BindingsModule from 'devtools/models/bindings/bindings.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Tests static content provider search.\n`);
@@ -26,7 +27,7 @@
   }
 
   async function step3() {
-    staticContentProvider = TextUtils.StaticContentProvider.fromString('', Common.ResourceType.resourceTypes.Script, resource.content);
+    staticContentProvider = TextUtils.StaticContentProvider.StaticContentProvider.fromString('', Common.ResourceType.resourceTypes.Script, resource.content);
     TestRunner.addResult(resource.url);
 
     var text = 'searchTestUniqueString';
diff --git a/third_party/blink/web_tests/http/tests/devtools/service-workers/service-worker-v8-cache.js b/third_party/blink/web_tests/http/tests/devtools/service-workers/service-worker-v8-cache.js
index f397e02..8eb30b1 100644
--- a/third_party/blink/web_tests/http/tests/devtools/service-workers/service-worker-v8-cache.js
+++ b/third_party/blink/web_tests/http/tests/devtools/service-workers/service-worker-v8-cache.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests V8 cache information of Service Worker Cache Storage in timeline\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.loadLegacyModule('console');
     // Note: every test that uses a storage API must manually clean-up state from previous tests.
   await ApplicationTestRunner.resetState();
diff --git a/third_party/blink/web_tests/http/tests/devtools/service-workers/service-workers-wasm-test.js b/third_party/blink/web_tests/http/tests/devtools/service-workers/service-workers-wasm-test.js
index 92ec5f5..01f12bc6 100644
--- a/third_party/blink/web_tests/http/tests/devtools/service-workers/service-workers-wasm-test.js
+++ b/third_party/blink/web_tests/http/tests/devtools/service-workers/service-workers-wasm-test.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests V8 code cache for WebAssembly resources using Service Workers.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.loadLegacyModule('console');
 
   await ApplicationTestRunner.resetState();
diff --git a/third_party/blink/web_tests/http/tests/devtools/sources/debugger-breakpoints/possible-breakpoints.js b/third_party/blink/web_tests/http/tests/devtools/sources/debugger-breakpoints/possible-breakpoints.js
index a77d360..dda4eca 100644
--- a/third_party/blink/web_tests/http/tests/devtools/sources/debugger-breakpoints/possible-breakpoints.js
+++ b/third_party/blink/web_tests/http/tests/devtools/sources/debugger-breakpoints/possible-breakpoints.js
@@ -5,6 +5,8 @@
 import {TestRunner} from 'test_runner';
 import {SourcesTestRunner} from 'sources_test_runner';
 
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
+
 (async function() {
   TestRunner.addResult(`Checks that BreakpointManager.possibleBreakpoints returns correct locations\n`);
   await TestRunner.showPanel('sources');
@@ -25,16 +27,16 @@
     var breakpointManager = Bindings.breakpointManager;
 
     TestRunner.addResult('Locations for first line');
-    breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange(0, 0, 1, 0))
+    breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange.TextRange(0, 0, 1, 0))
         .then(dumpLocations)
         .then(() => TestRunner.addResult('All locations'))
-        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange(0, 0, 6, 0)))
+        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange.TextRange(0, 0, 6, 0)))
         .then(dumpLocations)
         .then(() => TestRunner.addResult('Existing location by position'))
-        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange(2, 37, 2, 38)))
+        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange.TextRange(2, 37, 2, 38)))
         .then(dumpLocations)
         .then(() => TestRunner.addResult('Not existing location by position'))
-        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange(2, 38, 2, 39)))
+        .then(() => breakpointManager.possibleBreakpoints(uiSourceCode, new TextUtils.TextRange.TextRange(2, 38, 2, 39)))
         .then(dumpLocations)
         .then(() => SourcesTestRunner.completeDebuggerTest());
   }
diff --git a/third_party/blink/web_tests/http/tests/devtools/sources/debugger/debugger-minified-variables-evalution.js b/third_party/blink/web_tests/http/tests/devtools/sources/debugger/debugger-minified-variables-evalution.js
index c30c998..0c4c5f0 100644
--- a/third_party/blink/web_tests/http/tests/devtools/sources/debugger/debugger-minified-variables-evalution.js
+++ b/third_party/blink/web_tests/http/tests/devtools/sources/debugger/debugger-minified-variables-evalution.js
@@ -5,6 +5,7 @@
 import {TestRunner} from 'test_runner';
 import {SourcesTestRunner} from 'sources_test_runner';
 
+import * as SourceMapScopesModule from 'devtools/models/source_map_scopes/source_map_scopes.js';
 import * as SourcesModule from 'devtools/panels/sources/sources.js';
 import * as UIModule from 'devtools/ui/legacy/legacy.js';
 
@@ -45,7 +46,7 @@
   }
 
   function testAtPosition(uiSourceCode, position) {
-    return Sources.SourceMapNamesResolver
+    return SourceMapScopesModule.NamesResolver
         .resolveExpression(
             UIModule.Context.Context.instance().flavor(SDK.DebuggerModel.CallFrame), position.originText, uiSourceCode, position.line,
             position.startColumn, position.endColumn)
diff --git a/third_party/blink/web_tests/http/tests/devtools/sources/debugger/live-edit-no-reveal.js b/third_party/blink/web_tests/http/tests/devtools/sources/debugger/live-edit-no-reveal.js
index 566bf22..09ee810 100644
--- a/third_party/blink/web_tests/http/tests/devtools/sources/debugger/live-edit-no-reveal.js
+++ b/third_party/blink/web_tests/http/tests/devtools/sources/debugger/live-edit-no-reveal.js
@@ -6,6 +6,7 @@
 import {SourcesTestRunner} from 'sources_test_runner';
 
 import * as SourcesModule from 'devtools/panels/sources/sources.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Tests live edit feature.\n`);
@@ -37,7 +38,7 @@
       panel._updateLastModificationTimeForTest();
       SourcesTestRunner.replaceInSource(sourceFrame, oldText, newText);
       TestRunner.addResult('Moving cursor to (0, 0).');
-      sourceFrame.setSelection(TextUtils.TextRange.createFromLocation(0, 0));
+      sourceFrame.setSelection(TextUtils.TextRange.TextRange.createFromLocation(0, 0));
       TestRunner.addResult('Committing live edit.');
       SourcesTestRunner.commitSource(sourceFrame);
     }
@@ -71,7 +72,7 @@
       panel._lastModificationTimeoutPassedForTest();
       SourcesTestRunner.replaceInSource(sourceFrame, oldText, newText);
       TestRunner.addResult('Moving cursor to (0, 0).');
-      sourceFrame.setSelection(TextUtils.TextRange.createFromLocation(0, 0));
+      sourceFrame.setSelection(TextUtils.TextRange.TextRange.createFromLocation(0, 0));
       TestRunner.addResult('Committing live edit.');
       SourcesTestRunner.commitSource(sourceFrame);
     }
diff --git a/third_party/blink/web_tests/http/tests/devtools/sources/inline-styles-scripts-locations.js b/third_party/blink/web_tests/http/tests/devtools/sources/inline-styles-scripts-locations.js
index f57c2a5d..2d87258 100644
--- a/third_party/blink/web_tests/http/tests/devtools/sources/inline-styles-scripts-locations.js
+++ b/third_party/blink/web_tests/http/tests/devtools/sources/inline-styles-scripts-locations.js
@@ -6,6 +6,7 @@
 import {SourcesTestRunner} from 'sources_test_runner';
 
 import * as SDK from 'devtools/core/sdk/sdk.js';
+import * as TextUtils from 'devtools/models/text_utils/text_utils.js';
 
 (async function() {
   TestRunner.addResult(`Bindings should only generate locations for an inline script (style) if the location is inside of the inline script (style).\n`);
@@ -15,7 +16,7 @@
   const source = await TestRunner.waitForUISourceCode('inline-style.html', Workspace.projectTypes.Network);
   const { content } = await source.requestContent();
   TestRunner.addResult(`Content:\n${content}`);
-  const sourceText = new TextUtils.Text(content);
+  const sourceText = new TextUtils.Text.Text(content);
 
   await dumpLocations("css", sourceText.lineCount(), source);
   await dumpLocations("script", sourceText.lineCount(), source);
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/anonymous-image-object.js b/third_party/blink/web_tests/http/tests/devtools/tracing/anonymous-image-object.js
index e51728bb..259c98a8 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/anonymous-image-object.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/anonymous-image-object.js
@@ -8,7 +8,6 @@
 (async function() {
   TestRunner.addResult(
       `Tests the Timeline instrumentation does not crash the renderer upon encountering an anonymous image render object\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
       <style>
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/category-filter.js b/third_party/blink/web_tests/http/tests/devtools/tracing/category-filter.js
index ae9a5b902..6287d3e 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/category-filter.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/category-filter.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Test the set of visible records is correctly update when category filter changes\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   const sessionId = '4.20';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model-instrumentation.js b/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model-instrumentation.js
index 42a6c71..008f839 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model-instrumentation.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model-instrumentation.js
@@ -7,7 +7,6 @@
 
 (async function() {
   await TestRunner.loadHTML('ABC');
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.evaluateInPagePromise(`
   function doActions() {
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model.js b/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model.js
index 005beb21..5c3ee05 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/frame-model.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Test the frames are correctly built based on trace events\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var sessionId = '4.20';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/compile-script.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/compile-script.js
index 4ae3894..fa54ba4ea 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/compile-script.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/compile-script.js
@@ -7,7 +7,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests the Timeline instrumentation for CompileScript event.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.evaluateInPagePromise(`
       function performActions() {
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-js-streamed-cpu-profile.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-js-streamed-cpu-profile.js
index 5539c139..c0b1825 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-js-streamed-cpu-profile.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-js-streamed-cpu-profile.js
@@ -7,7 +7,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests streaming CPU profile within trace log.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var sessionId = '6.23';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-open-function-call.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-open-function-call.js
index c7f8087..8f372a31 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-open-function-call.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-open-function-call.js
@@ -7,7 +7,6 @@
 
 (async function() {
   TestRunner.addResult(`Checks the FunctionCall with no closing event processed properly.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var sessionId = '6.23';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-runtime-stats.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-runtime-stats.js
index 4b983b39..9134b96 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-runtime-stats.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-runtime-stats.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Check that RuntimeCallStats are present in profile.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
       <div id="foo">
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-script-tag-2.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-script-tag-2.js
index 6d275cb..db7f532 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-script-tag-2.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-js/timeline-script-tag-2.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests the Timeline API instrumentation of a script tag with an external script.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.loadLegacyModule('console');
   await TestRunner.showPanel('timeline');
   await TestRunner.evaluateInPagePromise(`
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations-expected.txt b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations-expected.txt
index c926dc4a..67178c25 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations-expected.txt
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations-expected.txt
@@ -4,7 +4,7 @@
 Running: testLocalFrame
 first layout invalidations[
     {
-        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:23}
+        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:22}
         changedAttribute : undefined
         changedClass : undefined
         changedId : undefined
@@ -17,7 +17,7 @@
 ]
 second layout invalidations[
     {
-        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:25}
+        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:24}
         changedAttribute : undefined
         changedClass : undefined
         changedId : undefined
@@ -32,7 +32,7 @@
 Running: testSubframe
 first layout invalidations[
     {
-        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:32}
+        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:31}
         changedAttribute : undefined
         changedClass : undefined
         changedId : undefined
@@ -45,7 +45,7 @@
 ]
 second layout invalidations[
     {
-        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:34}
+        cause : {reason: Style changed, stackTrace: test://evaluations/0/timeline-layout-with-invalidations.js:33}
         changedAttribute : undefined
         changedClass : undefined
         changedId : undefined
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations.js
index b930e1d0..a763989 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-layout/timeline-layout-with-invalidations.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests the Timeline API instrumentation of layout events with invalidations.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
       <!DOCTYPE HTML>
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-aggregated-details.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-aggregated-details.js
index 978fd71..46ee5bb 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-aggregated-details.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-aggregated-details.js
@@ -11,7 +11,6 @@
 
 (async function() {
   TestRunner.addResult(`Test timeline aggregated details.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   TestRunner.addResult('');
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering-self-time.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering-self-time.js
index 87cfe0a..dc576b7 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering-self-time.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering-self-time.js
@@ -10,7 +10,6 @@
 (async function() {
   TestRunner.addResult(
       `Test filtering in Bottom-Up Timeline Tree View panel.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var sessionId = '4.20';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering.js
index ba8c954..e9ceb7e 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-filtering.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Test filtering in Timeline Tree View panel.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var sessionId = '4.20';
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-range-stats.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-range-stats.js
index 843830a..a791f35 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-range-stats.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-misc/timeline-range-stats.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests that aggregated summary in Timeline is properly computed.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
 
   var mainThread = 1;
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/layer-tree.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/layer-tree.js
index 99d51cb..dff32ef 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/layer-tree.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/layer-tree.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests that LayerTreeModel successfully imports layers from a trace.\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
       <div id="a" style="width: 200px; height: 200px" class="layer">
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/paint-profiler-update.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/paint-profiler-update.js
index 147187fd..6182f5d 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/paint-profiler-update.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-paint/paint-profiler-update.js
@@ -9,7 +9,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests that paint profiler is properly update when an event is selected in Flame Chart\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.loadHTML(`
     <div id="square" style="width: 40px; height: 40px"></div>
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-event.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-event.js
index e1ef690..4d3f4110 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-event.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-event.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests the Timeline events for XMLHttpReqeust\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.evaluateInPagePromise(`
       function performActions()
diff --git a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-response-type-blob-event.js b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-response-type-blob-event.js
index 0e04e37..eca4d823 100644
--- a/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-response-type-blob-event.js
+++ b/third_party/blink/web_tests/http/tests/devtools/tracing/timeline-xhr-response-type-blob-event.js
@@ -8,7 +8,6 @@
 
 (async function() {
   TestRunner.addResult(`Tests the Timeline events for XMLHttpReqeust with responseType="blob"\n`);
-  await TestRunner.loadLegacyModule('timeline');
   await TestRunner.showPanel('timeline');
   await TestRunner.evaluateInPagePromise(`
       function performActions()
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
index 70048d8..7646ea8bb 100644
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
+++ b/third_party/blink/web_tests/http/tests/inspector-protocol/storage/interest-groups-expected.txt
@@ -12,10 +12,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render0
+            renderURL : https://example.com/render0
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 0
@@ -34,10 +34,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render1
+            renderURL : https://example.com/render1
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 1
@@ -56,10 +56,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render0
+            renderURL : https://example.com/render0
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 0
@@ -78,10 +78,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render1
+            renderURL : https://example.com/render1
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 1
@@ -100,10 +100,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render0
+            renderURL : https://example.com/render0
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 0
@@ -122,10 +122,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render1
+            renderURL : https://example.com/render1
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 1
@@ -144,10 +144,10 @@
     ads : [
         [0] : {
             metadata : {"ad":"metadata","here":[1,2,3]}
-            renderUrl : https://example.com/render1
+            renderURL : https://example.com/render1
         }
     ]
-    biddingUrl : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
+    biddingLogicURL : https://a.test:8443/inspector-protocol/resources/fledge_bidding_logic.js.php
     expirationTime : 0
     joiningOrigin : https://a.test:8443
     name : 1
diff --git a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
index 5bb27e4..ac72c84 100644
--- a/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/blink/web_tests/webexposed/global-interface-listing-expected.txt
@@ -6333,6 +6333,7 @@
     getter info
     getter navigationType
     getter signal
+    getter sourceElement
     getter userInitiated
     method commit
     method constructor
@@ -6813,6 +6814,9 @@
     getter deltaX
     getter deltaY
     method constructor
+interface PageRevealEvent : Event
+    attribute @@toStringTag
+    method constructor
 interface PageTransitionEvent : Event
     attribute @@toStringTag
     getter persisted
@@ -7715,9 +7719,6 @@
     method constructor
     method read
     method releaseLock
-interface ReadyToRenderEvent : Event
-    attribute @@toStringTag
-    method constructor
 interface RelativeOrientationSensor : OrientationSensor
     attribute @@toStringTag
     method constructor
@@ -12471,6 +12472,7 @@
     getter ononline
     getter onoverscroll
     getter onpagehide
+    getter onpagereveal
     getter onpageshow
     getter onpause
     getter onplay
@@ -12487,7 +12489,6 @@
     getter onpopstate
     getter onprogress
     getter onratechange
-    getter onreadytorender
     getter onrejectionhandled
     getter onreset
     getter onresize
@@ -12697,6 +12698,7 @@
     setter ononline
     setter onoverscroll
     setter onpagehide
+    setter onpagereveal
     setter onpageshow
     setter onpause
     setter onplay
@@ -12713,7 +12715,6 @@
     setter onpopstate
     setter onprogress
     setter onratechange
-    setter onreadytorender
     setter onrejectionhandled
     setter onreset
     setter onresize
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-nesting/ident/trailing-braces.html b/third_party/blink/web_tests/wpt_internal/css/css-nesting/ident/trailing-braces.html
new file mode 100644
index 0000000..ac2e8e6
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/css/css-nesting/ident/trailing-braces.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<title>CSS Syntax: trailing braces</title>
+<link rel="help" href="https://drafts.csswg.org/css-syntax-1/">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id=style>
+  #target1 {
+    color:green;
+    color:red{};
+    color:red {};
+  }
+</style>
+<div id=target1>Green</div>
+<script>
+  test(() => {
+    let rules = style.sheet.rules;
+    assert_equals(rules.length, 1);
+    assert_equals(rules[0].style.color, 'green');
+  }, 'Trailing braces are not valid');
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-bfcache.html b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-bfcache.html
similarity index 80%
rename from third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-bfcache.html
rename to third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-bfcache.html
index 6b3adbb..c7d57199 100644
--- a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-bfcache.html
+++ b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-bfcache.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<title>View transitions: readytorender event fires correctly on restoration from BFCache</title>
+<title>View transitions: pagereveal event fires correctly on restoration from BFCache</title>
 <link rel="help" href="https://drafts.csswg.org/css-view-transitions-2/">
 <link rel="author" href="mailto:bokan@chromium.org">
 <script src="/resources/testharness.js"></script>
@@ -15,7 +15,7 @@
     // This function executes in pageA
 
     // Wait for an animation frame to ensure the the initial-load
-    // `readytorender` has already been fired so it doesn't get recorded
+    // `pagereveal` has already been fired so it doesn't get recorded
     // below.
     const raf = new Promise(resolve => requestAnimationFrame(resolve));
     await raf;
@@ -43,25 +43,25 @@
         restored = true;
     });
 
-    addEventListener('readytorender', () => {
-      window.event_log.push('readytorender');
+    addEventListener('pagereveal', () => {
+      window.event_log.push('pagereveal');
     });
   },
   funcAfterAssertion: async (pageA, pageB, t) => {
     let event_log = await pageA.execute_script(async () => {
       // Ensure at least one animation frame is produced to ensure
-      // readytorender must have fired.
+      // pagereveal must have fired.
       await new Promise(requestAnimationFrame);
       return window.event_log;
     });
 
     // Expect that the events seen are:
-    // pageshow.persisted, readytorender, rAF, rAF, rAF, ...
+    // pageshow.persisted, pagereveal, rAF, rAF, rAF, ...
     assert_equals(event_log.slice(0, 3).toString(),
-        'pageshow.persisted,readytorender,rAF');
+        'pageshow.persisted,pagereveal,rAF');
     for (let i = 3; i < event_log.length; ++i) {
       assert_equals(event_log[i], 'rAF',
-          'All events following readytorender should be animation frames');
+          'All events following pagereveal should be animation frames');
     }
   },
   targetOrigin: originSameOrigin,
diff --git a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-navigation.html b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-navigation.html
similarity index 63%
rename from third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-navigation.html
rename to third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-navigation.html
index c546697..ed7d953 100644
--- a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-navigation.html
+++ b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-navigation.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<title>View transitions: readytorender event fires correctly on regular navigation</title>
+<title>View transitions: pagereveal event fires correctly on regular navigation</title>
 <link rel="help" href="https://drafts.csswg.org/css-view-transitions-2/">
 <link rel="author" href="mailto:bokan@chromium.org">
 <script>
@@ -9,7 +9,7 @@
   if (!bfrFired)
     rafFiredFirst = true;
 });
-addEventListener('readytorender', () => { bfrFired = true; });
+addEventListener('pagereveal', () => { bfrFired = true; });
 </script>
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
@@ -17,13 +17,13 @@
 promise_test(() => {
   return new Promise((resolve, reject) => {
     if (rafFiredFirst) {
-      reject('`readytorender` must fire before first rAF');
+      reject('`pagereveal` must fire before first rAF');
       return;
     }
 
-    addEventListener('readytorender', resolve);
+    addEventListener('pagereveal', resolve);
     requestAnimationFrame(
-        () => reject('`readytorender` must fire before first rAF'));
+        () => reject('`pagereveal` must fire before first rAF'));
   });
 });
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-prerender.html b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-prerender.html
similarity index 86%
rename from third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-prerender.html
rename to third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-prerender.html
index 8f7f363..8743c68 100644
--- a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/readytorender-event-prerender.html
+++ b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/pagereveal-event-prerender.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<title>View transitions: readytorender event ordering on prerender activation</title>
+<title>View transitions: pagereveal event ordering on prerender activation</title>
 <link rel="help" href="https://drafts.csswg.org/css-view-transitions-2/">
 <link rel="author" href="mailto:bokan@chromium.org">
 <script src="/resources/testharness.js"></script>
@@ -11,7 +11,7 @@
 setup(() => assertSpeculationRulesIsSupported());
 
 const uid = token();
-const initiator_url = `resources/readytorender-event-prerender.html?uid=${uid}`;
+const initiator_url = `resources/pagereveal-event-prerender.html?uid=${uid}`;
 
 // This test opens a popup to an initiator page. That page then prerenders a
 // "prerendering" version of itself (by adding a `prerendering` query param)
@@ -35,6 +35,6 @@
   // frame. Ensure their order and apparance is as expected.
   const events_in_order = result.events.join(',');
   assert_equals(events_in_order,
-      'pageshow,prerenderingchange,readytorender,raf');
+      'pageshow,prerenderingchange,pagereveal,raf');
 });
 </script>
diff --git a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/readytorender-event-prerender.html b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/pagereveal-event-prerender.html
similarity index 92%
rename from third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/readytorender-event-prerender.html
rename to third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/pagereveal-event-prerender.html
index 965afcf..b7df645 100644
--- a/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/readytorender-event-prerender.html
+++ b/third_party/blink/web_tests/wpt_internal/view-transition-on-navigation/resources/pagereveal-event-prerender.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<title>View transitions: readytorender event ordering on prerender activation (popup)</title>
+<title>View transitions: pagereveal event ordering on prerender activation (popup)</title>
 <link rel="help" href="https://drafts.csswg.org/css-view-transitions-2/">
 <link rel="author" href="mailto:bokan@chromium.org">
 <script src="/resources/testharness.js"></script>
@@ -63,8 +63,8 @@
     requestAnimationFrame(() => finish(result));
   });
 
-  addEventListener('readytorender', () => {
-    result.events.push('readytorender');
+  addEventListener('pagereveal', () => {
+    result.events.push('pagereveal');
   });
 
   addEventListener('load', () => {
diff --git a/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/GPUCommandEncoder.writeTimestamp.https-expected.txt b/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/GPUCommandEncoder.writeTimestamp.https-expected.txt
deleted file mode 100644
index f0d26a4..0000000
--- a/third_party/blink/web_tests/wpt_internal/webgpu/fast_api/GPUCommandEncoder.writeTimestamp.https-expected.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-This is a testharness.js-based test.
-FAIL GPUCommandEncoder promise_test: Unhandled rejection with value: object "TypeError: Failed to execute 'requestDevice' on 'GPUAdapter': Unsupported feature: timestamp-query"
-Harness: the test ran to completion.
-
diff --git a/third_party/catapult b/third_party/catapult
index 72a5f0b..a217ecb5 160000
--- a/third_party/catapult
+++ b/third_party/catapult
@@ -1 +1 @@
-Subproject commit 72a5f0b7ef081faf627913fb45d00c72b1cafc7d
+Subproject commit a217ecb5addbc06b125559679476960beb5e06d0
diff --git a/third_party/chromium-variations b/third_party/chromium-variations
index 97af9cd..c2e8652 160000
--- a/third_party/chromium-variations
+++ b/third_party/chromium-variations
@@ -1 +1 @@
-Subproject commit 97af9cd3bd80748042952adc1aba9cb3ece2349e
+Subproject commit c2e86526267a3e2f8c3a0c716a11d2b0866f7430
diff --git a/third_party/cros-components/src b/third_party/cros-components/src
index e012464..66ea0cc1 160000
--- a/third_party/cros-components/src
+++ b/third_party/cros-components/src
@@ -1 +1 @@
-Subproject commit e012464ddb8baeb541159efc7d6967c0481c298a
+Subproject commit 66ea0cc1c267d9a8a969e80b0d45fb6bf9cfb33b
diff --git a/third_party/dawn b/third_party/dawn
index b7bd8d8..6d8662d 160000
--- a/third_party/dawn
+++ b/third_party/dawn
@@ -1 +1 @@
-Subproject commit b7bd8d85b52e92ef2bf9aa901e7e6219dc9df3b0
+Subproject commit 6d8662dfd5e31ffd8bfe9cd9488a6a1864b429aa
diff --git a/third_party/depot_tools b/third_party/depot_tools
index 6831847..4c1d6d9 160000
--- a/third_party/depot_tools
+++ b/third_party/depot_tools
@@ -1 +1 @@
-Subproject commit 68318472db247d863355b56435d760fb1c102b35
+Subproject commit 4c1d6d90bc4326377ce670b74735029db9acde6a
diff --git a/third_party/devtools-frontend-internal b/third_party/devtools-frontend-internal
index e9922f2..0f63441 160000
--- a/third_party/devtools-frontend-internal
+++ b/third_party/devtools-frontend-internal
@@ -1 +1 @@
-Subproject commit e9922f2f73a60016909a907d08a94bc77b64ab7d
+Subproject commit 0f634419f74c7b73d8fef44d1a234dcd8072403c
diff --git a/third_party/iccjpeg/README.chromium b/third_party/iccjpeg/README.chromium
index 47b7628..262ce91 100644
--- a/third_party/iccjpeg/README.chromium
+++ b/third_party/iccjpeg/README.chromium
@@ -1,7 +1,7 @@
 Name: iccjpeg
 URL: http://www.ijg.org
 Version: unknown
-License: Custom license
+License: IJG
 License File: LICENSE
 Security Critical: yes
 Shipped: yes
diff --git a/third_party/libc++/src b/third_party/libc++/src
index 055d494..8a241ea0 160000
--- a/third_party/libc++/src
+++ b/third_party/libc++/src
@@ -1 +1 @@
-Subproject commit 055d494c5c65957c94c58ac17cbf8b1f22ac11a6
+Subproject commit 8a241ea043bc9d3f0712f3a908da49b89495cd00
diff --git a/third_party/perfetto b/third_party/perfetto
index a38d6a9..e879b2e 160000
--- a/third_party/perfetto
+++ b/third_party/perfetto
@@ -1 +1 @@
-Subproject commit a38d6a9fc06768cd6a0923a7c8c6e3d62d8d582a
+Subproject commit e879b2e2ee67ba3ce9bbb01bb7d776c8dff28897
diff --git a/third_party/skia b/third_party/skia
index 84b5116..a80c164 160000
--- a/third_party/skia
+++ b/third_party/skia
@@ -1 +1 @@
-Subproject commit 84b5116f98fbea7e625f0efc5ffa7943b1399746
+Subproject commit a80c164ffb8a26aa29020be39993581a6761ab6c
diff --git a/third_party/vulkan-deps b/third_party/vulkan-deps
index 3434928..4d31920 160000
--- a/third_party/vulkan-deps
+++ b/third_party/vulkan-deps
@@ -1 +1 @@
-Subproject commit 34349287c87688eb8a0d704829032bbf6cfc7c0a
+Subproject commit 4d31920a095aeda9fa83819cd17fdebc4d89220c
diff --git a/third_party/webgpu-cts/src b/third_party/webgpu-cts/src
index fc217e3..f2b59e0 160000
--- a/third_party/webgpu-cts/src
+++ b/third_party/webgpu-cts/src
@@ -1 +1 @@
-Subproject commit fc217e34b1792ab7600a42f0cf0aa5fa461efef3
+Subproject commit f2b59e03621238d0d0fd6305be2c406ce3e45ac2
diff --git a/third_party/webrtc b/third_party/webrtc
index 7d81e18..a61b334 160000
--- a/third_party/webrtc
+++ b/third_party/webrtc
@@ -1 +1 @@
-Subproject commit 7d81e18ac0d3c91ce77e0eeee4f1baa783ed2306
+Subproject commit a61b334a12c7556c64887e1c553fd822d060881d
diff --git a/third_party/zlib/README.chromium b/third_party/zlib/README.chromium
index 663ee05..1dfb996 100644
--- a/third_party/zlib/README.chromium
+++ b/third_party/zlib/README.chromium
@@ -5,7 +5,7 @@
 CPEPrefix: cpe:/a:zlib:zlib:1.2.13
 Security Critical: yes
 Shipped: yes
-License: Custom license
+License: Zlib
 License File: LICENSE
 License Android Compatible: yes
 
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index e31ea99..b3b71f7 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -3225,10 +3225,29 @@
   <int value="81" label="CREATE_WEBVIEW">WebViewCompat#createWebView()</int>
   <int value="82" label="GET_STATICS">WebViewCompat#getStatics()</int>
   <int value="83" label="GET_PROFILE_STORE">ProfileStore#getInstance()</int>
-  <int value="84" label="SET_WEBVIEW_PROFILE">
+  <int value="84" label="GET_OR_CREATE_PROFILE">
+    ProfileStore#getOrCreateProfile()
+  </int>
+  <int value="85" label="GET_PROFILE">ProfileStore#getProfile()</int>
+  <int value="86" label="GET_ALL_PROFILE_NAMES">
+    ProfileStore#getAllProfileNames()
+  </int>
+  <int value="87" label="DELETE_PROFILE">ProfileStore#deleteProfile()</int>
+  <int value="88" label="GET_PROFILE_NAME">Profile#getName()</int>
+  <int value="89" label="GET_PROFILE_COOKIE_MANAGER">
+    Profile#getCookieManager()
+  </int>
+  <int value="90" label="GET_PROFILE_WEB_STORAGE">Profile#getWebStorage()</int>
+  <int value="91" label="GET_PROFILE_GEO_LOCATION_PERMISSIONS">
+    Profile#getGeolocationPermissions()
+  </int>
+  <int value="92" label="GET_PROFILE_SERVICE_WORKER_CONTROLLER">
+    Profile#getServiceWorkerController()
+  </int>
+  <int value="93" label="SET_WEBVIEW_PROFILE">
     WebViewCompat#setProfile(WebView, String)
   </int>
-  <int value="85" label="GET_WEBVIEW_PROFILE">
+  <int value="94" label="GET_WEBVIEW_PROFILE">
     WebViewCompat#getProfile(WebView)
   </int>
   <int value="95" label="SET_ATTRIBUTION_BEHAVIOR">
@@ -14826,6 +14845,14 @@
   <int value="9" label="SessionAlreadyExists"/>
 </enum>
 
+<enum name="CdmStorageOpenError">
+  <int value="0" label="kInvalidBucket"/>
+  <int value="1" label="kNoFileSpecified"/>
+  <int value="2" label="kDatabaseOpenError"/>
+  <int value="3" label="kDatabaseRazeError"/>
+  <int value="4" label="kSQLExecutionError"/>
+</enum>
+
 <enum name="CdmStorageStatus">
   <int value="0" label="Success"/>
   <int value="1" label="In Use"/>
@@ -20184,6 +20211,15 @@
   </int>
 </enum>
 
+<enum name="CookieDeprecationFacilitatedTestingProfileEligibility">
+  <int value="0" label="Eligible"/>
+  <int value="1" label="Ineligible: 3P Cookies blocked"/>
+  <int value="2" label="Ineligible: Has not seen Ads API GA notice"/>
+  <int value="3" label="Ineligible: New user"/>
+  <int value="4" label="Ineligible: Enterprise user"/>
+  <int value="5" label="Ineligible: PWA or TWA installed on Android"/>
+</enum>
+
 <enum name="CookieEffectiveSameSite">
   <int value="0" label="No restriction"/>
   <int value="1" label="Lax mode"/>
@@ -20396,6 +20432,14 @@
   <int value="2" label="Cache miss"/>
 </enum>
 
+<enum name="CopyStackEvent">
+  <int value="0" label="Started"/>
+  <int value="1" label="Succeeded"/>
+  <int value="2" label="Sigaction failed"/>
+  <int value="3" label="Tgkill failed"/>
+  <int value="4" label="Wait failed"/>
+</enum>
+
 <enum name="CorbCanonicalMimeType">
   <summary>Coarse MIME type classification for CORB decisions.</summary>
   <int value="0" label="Html">
@@ -49474,6 +49518,9 @@
   <int value="11" label="Internal server error"/>
   <int value="12" label="Quota exceeded"/>
   <int value="13" label="Device has too many registrations"/>
+  <int value="14" label="Too many subscribers"/>
+  <int value="15" label="Invalid target version"/>
+  <int value="16" label="FIS auth error"/>
 </enum>
 
 <enum name="GCMSendMessageStatus">
@@ -64164,6 +64211,8 @@
   <int value="-436470115" label="TouchpadAndWheelScrollLatching:enabled"/>
   <int value="-435914745" label="ClipboardContentSetting:disabled"/>
   <int value="-435636565" label="EnableLensPing:disabled"/>
+  <int value="-435410583"
+      label="TrackingProtectionOnboardingResetEligibilityForTesting:disabled"/>
   <int value="-434474445" label="OmniboxSteadyStateBackgroundColor:enabled"/>
   <int value="-433879402"
       label="EnableAmbientAuthenticationInIncognito:disabled"/>
@@ -65206,6 +65255,8 @@
   <int value="79595680" label="OmniboxTabSwitchSuggestions:enabled"/>
   <int value="79729295" label="WebBundles:enabled"/>
   <int value="80036427" label="AutofillEnableCardProductName:disabled"/>
+  <int value="80225926"
+      label="TrackingProtectionOnboardingResetEligibilityForTesting:enabled"/>
   <int value="82303171" label="AccountIdMigration:disabled"/>
   <int value="82489049" label="AutofillSaveCardUiExperiment:enabled"/>
   <int value="82922831" label="NtpHistoryClustersModuleDiscounts:enabled"/>
@@ -78032,6 +78083,14 @@
   <int value="11" label="Has image and inline reply and grouped"/>
 </enum>
 
+<enum name="NotifierCollisionSurfaceType">
+  <int value="0" label="None"/>
+  <int value="1" label="Shelf Pod Bubble"/>
+  <int value="2" label="Slider Bubble"/>
+  <int value="3" label="Extended Hotseat"/>
+  <int value="4" label="Slider Bubble And Extended Hotseat"/>
+</enum>
+
 <enum name="NotifierType">
   <int value="0" label="Application"/>
   <int value="1" label="Arc++"/>
@@ -78988,6 +79047,9 @@
   <int value="7" label="getActions: invalid URL"/>
   <int value="8" label="getActions: no URL"/>
   <int value="9" label="getActions: access denied"/>
+  <int value="10" label="getActions: no email"/>
+  <int value="11" label="Conversion to ODFS URL error"/>
+  <int value="12" label="Emails do not match"/>
 </enum>
 
 <enum name="OfficeOpenExtensions">
@@ -84954,7 +85016,9 @@
 </enum>
 
 <enum name="PdfOcrUserSelection">
-  <int value="0" label="Turned on once from the context menu"/>
+  <int value="0"
+      label="(Obsolete) Turned on once from the context menu. Removed in
+             2023/09."/>
   <int value="1" label="Turned on always from the context menu"/>
   <int value="2" label="Turned off from the context menu"/>
   <int value="3" label="Turned on always from the more actions"/>
@@ -88859,7 +88923,7 @@
   <int value="4" label="kCrossOriginNavigation"/>
   <int value="5" label="kInvalidSchemeRedirect"/>
   <int value="6" label="kInvalidSchemeNavigation"/>
-  <int value="7" label="kInProgressNavigation"/>
+  <int value="7" label="kInProgressNavigation (obsoleted)"/>
   <int value="8" label="kNavigationRequestFailure (obsoleted)"/>
   <int value="9" label="kNavigationRequestBlockedByCsp"/>
   <int value="10" label="kMainFrameNavigation"/>
@@ -104616,7 +104680,7 @@
   <int value="5" label="Autofill Profile"/>
   <int value="6" label="Autocomplete"/>
   <int value="7" label="Themes"/>
-  <int value="8" label="Typed URLs"/>
+  <int value="8" label="(Obsolete) Typed URLs"/>
   <int value="9" label="Extensions"/>
   <int value="10" label="Search Engines"/>
   <int value="11" label="Sessions"/>
@@ -107851,6 +107915,7 @@
   <int value="1478323543" label="CROS Cr50 0.5.120"/>
   <int value="1490841853" label="IFX 9645 rev 49 fw 133.32 build 0050"/>
   <int value="1514324233" label="CROS Cr50 0.5.1"/>
+  <int value="1517782969" label="CROS Ti50 0.23.51"/>
   <int value="1520081310" label="CROS Cr50 0.0.22 variant"/>
   <int value="1521866659" label="CROS Cr50 0.4.13 Flags 0x10(pre-pvt)"/>
   <int value="1534426482" label="CROS Ti50 0.24.14"/>
diff --git a/tools/metrics/histograms/metadata/ash/histograms.xml b/tools/metrics/histograms/metadata/ash/histograms.xml
index 8071f48..41a5c49 100644
--- a/tools/metrics/histograms/metadata/ash/histograms.xml
+++ b/tools/metrics/histograms/metadata/ash/histograms.xml
@@ -4640,14 +4640,27 @@
   </summary>
 </histogram>
 
-<histogram name="Ash.NotificationPopup.OnTopOfBubbleCount" units="popups"
-    expires_after="2024-08-22">
+<histogram name="Ash.NotificationPopup.OnTopOfSurfacesPopupCount"
+    units="popups" expires_after="2024-08-22">
   <owner>leandre@chromium.org</owner>
   <owner>cros-status-area-eng@google.com</owner>
   <summary>
-    Record the number of popups that show up on top of a tray bubble. Emitted
-    when a tray bubble is shown or change height, and when there's a new
-    notification popup added when a tray bubble is open.
+    Records the number of popups that show up on top of a UI surface. Emitted
+    when the UI surface is shown or changes height when popup notifications are
+    visible, and when there's a new notification popup added when the UI surface
+    is visible.
+  </summary>
+</histogram>
+
+<histogram name="Ash.NotificationPopup.OnTopOfSurfacesType"
+    enum="NotifierCollisionSurfaceType" expires_after="2024-08-22">
+  <owner>leandre@chromium.org</owner>
+  <owner>cros-status-area-eng@google.com</owner>
+  <summary>
+    Records the type of UI surface that popup notifications are shown on top of.
+    Emitted when the UI surface is shown or changes height when popup
+    notifications are visible, and when there's a new notification popup added
+    when the UI surface is visible.
   </summary>
 </histogram>
 
@@ -6203,34 +6216,6 @@
   </summary>
 </histogram>
 
-<histogram name="Ash.Shelf.Palette.Usage.AutoOpened" enum="PaletteTrayOptions"
-    expires_after="2021-12-12">
-  <obsolete>
-    Kept to view historical data.
-  </obsolete>
-  <owner>amehfooz@chromium.org</owner>
-  <owner>cros-status-area@google.com</owner>
-  <owner>gzadina@chromium.org</owner>
-  <summary>
-    Recorded every time that the palette option has been selected from the
-    palette that has been opened automatically (by a stylus eject event).
-  </summary>
-</histogram>
-
-<histogram name="Ash.Shelf.Palette.Usage.Shortcut" enum="PaletteTrayOptions"
-    expires_after="2021-12-12">
-  <obsolete>
-    Kept to view historical data.
-  </obsolete>
-  <owner>amehfooz@chromium.org</owner>
-  <owner>cros-status-area@google.com</owner>
-  <owner>gzadina@chromium.org</owner>
-  <summary>
-    Recorded every time that the palette option has been selected by means other
-    that the palette menu (e.g. stylus barrel button or a keyboard accelerator).
-  </summary>
-</histogram>
-
 <histogram name="Ash.Shelf.ShowStackedHotseat" enum="Boolean"
     expires_after="2024-09-24">
   <owner>jiamingc@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/custom_tabs/histograms.xml b/tools/metrics/histograms/metadata/custom_tabs/histograms.xml
index 04ff1bb..7aa5e2c 100644
--- a/tools/metrics/histograms/metadata/custom_tabs/histograms.xml
+++ b/tools/metrics/histograms/metadata/custom_tabs/histograms.xml
@@ -662,7 +662,10 @@
   <summary>
     Startup time for a Custom Tab from the earliest point in startup to the
     first navigation commit. Split by varying degrees of warmness, from cold to
-    fully pre-loaded through mayLaunchUrl. Only reported on Android P+.
+    fully pre-loaded through mayLaunchUrl.
+
+    It is recommended to limit analysis of data to Android P+ because on prior
+    versions the cold start detection is less robust.
 
     {suffix}.
   </summary>
@@ -688,8 +691,10 @@
   <summary>
     Startup time for a Custom Tab from the earliest point in startup to the
     Largest Contentful Paint on the first navigation. Split by varying degrees
-    of warmness, from cold to fully pre-loaded through mayLaunchUrl. Only
-    reported on Android P+.
+    of warmness, from cold to fully pre-loaded through mayLaunchUrl.
+
+    It is recommended to limit analysis of data to Android P+ because on prior
+    versions the cold start detection is less robust.
 
     {suffix}.
   </summary>
diff --git a/tools/metrics/histograms/metadata/extensions/histograms.xml b/tools/metrics/histograms/metadata/extensions/histograms.xml
index 374a019c..9bc09b7 100644
--- a/tools/metrics/histograms/metadata/extensions/histograms.xml
+++ b/tools/metrics/histograms/metadata/extensions/histograms.xml
@@ -4290,7 +4290,9 @@
 </histogram>
 
 <histogram name="Extensions.ServiceWorkerBackground.StartWorker_FailStatus"
-    enum="ServiceWorkerStatusCode" expires_after="2023-04-28">
+    enum="ServiceWorkerStatusCode" expires_after="never">
+<!-- expires-never: Monitors core extension system health. -->
+
   <owner>jlulejian@chromium.org</owner>
   <owner>extensions-core@chromium.org</owner>
   <summary>
diff --git a/tools/metrics/histograms/metadata/media/histograms.xml b/tools/metrics/histograms/metadata/media/histograms.xml
index 8674b036..2f58110 100644
--- a/tools/metrics/histograms/metadata/media/histograms.xml
+++ b/tools/metrics/histograms/metadata/media/histograms.xml
@@ -2508,6 +2508,36 @@
   </summary>
 </histogram>
 
+<histogram name="Media.EME.CdmStorageManager.DatabaseOpenError"
+    enum="CdmStorageOpenError" expires_after="2024-08-11">
+  <owner>vpasupathy@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    This UMA logs overall error rates from opening the CdmStorageDatabase.
+    Recorded on every open.
+  </summary>
+</histogram>
+
+<histogram name="Media.EME.CdmStorageManager.DatabaseOpenError.Incognito"
+    enum="CdmStorageOpenError" expires_after="2024-08-11">
+  <owner>vpasupathy@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    This UMA logs overall error rates from opening the CdmStorageDatabase.
+    Recorded on every open from an Incognito profile.
+  </summary>
+</histogram>
+
+<histogram name="Media.EME.CdmStorageManager.DatabaseOpenError.NotIncognito"
+    enum="CdmStorageOpenError" expires_after="2024-08-11">
+  <owner>vpasupathy@chromium.org</owner>
+  <owner>media-dev-uma@chromium.org</owner>
+  <summary>
+    This UMA logs overall error rates from opening the CdmStorageDatabase.
+    Recorded on every open from a non-Incognito profile.
+  </summary>
+</histogram>
+
 <histogram name="Media.EME.CdmStorageManager.DeleteDatabaseError.Incognito"
     enum="BooleanError" expires_after="2024-08-11">
   <owner>vpasupathy@chromium.org</owner>
diff --git a/tools/metrics/histograms/metadata/oobe/histograms.xml b/tools/metrics/histograms/metadata/oobe/histograms.xml
index 063a500f..f2b136b 100644
--- a/tools/metrics/histograms/metadata/oobe/histograms.xml
+++ b/tools/metrics/histograms/metadata/oobe/histograms.xml
@@ -22,11 +22,64 @@
 
 <histograms>
 
+<variants name="CommonScreenName">
+  <variant name="Adb-sideloading"/>
+  <variant name="Add-child"/>
+  <variant name="App-downloading"/>
+  <variant name="Assistant-optin-flow"/>
+  <variant name="Autolaunch"/>
+  <variant name="Choobe"/>
+  <variant name="Consolidated-consent"/>
+  <variant name="Cryptohome-recovery"/>
+  <variant name="Cryptohome-recovery-setup"/>
+  <variant name="Device-disabled"/>
+  <variant name="Display-size"/>
+  <variant name="Drive-pinning"/>
+  <variant name="Edu-coexistence-login"/>
+  <variant name="Encryption-migration"/>
+  <variant name="Family-link-notice"/>
+  <variant name="Fingerprint-setup"/>
+  <variant name="Gaia-password-changed"/>
+  <variant name="Gaia-signin"/>
+  <variant name="Gesture-navigation"/>
+  <variant name="Guest-tos"/>
+  <variant name="Hw-data-collection"/>
+  <variant name="Kiosk-enable"/>
+  <variant name="Local-state-error"/>
+  <variant name="Locale-switch"/>
+  <variant name="Management-transition"/>
+  <variant name="Marketing-opt-in"/>
+  <variant name="Multidevice-setup-screen"/>
+  <variant name="Offline-login"/>
+  <variant name="Os-install"/>
+  <variant name="Os-trial"/>
+  <variant name="Parental-handoff"/>
+  <variant name="Pin-setup"/>
+  <variant name="Recommend-apps"/>
+  <variant name="Reset"/>
+  <variant name="Saml-confirm-password"/>
+  <variant name="Signin-fatal-error"/>
+  <variant name="Smart-privacy-protection"/>
+  <variant name="Sync-consent"/>
+  <variant name="Terms-of-service"/>
+  <variant name="Theme-selection"/>
+  <variant name="Touchpad-scroll"/>
+  <variant name="Tpm-error-message"/>
+  <variant name="Update-required"/>
+  <variant name="User-creation"/>
+  <variant name="Wrong-hwid"/>
+</variants>
+
 <variants name="ConsumerUpdateType">
   <variant name="Mandatory"/>
   <variant name="Optional"/>
 </variants>
 
+<variants name="OnboardingType">
+  <variant name="FirstOnboarding"/>
+  <variant name="SubsequentOnboarding"/>
+</variants>
+
 <variants name="OOBEGestureNavigationPage">
   <variant name="Back" summary="Gesture Navigation Back Page"/>
   <variant name="Home" summary="Gesture Navigation Home Page"/>
@@ -120,6 +173,25 @@
   <variant name="Proxy"/>
 </variants>
 
+<!-- OOBEOnlyScreenName variants must be kept in sync with the list of OOBEOnlyScreenNames in
+    chrome/browser/ash/login/oobe_metrics_helper.h-->
+
+<variants name="OOBEOnlyScreenName">
+  <variant name="Auto-enrollment-check"/>
+  <variant name="Connect"/>
+  <variant name="Consumer-update"/>
+  <variant name="Debugging"/>
+  <variant name="Demo-preferences"/>
+  <variant name="Demo-setup"/>
+  <variant name="Enterprise-enrollment"/>
+  <variant name="Gaia-info"/>
+  <variant name="Hid-detection"/>
+  <variant name="Network-selection"/>
+  <variant name="Oobe-update"/>
+  <variant name="Packaged-license"/>
+  <variant name="Quick-start"/>
+</variants>
+
 <variants name="OOBEOptionalScreens">
   <variant name="Display-size"/>
   <variant name="Drive-pinning"/>
@@ -715,10 +787,7 @@
     screen (before the start of the session). The time recorded includes the
     time while the device is turned off.
   </summary>
-  <token key="OnboardingType">
-    <variant name="FirstOnboarding"/>
-    <variant name="SubsequentOnboarding"/>
-  </token>
+  <token key="OnboardingType" variants="OnboardingType"/>
 </histogram>
 
 <histogram name="OOBE.OnboardingFlowStatus.{OnboardingType}"
@@ -732,10 +801,7 @@
     &quot;Completed&quot; is recorded after the completion of the last screen in
     the onboarding flow (before the start of the session).
   </summary>
-  <token key="OnboardingType">
-    <variant name="FirstOnboarding"/>
-    <variant name="SubsequentOnboarding"/>
-  </token>
+  <token key="OnboardingType" variants="OnboardingType"/>
 </histogram>
 
 <histogram name="OOBE.OobeFlowDuration" units="ms" expires_after="2024-03-18">
@@ -846,6 +912,29 @@
   <token key="OOBELegacyScreenName" variants="OOBELegacyScreenName"/>
 </histogram>
 
+<histogram name="OOBE.StepCompletionTime2.{CommonScreenName}.{OnboardingType}"
+    units="ms" expires_after="2024-03-26">
+  <owner>osamafathy@google.com</owner>
+  <owner>cros-oobe@google.com</owner>
+  <summary>
+    Time spent on {CommonScreenName} OOBE screen. Recorded only after exiting
+    the screen.
+  </summary>
+  <token key="CommonScreenName" variants="CommonScreenName"/>
+  <token key="OnboardingType" variants="OnboardingType"/>
+</histogram>
+
+<histogram name="OOBE.StepCompletionTime2.{OOBEOnlyScreenName}" units="ms"
+    expires_after="2024-03-26">
+  <owner>osamafathy@google.com</owner>
+  <owner>cros-oobe@google.com</owner>
+  <summary>
+    Time spent on {OOBEOnlyScreenName} OOBE screen. Recorded only after exiting
+    the screen.
+  </summary>
+  <token key="OOBEOnlyScreenName" variants="OOBEOnlyScreenName"/>
+</histogram>
+
 <histogram
     name="OOBE.StepCompletionTimeByExitReason.{OOBEScreenName_ExitReason}"
     units="ms" expires_after="2024-02-20">
@@ -880,6 +969,37 @@
   <token key="OOBELegacyScreenName" variants="OOBELegacyScreenName"/>
 </histogram>
 
+<histogram name="OOBE.StepShownStatus2.{CommonScreenName}.{OnboardingType}"
+    enum="BooleanShown" expires_after="2024-03-26">
+  <owner>osamafathy@google.com</owner>
+  <owner>cros-oobe@google.com</owner>
+  <summary>
+    Whether {CommonScreenName} screen was shown or not. Recorded before showing
+    screen.
+
+    The &quot;Not Shown&quot; bucket represents instances where the user reached
+    the point where the screen is usually shown, but the pre-conditions for
+    showing the screen was not met.
+  </summary>
+  <token key="CommonScreenName" variants="CommonScreenName"/>
+  <token key="OnboardingType" variants="OnboardingType"/>
+</histogram>
+
+<histogram name="OOBE.StepShownStatus2.{OOBEOnlyScreenName}"
+    enum="BooleanShown" expires_after="2024-03-26">
+  <owner>osamafathy@google.com</owner>
+  <owner>cros-oobe@google.com</owner>
+  <summary>
+    Whether {OOBEOnlyScreenName} screen was shown or not. Recorded before
+    showing screen.
+
+    The &quot;Not Shown&quot; bucket represents instances where the user reached
+    the point where the screen is usually shown, but the pre-conditions for
+    showing the screen was not met.
+  </summary>
+  <token key="OOBEOnlyScreenName" variants="OOBEOnlyScreenName"/>
+</histogram>
+
 <histogram name="OOBE.SyncConsentScreen.Behavior" enum="SyncConsentBehavior"
     expires_after="2024-03-03">
   <owner>osamafathy@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/others/histograms.xml b/tools/metrics/histograms/metadata/others/histograms.xml
index ac44b77..85ff5b4 100644
--- a/tools/metrics/histograms/metadata/others/histograms.xml
+++ b/tools/metrics/histograms/metadata/others/histograms.xml
@@ -7579,9 +7579,13 @@
 </histogram>
 
 <histogram name="InstanceID.GetToken.RequestStatus"
-    enum="GCMRegistrationRequestStatus" expires_after="2023-08-06">
+    enum="GCMRegistrationRequestStatus" expires_after="2024-02-20">
   <owner>peter@chromium.org</owner>
-  <summary>Status code of the outcome of GetToken request.</summary>
+  <owner>rushans@chromium.org</owner>
+  <summary>
+    Status code of the outcome of GetToken request. Warning: this histogram was
+    expired from 2023-08-06 to 2023-09-22; data may be missing.
+  </summary>
 </histogram>
 
 <histogram name="InstanceID.GetToken.RequestStatus.FcmInvalidations"
diff --git a/tools/metrics/histograms/metadata/privacy/histograms.xml b/tools/metrics/histograms/metadata/privacy/histograms.xml
index 9f8411b..a839a04 100644
--- a/tools/metrics/histograms/metadata/privacy/histograms.xml
+++ b/tools/metrics/histograms/metadata/privacy/histograms.xml
@@ -742,6 +742,21 @@
 </histogram>
 
 <histogram
+    name="PrivacySandbox.CookieDeprecationFacilitatedTesting.ProfileEligibility"
+    enum="CookieDeprecationFacilitatedTestingProfileEligibility"
+    expires_after="2024-10-25">
+  <owner>anthonygarant@chromium.org</owner>
+  <owner>linan@chromium.org</owner>
+  <owner>measurement-api-dev+metrics@google.com</owner>
+  <summary>
+    Records the profile eligibility to cookie deprecation facilitated testing.
+
+    The metric is only recorded when the client eligibility has not been
+    determined and then the profile eligibility is computed.
+  </summary>
+</histogram>
+
+<histogram
     name="PrivacySandbox.PrivateAggregation.Budgeter.BudgetValidityStatus2"
     enum="PrivacySandboxPrivateAggregationBudgeterBudgetValidityStatus2"
     expires_after="2023-11-23">
diff --git a/tools/metrics/histograms/metadata/sync/histograms.xml b/tools/metrics/histograms/metadata/sync/histograms.xml
index e3d4be93..52fd7d8 100644
--- a/tools/metrics/histograms/metadata/sync/histograms.xml
+++ b/tools/metrics/histograms/metadata/sync/histograms.xml
@@ -519,7 +519,7 @@
 </histogram>
 
 <histogram name="Sync.EntityEncryptionSucceeded{SyncModelType}"
-    units="BooleanSuccess" expires_after="2024-09-01">
+    enum="BooleanSuccess" expires_after="2024-09-01">
   <owner>rushans@google.com</owner>
   <owner>treib@chromium.org</owner>
   <component>Services&gt;Sync</component>
diff --git a/tools/metrics/histograms/metadata/uma/histograms.xml b/tools/metrics/histograms/metadata/uma/histograms.xml
index 6c37ac34..182153c8 100644
--- a/tools/metrics/histograms/metadata/uma/histograms.xml
+++ b/tools/metrics/histograms/metadata/uma/histograms.xml
@@ -805,6 +805,98 @@
   </summary>
 </histogram>
 
+<histogram name="UMA.StackProfiler.CopyStack.Event" enum="CopyStackEvent"
+    expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    Records events during StackCopierSignal::CopyStack(). For each run of
+    StackCopierSignal::CopyStack(), &quot;Started&quot; will be recorded once
+    and one of the other events (Succeeded or one of the Failed) will be
+    recorded. Only recorded on platforms that use StackCopierSignal for stack
+    profiling.
+  </summary>
+</histogram>
+
+<histogram name="UMA.StackProfiler.CopyStack.EventSignalToWaitEndTime"
+    units="microseconds" expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    The delay between when the sampled thread signals that it has finished
+    copying the stack and when StackCopierSignal::CopyStack() receives that
+    signal. Recorded at most once per successful StackCopierSignal::CopyStack()
+    call; will not be recorded if the signal handler was unable to get a clock
+    reading for its end time. Only recorded on platforms that use
+    StackCopierSignal for stack profiling. Reported for all clients regardless
+    of TimeTicks::IsHighResolution, since all platforms that use
+    StackCopierSignal always have high-resolution timers.
+  </summary>
+</histogram>
+
+<histogram name="UMA.StackProfiler.CopyStack.HandlerRunTime"
+    units="microseconds" expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    The total amount of time that StackCopierSignal::CopyStack()'s signal
+    handler spends running (on the sampled thread). Thus, the amount of time the
+    stack profiler is stealing from the sampled thread. Recorded at most once
+    per successful StackCopierSignal::CopyStack() call; will not be recorded if
+    the signal handler was unable to get a clock reading for its start or end
+    time. Only recorded on platforms that use StackCopierSignal for stack
+    profiling. Reported for all clients regardless of
+    TimeTicks::IsHighResolution, since all platforms that use StackCopierSignal
+    always have high-resolution timers.
+  </summary>
+</histogram>
+
+<histogram name="UMA.StackProfiler.CopyStack.ProfileThreadTotalWaitTime"
+    units="microseconds" expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    The total amount of time that StackCopierSignal::CopyStack() spends waiting
+    for the futex to becoming signaled. Recorded once per successful
+    StackCopierSignal::CopyStack() call. Only recorded on platforms that use
+    StackCopierSignal for stack profiling. Reported for all clients regardless
+    of TimeTicks::IsHighResolution, since all platforms that use
+    StackCopierSignal always have high-resolution timers.
+  </summary>
+</histogram>
+
+<histogram name="UMA.StackProfiler.CopyStack.SignalToHandlerTime"
+    units="microseconds" expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    The amount of time between when StackCopierSignal::CopyStack() sends a
+    signal to the sampled thread and the time the signal handler gets control of
+    the sampled thread. Recorded at most once per successful
+    StackCopierSignal::CopyStack() call; will not be recorded if the signal
+    handler was unable to get a clock reading for its start time. Only recorded
+    on platforms that use StackCopierSignal for stack profiling. Reported for
+    all clients regardless of TimeTicks::IsHighResolution, since all platforms
+    that use StackCopierSignal always have high-resolution timers.
+  </summary>
+</histogram>
+
+<histogram name="UMA.StackProfiler.CopyStack.TotalCrossThreadTime"
+    units="microseconds" expires_after="2024-09-12">
+  <owner>iby@google.com</owner>
+  <owner>src/base/metrics/OWNERS</owner>
+  <summary>
+    The total amount of time consumed by StackCopierSignal::CopyStack() doing
+    work between itself and the sampled thread; that is, the time between when
+    it sends a signal to the other thread and the time the other thread responds
+    by signaling completion. Recorded once per successful
+    StackCopierSignal::CopyStack() call. Only recorded on platforms that use
+    StackCopierSignal for stack profiling. Reported for all clients regardless
+    of TimeTicks::IsHighResolution, since all platforms that use
+    StackCopierSignal always have high-resolution timers.
+  </summary>
+</histogram>
+
 <histogram name="UMA.Startup.LocalStateFileExistence" enum="BooleanExists"
     expires_after="2023-12-24">
   <owner>caitlinfischer@google.com</owner>
diff --git a/tools/metrics/histograms/metadata/v8/histograms.xml b/tools/metrics/histograms/metadata/v8/histograms.xml
index 2e7b9d6..b111e8e 100644
--- a/tools/metrics/histograms/metadata/v8/histograms.xml
+++ b/tools/metrics/histograms/metadata/v8/histograms.xml
@@ -1101,15 +1101,13 @@
   <summary>Reason an incremental marking was started in V8.</summary>
 </histogram>
 
-<histogram name="V8.GCIncrementalMarkingStart" units="ms"
-    expires_after="2023-10-08">
+<histogram name="V8.GCIncrementalMarkingStart" units="ms" expires_after="M124">
   <owner>hpayer@chromium.org</owner>
   <owner>v8-memory-sheriffs@google.com</owner>
   <summary>Time spent in starting incremental marking.</summary>
 </histogram>
 
-<histogram name="V8.GCIncrementalMarkingSum" units="ms"
-    expires_after="2023-10-08">
+<histogram name="V8.GCIncrementalMarkingSum" units="ms" expires_after="M124">
   <owner>mlippautz@chromium.org</owner>
   <owner>v8-memory-sheriffs@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 19236010..d8d51b49 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/v37.0/linux-arm64/trace_processor_shell"
         },
         "win": {
-            "hash": "7ce80dce747cc609557c1dacc8104b9cbe8febc1",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/116a2d5dc0e6f86faf48fb5aa8b5977001990c67/trace_processor_shell.exe"
+            "hash": "a95a54e93fe90c0f4c8cd9eb6c0e623240519ec4",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/win/e879b2e2ee67ba3ce9bbb01bb7d776c8dff28897/trace_processor_shell.exe"
         },
         "linux_arm": {
             "hash": "4ad0dc8eeae3ad92d6a1da2f1653a81fb9e3c4c1",
@@ -21,8 +21,8 @@
             "full_remote_path": "perfetto-luci-artifacts/v35.0/mac-arm64/trace_processor_shell"
         },
         "linux": {
-            "hash": "31cde9fb37b6d1a55e8a027125d7b908e963709f",
-            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/a38d6a9fc06768cd6a0923a7c8c6e3d62d8d582a/trace_processor_shell"
+            "hash": "9a38db3d9d1808db8f543f6f2aec55c831058b66",
+            "full_remote_path": "chromium-telemetry/perfetto_binaries/trace_processor_shell/linux/e879b2e2ee67ba3ce9bbb01bb7d776c8dff28897/trace_processor_shell"
         }
     },
     "power_profile.sql": {
diff --git a/ui/base/BUILD.gn b/ui/base/BUILD.gn
index ff4cbb1..6eb8ca6 100644
--- a/ui/base/BUILD.gn
+++ b/ui/base/BUILD.gn
@@ -594,6 +594,8 @@
       "glib/glib_integers.h",
       "glib/glib_signal.h",
       "glib/scoped_gobject.h",
+      "glib/scoped_gsignal.cc",
+      "glib/scoped_gsignal.h",
     ]
   }
 
@@ -1299,6 +1301,10 @@
   if (is_chromeos_ash || ozone_platform_wayland) {
     deps += [ "//ui/base/wayland:unittests" ]
   }
+
+  if (use_glib) {
+    sources += [ "glib/scoped_gsignal_unittest.cc" ]
+  }
 }
 
 if (is_mac) {
diff --git a/ui/base/accelerators/mojom/BUILD.gn b/ui/base/accelerators/mojom/BUILD.gn
index cdc8ffd..7a3569f 100644
--- a/ui/base/accelerators/mojom/BUILD.gn
+++ b/ui/base/accelerators/mojom/BUILD.gn
@@ -11,7 +11,11 @@
     "//ui/events/mojom",
   ]
 
+  # Generate TypeScript WebUI bindings and JavaScript legacy JS bindings (for
+  # Blink).
   webui_module_path = "chrome://resources/mojo/ui/base/accelerators/mojom"
+  use_typescript_sources = true
+  generate_legacy_js_bindings = true
 
   cpp_typemaps = [
     {
diff --git a/ui/base/glib/scoped_gobject.h b/ui/base/glib/scoped_gobject.h
index 0467479..788ece7 100644
--- a/ui/base/glib/scoped_gobject.h
+++ b/ui/base/glib/scoped_gobject.h
@@ -12,7 +12,7 @@
 #include "base/check.h"
 #include "base/memory/raw_ptr.h"
 
-// Similar to a std::shared_ptr for GObject types.
+// Similar to a scoped_refptr for GObject types.
 template <typename T>
 class ScopedGObject {
  public:
@@ -28,24 +28,32 @@
     other.obj_ = nullptr;
   }
 
-  ~ScopedGObject() { Unref(); }
+  ~ScopedGObject() { Reset(); }
 
   ScopedGObject<T>& operator=(const ScopedGObject<T>& other) {
-    Unref();
+    Reset();
     obj_ = other.obj_;
     Ref();
     return *this;
   }
 
   ScopedGObject<T>& operator=(ScopedGObject<T>&& other) {
-    Unref();
+    Reset();
     obj_ = other.obj_;
     other.obj_ = nullptr;
     return *this;
   }
 
+  void Reset() {
+    if (obj_) {
+      g_object_unref(obj_.ExtractAsDangling());
+    }
+  }
+
   T* get() { return obj_; }
 
+  // Deliberately implicit to allow easier interaction with C APIs.
+  // NOLINTNEXTLINE(google-explicit-constructor)
   operator T*() { return obj_; }
 
  private:
@@ -58,8 +66,9 @@
 
   void RefSink() {
     // Remove the floating reference from |obj_| if it has one.
-    if (obj_ && g_object_is_floating(obj_))
+    if (obj_ && g_object_is_floating(obj_)) {
       g_object_ref_sink(obj_);
+    }
   }
 
   void Ref() {
@@ -69,14 +78,6 @@
     }
   }
 
-  // This function is necessary so that gtk can overload it in
-  // the case of T = GtkStyleContext.
-  void Unref() {
-    if (obj_) {
-      g_object_unref(obj_.ExtractAsDangling());
-    }
-  }
-
   raw_ptr<T> obj_ = nullptr;
 };
 
diff --git a/ui/base/glib/scoped_gsignal.cc b/ui/base/glib/scoped_gsignal.cc
new file mode 100644
index 0000000..f77c1c0
--- /dev/null
+++ b/ui/base/glib/scoped_gsignal.cc
@@ -0,0 +1,26 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/base/glib/scoped_gsignal.h"
+
+ScopedGSignal::ScopedGSignal() = default;
+
+ScopedGSignal::ScopedGSignal(ScopedGSignal&& other) noexcept = default;
+
+ScopedGSignal& ScopedGSignal::operator=(ScopedGSignal&& other) noexcept =
+    default;
+
+ScopedGSignal::~ScopedGSignal() = default;
+
+bool ScopedGSignal::Connected() const {
+  return impl_ && impl_->Connected();
+}
+
+void ScopedGSignal::Reset() {
+  impl_.reset();
+}
+
+ScopedGSignal::SignalBase::SignalBase() = default;
+
+ScopedGSignal::SignalBase::~SignalBase() = default;
diff --git a/ui/base/glib/scoped_gsignal.h b/ui/base/glib/scoped_gsignal.h
new file mode 100644
index 0000000..4dd95bd
--- /dev/null
+++ b/ui/base/glib/scoped_gsignal.h
@@ -0,0 +1,203 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_BASE_GLIB_SCOPED_GSIGNAL_H_
+#define UI_BASE_GLIB_SCOPED_GSIGNAL_H_
+
+#include <glib-object.h>
+#include <glib.h>
+
+#include <memory>
+#include <utility>
+
+#include "base/component_export.h"
+#include "base/functional/callback.h"
+#include "base/logging.h"
+#include "base/memory/raw_ptr.h"
+#include "base/sequence_checker.h"
+#include "ui/base/glib/scoped_gobject.h"
+
+// ScopedGSignal manages the lifecycle of a GLib signal connection.
+// It disconnects the signal when this object is destroyed or goes out of scope.
+// This class should be used on a single sequence.
+class COMPONENT_EXPORT(UI_BASE) ScopedGSignal {
+ public:
+  // Constructs and connects a GLib signal with specified attributes.
+  // Parameters:
+  // - instance: The GLib object instance emitting the signal.
+  // - detailed_signal: Signal name to connect.
+  // - handler: Callback function to invoke when the signal is emitted.
+  // - connect_flags: Optional flags to influence signal connection behavior.
+  // If signal connection fails, this object will be left unconnected.
+  // `Connected()` can be used to check for signal connection success.
+  template <typename Sender, typename Ret, typename... Args>
+  ScopedGSignal(Sender* instance,
+                const gchar* detailed_signal,
+                base::RepeatingCallback<Ret(Sender*, Args...)> handler,
+                GConnectFlags connect_flags = static_cast<GConnectFlags>(0))
+      : impl_(std::make_unique<SignalImpl<Sender, Ret, Args...>>(
+            instance,
+            detailed_signal,
+            std::move(handler),
+            connect_flags)) {
+    if (!Connected()) {
+      // No need to keep `impl_` around.
+      impl_.reset();
+    }
+  }
+
+  // Overload accepting a ScopedGObject.
+  template <typename Sender, typename Ret, typename... Args>
+  ScopedGSignal(ScopedGObject<Sender> instance,
+                const gchar* detailed_signal,
+                base::RepeatingCallback<Ret(Sender*, Args...)> handler,
+                GConnectFlags connect_flags = static_cast<GConnectFlags>(0))
+      : ScopedGSignal(instance.get(),
+                      detailed_signal,
+                      std::move(handler),
+                      connect_flags) {}
+
+  // Overload accepting a raw_ptr.
+  template <typename Sender, typename Ret, typename... Args>
+  ScopedGSignal(raw_ptr<Sender> instance,
+                const gchar* detailed_signal,
+                base::RepeatingCallback<Ret(Sender*, Args...)> handler,
+                GConnectFlags connect_flags = static_cast<GConnectFlags>(0))
+      : ScopedGSignal(instance.get(),
+                      detailed_signal,
+                      std::move(handler),
+                      connect_flags) {}
+
+  // Constructs an unconnected ScopedGSignal.
+  ScopedGSignal();
+
+  ScopedGSignal(ScopedGSignal&&) noexcept;
+  ScopedGSignal& operator=(ScopedGSignal&&) noexcept;
+
+  ~ScopedGSignal();
+
+  [[nodiscard]] bool Connected() const;
+
+  void Reset();
+
+ private:
+  // The implementation uses the PIMPL idiom for the following reasons:
+  // 1. GLib binds a user data pointer that gets passed to the callback.
+  //    This means the implementation class can't be movable.  To support
+  //    moves, keep a pointer to the implementation.
+  // 2. Type erasure: the derived class depends on the callback type and
+  //    sender type, so a virtual destructor is required.
+  class SignalBase {
+   public:
+    SignalBase(SignalBase&&) = delete;
+    SignalBase& operator=(SignalBase&&) = delete;
+
+    virtual ~SignalBase();
+
+    [[nodiscard]] bool Connected() const { return signal_id_; }
+
+   protected:
+    SignalBase();
+
+    [[nodiscard]] gulong signal_id() const { return signal_id_; }
+    void set_signal_id(gulong signal_id) { signal_id_ = signal_id; }
+
+   private:
+    gulong signal_id_ = 0;
+  };
+
+  template <typename Sender, typename Ret, typename... Args>
+  class SignalImpl final : public SignalBase {
+   public:
+    using Handler = base::RepeatingCallback<Ret(Sender*, Args...)>;
+
+    SignalImpl(Sender* instance,
+               const gchar* detailed_signal,
+               Handler handler,
+               GConnectFlags connect_flags) {
+      CHECK(instance);
+      CHECK(detailed_signal);
+      CHECK(handler);
+
+      const bool swapped = connect_flags & G_CONNECT_SWAPPED;
+      const bool after = connect_flags & G_CONNECT_AFTER;
+
+      auto* new_closure = swapped ? g_cclosure_new_swap : g_cclosure_new;
+      if (!(gclosure_ = new_closure(G_CALLBACK(OnSignalEmittedThunk), this,
+                                    OnDisconnectedThunk))) {
+        LOG(ERROR) << "Failed to create GClosure";
+        return;
+      }
+
+      set_signal_id(g_signal_connect_closure(instance, detailed_signal,
+                                             gclosure_, after));
+      if (!signal_id()) {
+        LOG(ERROR) << "Failed to connect to " << detailed_signal;
+        // Prevent OnDisconnectedThunk from running.
+        g_closure_remove_finalize_notifier(gclosure_, this,
+                                           OnDisconnectedThunk);
+        // Remove the floating reference to free `gclosure_`.  Note that this
+        // should not be called if the signal connected since it will take
+        // ownership of `gclosure_`.
+        g_closure_unref(gclosure_.ExtractAsDangling());
+        return;
+      }
+
+      sender_ = instance;
+      handler_ = std::move(handler);
+    }
+
+    ~SignalImpl() override {
+      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+      if (!Connected()) {
+        return;
+      }
+      // If the finalize notifier is not removed, it will get called some time
+      // after this object has been destroyed.  Remove the finalize notifier to
+      // prevent this.
+      g_closure_remove_finalize_notifier(gclosure_.ExtractAsDangling(), this,
+                                         OnDisconnectedThunk);
+      g_signal_handler_disconnect(sender_, signal_id());
+      // `OnDisconnected()` must be explicitly called since the finalize
+      // notifier was removed.
+      OnDisconnected();
+    }
+
+   private:
+    static Ret OnSignalEmittedThunk(Sender* sender,
+                                    Args... args,
+                                    gpointer self) {
+      return reinterpret_cast<SignalImpl*>(self)->OnSignalEmitted(sender,
+                                                                  args...);
+    }
+
+    static void OnDisconnectedThunk(gpointer self, GClosure* closure) {
+      reinterpret_cast<SignalImpl*>(self)->OnDisconnected();
+    }
+
+    Ret OnSignalEmitted(Sender* sender, Args... args) {
+      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+      return handler_.Run(sender, args...);
+    }
+
+    void OnDisconnected() {
+      DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+      CHECK(Connected());
+      set_signal_id(0);
+      sender_ = nullptr;
+      gclosure_ = nullptr;
+      handler_.Reset();
+    }
+
+    raw_ptr<Sender> sender_ = nullptr;
+    raw_ptr<GClosure> gclosure_ = nullptr;
+    Handler handler_;
+
+    SEQUENCE_CHECKER(sequence_checker_);
+  };
+
+  std::unique_ptr<SignalBase> impl_;
+};
+
+#endif  // UI_BASE_GLIB_SCOPED_GSIGNAL_H_
diff --git a/ui/base/glib/scoped_gsignal_unittest.cc b/ui/base/glib/scoped_gsignal_unittest.cc
new file mode 100644
index 0000000..e5e30d8
--- /dev/null
+++ b/ui/base/glib/scoped_gsignal_unittest.cc
@@ -0,0 +1,150 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/base/glib/scoped_gsignal.h"
+
+#include "base/functional/bind.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/glib/scoped_gobject.h"
+
+namespace ui {
+
+namespace {
+
+G_DECLARE_FINAL_TYPE(TestObject, test_object, TEST, OBJECT, GObject)
+
+// Used by G_DECLARE_FINAL_TYPE above.
+struct _TestObject {
+  GObject parent_instance;
+};
+
+G_DEFINE_TYPE(TestObject, test_object, G_TYPE_OBJECT)
+
+// Used by G_DEFINE_TYPE above.
+void test_object_class_init(TestObjectClass*) {
+  g_signal_newv("some-signal", test_object_get_type(),
+                (GSignalFlags)(G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE |
+                               G_SIGNAL_NO_HOOKS),
+                nullptr, nullptr, nullptr, nullptr, G_TYPE_NONE, 0, nullptr);
+}
+
+// Used by G_DEFINE_TYPE above.
+void test_object_init(TestObject* self) {}
+
+ScopedGObject<GObject> CreateGObject() {
+  return TakeGObject(G_OBJECT(g_object_new(test_object_get_type(), nullptr)));
+}
+
+void EmitSignal(GObject* instance, const gchar* detailed_signal) {
+  g_signal_emit_by_name(instance, detailed_signal);
+}
+
+}  // namespace
+
+TEST(ScopedGSignalTest, Empty) {
+  ScopedGSignal signal;
+  EXPECT_FALSE(signal.Connected());
+}
+
+TEST(ScopedGSignalTest, Construction) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal(instance, "some-signal",
+                       base::BindRepeating([](GObject* obj) {}));
+  EXPECT_TRUE(signal.Connected());
+}
+
+TEST(ScopedGSignalTest, DisconnectsOnDestruction) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  bool signal_fired = false;
+  {
+    ScopedGSignal signal(
+        instance, "some-signal",
+        base::BindRepeating([](bool* fired, GObject* obj) { *fired = true; },
+                            &signal_fired));
+    EXPECT_TRUE(signal.Connected());
+  }
+
+  EmitSignal(instance, "some-signal");
+  EXPECT_FALSE(signal_fired);
+}
+
+TEST(ScopedGSignalTest, DisconnectsOnGClosureFinalize) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal(instance, "some-signal",
+                       base::BindRepeating([](GObject* obj) {}));
+  EXPECT_TRUE(signal.Connected());
+
+  instance.Reset();
+  EXPECT_FALSE(signal.Connected());
+}
+
+TEST(ScopedGSignalTest, DisconnectsOnReset) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal(instance, "some-signal",
+                       base::BindRepeating([](GObject* obj) {}));
+  EXPECT_TRUE(signal.Connected());
+
+  signal.Reset();
+  EXPECT_FALSE(signal.Connected());
+}
+
+TEST(ScopedGSignalTest, MoveConstruct) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal1(instance, "some-signal",
+                        base::BindRepeating([](GObject* obj) {}));
+  EXPECT_TRUE(signal1.Connected());
+
+  ScopedGSignal signal2{std::move(signal1)};
+  EXPECT_FALSE(signal1.Connected());
+  EXPECT_TRUE(signal2.Connected());
+}
+
+TEST(ScopedGSignalTest, MoveAssign) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal1(instance, "some-signal",
+                        base::BindRepeating([](GObject* obj) {}));
+  EXPECT_TRUE(signal1.Connected());
+
+  ScopedGSignal signal2 = std::move(signal1);
+  EXPECT_FALSE(signal1.Connected());
+  EXPECT_TRUE(signal2.Connected());
+}
+
+TEST(ScopedGSignalTest, SignalHandlerCalled) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  bool signal_fired = false;
+  ScopedGSignal signal(
+      instance, "some-signal",
+      base::BindRepeating([](bool* fired, GObject* obj) { *fired = true; },
+                          &signal_fired));
+  EXPECT_TRUE(signal.Connected());
+
+  EmitSignal(instance, "some-signal");
+  EXPECT_TRUE(signal_fired);
+}
+
+TEST(ScopedGSignalTest, InvalidSignal) {
+  auto instance = CreateGObject();
+  ASSERT_TRUE(instance.get());
+
+  ScopedGSignal signal(instance, "invalid-signal",
+                       base::BindRepeating([](GObject* obj) {}));
+  EXPECT_FALSE(signal.Connected());
+}
+
+}  // namespace ui
diff --git a/ui/base/ime/ash/extension_ime_util.cc b/ui/base/ime/ash/extension_ime_util.cc
index 6c648b28..cee4171 100644
--- a/ui/base/ime/ash/extension_ime_util.cc
+++ b/ui/base/ime/ash/extension_ime_util.cc
@@ -178,5 +178,10 @@
 #endif
 }
 
+bool IsCros1pKorean(const std::string& input_method_id) {
+  return input_method_id == base::StrCat({kComponentExtensionIMEPrefix,
+                                          kXkbExtensionId, "ko-t-i0-und"});
+}
+
 }  // namespace extension_ime_util
 }  // namespace ash
diff --git a/ui/base/ime/ash/extension_ime_util.h b/ui/base/ime/ash/extension_ime_util.h
index 3ed4d622..e4d7f17 100644
--- a/ui/base/ime/ash/extension_ime_util.h
+++ b/ui/base/ime/ash/extension_ime_util.h
@@ -104,6 +104,11 @@
 bool COMPONENT_EXPORT(UI_BASE_IME_ASH)
     IsExperimentalMultilingual(const std::string& input_method_id);
 
+// Only used when ash::features::kImeKoreanModeSwitchDebug flag is enabled.
+// TODO(b/302460634): Remove when no longer needed.
+bool COMPONENT_EXPORT(UI_BASE_IME_ASH)
+    IsCros1pKorean(const std::string& input_method_id);
+
 }  // namespace extension_ime_util
 }  // namespace ash
 
diff --git a/ui/events/ash/event_rewriter_ash.cc b/ui/events/ash/event_rewriter_ash.cc
index d9f463d..37f7b92 100644
--- a/ui/events/ash/event_rewriter_ash.cc
+++ b/ui/events/ash/event_rewriter_ash.cc
@@ -288,6 +288,8 @@
       break;
 
     case KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
+    case KeyboardCapability::DeviceType::
+        kDeviceExternalNullTopRowChromeOsKeyboard:
     case KeyboardCapability::DeviceType::kDeviceExternalUnknown:
       pref_name = prefs::kLanguageRemapExternalMetaKeyTo;
       break;
@@ -1405,6 +1407,8 @@
       break;
     case KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
     case KeyboardCapability::DeviceType::kDeviceExternalUnknown:
+    case KeyboardCapability::DeviceType::
+        kDeviceExternalNullTopRowChromeOsKeyboard:
       UMA_HISTOGRAM_ENUMERATION(
           "ChromeOS.Inputs.Keyboard.RemappedModifierPressed.External",
           modifier_key_usage_mapping->modifier_key_enum);
@@ -1449,6 +1453,8 @@
           modifier_key_usage_mapping->modifier_key_enum);
       break;
     case KeyboardCapability::DeviceType::kDeviceExternalGenericKeyboard:
+    case KeyboardCapability::DeviceType::
+        kDeviceExternalNullTopRowChromeOsKeyboard:
     case KeyboardCapability::DeviceType::kDeviceExternalUnknown:
       UMA_HISTOGRAM_ENUMERATION(
           "ChromeOS.Inputs.Keyboard.ModifierPressed.External",
diff --git a/ui/events/ash/keyboard_capability.cc b/ui/events/ash/keyboard_capability.cc
index e5a845b..773bdfe 100644
--- a/ui/events/ash/keyboard_capability.cc
+++ b/ui/events/ash/keyboard_capability.cc
@@ -62,6 +62,10 @@
 // FKey is missing on the physical device.
 const int kCustomAbsentScanCode = 0x00;
 
+// Represents the "null" scancode used to represent the opting out of Meta +
+// F-Key rewrites functionality.
+const int kCustomNullScanCode = 0xC0000;
+
 // Hotrod controller vendor/product ids.
 const int kHotrodRemoteVendorId = 0x0471;
 const int kHotrodRemoteProductId = 0x21cc;
@@ -294,7 +298,8 @@
 // standard F1-F12 keys.
 KeyboardCapability::DeviceType IdentifyKeyboardType(
     const KeyboardDevice& keyboard_device,
-    bool has_chromeos_top_row) {
+    bool has_chromeos_top_row,
+    bool has_null_top_row) {
   if (keyboard_device.vendor_id == kHotrodRemoteVendorId &&
       keyboard_device.product_id == kHotrodRemoteProductId) {
     VLOG(1) << "Hotrod remote '" << keyboard_device.name
@@ -318,6 +323,13 @@
   }
 
   if (has_chromeos_top_row) {
+    if (has_null_top_row) {
+      VLOG(1) << "External Null Top Row keyboard '" << keyboard_device.name
+              << "' connected: id=" << keyboard_device.id;
+      return KeyboardCapability::DeviceType::
+          kDeviceExternalNullTopRowChromeOsKeyboard;
+    }
+
     // If the device was tagged as having Chrome OS top row layout it must be a
     // Chrome OS keyboard.
     VLOG(1) << "External Chrome OS keyboard '" << keyboard_device.name
@@ -369,8 +381,13 @@
   std::string layout_string;
   KeyboardTopRowLayout layout;
   std::vector<uint32_t> top_row_scan_codes = GetTopRowScanCodeVector(keyboard);
+  bool null_top_row = false;
   if (!top_row_scan_codes.empty()) {
     layout = KeyboardTopRowLayout::kKbdTopRowLayoutCustom;
+    null_top_row =
+        base::ranges::all_of(top_row_scan_codes, [](const uint32_t scancode) {
+          return scancode == kCustomNullScanCode;
+        });
   } else if (!GetTopRowLayoutProperty(keyboard, layout_string) ||
              !ParseKeyboardTopRowLayout(layout_string, layout)) {
     return {KeyboardCapability::DeviceType::kDeviceUnknown,
@@ -379,7 +396,8 @@
   }
 
   return {IdentifyKeyboardType(
-              keyboard, !top_row_scan_codes.empty() || !layout_string.empty()),
+              keyboard, !top_row_scan_codes.empty() || !layout_string.empty(),
+              null_top_row),
           layout, std::move(top_row_scan_codes)};
 }
 
diff --git a/ui/events/ash/keyboard_capability.h b/ui/events/ash/keyboard_capability.h
index af93e51..95fd840 100644
--- a/ui/events/ash/keyboard_capability.h
+++ b/ui/events/ash/keyboard_capability.h
@@ -172,6 +172,7 @@
     kDeviceInternalRevenKeyboard,
     kDeviceExternalAppleKeyboard,
     kDeviceExternalChromeOsKeyboard,
+    kDeviceExternalNullTopRowChromeOsKeyboard,
     kDeviceExternalGenericKeyboard,
     kDeviceExternalUnknown,
     kDeviceHotrodRemote,
diff --git a/ui/events/mojom/BUILD.gn b/ui/events/mojom/BUILD.gn
index 47a11bf..16989cc4 100644
--- a/ui/events/mojom/BUILD.gn
+++ b/ui/events/mojom/BUILD.gn
@@ -78,7 +78,12 @@
   ]
 
   blink_cpp_typemaps = shared_cpp_typemaps
+
+  # Generate TypeScript WebUI bindings and JavaScript legacy JS bindings (for
+  # Blink).
   webui_module_path = "chrome://resources/mojo/ui/events/mojom"
+  use_typescript_sources = true
+  generate_legacy_js_bindings = true
 }
 
 mojom("event_latency_metadata_mojom") {
diff --git a/ui/file_manager/file_manager/common/js/cr_ui.ts b/ui/file_manager/file_manager/common/js/cr_ui.ts
new file mode 100644
index 0000000..a35b9963
--- /dev/null
+++ b/ui/file_manager/file_manager/common/js/cr_ui.ts
@@ -0,0 +1,85 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {dispatchPropertyChange} from 'chrome://resources/ash/common/cr_deprecated.js';
+
+/**
+ * Setter used by the deprecated cr.ui elements.
+ * It sets the value of type T in the private `${name}_`.
+ *
+ * It also dispatches the event `${name}Changed` when the value actually
+ * changes.
+ */
+export function jsSetter<T>(self: any, name: string, value: T) {
+  const privateName = `${name}_`;
+  const oldValue = self[name];
+  if (value !== oldValue) {
+    self[privateName] = value;
+    dispatchPropertyChange(self, name, value, oldValue);
+  }
+}
+
+/** Converts camelCase to DOM style casing: myName => my-name. */
+function convertToKebabCase(jsName: string): string {
+  return jsName.replace(/([A-Z])/g, '-$1').toLowerCase();
+}
+
+/**
+ * Setter used by the deprecated cr.ui elements.
+ * It sets or removes the DOM attribute, the attribute name is converted
+ * from the camelCase to DOM style case myName => my-name.
+ *
+ * It also dispatches the event `${name}Changed` when the value actually
+ * changes.
+ */
+export function boolAttrSetter(self: any, name: string, value: boolean) {
+  const attributeName = convertToKebabCase(name);
+  const oldValue = self[name];
+  if (value !== oldValue) {
+    if (value) {
+      self.setAttribute(attributeName, name);
+    } else {
+      self.removeAttribute(attributeName);
+    }
+    dispatchPropertyChange(self, name, value, oldValue);
+  }
+}
+
+/**
+ * Setter used by the deprecated cr.ui elements.
+ * It sets the value of type T in the DOM `${name}`. NOTE: Name is converted
+ * from the camelCase to DOM style case myName => my-name.
+ *
+ * It also dispatches the event `${name}Changed` when the value actually
+ * changes.
+ */
+export function domAttrSetter(self: any, name: string, value: unknown) {
+  const attributeName = convertToKebabCase(name);
+  const oldValue = self[name];
+  if (value === undefined) {
+    self.removeAttribute(attributeName);
+  } else {
+    self.setAttribute(attributeName, value);
+  }
+  dispatchPropertyChange(self, name, value, oldValue);
+}
+
+/**
+ * Used by the deprecated cr.ui elements. It receives a regular DOM element
+ * (like a <div>) and injects the cr.ui element implementation methods in that
+ * instance.
+ *
+ * It then calls the cr.ui element's `decorate()` which is the initializer for
+ * its state, since it cannot run the constructor().
+ */
+export function decorate<T extends HTMLElement>(
+    el: T, implementationClass: any): T {
+  Object.setPrototypeOf(el, implementationClass.prototype);
+  if ('decorate' in el) {
+    // Calling instance decorate().
+    (el as any).decorate();
+  }
+
+  return el as unknown as T;
+}
diff --git a/ui/file_manager/file_manager/foreground/js/ui/BUILD.gn b/ui/file_manager/file_manager/foreground/js/ui/BUILD.gn
index e873cbb..e8010b6 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/BUILD.gn
+++ b/ui/file_manager/file_manager/foreground/js/ui/BUILD.gn
@@ -47,7 +47,6 @@
     ":install_linux_package_dialog",
     ":list",
     ":list_container",
-    ":list_item",
     ":list_selection_controller",
     ":list_selection_model",
     ":list_single_selection_model",
@@ -295,7 +294,6 @@
     ":file_tap_handler",
     ":grid",
     ":list",
-    ":list_item",
     ":list_selection_model",
     "//ash/webui/common/resources:assert",
     "//ash/webui/common/resources:cr_deprecated",
@@ -456,7 +454,6 @@
     ":file_list_selection_model",
     ":file_tap_handler",
     ":list",
-    ":list_item",
     ":list_selection_controller",
     ":list_selection_model",
     "table:table",
@@ -535,7 +532,6 @@
 js_library("grid") {
   deps = [
     ":list",
-    ":list_item",
     ":list_selection_controller",
     ":list_selection_model",
     "//ui/file_manager/file_manager/common/js:ui",
@@ -576,7 +572,6 @@
 
 js_library("list") {
   deps = [
-    ":list_item",
     ":list_selection_controller",
     ":list_selection_model",
     "//ash/webui/common/resources:cr_deprecated",
@@ -599,7 +594,6 @@
     ":file_grid",
     ":file_table",
     ":list",
-    ":list_item",
     ":list_selection_model",
     ":list_single_selection_model",
     "//ash/webui/common/resources:assert",
@@ -611,10 +605,6 @@
   ]
 }
 
-js_library("list_item") {
-  deps = [ "//ash/webui/common/resources:cr_deprecated" ]
-}
-
 js_library("list_selection_controller") {
   deps = [ ":list_selection_model" ]
 }
diff --git a/ui/file_manager/file_manager/foreground/js/ui/grid.js b/ui/file_manager/file_manager/foreground/js/ui/grid.js
index 8e5f06aa..e842416e 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/grid.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/grid.js
@@ -4,6 +4,7 @@
 
 // clang-format off
 import {define as crUiDefine} from '../../../common/js/ui.js';
+import {decorate} from '../../../common/js/cr_ui.js';
 
 import {ListSelectionModel} from './list_selection_model.js';
 import {ListSelectionController} from './list_selection_controller.js';
@@ -17,22 +18,25 @@
  * except the multiple columns layout.
  */
 
-
 /**
- * Creates a new grid item element.
  * @param {*} dataItem The data item.
- * @constructor
- * @extends {ListItem}
+ * @returns {!GridItem}
  */
-export function GridItem(dataItem) {
+export function createGridItem(dataItem) {
   const el = document.createElement('li');
   el.dataItem = dataItem;
-  el.__proto__ = GridItem.prototype;
-  return el;
+  return decorate(el, ListItem);
 }
 
-GridItem.prototype = {
-  __proto__: ListItem.prototype,
+/** Creates a new grid item element. */
+export class GridItem extends ListItem {
+  /** Unused, see the decorate() method instead. */
+  constructor() {
+    super();
+
+    /** @type {*} */
+    this.dataItem;
+  }
 
   /**
    * Called when an element is decorated as a grid item.
@@ -40,8 +44,8 @@
   decorate() {
     ListItem.prototype.decorate.apply(this, arguments);
     this.textContent = this.dataItem;
-  },
-};
+  }
+}
 
 /**
  * Creates a new grid element.
@@ -64,10 +68,10 @@
 
   /**
    * Function used to create grid items.
-   * @type {function(new:GridItem, *)}
+   * @type {function(*): GridItem}
    * @override
    */
-  itemConstructor_: GridItem,
+  itemConstructor_: createGridItem,
 
   /**
    * Whether or not the rows on list have various heights.
diff --git a/ui/file_manager/file_manager/foreground/js/ui/list.d.ts b/ui/file_manager/file_manager/foreground/js/ui/list.d.ts
index 8bd3d15..351e1ba 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/list.d.ts
+++ b/ui/file_manager/file_manager/foreground/js/ui/list.d.ts
@@ -4,7 +4,6 @@
 
 import {ArrayDataModel} from '../../../common/js/array_data_model.js';
 
-import {ListItem} from './list_item.d.js';
 import {ListSelectionModel} from './list_selection_model.js';
 
 export class List extends HTMLUListElement {
diff --git a/ui/file_manager/file_manager/foreground/js/ui/list.js b/ui/file_manager/file_manager/foreground/js/ui/list.js
index f5ac3f5..02724aa39 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/list.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/list.js
@@ -10,7 +10,7 @@
 
 import {ListSelectionModel} from './list_selection_model.js';
 import {ListSelectionController} from './list_selection_controller.js';
-import {ListItem} from './list_item.js';
+import {ListItem, createListItem} from './list_item.js';
 // clang-format on
 
 /**
@@ -98,14 +98,14 @@
 
   /**
    * Function used to create grid items.
-   * @type {function(new:ListItem, *)}
-   * @private
+   * @type {function(*): ListItem}
+   * @protected
    */
-  itemConstructor_: ListItem,
+  itemConstructor_: createListItem,
 
   /**
    * Function used to create grid items.
-   * @return {function(new:ListItem, *)}
+   * @return {function(*): ListItem}
    */
   get itemConstructor() {
     return this.itemConstructor_;
@@ -883,7 +883,7 @@
    * @return {!ListItem} The newly created list item.
    */
   createItem(value) {
-    const item = new this.itemConstructor_(value);
+    const item = this.itemConstructor_(value);
     item.label = value;
     if (typeof item.decorate === 'function') {
       item.decorate();
diff --git a/ui/file_manager/file_manager/foreground/js/ui/list_item.d.ts b/ui/file_manager/file_manager/foreground/js/ui/list_item.d.ts
deleted file mode 100644
index bc9270d5..0000000
--- a/ui/file_manager/file_manager/foreground/js/ui/list_item.d.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright 2022 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-export class ListItem extends HTMLLIElement {
-  // Plain text label.
-  label: string;
-
-  // Whether the item is teh lead in a selection.
-  lead: boolean;
-
-  // This item's index in teh containing list.
-  listIndex: number;
-
-  // Whether this item is selected.
-  selected: boolean;
-
-  // Called when an element is decorated as a list item.
-  decorate: () => void;
-
-  // Called when the selection state of this element changes.
-  selectionChanged: () => void;
-}
diff --git a/ui/file_manager/file_manager/foreground/js/ui/list_item.js b/ui/file_manager/file_manager/foreground/js/ui/list_item.js
deleted file mode 100644
index 9dad95f..0000000
--- a/ui/file_manager/file_manager/foreground/js/ui/list_item.js
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2012 The Chromium Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-// clang-format off
-import {PropertyKind, getPropertyDescriptor} from 'chrome://resources/ash/common/cr_deprecated.js';
-import {define as crUiDefine} from '../../../common/js/ui.js';
-// clang-format on
-
-/**
- * Creates a new list item element.
- * @constructor
- * @extends {HTMLLIElement}
- */
-export const ListItem = crUiDefine('li');
-
-/**
- * The next id suffix to use when giving each item an unique id.
- * @type {number}
- * @private
- */
-ListItem.nextUniqueIdSuffix_ = 0;
-
-ListItem.prototype = {
-  __proto__: HTMLLIElement.prototype,
-
-  /**
-   * Plain text label.
-   * @type {string}
-   */
-  get label() {
-    return this.textContent;
-  },
-  set label(label) {
-    this.textContent = label;
-  },
-
-  /**
-   * This item's index in the containing list.
-   * @type {number}
-   */
-  listIndex_: -1,
-
-  /**
-   * Called when an element is decorated as a list item.
-   */
-  decorate() {
-    this.setAttribute('role', 'listitem');
-    if (!this.id) {
-      this.id = 'listitem-' + ListItem.nextUniqueIdSuffix_++;
-    }
-  },
-
-  /**
-   * Called when the selection state of this element changes.
-   */
-  selectionChanged() {},
-};
-
-/**
- * Whether the item is selected. Setting this does not update the underlying
- * selection model. This is only used for display purpose.
- * @type {boolean}
- */
-ListItem.prototype.selected;
-Object.defineProperty(
-    ListItem.prototype, 'selected',
-    getPropertyDescriptor('selected', PropertyKind.BOOL_ATTR, function() {
-      this.selectionChanged();
-    }));
-
-/**
- * Whether the item is the lead in a selection. Setting this does not update
- * the underlying selection model. This is only used for display purpose.
- * @type {boolean}
- */
-ListItem.prototype.lead;
-Object.defineProperty(
-    ListItem.prototype, 'lead',
-    getPropertyDescriptor('lead', PropertyKind.BOOL_ATTR));
-
-/**
- * This item's index in the containing list.
- * type {number}
- */
-ListItem.prototype.listIndex;
-Object.defineProperty(
-    ListItem.prototype, 'listIndex', getPropertyDescriptor('listIndex'));
diff --git a/ui/file_manager/file_manager/foreground/js/ui/list_item.ts b/ui/file_manager/file_manager/foreground/js/ui/list_item.ts
new file mode 100644
index 0000000..364ab8c8
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/js/ui/list_item.ts
@@ -0,0 +1,71 @@
+// Copyright 2012 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {boolAttrSetter, decorate, jsSetter} from '../../../common/js/cr_ui.js';
+
+/** The next id suffix to use when giving each item an unique id. */
+let nextUniqueIdSuffix: number = 0;
+
+/** Creates a new list item element. */
+export function createListItem(): ListItem {
+  const el = document.createElement('li');
+  return decorate(el as ListItem, ListItem);
+}
+
+export class ListItem extends HTMLLIElement {
+  /** This item's index in the containing list. */
+  private listIndex_: number = -1;
+
+  /** Plain text label. */
+  get label(): string {
+    return this.textContent || '';
+  }
+
+  set label(label) {
+    this.textContent = label;
+  }
+
+  /** This item's index in the containing list. */
+  get listIndex() {
+    return this.listIndex_;
+  }
+
+  set listIndex(value: number) {
+    jsSetter(this, 'listIndex', value);
+  }
+
+  /**
+   * Whether the item is the lead in a selection. Setting this does not update
+   * the underlying selection model. This is only used for display purpose.
+   */
+  get lead(): boolean {
+    return this.hasAttribute('lead');
+  }
+
+  set lead(value: boolean) {
+    boolAttrSetter(this, 'lead', value);
+  }
+
+  /**
+   * Whether the item is selected. Setting this does not update the underlying
+   * selection model. This is only used for display purpose.
+   */
+  get selected(): boolean {
+    return this.hasAttribute('selected');
+  }
+
+  set selected(value: boolean) {
+    boolAttrSetter(this, 'selected', value);
+  }
+
+  /** Called when an element is decorated as a list item. */
+  decorate() {
+    this.listIndex_ = -1;
+
+    this.setAttribute('role', 'listitem');
+    if (!this.id) {
+      this.id = 'listitem-' + nextUniqueIdSuffix++;
+    }
+  }
+}
diff --git a/ui/file_manager/file_manager/foreground/js/ui/table/BUILD.gn b/ui/file_manager/file_manager/foreground/js/ui/table/BUILD.gn
index a163b046..74c8356 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/table/BUILD.gn
+++ b/ui/file_manager/file_manager/foreground/js/ui/table/BUILD.gn
@@ -31,7 +31,6 @@
     "//ash/webui/common/resources:cr_deprecated",
     "//ui/file_manager/file_manager/common/js:array_data_model",
     "//ui/file_manager/file_manager/foreground/js/ui:list",
-    "//ui/file_manager/file_manager/foreground/js/ui:list_item",
     "//ui/file_manager/file_manager/foreground/js/ui:list_single_selection_model",
   ]
 }
@@ -55,7 +54,6 @@
   deps = [
     "//ash/webui/common/resources:cr_deprecated",
     "//ui/file_manager/file_manager/foreground/js/ui:list",
-    "//ui/file_manager/file_manager/foreground/js/ui:list_item",
   ]
 }
 
diff --git a/ui/file_manager/file_names.gni b/ui/file_manager/file_names.gni
index 18b6ded4..692e6aa1 100644
--- a/ui/file_manager/file_names.gni
+++ b/ui/file_manager/file_names.gni
@@ -190,7 +190,6 @@
   "file_manager/foreground/js/ui/install_linux_package_dialog.js",
   "file_manager/foreground/js/ui/list.js",
   "file_manager/foreground/js/ui/list_container.js",
-  "file_manager/foreground/js/ui/list_item.js",
   "file_manager/foreground/js/ui/list_selection_controller.js",
   "file_manager/foreground/js/ui/list_selection_model.js",
   "file_manager/foreground/js/ui/list_single_selection_model.js",
@@ -217,6 +216,7 @@
 ts_files = [
   # Common.
   "file_manager/common/js/api.ts",
+  "file_manager/common/js/cr_ui.ts",
   "file_manager/common/js/dom_utils.ts",
   "file_manager/common/js/entry_utils.ts",
   "file_manager/common/js/file_tasks.ts",
@@ -277,6 +277,7 @@
   "file_manager/widgets/xf_tree.ts",
   "file_manager/widgets/xf_tree_item.ts",
   "file_manager/widgets/xf_password_dialog.ts",
+  "file_manager/foreground/js/ui/list_item.ts",
   "file_manager/foreground/js/ui/banners/drive_bulk_pinning_banner.ts",
   "file_manager/foreground/js/ui/banners/drive_low_individual_space_banner.ts",
   "file_manager/foreground/js/ui/banners/drive_low_shared_drive_space_banner.ts",
@@ -456,7 +457,6 @@
   "file_manager/definitions/navigator_connection.d.ts",
   "file_manager/foreground/js/ui/command.d.ts",
   "file_manager/foreground/js/ui/list.d.ts",
-  "file_manager/foreground/js/ui/list_item.d.ts",
   "file_manager/foreground/js/ui/grid.d.ts",
   "file_manager/foreground/js/ui/menu_item.d.ts",
   "file_manager/foreground/js/ui/multi_menu_button.d.ts",
diff --git a/ui/gfx/geometry/mojom/BUILD.gn b/ui/gfx/geometry/mojom/BUILD.gn
index aa57e72..96377bb 100644
--- a/ui/gfx/geometry/mojom/BUILD.gn
+++ b/ui/gfx/geometry/mojom/BUILD.gn
@@ -78,6 +78,8 @@
   cpp_typemaps = [ shared_cpp_typemap ]
   blink_cpp_typemaps = [ shared_cpp_typemap ]
   webui_module_path = "chrome://resources/mojo/ui/gfx/geometry/mojom"
+  use_typescript_sources = true
+  generate_legacy_js_bindings = true
 }
 
 mojom("test_interfaces") {
diff --git a/ui/gfx/mojom/BUILD.gn b/ui/gfx/mojom/BUILD.gn
index 456a1de..d4d479a 100644
--- a/ui/gfx/mojom/BUILD.gn
+++ b/ui/gfx/mojom/BUILD.gn
@@ -45,7 +45,6 @@
     "//skia/public/mojom",
     "//ui/gfx/geometry/mojom",
   ]
-  webui_module_path = "chrome://resources/mojo/ui/gfx/mojom"
 
   enabled_features = []
   if (ozone_platform_x11) {
diff --git a/ui/ozone/platform/wayland/host/wayland_input_method_context.cc b/ui/ozone/platform/wayland/host/wayland_input_method_context.cc
index 0d9883e2..caa5074 100644
--- a/ui/ozone/platform/wayland/host/wayland_input_method_context.cc
+++ b/ui/ozone/platform/wayland/host/wayland_input_method_context.cc
@@ -414,7 +414,9 @@
     LOG(ERROR) << "selection_range is not bounded by text_range: "
                << selection_range.ToString() << ", " << text_range.ToString();
     // Make a crash report for further investigation in the future.
-    base::debug::DumpWithoutCrashing();
+    // Temporarily disabling crash dump for release.
+    // TODO(crbug.com/1457178): restore this.
+    // base::debug::DumpWithoutCrashing();
     return;
   }
 
diff --git a/ui/views/accessibility/atomic_view_ax_tree_manager.cc b/ui/views/accessibility/atomic_view_ax_tree_manager.cc
index dfe8a99..31b899e 100644
--- a/ui/views/accessibility/atomic_view_ax_tree_manager.cc
+++ b/ui/views/accessibility/atomic_view_ax_tree_manager.cc
@@ -102,4 +102,8 @@
   return nullptr;
 }
 
+void AtomicViewAXTreeManager::ClearComputedRootData() {
+  return ax_tree_->root()->ClearComputedNodeData();
+}
+
 }  // namespace views
diff --git a/ui/views/accessibility/atomic_view_ax_tree_manager.h b/ui/views/accessibility/atomic_view_ax_tree_manager.h
index 59b2bcd42..7da1b8f6 100644
--- a/ui/views/accessibility/atomic_view_ax_tree_manager.h
+++ b/ui/views/accessibility/atomic_view_ax_tree_manager.h
@@ -44,6 +44,8 @@
   ui::AXNode* GetRoot() const override;
   ui::AXNode* GetParentNodeFromParentTree() const override;
 
+  void ClearComputedRootData();
+
  private:
   explicit AtomicViewAXTreeManager(ViewAXPlatformNodeDelegate* delegate,
                                    ui::AXNodeData node_data);
diff --git a/ui/views/accessibility/view_ax_platform_node_delegate.cc b/ui/views/accessibility/view_ax_platform_node_delegate.cc
index 96a8e2d8..be874735 100644
--- a/ui/views/accessibility/view_ax_platform_node_delegate.cc
+++ b/ui/views/accessibility/view_ax_platform_node_delegate.cc
@@ -280,6 +280,17 @@
 #endif  // BUILDFLAG(IS_MAC)
 
 const ui::AXNodeData& ViewAXPlatformNodeDelegate::GetData() const {
+  // Clear computed node so AXNode::GetInnerText() value won't be stale.
+  if (atomic_view_ax_tree_manager_) {
+    // We can't call `GetRoot()->ClearComputedNodeData()` from here since
+    // `AtomicViewAXTreeManager::GetRoot` calls this function
+    // (`ViewAXplatformNodeDelegate::GetData`), which leads to an infinite loop.
+    //
+    // TODO(1468416): This code is temporary until the ViewsAX project is
+    // completed.
+    atomic_view_ax_tree_manager_->ClearComputedRootData();
+  }
+
   // Clear the data, then populate it.
   data_ = ui::AXNodeData();
   GetAccessibleNodeData(&data_);
diff --git a/ui/views/controls/menu/menu_runner_impl_cocoa.h b/ui/views/controls/menu/menu_runner_impl_cocoa.h
index 2056016..2325534 100644
--- a/ui/views/controls/menu/menu_runner_impl_cocoa.h
+++ b/ui/views/controls/menu/menu_runner_impl_cocoa.h
@@ -27,7 +27,7 @@
 // A menu runner implementation that uses NSMenu to show a context menu.
 class VIEWS_EXPORT MenuRunnerImplCocoa : public MenuRunnerImplInterface {
  public:
-  MenuRunnerImplCocoa(ui::MenuModel* menu,
+  MenuRunnerImplCocoa(ui::MenuModel* menu_model,
                       base::RepeatingClosure on_menu_closed_callback);
 
   MenuRunnerImplCocoa(const MenuRunnerImplCocoa&) = delete;
diff --git a/ui/views/controls/menu/menu_runner_impl_cocoa.mm b/ui/views/controls/menu/menu_runner_impl_cocoa.mm
index f137d6f..c3c80f6 100644
--- a/ui/views/controls/menu/menu_runner_impl_cocoa.mm
+++ b/ui/views/controls/menu/menu_runner_impl_cocoa.mm
@@ -152,11 +152,11 @@
 }
 
 MenuRunnerImplCocoa::MenuRunnerImplCocoa(
-    ui::MenuModel* menu,
+    ui::MenuModel* menu_model,
     base::RepeatingClosure on_menu_closed_callback)
     : on_menu_closed_callback_(std::move(on_menu_closed_callback)) {
   menu_delegate_ = [[MenuControllerCocoaDelegateImpl alloc] init];
-  menu_controller_ = [[MenuControllerCocoa alloc] initWithModel:menu
+  menu_controller_ = [[MenuControllerCocoa alloc] initWithModel:menu_model
                                                        delegate:menu_delegate_
                                          useWithPopUpButtonCell:NO];
 }
@@ -193,6 +193,26 @@
     absl::optional<gfx::RoundedCornersF> corners) {
   DCHECK(!IsRunning());
   DCHECK(parent);
+
+  // If you call this method to present a menu and in your model's
+  // MenuWillShow() method you Cancel the menu, the cancel executes
+  // before the menu appears. If you immediately call RunMenuAt() again,
+  // -popUpContextMenu:withEvent:forView: will hang waiting for a menu
+  // tracking event. This occurs as of macOS 14, and this particular sequence
+  // occurs in the Views unittest "MenuRunnerCocoaTest.ComboboxAnchoring".
+  // Creating a fresh menu controller (with its fresh NSMenu) avoids the
+  // problem, and is the most natural fix for the test (vs. creating a
+  // MenuRunnerImplCocoa::CancelWithoutAnimation method that would be called
+  // in the test but never in production). Rebuilding the controller is
+  // harmless, and even though you would never cancel a menu before it appears
+  // in production code, the rebuild ensures there won't be any weirdness if
+  // the MenuRunner is reused.
+  BOOL useWithPopUpButtonCell = menu_controller_.useWithPopUpButtonCell;
+  menu_controller_ =
+      [[MenuControllerCocoa alloc] initWithModel:[menu_controller_ model]
+                                        delegate:menu_delegate_
+                          useWithPopUpButtonCell:useWithPopUpButtonCell];
+
   closing_event_time_ = base::TimeTicks();
   running_ = true;
   [menu_delegate_ setAnchorRect:bounds];
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble.ts b/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
index 241d49c..81aaf04e 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble.ts
@@ -107,7 +107,7 @@
   timeoutMs: number|null = null;
   timeoutTimerId: number|null = null;
   debouncedUpdate: (() => void)|null = null;
-  padding: InsetsF = new InsetsF();
+  padding: InsetsF = {top: 0, bottom: 0, left: 0, right: 0};
   fixed: boolean = false;
 
   /**
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble_controller.ts b/ui/webui/resources/cr_components/help_bubble/help_bubble_controller.ts
index d52b8f0..47e14b1 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble_controller.ts
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble_controller.ts
@@ -73,7 +73,8 @@
   private root_: ShadowRoot;
   private anchor_: HTMLElement|null = null;
   private bubble_: HelpBubbleElement|null = null;
-  private options_: Options = {padding: new InsetsF(), fixed: false};
+  private options_:
+      Options = {padding: {top: 0, bottom: 0, left: 0, right: 0}, fixed: false};
 
   /**
    * Whether a help bubble (webui or external) is being shown for this
@@ -85,7 +86,7 @@
   private isAnchorVisible_: boolean = false;
 
   /** Keep track of last known anchor bounds. */
-  private lastAnchorBounds_: RectF = new RectF();
+  private lastAnchorBounds_: RectF = {x: 0, y: 0, width: 0, height: 0};
 
   /*
    * This flag is used to know whether to send position updates for
diff --git a/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts b/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
index 29fd930..40ec0e0 100644
--- a/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
+++ b/ui/webui/resources/cr_components/help_bubble/help_bubble_mixin.ts
@@ -419,8 +419,8 @@
             this.helpBubbleHandler_.helpBubbleClosed(
                 nativeId, HelpBubbleClosedReason.kPageChanged);
           }
-          const bounds =
-              isVisible ? this.getElementBounds_(target) : new RectF();
+          const bounds: RectF = isVisible ? this.getElementBounds_(target) :
+                                            {x: 0, y: 0, width: 0, height: 0};
           if (!ctrl || ctrl.updateAnchorVisibility(isVisible, bounds)) {
             this.helpBubbleHandler_.helpBubbleAnchorVisibilityChanged(
                 nativeId, isVisible, bounds);
@@ -447,7 +447,7 @@
          * Returns bounds of the anchor element
          */
         private getElementBounds_(element: HTMLElement) {
-          const rect = new RectF();
+          const rect: RectF = {x: 0, y: 0, width: 0, height: 0};
           const bounds = element.getBoundingClientRect();
           rect.x = bounds.x;
           rect.y = bounds.y;
@@ -586,7 +586,7 @@
 }
 
 export function parseOptions(options: Options) {
-  const padding = new InsetsF();
+  const padding: InsetsF = {top: 0, bottom: 0, left: 0, right: 0};
   padding.top = clampPadding(options.anchorPaddingTop);
   padding.left = clampPadding(options.anchorPaddingLeft);
   padding.bottom = clampPadding(options.anchorPaddingBottom);
diff --git a/ui/webui/resources/mojo/BUILD.gn b/ui/webui/resources/mojo/BUILD.gn
index d7bda04c4..0a1728f 100644
--- a/ui/webui/resources/mojo/BUILD.gn
+++ b/ui/webui/resources/mojo/BUILD.gn
@@ -28,7 +28,6 @@
   "skia/public/mojom/skcolor.mojom-webui.js",
   "skia/public/mojom/bitmap.mojom-webui.js",
   "skia/public/mojom/image_info.mojom-webui.js",
-  "ui/gfx/geometry/mojom/geometry.mojom-webui.js",
   "url/mojom/url.mojom-webui.js",
   "url/mojom/origin.mojom-webui.js",
 ]
@@ -38,6 +37,7 @@
   "ui/base/mojom/window_open_disposition.mojom-webui.ts",
   "ui/gfx/image/mojom/image.mojom-webui.ts",
   "ui/gfx/range/mojom/range.mojom-webui.ts",
+  "ui/gfx/geometry/mojom/geometry.mojom-webui.ts",
 ]
 
 if (is_ios) {
@@ -62,14 +62,15 @@
     "chromeos/services/network_health/public/mojom/network_health.mojom-webui.js",
     "chromeos/services/network_health/public/mojom/network_health_types.mojom-webui.js",
     "services/network/public/mojom/ip_address.mojom-webui.js",
-    "ui/base/accelerators/mojom/accelerator.mojom-webui.js",
-    "ui/events/mojom/event.mojom-webui.js",
     "ui/latency/mojom/latency_info.mojom-webui.js",
   ]
 
   mojo_ts_files += [
     "chromeos/ash/services/connectivity/public/mojom/passpoint.mojom-webui.ts",
     "chromeos/ash/services/hotspot_config/public/mojom/cros_hotspot_config.mojom-webui.ts",
+    "ui/base/accelerators/mojom/accelerator.mojom-webui.ts",
+    "ui/events/mojom/event.mojom-webui.ts",
+    "ui/events/mojom/event_constants.mojom-webui.ts",
   ]
 }
 
@@ -81,6 +82,7 @@
   in_files = mojo_ts_files
   deps = [
     "//ui/base/mojom:mojom_ts__generator",
+    "//ui/gfx/geometry/mojom:mojom_ts__generator",
     "//ui/gfx/image/mojom:mojom_ts__generator",
     "//ui/gfx/range/mojom:mojom_ts__generator",
   ]
@@ -89,6 +91,8 @@
     deps += [
       "//chromeos/ash/services/connectivity/public/mojom:mojom_ts__generator",
       "//chromeos/ash/services/hotspot_config/public/mojom:mojom_ts__generator",
+      "//ui/base/accelerators/mojom:mojom_ts__generator",
+      "//ui/events/mojom:mojom_ts__generator",
     ]
   }
 }
@@ -104,7 +108,6 @@
   deps = [
     "//mojo/public/mojom/base:base_js__generator",
     "//skia/public/mojom:mojom_js__generator",
-    "//ui/gfx/geometry/mojom:mojom_js__generator",
     "//url/mojom:url_mojom_gurl_js__generator",
     "//url/mojom:url_mojom_origin_js__generator",
   ]
@@ -123,8 +126,6 @@
       "//chromeos/services/network_health/public/mojom:mojom_js__generator",
       "//chromeos/services/network_health/public/mojom:types_js__generator",
       "//services/network/public/mojom:mojom_ip_address_js__generator",
-      "//ui/base/accelerators/mojom:mojom_js__generator",
-      "//ui/events/mojom:mojom_js__generator",
       "//ui/latency/mojom:mojom_js__generator",
     ]
   }
diff --git a/url/gurl.cc b/url/gurl.cc
index 9f2e5fa..018ae11 100644
--- a/url/gurl.cc
+++ b/url/gurl.cc
@@ -9,12 +9,12 @@
 #include <algorithm>
 #include <memory>
 #include <ostream>
+#include <string_view>
 #include <utility>
 
 #include "base/check_op.h"
 #include "base/no_destructor.h"
 #include "base/notreached.h"
-#include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "base/trace_event/base_tracing.h"
 #include "base/trace_event/memory_usage_estimator.h"
@@ -43,11 +43,11 @@
   other.parsed_ = url::Parsed();
 }
 
-GURL::GURL(base::StringPiece url_string) {
+GURL::GURL(std::string_view url_string) {
   InitCanonical(url_string, true);
 }
 
-GURL::GURL(base::StringPiece16 url_string) {
+GURL::GURL(std::u16string_view url_string) {
   InitCanonical(url_string, true);
 }
 
@@ -174,7 +174,7 @@
 }
 
 // Note: code duplicated below (it's inconvenient to use a template here).
-GURL GURL::Resolve(base::StringPiece relative) const {
+GURL GURL::Resolve(std::string_view relative) const {
   // Not allowed for invalid URLs.
   if (!is_valid_)
     return GURL();
@@ -200,7 +200,7 @@
 }
 
 // Note: code duplicated above (it's inconvenient to use a template here).
-GURL GURL::Resolve(base::StringPiece16 relative) const {
+GURL GURL::Resolve(std::u16string_view relative) const {
   // Not allowed for invalid URLs.
   if (!is_valid_)
     return GURL();
@@ -355,7 +355,7 @@
   return IsAboutUrl(url::kAboutSrcdocPath);
 }
 
-bool GURL::SchemeIs(base::StringPiece lower_ascii_scheme) const {
+bool GURL::SchemeIs(std::string_view lower_ascii_scheme) const {
   DCHECK(base::IsStringASCII(lower_ascii_scheme));
   DCHECK(base::ToLowerASCII(lower_ascii_scheme) == lower_ascii_scheme);
 
@@ -378,7 +378,7 @@
   return SchemeIsCryptographic(scheme_piece());
 }
 
-bool GURL::SchemeIsCryptographic(base::StringPiece lower_ascii_scheme) {
+bool GURL::SchemeIsCryptographic(std::string_view lower_ascii_scheme) {
   DCHECK(base::IsStringASCII(lower_ascii_scheme));
   DCHECK(base::ToLowerASCII(lower_ascii_scheme) == lower_ascii_scheme);
 
@@ -413,13 +413,13 @@
   return ComponentString(file_component);
 }
 
-base::StringPiece GURL::PathForRequestPiece() const {
+std::string_view GURL::PathForRequestPiece() const {
   DCHECK(parsed_.path.is_nonempty())
       << "Canonical path for requests should be non-empty";
   if (parsed_.ref.is_valid()) {
     // Clip off the reference when it exists. The reference starts after the
     // #-sign, so we have to subtract one to also remove it.
-    return base::StringPiece(spec_).substr(
+    return std::string_view(spec_).substr(
         parsed_.path.begin, parsed_.ref.begin - parsed_.path.begin - 1);
   }
   // Compute the actual path length, rather than depending on the spec's
@@ -429,7 +429,7 @@
   if (parsed_.query.is_valid())
     path_len = parsed_.query.end() - parsed_.path.begin;
 
-  return base::StringPiece(spec_).substr(parsed_.path.begin, path_len);
+  return std::string_view(spec_).substr(parsed_.path.begin, path_len);
 }
 
 std::string GURL::PathForRequest() const {
@@ -440,7 +440,7 @@
   return std::string(HostNoBracketsPiece());
 }
 
-base::StringPiece GURL::HostNoBracketsPiece() const {
+std::string_view GURL::HostNoBracketsPiece() const {
   // If host looks like an IPv6 literal, strip the square brackets.
   url::Component h(parsed_.host);
   if (h.len >= 2 && spec_[h.begin] == '[' && spec_[h.end() - 1] == ']') {
@@ -454,9 +454,9 @@
   return std::string(GetContentPiece());
 }
 
-base::StringPiece GURL::GetContentPiece() const {
+std::string_view GURL::GetContentPiece() const {
   if (!is_valid_)
-    return base::StringPiece();
+    return std::string_view();
   url::Component content_component = parsed_.GetContent();
   if (!SchemeIs(url::kJavaScriptScheme) && parsed_.ref.is_valid())
     content_component.len -= parsed_.ref.len + 1;
@@ -472,7 +472,7 @@
   return *empty_gurl;
 }
 
-bool GURL::DomainIs(base::StringPiece canonical_domain) const {
+bool GURL::DomainIs(std::string_view canonical_domain) const {
   if (!is_valid_)
     return false;
 
@@ -486,8 +486,8 @@
   int ref_position = parsed_.CountCharactersBefore(url::Parsed::REF, true);
   int ref_position_other =
       other.parsed_.CountCharactersBefore(url::Parsed::REF, true);
-  return base::StringPiece(spec_).substr(0, ref_position) ==
-         base::StringPiece(other.spec_).substr(0, ref_position_other);
+  return std::string_view(spec_).substr(0, ref_position) ==
+         std::string_view(other.spec_).substr(0, ref_position_other);
 }
 
 void GURL::Swap(GURL* other) {
@@ -503,7 +503,7 @@
          (parsed_.inner_parsed() ? sizeof(url::Parsed) : 0);
 }
 
-bool GURL::IsAboutUrl(base::StringPiece allowed_path) const {
+bool GURL::IsAboutUrl(std::string_view allowed_path) const {
   if (!SchemeIs(url::kAboutScheme))
     return false;
 
@@ -514,8 +514,8 @@
 }
 
 // static
-bool GURL::IsAboutPath(base::StringPiece actual_path,
-                       base::StringPiece allowed_path) {
+bool GURL::IsAboutPath(std::string_view actual_path,
+                       std::string_view allowed_path) {
   if (!base::StartsWith(actual_path, allowed_path))
     return false;
 
@@ -549,22 +549,22 @@
   return !(x == y);
 }
 
-bool operator==(const GURL& x, const base::StringPiece& spec) {
+bool operator==(const GURL& x, std::string_view spec) {
   DCHECK_EQ(GURL(spec).possibly_invalid_spec(), spec)
       << "Comparisons of GURLs and strings must ensure as a precondition that "
          "the string is fully canonicalized.";
   return x.possibly_invalid_spec() == spec;
 }
 
-bool operator==(const base::StringPiece& spec, const GURL& x) {
+bool operator==(std::string_view spec, const GURL& x) {
   return x == spec;
 }
 
-bool operator!=(const GURL& x, const base::StringPiece& spec) {
+bool operator!=(const GURL& x, std::string_view spec) {
   return !(x == spec);
 }
 
-bool operator!=(const base::StringPiece& spec, const GURL& x) {
+bool operator!=(std::string_view spec, const GURL& x) {
   return !(x == spec);
 }
 
diff --git a/url/gurl.h b/url/gurl.h
index 688a1018..aed95bb 100644
--- a/url/gurl.h
+++ b/url/gurl.h
@@ -10,11 +10,11 @@
 #include <iosfwd>
 #include <memory>
 #include <string>
+#include <string_view>
 
 #include "base/component_export.h"
 #include "base/debug/alias.h"
 #include "base/debug/crash_logging.h"
-#include "base/strings/string_piece.h"
 #include "base/trace_event/base_tracing_forward.h"
 #include "url/third_party/mozilla/url_parse.h"
 #include "url/url_canon.h"
@@ -46,8 +46,8 @@
 // will know to escape this and produce the desired result.
 class COMPONENT_EXPORT(URL) GURL {
  public:
-  typedef url::StringPieceReplacements<char> Replacements;
-  typedef url::StringPieceReplacements<char16_t> ReplacementsW;
+  using Replacements = url::StringViewReplacements<char>;
+  using ReplacementsW = url::StringViewReplacements<char16_t>;
 
   // Creates an empty, invalid URL.
   GURL();
@@ -57,9 +57,9 @@
   GURL(const GURL& other);
   GURL(GURL&& other) noexcept;
 
-  // The strings to this contructor should be UTF-8 / UTF-16.
-  explicit GURL(base::StringPiece url_string);
-  explicit GURL(base::StringPiece16 url_string);
+  // The strings to this constructor should be UTF-8 / UTF-16.
+  explicit GURL(std::string_view url_string);
+  explicit GURL(std::u16string_view url_string);
 
   // Constructor for URLs that have already been parsed and canonicalized. This
   // is used for conversions from KURL, for example. The caller must supply all
@@ -152,8 +152,8 @@
   //
   // It is an error to resolve a URL relative to an invalid URL. The result
   // will be the empty URL.
-  GURL Resolve(base::StringPiece relative) const;
-  GURL Resolve(base::StringPiece16 relative) const;
+  GURL Resolve(std::string_view relative) const;
+  GURL Resolve(std::u16string_view relative) const;
 
   // Creates a new GURL by replacing the current URL's components with the
   // supplied versions. See the Replacements class in url_canon.h for more.
@@ -238,7 +238,7 @@
   // Returns true if the given parameter (should be lower-case ASCII to match
   // the canonicalized scheme) is the scheme for this URL. Do not include a
   // colon.
-  bool SchemeIs(base::StringPiece lower_ascii_scheme) const;
+  bool SchemeIs(std::string_view lower_ascii_scheme) const;
 
   // Returns true if the scheme is "http" or "https".
   bool SchemeIsHTTPOrHTTPS() const;
@@ -267,7 +267,7 @@
   bool SchemeIsCryptographic() const;
 
   // As above, but static. Parameter should be lower-case ASCII.
-  static bool SchemeIsCryptographic(base::StringPiece lower_ascii_scheme);
+  static bool SchemeIsCryptographic(std::string_view lower_ascii_scheme);
 
   // Returns true if the scheme is "blob".
   bool SchemeIsBlob() const {
@@ -286,7 +286,7 @@
   // It is an error to get the content of an invalid URL: the result will be an
   // empty string.
   std::string GetContent() const;
-  base::StringPiece GetContentPiece() const;
+  std::string_view GetContentPiece() const;
 
   // Returns true if the hostname is an IP address. Note: this function isn't
   // as cheap as a simple getter because it re-parses the hostname to verify.
@@ -297,7 +297,7 @@
   std::string scheme() const {
     return ComponentString(parsed_.scheme);
   }
-  base::StringPiece scheme_piece() const {
+  std::string_view scheme_piece() const {
     return ComponentStringPiece(parsed_.scheme);
   }
 
@@ -305,7 +305,7 @@
   std::string username() const {
     return ComponentString(parsed_.username);
   }
-  base::StringPiece username_piece() const {
+  std::string_view username_piece() const {
     return ComponentStringPiece(parsed_.username);
   }
 
@@ -313,7 +313,7 @@
   std::string password() const {
     return ComponentString(parsed_.password);
   }
-  base::StringPiece password_piece() const {
+  std::string_view password_piece() const {
     return ComponentStringPiece(parsed_.password);
   }
 
@@ -327,7 +327,7 @@
   std::string host() const {
     return ComponentString(parsed_.host);
   }
-  base::StringPiece host_piece() const {
+  std::string_view host_piece() const {
     return ComponentStringPiece(parsed_.host);
   }
 
@@ -338,7 +338,7 @@
   std::string port() const {
     return ComponentString(parsed_.port);
   }
-  base::StringPiece port_piece() const {
+  std::string_view port_piece() const {
     return ComponentStringPiece(parsed_.port);
   }
 
@@ -348,7 +348,7 @@
   std::string path() const {
     return ComponentString(parsed_.path);
   }
-  base::StringPiece path_piece() const {
+  std::string_view path_piece() const {
     return ComponentStringPiece(parsed_.path);
   }
 
@@ -357,7 +357,7 @@
   std::string query() const {
     return ComponentString(parsed_.query);
   }
-  base::StringPiece query_piece() const {
+  std::string_view query_piece() const {
     return ComponentStringPiece(parsed_.query);
   }
 
@@ -367,7 +367,7 @@
   std::string ref() const {
     return ComponentString(parsed_.ref);
   }
-  base::StringPiece ref_piece() const {
+  std::string_view ref_piece() const {
     return ComponentStringPiece(parsed_.ref);
   }
 
@@ -389,14 +389,14 @@
   std::string PathForRequest() const;
 
   // Returns the same characters as PathForRequest(), avoiding a copy.
-  base::StringPiece PathForRequestPiece() const;
+  std::string_view PathForRequestPiece() const;
 
   // Returns the host, excluding the square brackets surrounding IPv6 address
   // literals. This can be useful for passing to getaddrinfo().
   std::string HostNoBrackets() const;
 
   // Returns the same characters as HostNoBrackets(), avoiding a copy.
-  base::StringPiece HostNoBracketsPiece() const;
+  std::string_view HostNoBracketsPiece() const;
 
   // Returns true if this URL's host matches or is in the same domain as
   // the given input string. For example, if the hostname of the URL is
@@ -409,7 +409,7 @@
   // This call is more efficient than getting the host and checking whether the
   // host has the specific domain or not because no copies or object
   // constructions are done.
-  bool DomainIs(base::StringPiece canonical_domain) const;
+  bool DomainIs(std::string_view canonical_domain) const;
 
   // Checks whether or not two URLs differ only in the ref (the part after
   // the # character).
@@ -440,8 +440,8 @@
   size_t EstimateMemoryUsage() const;
 
   // Helper used by GURL::IsAboutUrl and KURL::IsAboutURL.
-  static bool IsAboutPath(base::StringPiece actual_path,
-                          base::StringPiece allowed_path);
+  static bool IsAboutPath(std::string_view actual_path,
+                          std::string_view allowed_path);
 
   void WriteIntoTrace(perfetto::TracedValue context) const;
 
@@ -460,17 +460,17 @@
   void InitializeFromCanonicalSpec();
 
   // Helper used by IsAboutBlank and IsAboutSrcdoc.
-  bool IsAboutUrl(base::StringPiece allowed_path) const;
+  bool IsAboutUrl(std::string_view allowed_path) const;
 
   // Returns the substring of the input identified by the given component.
   std::string ComponentString(const url::Component& comp) const {
     return std::string(ComponentStringPiece(comp));
   }
-  base::StringPiece ComponentStringPiece(const url::Component& comp) const {
+  std::string_view ComponentStringPiece(const url::Component& comp) const {
     if (comp.is_empty())
-      return base::StringPiece();
-    return base::StringPiece(spec_).substr(static_cast<size_t>(comp.begin),
-                                           static_cast<size_t>(comp.len));
+      return std::string_view();
+    return std::string_view(spec_).substr(static_cast<size_t>(comp.begin),
+                                          static_cast<size_t>(comp.len));
   }
 
   void ProcessFileSystemURLAfterReplaceComponents();
@@ -501,13 +501,13 @@
 // url == GURL(spec) where |spec| is known (i.e. constants). This is to prevent
 // needlessly re-parsing |spec| into a temporary GURL.
 COMPONENT_EXPORT(URL)
-bool operator==(const GURL& x, const base::StringPiece& spec);
+bool operator==(const GURL& x, std::string_view spec);
 COMPONENT_EXPORT(URL)
-bool operator==(const base::StringPiece& spec, const GURL& x);
+bool operator==(std::string_view spec, const GURL& x);
 COMPONENT_EXPORT(URL)
-bool operator!=(const GURL& x, const base::StringPiece& spec);
+bool operator!=(const GURL& x, std::string_view spec);
 COMPONENT_EXPORT(URL)
-bool operator!=(const base::StringPiece& spec, const GURL& x);
+bool operator!=(std::string_view spec, const GURL& x);
 
 // DEBUG_ALIAS_FOR_GURL(var_name, url) copies |url| into a new stack-allocated
 // variable named |<var_name>|.  This helps ensure that the value of |url| gets
diff --git a/url/gurl_abstract_tests.h b/url/gurl_abstract_tests.h
index 3cde8420..6ef976c 100644
--- a/url/gurl_abstract_tests.h
+++ b/url/gurl_abstract_tests.h
@@ -11,7 +11,7 @@
 // by parametrizing the tests with a class that has to expose the following
 // members:
 //   using UrlType = ...;
-//   static UrlType CreateUrlFromString(base::StringPiece s);
+//   static UrlType CreateUrlFromString(std::string_view s);
 //   static bool IsAboutBlank(const UrlType& url);
 //   static bool IsAboutSrcdoc(const UrlType& url);
 template <typename TUrlTraits>
@@ -23,7 +23,7 @@
   // avoid hitting: explicit qualification required to use member 'IsAboutBlank'
   // from dependent base class.
   using UrlType = typename TUrlTraits::UrlType;
-  UrlType CreateUrlFromString(base::StringPiece s) {
+  UrlType CreateUrlFromString(std::string_view s) {
     return TUrlTraits::CreateUrlFromString(s);
   }
   bool IsAboutBlank(const UrlType& url) {
diff --git a/url/gurl_fuzzer.cc b/url/gurl_fuzzer.cc
index 029a387..34c3773 100644
--- a/url/gurl_fuzzer.cc
+++ b/url/gurl_fuzzer.cc
@@ -45,15 +45,15 @@
   if (size < 1)
     return 0;
   {
-    base::StringPiece string_piece_input(reinterpret_cast<const char*>(data),
-                                         size);
+    std::string_view string_piece_input(reinterpret_cast<const char*>(data),
+                                        size);
     const GURL url_from_string_piece(string_piece_input);
     CheckIdempotency(url_from_string_piece);
     CheckReplaceComponentsPreservesSpec(url_from_string_piece);
   }
-  // Test for StringPiece16 if size is even.
+  // Test for std::u16string_view if size is even.
   if (size % sizeof(char16_t) == 0) {
-    base::StringPiece16 string_piece_input16(
+    std::u16string_view string_piece_input16(
         reinterpret_cast<const char16_t*>(data), size / sizeof(char16_t));
     const GURL url_from_string_piece16(string_piece_input16);
     CheckIdempotency(url_from_string_piece16);
@@ -69,7 +69,7 @@
         *reinterpret_cast<const size_t*>(data) % (size - size_t_bytes);
     std::string relative_string(
         reinterpret_cast<const char*>(data + size_t_bytes), relative_size);
-    base::StringPiece string_piece_part_input(
+    std::string_view string_piece_part_input(
         reinterpret_cast<const char*>(data + size_t_bytes + relative_size),
         size - relative_size - size_t_bytes);
     const GURL url_from_string_piece_part(string_piece_part_input);
diff --git a/url/gurl_unittest.cc b/url/gurl_unittest.cc
index 6283cd8..284dfbc6 100644
--- a/url/gurl_unittest.cc
+++ b/url/gurl_unittest.cc
@@ -1167,7 +1167,7 @@
  public:
   using UrlType = GURL;
 
-  static UrlType CreateUrlFromString(base::StringPiece s) { return GURL(s); }
+  static UrlType CreateUrlFromString(std::string_view s) { return GURL(s); }
   static bool IsAboutBlank(const UrlType& url) { return url.IsAboutBlank(); }
   static bool IsAboutSrcdoc(const UrlType& url) { return url.IsAboutSrcdoc(); }
 
diff --git a/url/mojom/origin_mojom_traits.cc b/url/mojom/origin_mojom_traits.cc
index 9e8475a..5b728b46 100644
--- a/url/mojom/origin_mojom_traits.cc
+++ b/url/mojom/origin_mojom_traits.cc
@@ -4,7 +4,7 @@
 
 #include "url/mojom/origin_mojom_traits.h"
 
-#include "base/strings/string_piece.h"
+#include <string_view>
 
 namespace mojo {
 
@@ -12,7 +12,7 @@
 bool StructTraits<url::mojom::OriginDataView, url::Origin>::Read(
     url::mojom::OriginDataView data,
     url::Origin* out) {
-  base::StringPiece scheme, host;
+  std::string_view scheme, host;
   absl::optional<base::UnguessableToken> nonce_if_opaque;
   if (!data.ReadScheme(&scheme) || !data.ReadHost(&host) ||
       !data.ReadNonceIfOpaque(&nonce_if_opaque))
diff --git a/url/mojom/scheme_host_port_mojom_traits.cc b/url/mojom/scheme_host_port_mojom_traits.cc
index 63f6af4..01a50fa 100644
--- a/url/mojom/scheme_host_port_mojom_traits.cc
+++ b/url/mojom/scheme_host_port_mojom_traits.cc
@@ -4,7 +4,8 @@
 
 #include "url/mojom/scheme_host_port_mojom_traits.h"
 
-#include "base/strings/string_piece.h"
+#include <string_view>
+
 #include "url/mojom/scheme_host_port.mojom-shared.h"
 #include "url/scheme_host_port.h"
 
@@ -13,7 +14,7 @@
 // static
 bool StructTraits<url::mojom::SchemeHostPortDataView, url::SchemeHostPort>::
     Read(url::mojom::SchemeHostPortDataView data, url::SchemeHostPort* out) {
-  base::StringPiece scheme, host;
+  std::string_view scheme, host;
   if (!data.ReadScheme(&scheme) || !data.ReadHost(&host))
     return false;
 
diff --git a/url/mojom/url_gurl_mojom_traits.cc b/url/mojom/url_gurl_mojom_traits.cc
index 97b301a..722d6ce 100644
--- a/url/mojom/url_gurl_mojom_traits.cc
+++ b/url/mojom/url_gurl_mojom_traits.cc
@@ -9,23 +9,23 @@
 namespace mojo {
 
 // static
-base::StringPiece StructTraits<url::mojom::UrlDataView, GURL>::url(
+std::string_view StructTraits<url::mojom::UrlDataView, GURL>::url(
     const GURL& r) {
   if (r.possibly_invalid_spec().length() > url::kMaxURLChars || !r.is_valid()) {
-    return base::StringPiece();
+    return std::string_view();
   }
 
-  return base::StringPiece(r.possibly_invalid_spec().c_str(),
-                           r.possibly_invalid_spec().length());
+  return r.possibly_invalid_spec();
 }
 
 // static
 bool StructTraits<url::mojom::UrlDataView, GURL>::Read(
     url::mojom::UrlDataView data,
     GURL* out) {
-  base::StringPiece url_string;
-  if (!data.ReadUrl(&url_string))
+  std::string_view url_string;
+  if (!data.ReadUrl(&url_string)) {
     return false;
+  }
 
   if (url_string.length() > url::kMaxURLChars)
     return false;
diff --git a/url/mojom/url_gurl_mojom_traits.h b/url/mojom/url_gurl_mojom_traits.h
index 19ac049..ae29cc3 100644
--- a/url/mojom/url_gurl_mojom_traits.h
+++ b/url/mojom/url_gurl_mojom_traits.h
@@ -5,8 +5,9 @@
 #ifndef URL_MOJOM_URL_GURL_MOJOM_TRAITS_H_
 #define URL_MOJOM_URL_GURL_MOJOM_TRAITS_H_
 
+#include <string_view>
+
 #include "base/component_export.h"
-#include "base/strings/string_piece.h"
 #include "mojo/public/cpp/bindings/struct_traits.h"
 #include "url/gurl.h"
 #include "url/mojom/url.mojom-shared.h"
@@ -16,7 +17,7 @@
 template <>
 struct COMPONENT_EXPORT(URL_MOJOM_TRAITS)
     StructTraits<url::mojom::UrlDataView, GURL> {
-  static base::StringPiece url(const GURL& r);
+  static std::string_view url(const GURL& r);
   static bool Read(url::mojom::UrlDataView data, GURL* out);
 };
 
diff --git a/url/origin.cc b/url/origin.cc
index 56927b8d..ce3e080 100644
--- a/url/origin.cc
+++ b/url/origin.cc
@@ -9,6 +9,7 @@
 #include <algorithm>
 #include <ostream>
 #include <string>
+#include <string_view>
 #include <tuple>
 #include <utility>
 
@@ -20,7 +21,6 @@
 #include "base/debug/crash_logging.h"
 #include "base/pickle.h"
 #include "base/strings/strcat.h"
-#include "base/strings/string_piece.h"
 #include "base/trace_event/base_tracing.h"
 #include "base/trace_event/memory_usage_estimator.h"
 #include "base/unguessable_token.h"
@@ -79,8 +79,8 @@
 
 // static
 absl::optional<Origin> Origin::UnsafelyCreateTupleOriginWithoutNormalization(
-    base::StringPiece scheme,
-    base::StringPiece host,
+    std::string_view scheme,
+    std::string_view host,
     uint16_t port) {
   SchemeHostPort tuple(std::string(scheme), std::string(host), port,
                        SchemeHostPort::CHECK_CANONICALIZATION);
@@ -91,8 +91,8 @@
 
 // static
 absl::optional<Origin> Origin::UnsafelyCreateOpaqueOriginWithoutNormalization(
-    base::StringPiece precursor_scheme,
-    base::StringPiece precursor_host,
+    std::string_view precursor_scheme,
+    std::string_view precursor_host,
     uint16_t precursor_port,
     const Origin::Nonce& nonce) {
   SchemeHostPort precursor(std::string(precursor_scheme),
@@ -249,7 +249,7 @@
   return url.scheme() == tuple_.scheme();
 }
 
-bool Origin::DomainIs(base::StringPiece canonical_domain) const {
+bool Origin::DomainIs(std::string_view canonical_domain) const {
   return !opaque() && url::DomainIs(tuple_.host(), canonical_domain);
 }
 
diff --git a/url/origin.h b/url/origin.h
index 74765ef..eaf109c 100644
--- a/url/origin.h
+++ b/url/origin.h
@@ -9,12 +9,12 @@
 
 #include <memory>
 #include <string>
+#include <string_view>
 
 #include "base/component_export.h"
 #include "base/debug/alias.h"
 #include "base/debug/crash_logging.h"
 #include "base/gtest_prod_util.h"
-#include "base/strings/string_piece_forward.h"
 #include "base/strings/string_util.h"
 #include "base/trace_event/base_tracing_forward.h"
 #include "base/unguessable_token.h"
@@ -191,8 +191,8 @@
   // dangerous recanonicalization); other potential callers should prefer the
   // 'GURL'-based constructor.
   static absl::optional<Origin> UnsafelyCreateTupleOriginWithoutNormalization(
-      base::StringPiece scheme,
-      base::StringPiece host,
+      std::string_view scheme,
+      std::string_view host,
       uint16_t port);
 
   // Creates an origin without sanity checking that the host is canonicalized.
@@ -277,7 +277,7 @@
   GURL GetURL() const;
 
   // Same as GURL::DomainIs. If |this| origin is opaque, then returns false.
-  bool DomainIs(base::StringPiece canonical_domain) const;
+  bool DomainIs(std::string_view canonical_domain) const;
 
   // Allows Origin to be used as a key in STL (for example, a std::set or
   // std::map).
@@ -418,8 +418,8 @@
   // back and forth over IPC (as transitioning through GURL would risk
   // potentially dangerous recanonicalization).
   static absl::optional<Origin> UnsafelyCreateOpaqueOriginWithoutNormalization(
-      base::StringPiece precursor_scheme,
-      base::StringPiece precursor_host,
+      std::string_view precursor_scheme,
+      std::string_view precursor_host,
       uint16_t precursor_port,
       const Nonce& nonce);
 
diff --git a/url/origin_abstract_tests.cc b/url/origin_abstract_tests.cc
index 1bc032e..175abff 100644
--- a/url/origin_abstract_tests.cc
+++ b/url/origin_abstract_tests.cc
@@ -29,7 +29,7 @@
 }
 
 // static
-Origin UrlOriginTestTraits::CreateOriginFromString(base::StringPiece s) {
+Origin UrlOriginTestTraits::CreateOriginFromString(std::string_view s) {
   return Origin::Create(GURL(s));
 }
 
@@ -40,7 +40,7 @@
 
 // static
 Origin UrlOriginTestTraits::CreateWithReferenceOrigin(
-    base::StringPiece url,
+    std::string_view url,
     const Origin& reference_origin) {
   return Origin::Resolve(GURL(url), reference_origin);
 }
@@ -94,7 +94,7 @@
 }
 
 // static
-bool UrlOriginTestTraits::IsValidUrl(base::StringPiece str) {
+bool UrlOriginTestTraits::IsValidUrl(std::string_view str) {
   return GURL(str).is_valid();
 }
 
diff --git a/url/origin_abstract_tests.h b/url/origin_abstract_tests.h
index 63dded6..cb6cf9b4 100644
--- a/url/origin_abstract_tests.h
+++ b/url/origin_abstract_tests.h
@@ -6,10 +6,10 @@
 #define URL_ORIGIN_ABSTRACT_TESTS_H_
 
 #include <string>
+#include <string_view>
 #include <type_traits>
 
 #include "base/containers/contains.h"
-#include "base/strings/string_piece.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
 #include "url/origin.h"
@@ -28,10 +28,10 @@
   using OriginType = Origin;
 
   // Constructing an origin.
-  static OriginType CreateOriginFromString(base::StringPiece s);
+  static OriginType CreateOriginFromString(std::string_view s);
   static OriginType CreateUniqueOpaqueOrigin();
   static OriginType CreateWithReferenceOrigin(
-      base::StringPiece url,
+      std::string_view url,
       const OriginType& reference_origin);
   static OriginType DeriveNewOpaqueOrigin(const OriginType& reference_origin);
 
@@ -51,7 +51,7 @@
   //
   // TODO(lukasza): Consider merging together OriginTraitsBase here and
   // UrlTraitsBase in //url/gurl_abstract_tests.h.
-  static bool IsValidUrl(base::StringPiece str);
+  static bool IsValidUrl(std::string_view str);
 
   // Only static members = no constructors are needed.
   UrlOriginTestTraits() = delete;
@@ -95,13 +95,13 @@
   // avoid hitting: explicit qualification required to use member 'IsOpaque'
   // from dependent base class.
   using OriginType = typename TOriginTraits::OriginType;
-  OriginType CreateOriginFromString(base::StringPiece s) {
+  OriginType CreateOriginFromString(std::string_view s) {
     return TOriginTraits::CreateOriginFromString(s);
   }
   OriginType CreateUniqueOpaqueOrigin() {
     return TOriginTraits::CreateUniqueOpaqueOrigin();
   }
-  OriginType CreateWithReferenceOrigin(base::StringPiece url,
+  OriginType CreateWithReferenceOrigin(std::string_view url,
                                        const OriginType& reference_origin) {
     return TOriginTraits::CreateWithReferenceOrigin(url, reference_origin);
   }
@@ -132,7 +132,7 @@
   std::string Serialize(const OriginType& origin) {
     return TOriginTraits::Serialize(origin);
   }
-  bool IsValidUrl(base::StringPiece str) {
+  bool IsValidUrl(std::string_view str) {
     return TOriginTraits::IsValidUrl(str);
   }
 
@@ -213,7 +213,7 @@
     VerifyOriginInvariants(origin);
   }
 
-  void TestUniqueOpaqueOrigin(base::StringPiece test_input) {
+  void TestUniqueOpaqueOrigin(std::string_view test_input) {
     auto origin = this->CreateOriginFromString(test_input);
     this->VerifyUniqueOpaqueOriginInvariants(origin);
 
diff --git a/url/origin_unittest.cc b/url/origin_unittest.cc
index 47cca81..5bca576 100644
--- a/url/origin_unittest.cc
+++ b/url/origin_unittest.cc
@@ -76,8 +76,8 @@
   // Wrappers around url::Origin methods to expose it to tests.
 
   absl::optional<Origin> UnsafelyCreateOpaqueOriginWithoutNormalization(
-      base::StringPiece precursor_scheme,
-      base::StringPiece precursor_host,
+      std::string_view precursor_scheme,
+      std::string_view precursor_host,
       uint16_t precursor_port,
       const Origin::Nonce& nonce) {
     return Origin::UnsafelyCreateOpaqueOriginWithoutNormalization(
@@ -371,8 +371,8 @@
 
 TEST_F(OriginTest, UnsafelyCreateUniqueViaEmbeddedNulls) {
   struct TestCases {
-    base::StringPiece scheme;
-    base::StringPiece host;
+    std::string_view scheme;
+    std::string_view host;
     uint16_t port = 80;
   } cases[] = {{{"http\0more", 9}, {"example.com", 11}},
                {{"http\0", 5}, {"example.com", 11}},
diff --git a/url/scheme_host_port.cc b/url/scheme_host_port.cc
index 2d123dc..6b617dcd 100644
--- a/url/scheme_host_port.cc
+++ b/url/scheme_host_port.cc
@@ -8,6 +8,7 @@
 #include <string.h>
 
 #include <ostream>
+#include <string_view>
 #include <tuple>
 
 #include "base/check_op.h"
@@ -15,7 +16,6 @@
 #include "base/notreached.h"
 #include "base/numerics/safe_conversions.h"
 #include "base/strings/string_number_conversions.h"
-#include "base/strings/string_piece.h"
 #include "base/trace_event/memory_usage_estimator.h"
 #include "url/gurl.h"
 #include "url/third_party/mozilla/url_parse.h"
@@ -28,7 +28,7 @@
 
 namespace {
 
-bool IsCanonicalHost(const base::StringPiece& host) {
+bool IsCanonicalHost(const std::string_view& host) {
   std::string canon_host;
 
   // Try to canonicalize the host (copy/pasted from net/base. :( ).
@@ -56,8 +56,8 @@
 // ShouldTreatAsOpaqueOrigin in Blink (there might be existing differences in
 // behavior between these 2 layers, but we should avoid introducing new
 // differences).
-bool IsValidInput(const base::StringPiece& scheme,
-                  const base::StringPiece& host,
+bool IsValidInput(const std::string_view& scheme,
+                  const std::string_view& host,
                   uint16_t port,
                   SchemeHostPort::ConstructPolicy policy) {
   // Empty schemes are never valid.
@@ -159,8 +159,8 @@
                     << " Port: " << port;
 }
 
-SchemeHostPort::SchemeHostPort(base::StringPiece scheme,
-                               base::StringPiece host,
+SchemeHostPort::SchemeHostPort(std::string_view scheme,
+                               std::string_view host,
                                uint16_t port)
     : SchemeHostPort(std::string(scheme),
                      std::string(host),
@@ -171,8 +171,8 @@
   if (!url.is_valid())
     return;
 
-  base::StringPiece scheme = url.scheme_piece();
-  base::StringPiece host = url.host_piece();
+  std::string_view scheme = url.scheme_piece();
+  std::string_view host = url.host_piece();
 
   // A valid GURL never returns PORT_INVALID.
   int port = url.EffectiveIntPort();
diff --git a/url/scheme_host_port.h b/url/scheme_host_port.h
index d2f4acc4..b649c92 100644
--- a/url/scheme_host_port.h
+++ b/url/scheme_host_port.h
@@ -8,9 +8,9 @@
 #include <stdint.h>
 
 #include <string>
+#include <string_view>
 
 #include "base/component_export.h"
-#include "base/strings/string_piece.h"
 
 class GURL;
 
@@ -84,9 +84,7 @@
   // ports (e.g. 'file').
   //
   // Copies the data in |scheme| and |host|.
-  SchemeHostPort(base::StringPiece scheme,
-                 base::StringPiece host,
-                 uint16_t port);
+  SchemeHostPort(std::string_view scheme, std::string_view host, uint16_t port);
 
   // Metadata influencing whether or not the constructor should sanity check
   // host canonicalization.
diff --git a/url/url_canon_fileurl.cc b/url/url_canon_fileurl.cc
index b45114d7..712fbce 100644
--- a/url/url_canon_fileurl.cc
+++ b/url/url_canon_fileurl.cc
@@ -4,7 +4,8 @@
 
 // Functions for canonicalizing "file:" URLs.
 
-#include "base/strings/string_piece.h"
+#include <string_view>
+
 #include "base/strings/string_util.h"
 #include "url/url_canon.h"
 #include "url/url_canon_internal.h"
@@ -18,13 +19,13 @@
 bool IsLocalhost(const char* spec, int begin, int end) {
   if (begin > end)
     return false;
-  return base::StringPiece(&spec[begin], end - begin) == "localhost";
+  return std::string_view(&spec[begin], end - begin) == "localhost";
 }
 
 bool IsLocalhost(const char16_t* spec, int begin, int end) {
   if (begin > end)
     return false;
-  return base::StringPiece16(&spec[begin], end - begin) == u"localhost";
+  return std::u16string_view(&spec[begin], end - begin) == u"localhost";
 }
 
 template <typename CHAR>
diff --git a/url/url_canon_stdstring.h b/url/url_canon_stdstring.h
index 528f91f..542860a0 100644
--- a/url/url_canon_stdstring.h
+++ b/url/url_canon_stdstring.h
@@ -10,11 +10,11 @@
 // we have segregated it here.
 
 #include <string>
+#include <string_view>
 
 #include "base/compiler_specific.h"
 #include "base/component_export.h"
 #include "base/memory/raw_ptr_exclusion.h"
-#include "base/strings/string_piece.h"
 #include "url/url_canon.h"
 
 namespace url {
@@ -55,64 +55,64 @@
 };
 
 // An extension of the Replacements class that allows the setters to use
-// StringPieces (implicitly allowing strings or char*s).
+// string_views (implicitly allowing strings or char*s).
 //
-// The contents of the StringPieces are not copied and must remain valid until
-// the StringPieceReplacements object goes out of scope.
+// The contents of the string_views are not copied and must remain valid until
+// the StringViewReplacements object goes out of scope.
 //
 // In order to make it harder to misuse the API the setters do not accept rvalue
 // references to std::strings.
 // Note: Extra const char* overloads are necessary to break ambiguities that
 // would otherwise exist for char literals.
 template <typename CharT>
-class StringPieceReplacements : public Replacements<CharT> {
+class StringViewReplacements : public Replacements<CharT> {
  private:
   using StringT = std::basic_string<CharT>;
-  using StringPieceT = base::BasicStringPiece<CharT>;
+  using StringViewT = std::basic_string_view<CharT>;
   using ParentT = Replacements<CharT>;
   using SetterFun = void (ParentT::*)(const CharT*, const Component&);
 
-  void SetImpl(SetterFun fun, StringPieceT str) {
+  void SetImpl(SetterFun fun, StringViewT str) {
     (this->*fun)(str.data(), Component(0, static_cast<int>(str.size())));
   }
 
  public:
   void SetSchemeStr(const CharT* str) { SetImpl(&ParentT::SetScheme, str); }
-  void SetSchemeStr(StringPieceT str) { SetImpl(&ParentT::SetScheme, str); }
+  void SetSchemeStr(StringViewT str) { SetImpl(&ParentT::SetScheme, str); }
   void SetSchemeStr(const StringT&&) = delete;
 
   void SetUsernameStr(const CharT* str) { SetImpl(&ParentT::SetUsername, str); }
-  void SetUsernameStr(StringPieceT str) { SetImpl(&ParentT::SetUsername, str); }
+  void SetUsernameStr(StringViewT str) { SetImpl(&ParentT::SetUsername, str); }
   void SetUsernameStr(const StringT&&) = delete;
   using ParentT::ClearUsername;
 
   void SetPasswordStr(const CharT* str) { SetImpl(&ParentT::SetPassword, str); }
-  void SetPasswordStr(StringPieceT str) { SetImpl(&ParentT::SetPassword, str); }
+  void SetPasswordStr(StringViewT str) { SetImpl(&ParentT::SetPassword, str); }
   void SetPasswordStr(const StringT&&) = delete;
   using ParentT::ClearPassword;
 
   void SetHostStr(const CharT* str) { SetImpl(&ParentT::SetHost, str); }
-  void SetHostStr(StringPieceT str) { SetImpl(&ParentT::SetHost, str); }
+  void SetHostStr(StringViewT str) { SetImpl(&ParentT::SetHost, str); }
   void SetHostStr(const StringT&&) = delete;
   using ParentT::ClearHost;
 
   void SetPortStr(const CharT* str) { SetImpl(&ParentT::SetPort, str); }
-  void SetPortStr(StringPieceT str) { SetImpl(&ParentT::SetPort, str); }
+  void SetPortStr(StringViewT str) { SetImpl(&ParentT::SetPort, str); }
   void SetPortStr(const StringT&&) = delete;
   using ParentT::ClearPort;
 
   void SetPathStr(const CharT* str) { SetImpl(&ParentT::SetPath, str); }
-  void SetPathStr(StringPieceT str) { SetImpl(&ParentT::SetPath, str); }
+  void SetPathStr(StringViewT str) { SetImpl(&ParentT::SetPath, str); }
   void SetPathStr(const StringT&&) = delete;
   using ParentT::ClearPath;
 
   void SetQueryStr(const CharT* str) { SetImpl(&ParentT::SetQuery, str); }
-  void SetQueryStr(StringPieceT str) { SetImpl(&ParentT::SetQuery, str); }
+  void SetQueryStr(StringViewT str) { SetImpl(&ParentT::SetQuery, str); }
   void SetQueryStr(const StringT&&) = delete;
   using ParentT::ClearQuery;
 
   void SetRefStr(const CharT* str) { SetImpl(&ParentT::SetRef, str); }
-  void SetRefStr(StringPieceT str) { SetImpl(&ParentT::SetRef, str); }
+  void SetRefStr(StringViewT str) { SetImpl(&ParentT::SetRef, str); }
   void SetRefStr(const StringT&&) = delete;
   using ParentT::ClearRef;
 
diff --git a/url/url_canon_unittest.cc b/url/url_canon_unittest.cc
index 4a4f8bb3..4da96e4 100644
--- a/url/url_canon_unittest.cc
+++ b/url/url_canon_unittest.cc
@@ -2757,7 +2757,7 @@
 
 TEST(URLCanonTest, FindWindowsDriveLetter) {
   struct TestCase {
-    base::StringPiece spec;
+    std::string_view spec;
     int begin;
     int end;  // -1 for end of spec
     int expected_drive_letter_pos;
@@ -2871,7 +2871,7 @@
 
 TEST_P(URLCanonAsciiPercentEncodePathTest, UnescapePathCharHistogram) {
   struct TestCase {
-    base::StringPiece path;
+    std::string_view path;
     base::HistogramBase::Count cnt;
   } cases[] = {
       {"/a", 0},
diff --git a/url/url_idna_icu_alternatives_android.cc b/url/url_idna_icu_alternatives_android.cc
index 9faf5710..b94f100 100644
--- a/url/url_idna_icu_alternatives_android.cc
+++ b/url/url_idna_icu_alternatives_android.cc
@@ -5,10 +5,10 @@
 #include <string.h>
 
 #include <string>
+#include <string_view>
 
 #include "base/android/jni_android.h"
 #include "base/android/jni_string.h"
-#include "base/strings/string_piece.h"
 #include "url/url_canon_internal.h"
 #include "url/url_jni_headers/IDNStringUtil_jni.h"
 
@@ -24,7 +24,7 @@
   JNIEnv* env = base::android::AttachCurrentThread();
   base::android::ScopedJavaLocalRef<jstring> java_src =
       base::android::ConvertUTF16ToJavaString(
-          env, base::StringPiece16(src, src_len));
+          env, std::u16string_view(src, src_len));
   ScopedJavaLocalRef<jstring> java_result =
       android::Java_IDNStringUtil_idnToASCII(env, java_src);
   // NULL indicates failure.
diff --git a/url/url_idna_icu_alternatives_ios.mm b/url/url_idna_icu_alternatives_ios.mm
index d604b35..2741859 100644
--- a/url/url_idna_icu_alternatives_ios.mm
+++ b/url/url_idna_icu_alternatives_ios.mm
@@ -6,8 +6,8 @@
 
 #include <ostream>
 #include <string>
+#include <string_view>
 
-#include "base/strings/string_piece.h"
 #include "base/strings/string_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "url/url_canon_internal.h"
@@ -17,7 +17,7 @@
 // Only allow ASCII to avoid ICU dependency. Use NSString+IDN
 // to convert non-ASCII URL prior to passing to API.
 bool IDNToASCII(const char16_t* src, int src_len, CanonOutputW* output) {
-  if (base::IsStringASCII(base::StringPiece16(src, src_len))) {
+  if (base::IsStringASCII(std::u16string_view(src, src_len))) {
     output->Append(src, src_len);
     return true;
   }
diff --git a/url/url_parse_perftest.cc b/url/url_parse_perftest.cc
index 7fe1d39..f06e019 100644
--- a/url/url_parse_perftest.cc
+++ b/url/url_parse_perftest.cc
@@ -2,7 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "base/strings/string_piece.h"
+#include <string_view>
+
 #include "base/test/perf_time_logger.h"
 #include "testing/gtest/include/gtest/gtest.h"
 #include "url/gurl.h"
@@ -13,7 +14,7 @@
 namespace {
 
 TEST(URLParse, FullURL) {
-  constexpr base::StringPiece kUrl =
+  constexpr std::string_view kUrl =
       "http://me:pass@host/foo/bar.html;param?query=yes#ref";
 
   url::Parsed parsed;
@@ -24,16 +25,16 @@
   timer.Done();
 }
 
-constexpr base::StringPiece kTypicalUrl1 =
+constexpr std::string_view kTypicalUrl1 =
     "http://www.google.com/"
     "search?q=url+parsing&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:"
     "official&client=firefox-a";
 
-constexpr base::StringPiece kTypicalUrl2 =
+constexpr std::string_view kTypicalUrl2 =
     "http://www.amazon.com/Stephen-King-Thrillers-Horror-People/dp/0766012336/"
     "ref=sr_1_2/133-4144931-4505264?ie=UTF8&s=books&qid=2144880915&sr=8-2";
 
-constexpr base::StringPiece kTypicalUrl3 =
+constexpr std::string_view kTypicalUrl3 =
     "http://store.apple.com/1-800-MY-APPLE/WebObjects/AppleStore.woa/wa/"
     "RSLID?nnmm=browse&mco=578E9744&node=home/desktop/mac_pro";
 
diff --git a/url/url_util.cc b/url/url_util.cc
index 3cadd6e..81b54621 100644
--- a/url/url_util.cc
+++ b/url/url_util.cc
@@ -147,18 +147,6 @@
   DO_NOT_REMOVE_WHITESPACE,
 };
 
-// This template converts a given character type to the corresponding
-// StringPiece type.
-template<typename CHAR> struct CharToStringPiece {
-};
-template<> struct CharToStringPiece<char> {
-  typedef base::StringPiece Piece;
-};
-template <>
-struct CharToStringPiece<char16_t> {
-  typedef base::StringPiece16 Piece;
-};
-
 // Given a string and a range inside the string, compares it to the given
 // lower-case |compare_to| buffer.
 template<typename CHAR>
@@ -168,8 +156,7 @@
   if (component.is_empty())
     return compare_to[0] == 0;  // When component is empty, match empty scheme.
   return base::EqualsCaseInsensitiveASCII(
-      typename CharToStringPiece<CHAR>::Piece(&spec[component.begin],
-                                              component.len),
+      std::basic_string_view(&spec[component.begin], component.len),
       compare_to);
 }
 
@@ -185,8 +172,7 @@
 
   for (const SchemeWithType& scheme_with_type : schemes) {
     if (base::EqualsCaseInsensitiveASCII(
-            typename CharToStringPiece<CHAR>::Piece(&spec[scheme.begin],
-                                                    scheme.len),
+            std::basic_string_view(&spec[scheme.begin], scheme.len),
             scheme_with_type.scheme)) {
       *type = scheme_with_type.type;
       return true;
@@ -735,8 +721,8 @@
   return DoFindAndCompareScheme(str, str_len, compare, found_scheme);
 }
 
-bool DomainIs(base::StringPiece canonical_host,
-              base::StringPiece canonical_domain) {
+bool DomainIs(std::string_view canonical_host,
+              std::string_view canonical_domain) {
   if (canonical_host.empty() || canonical_domain.empty())
     return false;
 
@@ -754,7 +740,7 @@
   const char* host_first_pos =
       canonical_host.data() + host_len - canonical_domain.length();
 
-  if (base::StringPiece(host_first_pos, canonical_domain.length()) !=
+  if (std::string_view(host_first_pos, canonical_domain.length()) !=
       canonical_domain) {
     return false;
   }
@@ -771,7 +757,7 @@
   return true;
 }
 
-bool HostIsIPAddress(base::StringPiece host) {
+bool HostIsIPAddress(std::string_view host) {
   STACK_UNINITIALIZED url::RawCanonOutputT<char, 128> ignored_output;
   url::CanonHostInfo host_info;
   url::CanonicalizeIPAddress(host.data(), Component(0, host.length()),
diff --git a/url/url_util.h b/url/url_util.h
index fe816ca..4084bd6a 100644
--- a/url/url_util.h
+++ b/url/url_util.h
@@ -11,7 +11,6 @@
 #include <vector>
 
 #include "base/component_export.h"
-#include "base/strings/string_piece.h"
 #include "url/third_party/mozilla/url_parse.h"
 #include "url/url_canon.h"
 #include "url/url_constants.h"
@@ -204,12 +203,12 @@
 // input domain should match host canonicalization rules. i.e. it should be
 // lowercase except for escape chars.
 COMPONENT_EXPORT(URL)
-bool DomainIs(base::StringPiece canonical_host,
-              base::StringPiece canonical_domain);
+bool DomainIs(std::string_view canonical_host,
+              std::string_view canonical_domain);
 
 // Returns true if the hostname is an IP address. Note: this function isn't very
 // cheap, as it must re-parse the host to verify.
-COMPONENT_EXPORT(URL) bool HostIsIPAddress(base::StringPiece host);
+COMPONENT_EXPORT(URL) bool HostIsIPAddress(std::string_view host);
 
 // URL library wrappers --------------------------------------------------------
 
diff --git a/url/url_util_unittest.cc b/url/url_util_unittest.cc
index fc6ab4e3..47ac7f08 100644
--- a/url/url_util_unittest.cc
+++ b/url/url_util_unittest.cc
@@ -6,7 +6,8 @@
 
 #include <stddef.h>
 
-#include "base/strings/string_piece.h"
+#include <string_view>
+
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest-message.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -588,7 +589,7 @@
 }
 
 namespace {
-absl::optional<std::string> CanonicalizeSpec(base::StringPiece spec,
+absl::optional<std::string> CanonicalizeSpec(std::string_view spec,
                                              bool trim_path_end) {
   std::string canonicalized;
   StdStringCanonOutput output(&canonicalized);
diff --git a/v8 b/v8
index aa83979..ec0c34c 160000
--- a/v8
+++ b/v8
@@ -1 +1 @@
-Subproject commit aa8397922f9d7b4dadfab77f91d618cb0b7218d7
+Subproject commit ec0c34c6196047f1e00f8a532222c29f1dd165e4