diff --git a/DEPS b/DEPS
index 556c173..8ace85df8e 100644
--- a/DEPS
+++ b/DEPS
@@ -45,7 +45,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '90f28ec3da530d1720a0a74283a44cfd9c207126',
+  'skia_revision': '6e0a6b3c40de254268b48deea6184cf10e945bd4',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
@@ -69,7 +69,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling PDFium
   # and whatever else without interference from each other.
-  'pdfium_revision': '7d04f1b0ab4848f1d10983b7a7b1444ac93dec70',
+  'pdfium_revision': '3070e94f608f36e7312fad2d471e3d7546f82ca2',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling openmax_dl
   # and whatever else without interference from each other.
@@ -101,7 +101,7 @@
   # 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': 'cf05c91b67574954c0f62d369077943f04f5de07',
+  'catapult_revision': 'a17499a8648f2707aa6831683ab5db2319ac2534',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling libFuzzer
   # and whatever else without interference from each other.
diff --git a/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java b/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
index 0a4274fc..9e6ba24 100644
--- a/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
+++ b/android_webview/javatests/src/org/chromium/android_webview/test/PostMessageTest.java
@@ -639,11 +639,10 @@
     // transferred to JS and full communication can happen on it.
     // Do this by sending a message to JS and let it echo'ing the message with
     // some text prepended to it.
-    // Disabled because flaky, see crbug.com/715960.
-    // @SmallTest
-    // @Feature({"AndroidWebView", "Android-PostMessage"})
+    @RetryOnFailure // crbug.com/715960
+    @SmallTest
+    @Feature({"AndroidWebView", "Android-PostMessage"})
     @Test
-    @DisabledTest
     public void testMessageChannelUsingPendingPort() throws Throwable {
         final ChannelContainer channelContainer = new ChannelContainer();
         loadPage(ECHO_PAGE);
@@ -720,10 +719,10 @@
     // 3. Java sends a message using the new channel in 2.
     // 4. Js responds to this message using the channel in 2.
     // 5. Java responds to message in 4 using the channel in 2.
-    // @SmallTest
-    // @Feature({"AndroidWebView", "Android-PostMessage"})
+    @RetryOnFailure // crbug.com/708821
+    @SmallTest
+    @Feature({"AndroidWebView", "Android-PostMessage"})
     @Test
-    @DisabledTest
     public void testCanUseReceivedAwMessagePortFromJS() throws Throwable {
         loadPage(RECEIVE_JS_MESSAGE_CHANNEL_PAGE);
         InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
diff --git a/ash/mus/main.cc b/ash/mus/main.cc
index d152d51..edc6eb6 100644
--- a/ash/mus/main.cc
+++ b/ash/mus/main.cc
@@ -10,6 +10,7 @@
   const bool show_primary_host_on_connect = true;
   ash::mus::WindowManagerApplication* app =
       new ash::mus::WindowManagerApplication(show_primary_host_on_connect);
+  app->set_running_standalone(true);
   service_manager::ServiceRunner runner(app);
   return runner.Run(service_request_handle);
 }
diff --git a/ash/mus/window_manager_application.cc b/ash/mus/window_manager_application.cc
index 18971f6..1ac4f34 100644
--- a/ash/mus/window_manager_application.cc
+++ b/ash/mus/window_manager_application.cc
@@ -129,10 +129,11 @@
   mojo_interface_factory::RegisterInterfaces(
       &registry_, base::ThreadTaskRunnerHandle::Get());
 
+  const bool register_path_provider = running_standalone_;
   aura_init_ = views::AuraInit::Create(
       context()->connector(), context()->identity(), "ash_mus_resources.pak",
       "ash_mus_resources_200.pak", nullptr,
-      views::AuraInit::Mode::AURA_MUS_WINDOW_MANAGER);
+      views::AuraInit::Mode::AURA_MUS_WINDOW_MANAGER, register_path_provider);
   if (!aura_init_) {
     context()->QuitNow();
     return;
diff --git a/ash/mus/window_manager_application.h b/ash/mus/window_manager_application.h
index 647af79f..2a3a579 100644
--- a/ash/mus/window_manager_application.h
+++ b/ash/mus/window_manager_application.h
@@ -1,3 +1,4 @@
+
 // Copyright 2015 The Chromium Authors. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
@@ -61,6 +62,8 @@
 
   service_manager::Connector* GetConnector();
 
+  void set_running_standalone(bool value) { running_standalone_ = value; }
+
  private:
   friend class ash::AshTestHelper;
 
@@ -80,6 +83,7 @@
                        mojo::ScopedMessagePipeHandle interface_pipe) override;
 
   const bool show_primary_host_on_connect_;
+  bool running_standalone_ = false;
 
   std::unique_ptr<views::AuraInit> aura_init_;
 
diff --git a/ash/shelf/app_list_button.cc b/ash/shelf/app_list_button.cc
index 346a016..e01832e7 100644
--- a/ash/shelf/app_list_button.cc
+++ b/ash/shelf/app_list_button.cc
@@ -148,7 +148,17 @@
   switch (event->type()) {
     case ui::ET_GESTURE_SCROLL_BEGIN:
       AnimateInkDrop(views::InkDropState::HIDDEN, event);
-      ImageButton::OnGestureEvent(event);
+      shelf_view_->PointerPressedOnButton(this, ShelfView::TOUCH, *event);
+      event->SetHandled();
+      return;
+    case ui::ET_GESTURE_SCROLL_UPDATE:
+      shelf_view_->PointerDraggedOnButton(this, ShelfView::TOUCH, *event);
+      event->SetHandled();
+      return;
+    case ui::ET_GESTURE_SCROLL_END:
+    case ui::ET_SCROLL_FLING_START:
+      shelf_view_->PointerReleasedOnButton(this, ShelfView::TOUCH, false);
+      event->SetHandled();
       return;
     case ui::ET_GESTURE_TAP:
     case ui::ET_GESTURE_TAP_CANCEL:
diff --git a/ash/shelf/app_list_button_unittest.cc b/ash/shelf/app_list_button_unittest.cc
index bff24bd1..7612284 100644
--- a/ash/shelf/app_list_button_unittest.cc
+++ b/ash/shelf/app_list_button_unittest.cc
@@ -4,11 +4,9 @@
 
 #include "ash/shelf/app_list_button.h"
 
-#include "ash/public/cpp/config.h"
 #include "ash/root_window_controller.h"
 #include "ash/session/session_controller.h"
 #include "ash/shelf/shelf.h"
-#include "ash/shelf/shelf_layout_manager.h"
 #include "ash/shelf/shelf_view.h"
 #include "ash/shelf/shelf_view_test_api.h"
 #include "ash/shell.h"
@@ -21,7 +19,6 @@
 #include "ui/app_list/presenter/app_list.h"
 #include "ui/app_list/presenter/test/test_app_list_presenter.h"
 #include "ui/events/event_constants.h"
-#include "ui/events/test/event_generator.h"
 
 namespace ash {
 
@@ -94,34 +91,6 @@
   EXPECT_EQ(0u, test_app_list_presenter.voice_session_count());
 }
 
-TEST_F(AppListButtonTest, SwipingupToOpenFullscreenAppList) {
-  // TODO: investigate failure in mash, http://crbug.com/695686.
-  if (Shell::GetAshConfig() == Config::MASH)
-    return;
-
-  Shelf* shelf = GetPrimaryShelf();
-  EXPECT_EQ(SHELF_ALIGNMENT_BOTTOM, shelf->alignment());
-
-  // Start the drag from the center of the applist button's bottom.
-  gfx::Point center_point = app_list_button()->GetAppListButtonCenterPoint();
-  gfx::Point start(center_point.x(),
-                   center_point.y() + app_list_button()->height() / 2.f);
-  views::View::ConvertPointToScreen(app_list_button(), &start);
-  // Swiping up less than peeking threshold should keep the app list at PEEKING
-  // state.
-  gfx::Point end =
-      start -
-      gfx::Vector2d(
-          0, ShelfLayoutManager::kAppListDragSnapToPeekingThreshold - 10);
-  GetEventGenerator().GestureScrollSequence(
-      start, end, base::TimeDelta::FromMilliseconds(100), 4 /* steps */);
-  RunAllPendingInMessageLoop();
-  EXPECT_EQ(1u, test_app_list_presenter.show_count());
-  EXPECT_GE(test_app_list_presenter.set_y_position_count(), 1u);
-  EXPECT_EQ(app_list::mojom::AppListState::PEEKING,
-            test_app_list_presenter.app_list_state());
-}
-
 class VoiceInteractionAppListButtonTest : public AppListButtonTest {
  public:
   VoiceInteractionAppListButtonTest() {}
diff --git a/ash/shelf/shelf_layout_manager_unittest.cc b/ash/shelf/shelf_layout_manager_unittest.cc
index 7326996..9a13c28 100644
--- a/ash/shelf/shelf_layout_manager_unittest.cc
+++ b/ash/shelf/shelf_layout_manager_unittest.cc
@@ -1656,7 +1656,6 @@
             test_app_list_presenter.app_list_state());
 
   // Swiping up more than the close threshold but less than peeking threshold
-  // should keep the app list at PEEKING state.
   delta.set_y(ShelfLayoutManager::kAppListDragSnapToPeekingThreshold - 10);
   end = start - delta;
   generator.GestureScrollSequence(start, end, kTimeDelta, kNumScrollSteps);
diff --git a/base/metrics/histogram.cc b/base/metrics/histogram.cc
index 4d6cf91..0ca42ad 100644
--- a/base/metrics/histogram.cc
+++ b/base/metrics/histogram.cc
@@ -606,7 +606,7 @@
       debug_string += allocation.context.type_name;
       debug_string += ")";
     }
-    for (size_t i = 0; i < allocation.context.backtrace.frame_count; ++i) {
+    for (int i = allocation.context.backtrace.frame_count - 1; i >= 0; --i) {
       base::trace_event::StackFrame& frame =
           allocation.context.backtrace.frames[i];
       switch (frame.type) {
diff --git a/build/android/play_services/update.py b/build/android/play_services/update.py
index 5297b3cf..d672e2f 100755
--- a/build/android/play_services/update.py
+++ b/build/android/play_services/update.py
@@ -14,13 +14,13 @@
 import re
 import shutil
 import sys
-import tempfile
 import zipfile
 
 sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
 import devil_chromium
 from devil.utils import cmd_helper
 from play_services import utils
+from py_utils import tempfile_ext
 from pylib import constants
 from pylib.constants import host_paths
 from pylib.utils import logging_utils
@@ -47,10 +47,22 @@
 
 LICENSE_FILE_NAME = 'LICENSE'
 ZIP_FILE_NAME = 'google_play_services_library.zip'
-GMS_PACKAGE_ID = 'extras;google;m2repository'  # used by sdk manager
 
 LICENSE_PATTERN = re.compile(r'^Pkg\.License=(?P<text>.*)$', re.MULTILINE)
 
+# Template for a Maven settings.xml which instructs Maven to download to the
+# given directory
+MAVEN_SETTINGS_TEMPLATE = '''\
+<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
+                              https://maven.apache.org/xsd/settings-1.0.0.xsd" >
+  <localRepository>{}</localRepository>
+</settings>
+'''
+
+COM_GOOGLE_ANDROID_GMS = os.path.join('com', 'google', 'android', 'gms')
+EXTRAS_GOOGLE_M2REPOSITORY = os.path.join('extras', 'google', 'm2repository')
 
 def main(raw_args):
   parser = argparse.ArgumentParser(
@@ -71,7 +83,7 @@
   # SDK Update arguments
   parser_sdk = subparsers.add_parser(
       'sdk',
-      help='get the latest Google Play services SDK using Android SDK Manager',
+      help='get the latest Google Play services SDK using Maven',
       description=UpdateSdk.__doc__,
       formatter_class=utils.DefaultsRawHelpFormatter)
   parser_sdk.set_defaults(func=UpdateSdk)
@@ -103,6 +115,10 @@
   instead of `foo.py --debug upload --force`
   '''
 
+  parser.add_argument('--config',
+                      help='JSON Configuration file',
+                      default=CONFIG_DEFAULT_PATH)
+
   parser.add_argument('--sdk-root',
                       help='base path to the Android SDK tools root',
                       default=constants.ANDROID_SDK_ROOT)
@@ -117,10 +133,6 @@
                       help='name of the bucket where the files are stored',
                       default=GMS_CLOUD_STORAGE)
 
-  parser.add_argument('--config',
-                      help='JSON Configuration file',
-                      default=CONFIG_DEFAULT_PATH)
-
   parser.add_argument('--dry-run',
                       action='store_true',
                       help=('run the script in dry run mode. Files will be '
@@ -173,11 +185,9 @@
                                         config.version_number,
                                         args.dry_run)
 
-  tmp_root = tempfile.mkdtemp()
-  try:
+  with tempfile_ext.NamedTemporaryDirectory() as tmp_root:
     # setup the destination directory
-    if not os.path.isdir(paths.package):
-      os.makedirs(paths.package)
+    _MakeDirIfAbsent(paths.package)
 
     # download license file from bucket/{version_number}/license.sha1
     new_license = os.path.join(tmp_root, LICENSE_FILE_NAME)
@@ -228,17 +238,22 @@
                     'An error occurred while installing the new version in '
                     'the SDK directory: %s ', e)
       return -3
-  finally:
-    shutil.rmtree(tmp_root)
 
   return 0
 
 
+def _MakeDirIfAbsent(path):
+  try:
+    os.makedirs(path)
+  except OSError as e:
+    if e.errno != os.errno.EEXIST:
+      raise
+
+
 def UpdateSdk(args):
   '''
-  Uses the Android SDK Manager to download the latest Google Play services SDK
-  locally. Its usual installation path is
-  //third_party/android_tools/sdk/extras/google/m2repository
+  Uses Maven to download the latest Google Play Services SDK. Its installation
+  path is //third_party/android_tools/sdk/extras/google/m2repository.
   '''
 
   # This should function should not run on bots and could fail for many user
@@ -246,19 +261,43 @@
   # disable breakpad to avoid spamming the logs.
   breakpad.IS_ENABLED = False
 
-  # `android update sdk` fails if the library is not installed yet, but it does
-  # not allow to install it from scratch using the command line. We then create
-  # a fake outdated installation.
-  paths = PlayServicesPaths(args.sdk_root, 'no_version_number', [])
-  if not os.path.isfile(paths.source_prop):
-    if not os.path.exists(os.path.dirname(paths.source_prop)):
-      os.makedirs(os.path.dirname(paths.source_prop))
-    with open(paths.source_prop, 'w') as prop_file:
-      prop_file.write('Pkg.Revision=0.0.0\n')
+  config = utils.ConfigParser(args.config)
 
-  sdk_manager = os.path.join(args.sdk_root, 'tools', 'bin', 'sdkmanager')
-  cmd_helper.Call([sdk_manager, GMS_PACKAGE_ID])
-  # If no update is needed, it still returns successfully so we just do nothing
+  # Remove the old SDK.
+  shutil.rmtree(os.path.join(args.sdk_root, EXTRAS_GOOGLE_M2REPOSITORY))
+
+  with tempfile_ext.NamedTemporaryDirectory() as temp_dir:
+    # Configure temp_dir as the Maven repository.
+    settings_path = os.path.join(temp_dir, 'settings.xml')
+    with open(settings_path, 'w') as settings:
+      settings.write(MAVEN_SETTINGS_TEMPLATE.format(temp_dir))
+
+    for client in config.clients:
+      # Download the client.
+      artifact = 'com.google.android.gms:{}:{}:aar'.format(
+          client, config.version_number)
+      try:
+        cmd_helper.Call([
+            'mvn', '--global-settings', settings_path,
+            'org.apache.maven.plugins:maven-dependency-plugin:2.1:get',
+            '-DrepoUrl=https://maven.google.com', '-Dartifact=' + artifact])
+      except OSError as e:
+        if e.errno == os.errno.ENOENT:
+          logging.error('mvn command not found. Please install Maven.')
+          return -1
+        else:
+          raise
+
+      # Copy the client .aar file from temp_dir into the tree.
+      src_path = os.path.join(
+          temp_dir, COM_GOOGLE_ANDROID_GMS,
+          client, config.version_number,
+          '{}-{}.aar'.format(client, config.version_number))
+      dst_path = os.path.join(
+          args.sdk_root, EXTRAS_GOOGLE_M2REPOSITORY, COM_GOOGLE_ANDROID_GMS,
+          client, config.version_number)
+      _MakeDirIfAbsent(dst_path)
+      shutil.copy(src_path, dst_path)
 
   return 0
 
@@ -282,8 +321,7 @@
                             config.clients)
   logging.debug('-- Loaded paths --\n%s\n------------------', paths)
 
-  tmp_root = tempfile.mkdtemp()
-  try:
+  with tempfile_ext.NamedTemporaryDirectory() as tmp_root:
     new_lib_zip = os.path.join(tmp_root, ZIP_FILE_NAME)
     new_license = os.path.join(tmp_root, LICENSE_FILE_NAME)
 
@@ -302,8 +340,6 @@
                                     LICENSE_FILE_NAME + '.sha1')
     shutil.copy(new_lib_zip + '.sha1', new_lib_zip_sha1)
     shutil.copy(new_license + '.sha1', new_license_sha1)
-  finally:
-    shutil.rmtree(tmp_root)
 
   logging.info('Update to version %s complete', config.version_number)
   return 0
@@ -475,11 +511,10 @@
     client_names: names of client libraries to be uploaded. See
         utils.ConfigParser for more info.
     '''
-    relative_package = os.path.join('extras', 'google', 'm2repository')
     self.sdk_root = sdk_root
     self.version_number = version_number
 
-    self.package = os.path.join(sdk_root, relative_package)
+    self.package = os.path.join(sdk_root, EXTRAS_GOOGLE_M2REPOSITORY)
     self.lib_zip_sha1 = os.path.join(self.package, ZIP_FILE_NAME + '.sha1')
     self.license = os.path.join(self.package, LICENSE_FILE_NAME)
     self.source_prop = os.path.join(self.package, 'source.properties')
@@ -487,8 +522,8 @@
     self.client_paths = []
     for client in client_names:
       self.client_paths.append(os.path.join(
-              self.package, 'com', 'google', 'android', 'gms', client,
-              version_number, '%s-%s.aar' % (client, version_number)))
+          self.package, COM_GOOGLE_ANDROID_GMS, client, version_number,
+          '{}-{}.aar'.format(client, version_number)))
 
   def __repr__(self):
     return ("\nsdk_root: " + self.sdk_root +
diff --git a/build/android/pylib/android/logdog_logcat_monitor.py b/build/android/pylib/android/logdog_logcat_monitor.py
index 7b278db6..e1c4371 100644
--- a/build/android/pylib/android/logdog_logcat_monitor.py
+++ b/build/android/pylib/android/logdog_logcat_monitor.py
@@ -37,7 +37,6 @@
     try:
       super(LogdogLogcatMonitor, self)._StopRecording()
       if self._logdog_stream:
-        self._logcat_url = logdog_helper.get_viewer_url(self._stream_name)
         self._logdog_stream.close()
     except Exception as e: # pylint: disable=broad-except
       logging.exception('Unknown Error: %s.', e)
@@ -51,6 +50,8 @@
       self._adb.Logcat(clear=True)
 
     self._logdog_stream = logdog_helper.open_text(self._stream_name)
+    self._logcat_url = logdog_helper.get_viewer_url(self._stream_name)
+    logging.info('Logcat will be saved to %s', self._logcat_url)
     self._StartRecording()
 
   def _StartRecording(self):
diff --git a/chrome/app/settings_strings.grdp b/chrome/app/settings_strings.grdp
index 77f0b0c..9b0b023 100644
--- a/chrome/app/settings_strings.grdp
+++ b/chrome/app/settings_strings.grdp
@@ -1783,7 +1783,7 @@
       Unknown carrier
     </message>
     <message name="IDS_ONC_VPN_HOST" desc="ONC Property label for VPN.Host">
-      Host
+      Server hostname
     </message>
     <message name="IDS_ONC_VPN_L2TP_USERNAME" desc="ONC Property label for VPN.L2TP.Username">
       Username
@@ -1797,6 +1797,15 @@
     <message name="IDS_ONC_VPN_TYPE" desc="ONC Property label for VPN.Type">
       Provider type
     </message>
+    <message name="IDS_ONC_VPN_TYPE_L2TP_IPSEC" desc="ONC Property label for VPN.Type.L2TP-IPSec">
+      L2TP/IPsec
+    </message>
+    <message name="IDS_ONC_VPN_TYPE_OPENVPN" desc="ONC Property label for VPN.Type.OpenVPN">
+      Open VPN
+    </message>
+    <message name="IDS_ONC_VPN_TYPE_ARCVPN" desc="ONC Property label for VPN.Type.ARCVPN">
+      Android VPN
+    </message>
     <message name="IDS_ONC_WIFI_FREQUENCY" desc="ONC Property label for WiFi.Frequency">
       Frequency
     </message>
diff --git a/chrome/browser/chromeos/arc/arc_session_manager.cc b/chrome/browser/chromeos/arc/arc_session_manager.cc
index 061f722..5c2c584 100644
--- a/chrome/browser/chromeos/arc/arc_session_manager.cc
+++ b/chrome/browser/chromeos/arc/arc_session_manager.cc
@@ -26,6 +26,7 @@
 #include "chrome/browser/chromeos/arc/policy/arc_android_management_checker.h"
 #include "chrome/browser/chromeos/arc/policy/arc_policy_util.h"
 #include "chrome/browser/chromeos/login/ui/login_display_host.h"
+#include "chrome/browser/chromeos/login/wizard_controller.h"
 #include "chrome/browser/lifetime/application_lifetime.h"
 #include "chrome/browser/policy/profile_policy_connector.h"
 #include "chrome/browser/policy/profile_policy_connector_factory.h"
@@ -163,17 +164,22 @@
 
 // static
 bool ArcSessionManager::IsOobeOptInActive() {
-  // ARC OOBE OptIn is optional for now. Test if it exists and login host is
-  // active.
-  if (!user_manager::UserManager::Get()->IsCurrentUserNew())
+  // Check if ARC ToS screen for OOBE or OPA OptIn flow is currently showing.
+  // TODO(b/65861628): Rename the method since it is no longer accurate.
+  // Redesign the OptIn flow since there is no longer reason to have two
+  // different OptIn flows.
+  chromeos::LoginDisplayHost* host = chromeos::LoginDisplayHost::default_host();
+  if (!host)
     return false;
-  if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
-          chromeos::switches::kEnableArcOOBEOptIn)) {
+  const chromeos::WizardController* wizard_controller =
+      host->GetWizardController();
+  if (!wizard_controller)
     return false;
-  }
-  if (!chromeos::LoginDisplayHost::default_host())
+  const chromeos::BaseScreen* screen = wizard_controller->current_screen();
+  if (!screen)
     return false;
-  return true;
+  return screen->screen_id() ==
+         chromeos::OobeScreen::SCREEN_ARC_TERMS_OF_SERVICE;
 }
 
 // static
diff --git a/chrome/browser/chromeos/arc/arc_session_manager_unittest.cc b/chrome/browser/chromeos/arc/arc_session_manager_unittest.cc
index b056d1e1..f3a0cec 100644
--- a/chrome/browser/chromeos/arc/arc_session_manager_unittest.cc
+++ b/chrome/browser/chromeos/arc/arc_session_manager_unittest.cc
@@ -29,6 +29,7 @@
 #include "chrome/browser/chromeos/login/users/fake_chrome_user_manager.h"
 #include "chrome/browser/chromeos/login/users/scoped_user_manager_enabler.h"
 #include "chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h"
+#include "chrome/browser/chromeos/login/wizard_controller.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/policy/profile_policy_connector.h"
 #include "chrome/browser/policy/profile_policy_connector_factory.h"
@@ -48,6 +49,7 @@
 #include "components/arc/test/fake_arc_session.h"
 #include "components/prefs/pref_service.h"
 #include "components/prefs/testing_pref_service.h"
+#include "components/session_manager/core/session_manager.h"
 #include "components/signin/core/account_id/account_id.h"
 #include "components/sync/model/fake_sync_change_processor.h"
 #include "components/sync/model/sync_error_factory_mock.h"
@@ -65,6 +67,21 @@
 
 namespace {
 
+class FakeBaseScreen : public chromeos::BaseScreen {
+ public:
+  explicit FakeBaseScreen(chromeos::OobeScreen screen_id)
+      : BaseScreen(nullptr, screen_id) {}
+
+  ~FakeBaseScreen() override = default;
+
+  // chromeos::BaseScreen:
+  void Show() override {}
+  void Hide() override {}
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(FakeBaseScreen);
+};
+
 class FakeLoginDisplayHost : public chromeos::LoginDisplayHost {
  public:
   FakeLoginDisplayHost() {
@@ -91,8 +108,19 @@
   void Finalize(base::OnceClosure) override {}
   void OpenProxySettings(const std::string& network_id) override {}
   void SetStatusAreaVisible(bool visible) override {}
-  void StartWizard(chromeos::OobeScreen first_screen) override {}
-  chromeos::WizardController* GetWizardController() override { return nullptr; }
+  void StartWizard(chromeos::OobeScreen first_screen) override {
+    // Reset the controller first since there could only be one wizard
+    // controller at any time.
+    wizard_controller_.reset();
+    wizard_controller_ =
+        std::make_unique<chromeos::WizardController>(nullptr, nullptr);
+
+    fake_screen_ = std::make_unique<FakeBaseScreen>(first_screen);
+    wizard_controller_->SetCurrentScreenForTesting(fake_screen_.get());
+  }
+  chromeos::WizardController* GetWizardController() override {
+    return wizard_controller_.get();
+  }
   chromeos::AppLaunchController* GetAppLaunchController() override {
     return nullptr;
   }
@@ -111,6 +139,12 @@
   bool IsVoiceInteractionOobe() override { return false; }
 
  private:
+  // SessionManager is required by the constructor of WizardController.
+  std::unique_ptr<session_manager::SessionManager> session_manager_ =
+      std::make_unique<session_manager::SessionManager>();
+  std::unique_ptr<FakeBaseScreen> fake_screen_;
+  std::unique_ptr<chromeos::WizardController> wizard_controller_;
+
   DISALLOW_COPY_AND_ASSIGN(FakeLoginDisplayHost);
 };
 
@@ -798,6 +832,10 @@
     fake_login_display_host_ = base::MakeUnique<FakeLoginDisplayHost>();
   }
 
+  FakeLoginDisplayHost* login_display_host() {
+    return fake_login_display_host_.get();
+  }
+
   void CloseLoginDisplayHost() { fake_login_display_host_.reset(); }
 
   void AppendEnableArcOOBEOptInSwitch() {
@@ -812,21 +850,16 @@
 };
 
 TEST_F(ArcSessionOobeOptInTest, OobeOptInActive) {
-  // OOBE OptIn is active in case of OOBE is started for new user and ARC OOBE
-  // is enabled by switch.
-  EXPECT_FALSE(ArcSessionManager::IsOobeOptInActive());
-  GetFakeUserManager()->set_current_user_new(true);
+  // OOBE OptIn is active in case of OOBE controller is alive and the ARC ToS
+  // screen is currently showing.
   EXPECT_FALSE(ArcSessionManager::IsOobeOptInActive());
   CreateLoginDisplayHost();
   EXPECT_FALSE(ArcSessionManager::IsOobeOptInActive());
-
-  AppendEnableArcOOBEOptInSwitch();
-  GetFakeUserManager()->set_current_user_new(false);
-  CloseLoginDisplayHost();
+  login_display_host()->StartWizard(
+      chromeos::OobeScreen::SCREEN_VOICE_INTERACTION_VALUE_PROP);
   EXPECT_FALSE(ArcSessionManager::IsOobeOptInActive());
-  GetFakeUserManager()->set_current_user_new(true);
-  EXPECT_FALSE(ArcSessionManager::IsOobeOptInActive());
-  CreateLoginDisplayHost();
+  login_display_host()->StartWizard(
+      chromeos::OobeScreen::SCREEN_ARC_TERMS_OF_SERVICE);
   EXPECT_TRUE(ArcSessionManager::IsOobeOptInActive());
 }
 
@@ -861,6 +894,10 @@
 
     arc_session_manager()->SetProfile(profile());
     arc_session_manager()->Initialize();
+
+    login_display_host()->StartWizard(
+        chromeos::OobeScreen::SCREEN_ARC_TERMS_OF_SERVICE);
+
     if (IsArcPlayStoreEnabledForProfile(profile()))
       arc_session_manager()->RequestEnable();
   }
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
index f016a8f..c030cf6a 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
@@ -403,7 +403,7 @@
 WallpaperManager::~WallpaperManager() {
   show_user_name_on_signin_subscription_.reset();
   device_wallpaper_image_subscription_.reset();
-  user_manager::UserManager::Get()->RemoveSessionStateObserver(this);
+  user_manager::UserManager::Get()->RemoveObserver(this);
   weak_factory_.InvalidateWeakPtrs();
 }
 
@@ -962,7 +962,7 @@
       {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
        base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN});
 
-  user_manager::UserManager::Get()->AddSessionStateObserver(this);
+  user_manager::UserManager::Get()->AddObserver(this);
 
   content::ServiceManagerConnection* connection =
       content::ServiceManagerConnection::GetForProcess();
@@ -1381,8 +1381,8 @@
   return false;
 }
 
-void WallpaperManager::UserChangedChildStatus(user_manager::User* user) {
-  SetUserWallpaperNow(user->GetAccountId());
+void WallpaperManager::OnChildStatusChanged(const user_manager::User& user) {
+  SetUserWallpaperNow(user.GetAccountId());
 }
 
 void WallpaperManager::SetDefaultWallpaperPathsFromCommandLine(
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
index 3516205..2239d4d 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.h
@@ -36,16 +36,20 @@
 
 namespace chromeos {
 
-class WallpaperManager
-    : public wallpaper::WallpaperManagerBase,
-      public ash::mojom::WallpaperPicker,
-      public content::NotificationObserver,
-      public user_manager::UserManager::UserSessionStateObserver,
-      public wm::ActivationChangeObserver,
-      public aura::WindowObserver {
+class WallpaperManager : public wallpaper::WallpaperManagerBase,
+                         public ash::mojom::WallpaperPicker,
+                         public content::NotificationObserver,
+                         public user_manager::UserManager::Observer,
+                         public wm::ActivationChangeObserver,
+                         public aura::WindowObserver {
  public:
   class PendingWallpaper;
 
+  // Needed to avoid ambiguity of WallpaperManager::Observer. This can be
+  // removed when either WallpaperManagerBase::Observer or UserManager::Observer
+  // is moved its own header file.
+  using Observer = wallpaper::WallpaperManagerBase::Observer;
+
   ~WallpaperManager() override;
 
   // Expects there is no instance of WallpaperManager and create one.
@@ -107,8 +111,8 @@
                const content::NotificationSource& source,
                const content::NotificationDetails& details) override;
 
-  // user_manager::UserManager::UserSessionStateObserver:
-  void UserChangedChildStatus(user_manager::User* user) override;
+  // user_manager::UserManager::Observer:
+  void OnChildStatusChanged(const user_manager::User& user) override;
 
   // wm::ActivationChangeObserver:
   void OnWindowActivated(ActivationReason reason,
diff --git a/chrome/browser/chromeos/login/wizard_controller.cc b/chrome/browser/chromeos/login/wizard_controller.cc
index 17f1f95..4d4f6fc 100644
--- a/chrome/browser/chromeos/login/wizard_controller.cc
+++ b/chrome/browser/chromeos/login/wizard_controller.cc
@@ -266,10 +266,12 @@
       session_manager::SessionManager::Get()->IsSessionStarted();
   if (!ash_util::IsRunningInMash()) {
     AccessibilityManager* accessibility_manager = AccessibilityManager::Get();
-    CHECK(accessibility_manager);
-    accessibility_subscription_ = accessibility_manager->RegisterCallback(
-        base::Bind(&WizardController::OnAccessibilityStatusChanged,
-                   base::Unretained(this)));
+    if (accessibility_manager) {
+      // accessibility_manager could be null in Tests.
+      accessibility_subscription_ = accessibility_manager->RegisterCallback(
+          base::Bind(&WizardController::OnAccessibilityStatusChanged,
+                     base::Unretained(this)));
+    }
   } else {
     NOTIMPLEMENTED();
   }
@@ -448,6 +450,10 @@
   return nullptr;
 }
 
+void WizardController::SetCurrentScreenForTesting(BaseScreen* screen) {
+  current_screen_ = screen;
+}
+
 void WizardController::ShowNetworkScreen() {
   VLOG(1) << "Showing network screen.";
   UpdateStatusAreaVisibilityForScreen(OobeScreen::SCREEN_OOBE_NETWORK);
diff --git a/chrome/browser/chromeos/login/wizard_controller.h b/chrome/browser/chromeos/login/wizard_controller.h
index c6616f6..7876b01 100644
--- a/chrome/browser/chromeos/login/wizard_controller.h
+++ b/chrome/browser/chromeos/login/wizard_controller.h
@@ -129,6 +129,9 @@
   // |screen_manager_|.
   BaseScreen* CreateScreen(OobeScreen screen);
 
+  // Set the current screen. For Test use only.
+  void SetCurrentScreenForTesting(BaseScreen* screen);
+
  private:
   // Show specific screen.
   void ShowNetworkScreen();
diff --git a/chrome/browser/devtools/BUILD.gn b/chrome/browser/devtools/BUILD.gn
index e5f6ebd..bc41927 100644
--- a/chrome/browser/devtools/BUILD.gn
+++ b/chrome/browser/devtools/BUILD.gn
@@ -34,6 +34,8 @@
   import("$_inspector_protocol/inspector_protocol.gni")
 
   _protocol_generated = [
+    "protocol/browser.cc",
+    "protocol/browser.h",
     "protocol/forward.h",
     "protocol/page.cc",
     "protocol/page.h",
@@ -170,6 +172,8 @@
   if (!is_android) {
     deps += [ ":protocol_generated_sources" ]
     sources += [
+      "protocol/browser_handler.cc",
+      "protocol/browser_handler.h",
       "protocol/page_handler.cc",
       "protocol/page_handler.h",
       "protocol_string.cc",
diff --git a/chrome/browser/devtools/chrome_devtools_manager_delegate.cc b/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
index cf962f9..23c12309 100644
--- a/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
+++ b/chrome/browser/devtools/chrome_devtools_manager_delegate.cc
@@ -21,10 +21,7 @@
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/ui/browser_navigator.h"
 #include "chrome/browser/ui/browser_navigator_params.h"
-#include "chrome/browser/ui/browser_window.h"
-#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h"
-#include "chrome/browser/ui/tabs/tab_strip_model.h"
 #include "chrome/grit/browser_resources.h"
 #include "components/guest_view/browser/guest_view_base.h"
 #include "content/public/browser/devtools_agent_host.h"
@@ -46,43 +43,6 @@
 char kHostParam[] = "host";
 char kPortParam[] = "port";
 
-BrowserWindow* GetBrowserWindow(int window_id) {
-  for (auto* b : *BrowserList::GetInstance()) {
-    if (b->session_id().id() == window_id)
-      return b->window();
-  }
-  return nullptr;
-}
-
-// Get the bounds and state of the browser window. The bounds is for the
-// restored window when the window is minimized. Otherwise, it is for the actual
-// window.
-std::unique_ptr<base::DictionaryValue> GetBounds(BrowserWindow* window) {
-  gfx::Rect bounds;
-  if (window->IsMinimized())
-    bounds = window->GetRestoredBounds();
-  else
-    bounds = window->GetBounds();
-
-  auto bounds_object = base::MakeUnique<base::DictionaryValue>();
-
-  bounds_object->SetInteger("left", bounds.x());
-  bounds_object->SetInteger("top", bounds.y());
-  bounds_object->SetInteger("width", bounds.width());
-  bounds_object->SetInteger("height", bounds.height());
-
-  std::string window_state = "normal";
-  if (window->IsMinimized())
-    window_state = "minimized";
-  if (window->IsMaximized())
-    window_state = "maximized";
-  if (window->IsFullscreen())
-    window_state = "fullscreen";
-  bounds_object->SetString("windowState", window_state);
-
-  return bounds_object;
-}
-
 bool GetExtensionInfo(content::WebContents* wc,
                       std::string* name,
                       std::string* type) {
@@ -118,184 +78,6 @@
 
 }  // namespace
 
-// static
-std::unique_ptr<base::DictionaryValue>
-ChromeDevToolsManagerDelegate::GetWindowForTarget(
-    int id,
-    base::DictionaryValue* params) {
-  std::string target_id;
-  if (!params->GetString("targetId", &target_id))
-    return DevToolsProtocol::CreateInvalidParamsResponse(id, "targetId");
-
-  Browser* browser = nullptr;
-  scoped_refptr<DevToolsAgentHost> host =
-      DevToolsAgentHost::GetForId(target_id);
-  if (!host)
-    return DevToolsProtocol::CreateErrorResponse(id, "No target with given id");
-  content::WebContents* web_contents = host->GetWebContents();
-  if (!web_contents) {
-    return DevToolsProtocol::CreateErrorResponse(
-        id, "No web contents in the target");
-  }
-  for (auto* b : *BrowserList::GetInstance()) {
-    int tab_index = b->tab_strip_model()->GetIndexOfWebContents(web_contents);
-    if (tab_index != TabStripModel::kNoTab)
-      browser = b;
-  }
-  if (!browser) {
-    return DevToolsProtocol::CreateErrorResponse(id,
-                                                 "Browser window not found");
-  }
-
-  auto result = base::MakeUnique<base::DictionaryValue>();
-  result->SetInteger("windowId", browser->session_id().id());
-  result->Set("bounds", GetBounds(browser->window()));
-  return DevToolsProtocol::CreateSuccessResponse(id, std::move(result));
-}
-
-// static
-std::unique_ptr<base::DictionaryValue>
-ChromeDevToolsManagerDelegate::GetWindowBounds(int id,
-                                               base::DictionaryValue* params) {
-  int window_id;
-  if (!params->GetInteger("windowId", &window_id))
-    return DevToolsProtocol::CreateInvalidParamsResponse(id, "windowId");
-  BrowserWindow* window = GetBrowserWindow(window_id);
-  if (!window) {
-    return DevToolsProtocol::CreateErrorResponse(id,
-                                                 "Browser window not found");
-  }
-
-  auto result = base::MakeUnique<base::DictionaryValue>();
-  result->Set("bounds", GetBounds(window));
-  return DevToolsProtocol::CreateSuccessResponse(id, std::move(result));
-}
-
-// static
-std::unique_ptr<base::DictionaryValue>
-ChromeDevToolsManagerDelegate::SetWindowBounds(int id,
-                                               base::DictionaryValue* params) {
-  int window_id;
-  if (!params->GetInteger("windowId", &window_id))
-    return DevToolsProtocol::CreateInvalidParamsResponse(id, "windowId");
-  BrowserWindow* window = GetBrowserWindow(window_id);
-  if (!window) {
-    return DevToolsProtocol::CreateErrorResponse(id,
-                                                 "Browser window not found");
-  }
-
-  const base::Value* value = nullptr;
-  const base::DictionaryValue* bounds_dict = nullptr;
-  if (!params->Get("bounds", &value) || !value->GetAsDictionary(&bounds_dict))
-    return DevToolsProtocol::CreateInvalidParamsResponse(id, "bounds");
-
-  std::string window_state;
-  if (!bounds_dict->GetString("windowState", &window_state))
-    window_state = "normal";
-  else if (window_state != "normal" && window_state != "minimized" &&
-           window_state != "maximized" && window_state != "fullscreen")
-    return DevToolsProtocol::CreateInvalidParamsResponse(id, "windowState");
-
-  // Compute updated bounds when window state is normal.
-  bool set_bounds = false;
-  gfx::Rect bounds = window->GetBounds();
-  int left, top, width, height;
-  if (bounds_dict->GetInteger("left", &left)) {
-    bounds.set_x(left);
-    set_bounds = true;
-  }
-  if (bounds_dict->GetInteger("top", &top)) {
-    bounds.set_y(top);
-    set_bounds = true;
-  }
-  if (bounds_dict->GetInteger("width", &width)) {
-    if (width < 0)
-      return DevToolsProtocol::CreateInvalidParamsResponse(id, "width");
-    bounds.set_width(width);
-    set_bounds = true;
-  }
-  if (bounds_dict->GetInteger("height", &height)) {
-    if (height < 0)
-      return DevToolsProtocol::CreateInvalidParamsResponse(id, "height");
-    bounds.set_height(height);
-    set_bounds = true;
-  }
-
-  if (set_bounds && window_state != "normal") {
-    return DevToolsProtocol::CreateErrorResponse(
-        id,
-        "The 'minimized', 'maximized' and 'fullscreen' states cannot be "
-        "combined with 'left', 'top', 'width' or 'height'");
-  }
-
-  if (set_bounds && (window->IsMinimized() || window->IsMaximized() ||
-                     window->IsFullscreen())) {
-    return DevToolsProtocol::CreateErrorResponse(
-        id,
-        "To resize minimized/maximized/fullscreen window, restore it to normal "
-        "state first.");
-  }
-
-  if (window_state == "fullscreen") {
-    if (window->IsMinimized()) {
-      return DevToolsProtocol::CreateErrorResponse(id,
-                                                   "To make minimized window "
-                                                   "fullscreen, restore it to "
-                                                   "normal state first.");
-    }
-    window->GetExclusiveAccessContext()->EnterFullscreen(
-        GURL(), EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE);
-  }
-
-  if (window_state == "maximized") {
-    if (window->IsMinimized() || window->IsFullscreen()) {
-      return DevToolsProtocol::CreateErrorResponse(
-          id,
-          "To maximize a minimized or fullscreen window, restore it to normal "
-          "state first.");
-    }
-    window->Maximize();
-  }
-
-  if (window_state == "minimized") {
-    if (window->IsFullscreen()) {
-      return DevToolsProtocol::CreateErrorResponse(
-          id,
-          "To minimize a fullscreen window, restore it to normal "
-          "state first.");
-    }
-    window->Minimize();
-  }
-
-  if (window_state == "normal") {
-    if (window->IsFullscreen()) {
-      window->GetExclusiveAccessContext()->ExitFullscreen();
-    } else if (window->IsMinimized()) {
-      window->Show();
-    } else if (window->IsMaximized()) {
-      window->Restore();
-    } else if (set_bounds) {
-      window->SetBounds(bounds);
-    }
-  }
-
-  return DevToolsProtocol::CreateSuccessResponse(id, nullptr);
-}
-
-std::unique_ptr<base::DictionaryValue>
-ChromeDevToolsManagerDelegate::HandleBrowserCommand(
-    int id,
-    std::string method,
-    base::DictionaryValue* params) {
-  if (method == chrome::devtools::Browser::getWindowForTarget::kName)
-    return GetWindowForTarget(id, params);
-  if (method == chrome::devtools::Browser::getWindowBounds::kName)
-    return GetWindowBounds(id, params);
-  if (method == chrome::devtools::Browser::setWindowBounds::kName)
-    return SetWindowBounds(id, params);
-  return nullptr;
-}
-
 class ChromeDevToolsManagerDelegate::HostData {
  public:
   HostData() {}
@@ -334,15 +116,8 @@
   if (!DevToolsProtocol::ParseCommand(command_dict, &id, &method, &params))
     return false;
 
-  auto result = HandleBrowserCommand(id, method, params);
-  if (result) {
-    agent_host->SendProtocolMessageToClient(session_id,
-                                            ToString(std::move(result)));
-    return true;
-  }
-
   if (method == chrome::devtools::Target::setRemoteLocations::kName) {
-    result = SetRemoteLocations(agent_host, id, params);
+    auto result = SetRemoteLocations(agent_host, id, params);
     DCHECK(result);
     agent_host->SendProtocolMessageToClient(session_id,
                                             ToString(std::move(result)));
diff --git a/chrome/browser/devtools/chrome_devtools_manager_delegate.h b/chrome/browser/devtools/chrome_devtools_manager_delegate.h
index c0e2026..f426f7b0 100644
--- a/chrome/browser/devtools/chrome_devtools_manager_delegate.h
+++ b/chrome/browser/devtools/chrome_devtools_manager_delegate.h
@@ -67,20 +67,6 @@
       int command_id,
       base::DictionaryValue* params);
 
-  std::unique_ptr<base::DictionaryValue> HandleBrowserCommand(
-      int id,
-      std::string method,
-      base::DictionaryValue* params);
-  static std::unique_ptr<base::DictionaryValue> GetWindowForTarget(
-      int id,
-      base::DictionaryValue* params);
-  static std::unique_ptr<base::DictionaryValue> GetWindowBounds(
-      int id,
-      base::DictionaryValue* params);
-  static std::unique_ptr<base::DictionaryValue> SetWindowBounds(
-      int id,
-      base::DictionaryValue* params);
-
   std::map<content::DevToolsAgentHost*, std::unique_ptr<HostData>> host_data_;
 
   std::map<int, std::unique_ptr<ChromeDevToolsSession>> sessions_;
diff --git a/chrome/browser/devtools/chrome_devtools_session.cc b/chrome/browser/devtools/chrome_devtools_session.cc
index 45058687..ab4d843a 100644
--- a/chrome/browser/devtools/chrome_devtools_session.cc
+++ b/chrome/browser/devtools/chrome_devtools_session.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/devtools/chrome_devtools_session.h"
 
+#include "chrome/browser/devtools/protocol/browser_handler.h"
 #include "chrome/browser/devtools/protocol/page_handler.h"
 #include "content/public/browser/devtools_agent_host.h"
 
@@ -18,6 +19,7 @@
     page_handler_ = std::make_unique<PageHandler>(agent_host->GetWebContents(),
                                                   dispatcher_.get());
   }
+  browser_handler_ = std::make_unique<BrowserHandler>(dispatcher_.get());
 }
 
 ChromeDevToolsSession::~ChromeDevToolsSession() = default;
diff --git a/chrome/browser/devtools/chrome_devtools_session.h b/chrome/browser/devtools/chrome_devtools_session.h
index 63f2aeb..c4acda94 100644
--- a/chrome/browser/devtools/chrome_devtools_session.h
+++ b/chrome/browser/devtools/chrome_devtools_session.h
@@ -14,6 +14,7 @@
 class DevToolsAgentHost;
 }
 
+class BrowserHandler;
 class PageHandler;
 
 class ChromeDevToolsSession : public protocol::FrontendChannel {
@@ -36,6 +37,7 @@
   const int session_id_;
 
   std::unique_ptr<protocol::UberDispatcher> dispatcher_;
+  std::unique_ptr<BrowserHandler> browser_handler_;
   std::unique_ptr<PageHandler> page_handler_;
 
   DISALLOW_COPY_AND_ASSIGN(ChromeDevToolsSession);
diff --git a/chrome/browser/devtools/devtools_sanity_interactive_browsertest.cc b/chrome/browser/devtools/devtools_sanity_interactive_browsertest.cc
index f8745e28..6bfc76a9 100644
--- a/chrome/browser/devtools/devtools_sanity_interactive_browsertest.cc
+++ b/chrome/browser/devtools/devtools_sanity_interactive_browsertest.cc
@@ -8,6 +8,7 @@
 #include "base/run_loop.h"
 #include "build/build_config.h"
 #include "chrome/browser/devtools/chrome_devtools_manager_delegate.h"
+#include "chrome/browser/devtools/protocol/browser_handler.h"
 #include "chrome/browser/ui/browser_window.h"
 #include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
 #include "chrome/test/base/in_process_browser_test.h"
@@ -65,24 +66,23 @@
 
 class DevToolsManagerDelegateTest : public InProcessBrowserTest {
  public:
-  std::unique_ptr<base::DictionaryValue> SendCommand(std::string state) {
-    auto params = base::MakeUnique<base::DictionaryValue>();
-    auto bounds_object = base::MakeUnique<base::DictionaryValue>();
-    bounds_object->SetString("windowState", state);
-    params->Set("bounds", std::move(bounds_object));
-    params->SetInteger("windowId", browser()->session_id().id());
-    return ChromeDevToolsManagerDelegate::SetWindowBounds(0, params.get());
+  void SendCommand(std::string state) {
+    auto window_bounds =
+        protocol::Browser::Bounds::Create().SetWindowState(state).Build();
+    BrowserHandler handler(nullptr);
+    handler.SetWindowBounds(browser()->session_id().id(),
+                            std::move(window_bounds));
   }
 
-  std::unique_ptr<base::DictionaryValue> UpdateBounds() {
-    auto params = base::MakeUnique<base::DictionaryValue>();
-    auto bounds_object = base::MakeUnique<base::DictionaryValue>();
-    bounds_object->SetString("windowState", "normal");
-    bounds_object->SetInteger("left", 200);
-    bounds_object->SetInteger("height", 400);
-    params->Set("bounds", std::move(bounds_object));
-    params->SetInteger("windowId", browser()->session_id().id());
-    return ChromeDevToolsManagerDelegate::SetWindowBounds(0, params.get());
+  void UpdateBounds() {
+    auto window_bounds = protocol::Browser::Bounds::Create()
+                             .SetWindowState("normal")
+                             .SetLeft(200)
+                             .SetHeight(400)
+                             .Build();
+    BrowserHandler handler(nullptr);
+    handler.SetWindowBounds(browser()->session_id().id(),
+                            std::move(window_bounds));
   }
 
   void CheckIsMaximized(bool maximized) {
diff --git a/chrome/browser/devtools/inspector_protocol_config.json b/chrome/browser/devtools/inspector_protocol_config.json
index 58272d8..9105868 100644
--- a/chrome/browser/devtools/inspector_protocol_config.json
+++ b/chrome/browser/devtools/inspector_protocol_config.json
@@ -11,6 +11,12 @@
                 "include": [ "enable", "disable", "setAdBlockingEnabled" ],
                 "include_types": [],
                 "include_events": []
+            },
+            {
+                "domain": "Browser",
+                "include": [ "getWindowForTarget", "getWindowBounds", "setWindowBounds" ],
+                "include_types": [ "Bounds" ],
+                "include_events": []
             }
         ]
     },
diff --git a/chrome/browser/devtools/protocol/browser_handler.cc b/chrome/browser/devtools/protocol/browser_handler.cc
new file mode 100644
index 0000000..498135e5
--- /dev/null
+++ b/chrome/browser/devtools/protocol/browser_handler.cc
@@ -0,0 +1,154 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/devtools/protocol/browser_handler.h"
+
+#include "chrome/browser/ui/browser_list.h"
+#include "chrome/browser/ui/browser_window.h"
+#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
+#include "chrome/browser/ui/tabs/tab_strip_model.h"
+#include "content/public/browser/devtools_agent_host.h"
+
+namespace {
+
+BrowserWindow* GetBrowserWindow(int window_id) {
+  for (auto* b : *BrowserList::GetInstance()) {
+    if (b->session_id().id() == window_id)
+      return b->window();
+  }
+  return nullptr;
+}
+
+std::unique_ptr<protocol::Browser::Bounds> GetBrowserWindowBounds(
+    BrowserWindow* window) {
+  std::string window_state = "normal";
+  if (window->IsMinimized())
+    window_state = "minimized";
+  if (window->IsMaximized())
+    window_state = "maximized";
+  if (window->IsFullscreen())
+    window_state = "fullscreen";
+
+  gfx::Rect bounds;
+  if (window->IsMinimized())
+    bounds = window->GetRestoredBounds();
+  else
+    bounds = window->GetBounds();
+  return protocol::Browser::Bounds::Create()
+      .SetLeft(bounds.x())
+      .SetTop(bounds.y())
+      .SetWidth(bounds.width())
+      .SetHeight(bounds.height())
+      .SetWindowState(window_state)
+      .Build();
+}
+
+}  // namespace
+
+BrowserHandler::BrowserHandler(protocol::UberDispatcher* dispatcher) {
+  // Dispatcher can be null in tests.
+  if (dispatcher)
+    protocol::Browser::Dispatcher::wire(dispatcher, this);
+}
+
+BrowserHandler::~BrowserHandler() = default;
+
+protocol::Response BrowserHandler::GetWindowForTarget(
+    const std::string& target_id,
+    int* out_window_id,
+    std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
+  auto host = content::DevToolsAgentHost::GetForId(target_id);
+  if (!host)
+    return protocol::Response::Error("No target with given id");
+  content::WebContents* web_contents = host->GetWebContents();
+  if (!web_contents)
+    return protocol::Response::Error("No web contents in the target");
+
+  Browser* browser = nullptr;
+  for (auto* b : *BrowserList::GetInstance()) {
+    int tab_index = b->tab_strip_model()->GetIndexOfWebContents(web_contents);
+    if (tab_index != TabStripModel::kNoTab)
+      browser = b;
+  }
+  if (!browser)
+    return protocol::Response::Error("Browser window not found");
+
+  BrowserWindow* window = browser->window();
+  *out_window_id = browser->session_id().id();
+  *out_bounds = GetBrowserWindowBounds(window);
+  return protocol::Response::OK();
+}
+
+protocol::Response BrowserHandler::GetWindowBounds(
+    int window_id,
+    std::unique_ptr<protocol::Browser::Bounds>* out_bounds) {
+  BrowserWindow* window = GetBrowserWindow(window_id);
+  if (!window)
+    return protocol::Response::Error("Browser window not found");
+
+  *out_bounds = GetBrowserWindowBounds(window);
+  return protocol::Response::OK();
+}
+
+protocol::Response BrowserHandler::SetWindowBounds(
+    int window_id,
+    std::unique_ptr<protocol::Browser::Bounds> window_bounds) {
+  BrowserWindow* window = GetBrowserWindow(window_id);
+  if (!window)
+    return protocol::Response::Error("Browser window not found");
+  gfx::Rect bounds = window->GetBounds();
+  const bool set_bounds = window_bounds->HasLeft() || window_bounds->HasTop() ||
+                          window_bounds->HasWidth() ||
+                          window_bounds->HasHeight();
+  if (set_bounds) {
+    bounds.set_x(window_bounds->GetLeft(bounds.x()));
+    bounds.set_y(window_bounds->GetTop(bounds.y()));
+    bounds.set_width(window_bounds->GetWidth(bounds.width()));
+    bounds.set_height(window_bounds->GetHeight(bounds.height()));
+  }
+
+  const std::string& window_state = window_bounds->GetWindowState("normal");
+  if (set_bounds && window_state != "normal") {
+    return protocol::Response::Error(
+        "The 'minimized', 'maximized' and 'fullscreen' states cannot be "
+        "combined with 'left', 'top', 'width' or 'height'");
+  }
+
+  if (window_state == "fullscreen") {
+    if (window->IsMinimized()) {
+      return protocol::Response::Error(
+          "To make minimized window fullscreen, "
+          "restore it to normal state first.");
+    }
+    window->GetExclusiveAccessContext()->EnterFullscreen(
+        GURL(), EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE);
+  } else if (window_state == "maximized") {
+    if (window->IsMinimized() || window->IsFullscreen()) {
+      return protocol::Response::Error(
+          "To maximize a minimized or fullscreen "
+          "window, restore it to normal state first.");
+    }
+    window->Maximize();
+  } else if (window_state == "minimized") {
+    if (window->IsFullscreen()) {
+      return protocol::Response::Error(
+          "To minimize a fullscreen window, restore it to normal "
+          "state first.");
+    }
+    window->Minimize();
+  } else if (window_state == "normal") {
+    if (window->IsFullscreen())
+      window->GetExclusiveAccessContext()->ExitFullscreen();
+    else if (window->IsMinimized())
+      window->Show();
+    else if (window->IsMaximized())
+      window->Restore();
+    else if (set_bounds)
+      window->SetBounds(bounds);
+  } else {
+    NOTREACHED();
+  }
+
+  return protocol::Response::OK();
+}
diff --git a/chrome/browser/devtools/protocol/browser_handler.h b/chrome/browser/devtools/protocol/browser_handler.h
new file mode 100644
index 0000000..342af51
--- /dev/null
+++ b/chrome/browser/devtools/protocol/browser_handler.h
@@ -0,0 +1,31 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_
+#define CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_
+
+#include "chrome/browser/devtools/protocol/browser.h"
+
+class BrowserHandler : public protocol::Browser::Backend {
+ public:
+  explicit BrowserHandler(protocol::UberDispatcher* dispatcher);
+  ~BrowserHandler() override;
+
+  // Browser::Backend:
+  protocol::Response GetWindowForTarget(
+      const std::string& target_id,
+      int* out_window_id,
+      std::unique_ptr<protocol::Browser::Bounds>* out_bounds) override;
+  protocol::Response GetWindowBounds(
+      int window_id,
+      std::unique_ptr<protocol::Browser::Bounds>* out_bounds) override;
+  protocol::Response SetWindowBounds(
+      int window_id,
+      std::unique_ptr<protocol::Browser::Bounds> out_bounds) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(BrowserHandler);
+};
+
+#endif  // CHROME_BROWSER_DEVTOOLS_PROTOCOL_BROWSER_HANDLER_H_
diff --git a/chrome/browser/extensions/api/web_request/web_request_apitest.cc b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
index 873abeb..2c1e20b13 100644
--- a/chrome/browser/extensions/api/web_request/web_request_apitest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_apitest.cc
@@ -1102,6 +1102,51 @@
   }
 }
 
+// Test that initiator is only included as part of event details when the
+// extension has a permission matching the initiator.
+IN_PROC_BROWSER_TEST_F(ExtensionWebRequestApiTest, MinimumAccessInitiator) {
+  ASSERT_TRUE(StartEmbeddedTestServer());
+
+  ExtensionTestMessageListener listener("ready", false);
+  const Extension* extension = LoadExtension(
+      test_data_dir_.AppendASCII("webrequest_permissions/initiator"));
+  ASSERT_TRUE(extension) << message_;
+  EXPECT_TRUE(listener.WaitUntilSatisfied());
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  ASSERT_TRUE(web_contents);
+
+  struct TestCase {
+    std::string navigate_before_start;
+    std::string xhr_domain;
+    std::string expected_initiator;
+  } testcases[] = {{"example.com", "example.com", "example.com"},
+                   {"example2.com", "example3.com", "example2.com"},
+                   {"no-permission.com", "example4.com", ""}};
+
+  int port = embedded_test_server()->port();
+  for (const auto& testcase : testcases) {
+    SCOPED_TRACE(testcase.navigate_before_start + ":" + testcase.xhr_domain +
+                 ":" + testcase.expected_initiator);
+    ExtensionTestMessageListener initiator_listener(false);
+    initiator_listener.set_extension_id(extension->id());
+    ui_test_utils::NavigateToURL(browser(), embedded_test_server()->GetURL(
+                                                testcase.navigate_before_start,
+                                                "/extensions/body1.html"));
+    PerformXhrInFrame(web_contents->GetMainFrame(), testcase.xhr_domain, port,
+                      "extensions/api_test/webrequest/xhr/data.json");
+    EXPECT_TRUE(initiator_listener.WaitUntilSatisfied());
+    if (testcase.expected_initiator.empty()) {
+      ASSERT_EQ("NO_INITIATOR", initiator_listener.message());
+    } else {
+      ASSERT_EQ(
+          "http://" + testcase.expected_initiator + ":" + std::to_string(port),
+          initiator_listener.message());
+    }
+  }
+}
+
 // Tests that the webRequest events aren't dispatched when the request initiator
 // is protected by policy.
 IN_PROC_BROWSER_TEST_F(ExtensionApiTestWithManagementPolicy,
diff --git a/chrome/browser/extensions/api/web_request/web_request_event_details_unittest.cc b/chrome/browser/extensions/api/web_request/web_request_event_details_unittest.cc
index b68ccc59..266fbe8f 100644
--- a/chrome/browser/extensions/api/web_request/web_request_event_details_unittest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_event_details_unittest.cc
@@ -97,7 +97,7 @@
     WebRequestEventDetails details(request.get(), kFilter);
     details.SetResponseHeaders(request.get(), headers.get());
     std::unique_ptr<base::DictionaryValue> dict =
-        details.GetFilteredDict(kFilter);
+        details.GetFilteredDict(kFilter, nullptr, std::string(), false);
     base::Value* filtered_headers = dict->FindPath({"responseHeaders"});
     ASSERT_TRUE(filtered_headers);
     EXPECT_EQ(2u, filtered_headers->GetList().size());
@@ -119,7 +119,7 @@
     WebRequestEventDetails gaia_details(gaia_request.get(), kFilter);
     gaia_details.SetResponseHeaders(gaia_request.get(), headers.get());
     std::unique_ptr<base::DictionaryValue> dict =
-        gaia_details.GetFilteredDict(kFilter);
+        gaia_details.GetFilteredDict(kFilter, nullptr, std::string(), false);
     base::Value* filtered_headers = dict->FindPath({"responseHeaders"});
     ASSERT_TRUE(filtered_headers);
     EXPECT_EQ(1u, filtered_headers->GetList().size());
diff --git a/chrome/browser/extensions/bookmark_app_url_redirector_browsertest.cc b/chrome/browser/extensions/bookmark_app_url_redirector_browsertest.cc
index 4b95f40e..4a35967 100644
--- a/chrome/browser/extensions/bookmark_app_url_redirector_browsertest.cc
+++ b/chrome/browser/extensions/bookmark_app_url_redirector_browsertest.cc
@@ -5,8 +5,10 @@
 #include "base/memory/ptr_util.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/scoped_feature_list.h"
+#include "chrome/app/chrome_command_ids.h"
 #include "chrome/browser/extensions/bookmark_app_helper.h"
 #include "chrome/browser/extensions/extension_browsertest.h"
+#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
 #include "chrome/browser/ui/browser_commands.h"
 #include "chrome/browser/ui/browser_finder.h"
 #include "chrome/browser/ui/extensions/app_launch_params.h"
@@ -19,6 +21,7 @@
 #include "content/public/browser/notification_details.h"
 #include "content/public/browser/notification_service.h"
 #include "content/public/browser/render_frame_host.h"
+#include "content/public/common/context_menu_params.h"
 #include "content/public/test/browser_test_utils.h"
 #include "content/public/test/test_frame_navigation_observer.h"
 #include "content/public/test/test_utils.h"
@@ -26,6 +29,7 @@
 #include "extensions/browser/notification_types.h"
 #include "net/test/embedded_test_server/embedded_test_server.h"
 #include "net/test/embedded_test_server/http_response.h"
+#include "net/url_request/url_fetcher.h"
 
 using namespace net::test_server;
 
@@ -69,15 +73,51 @@
   ui_test_utils::UrlLoadObserver url_observer(
       target_url, content::NotificationService::AllSources());
   std::string script = base::StringPrintf(
+      "(() => {"
       "const link = document.createElement('a');"
       "link.href = '%s';"
       "link.target = '%s';"
       "document.body.appendChild(link);"
       "const event = new MouseEvent('click', {'view': window});"
-      "link.dispatchEvent(event);",
+      "link.dispatchEvent(event);"
+      "})();",
       target_url.spec().c_str(),
       target == LinkTarget::SELF ? "_self" : "_blank");
-  EXPECT_TRUE(content::ExecuteScript(web_contents, script));
+  ASSERT_TRUE(content::ExecuteScript(web_contents, script));
+  url_observer.Wait();
+}
+
+// Creates a <form> element with a |target_url| action and |method| method. Adds
+// the form to the DOM with a button and clicks the button. Returns once
+// |target_url| has been loaded.
+//
+// If |method| is net::URLFetcher::RequestType::GET, |target_url| should contain
+// an empty query string, since that URL will be loaded when submitting the form
+// e.g. "https://www.example.com/?".
+void SubmitFormAndWait(content::WebContents* web_contents,
+                       const GURL& target_url,
+                       net::URLFetcher::RequestType method) {
+  if (method == net::URLFetcher::RequestType::GET) {
+    ASSERT_TRUE(target_url.has_query());
+    ASSERT_TRUE(target_url.query().empty());
+  }
+
+  ui_test_utils::UrlLoadObserver url_observer(
+      target_url, content::NotificationService::AllSources());
+  std::string script = base::StringPrintf(
+      "(() => {"
+      "const form = document.createElement('form');"
+      "form.action = '%s';"
+      "form.method = '%s';"
+      "const button = document.createElement('input');"
+      "button.type = 'submit';"
+      "form.appendChild(button);"
+      "document.body.appendChild(form);"
+      "button.dispatchEvent(new MouseEvent('click', {'view': window}));"
+      "})();",
+      target_url.spec().c_str(),
+      method == net::URLFetcher::RequestType::POST ? "post" : "get");
+  ASSERT_TRUE(content::ExecuteScript(web_contents, script));
   url_observer.Wait();
 }
 
@@ -118,6 +158,7 @@
         base::BindRepeating([](const HttpRequest& request) {
           auto response = base::MakeUnique<BasicHttpResponse>();
           response->set_content_type("text/html");
+          response->AddCustomHeader("Access-Control-Allow-Origin", "*");
           return static_cast<std::unique_ptr<HttpResponse>>(
               std::move(response));
         }));
@@ -163,9 +204,33 @@
     return chrome::FindLastActive();
   }
 
+  // Navigates the active tab in |browser| to the launching page.
+  void NavigateToLaunchingPage(Browser* browser) {
+    ui_test_utils::NavigateToURL(browser, GetLaunchingPageURL());
+  }
+
   // Navigates the active tab to the launching page.
-  void NavigateToLaunchingPage() {
-    ui_test_utils::NavigateToURL(browser(), GetLaunchingPageURL());
+  void NavigateToLaunchingPage() { NavigateToLaunchingPage(browser()); }
+
+  // Checks that, after running |action|, the initial tab's window doesn't have
+  // any new tabs, the initial tab did not navigate, and that no new windows
+  // have been opened.
+  void TestTabActionDoesNotNavigateOrOpenAppWindow(
+      const base::Closure& action) {
+    size_t num_browsers = chrome::GetBrowserCount(profile());
+    int num_tabs = browser()->tab_strip_model()->count();
+    content::WebContents* initial_tab =
+        browser()->tab_strip_model()->GetActiveWebContents();
+    GURL initial_url = initial_tab->GetLastCommittedURL();
+
+    action.Run();
+
+    EXPECT_EQ(num_browsers, chrome::GetBrowserCount(profile()));
+    EXPECT_EQ(browser(), chrome::FindLastActive());
+    EXPECT_EQ(num_tabs, browser()->tab_strip_model()->count());
+    EXPECT_EQ(initial_tab,
+              browser()->tab_strip_model()->GetActiveWebContents());
+    EXPECT_EQ(initial_url, initial_tab->GetLastCommittedURL());
   }
 
   // Checks that, after running |action|, the initial tab's window doesn't have
@@ -193,28 +258,36 @@
   }
 
   // Checks that no new windows are opened after running |action| and that the
-  // existing window is still the active one and navigated to |target_url|.
-  // Returns true if there were no errors.
-  bool TestTabActionDoesNotOpenAppWindow(const GURL& target_url,
+  // existing |browser| window is still the active one and navigated to
+  // |target_url|. Returns true if there were no errors.
+  bool TestTabActionDoesNotOpenAppWindow(Browser* browser,
+                                         const GURL& target_url,
                                          const base::Closure& action) {
     content::WebContents* initial_tab =
-        browser()->tab_strip_model()->GetActiveWebContents();
-    int num_tabs = browser()->tab_strip_model()->count();
-    size_t num_browsers = chrome::GetBrowserCount(profile());
+        browser->tab_strip_model()->GetActiveWebContents();
+    int num_tabs = browser->tab_strip_model()->count();
+    size_t num_browsers = chrome::GetBrowserCount(browser->profile());
 
     action.Run();
 
-    EXPECT_EQ(num_tabs, browser()->tab_strip_model()->count());
-    EXPECT_EQ(num_browsers, chrome::GetBrowserCount(profile()));
-    EXPECT_EQ(browser(), chrome::FindLastActive());
-    EXPECT_EQ(initial_tab,
-              browser()->tab_strip_model()->GetActiveWebContents());
+    EXPECT_EQ(num_tabs, browser->tab_strip_model()->count());
+    EXPECT_EQ(num_browsers, chrome::GetBrowserCount(browser->profile()));
+    EXPECT_EQ(browser, chrome::FindLastActive());
+    EXPECT_EQ(initial_tab, browser->tab_strip_model()->GetActiveWebContents());
     EXPECT_EQ(target_url, initial_tab->GetLastCommittedURL());
 
     return !HasFailure();
   }
 
   // Checks that no new windows are opened after running |action| and that the
+  // main browser window is still the active one and navigated to |target_url|.
+  // Returns true if there were no errors.
+  bool TestTabActionDoesNotOpenAppWindow(const GURL& target_url,
+                                         const base::Closure& action) {
+    return TestTabActionDoesNotOpenAppWindow(browser(), target_url, action);
+  }
+
+  // Checks that no new windows are opened after running |action| and that the
   // iframe in the initial tab navigated to |target_url|. Returns true if there
   // were no errors.
   bool TestIFrameActionDoesNotOpenAppWindow(const GURL& target_url,
@@ -426,6 +499,139 @@
                  out_of_scope_url, LinkTarget::SELF));
 }
 
+// Tests that submitting a form using POST does not open a new app window.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest,
+                       PostFormSubmission) {
+  InstallTestBookmarkApp();
+  NavigateToLaunchingPage();
+
+  const GURL in_scope_url = embedded_test_server()->GetURL(kInScopeUrlPath);
+  TestTabActionDoesNotOpenAppWindow(
+      in_scope_url,
+      base::Bind(&SubmitFormAndWait,
+                 browser()->tab_strip_model()->GetActiveWebContents(),
+                 in_scope_url, net::URLFetcher::RequestType::POST));
+}
+
+// Tests that submitting a form using GET does not open a new app window.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest, GetFormSubmission) {
+  InstallTestBookmarkApp();
+  NavigateToLaunchingPage();
+
+  GURL::Replacements replacements;
+  replacements.SetQuery("", url::Component(0, 0));
+  const GURL in_scope_form_url = embedded_test_server()
+                                     ->GetURL(kInScopeUrlPath)
+                                     .ReplaceComponents(replacements);
+  TestTabActionDoesNotOpenAppWindow(
+      in_scope_form_url,
+      base::Bind(&SubmitFormAndWait,
+                 browser()->tab_strip_model()->GetActiveWebContents(),
+                 in_scope_form_url, net::URLFetcher::RequestType::GET));
+}
+
+// Tests that prerender links don't open the app.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest, PrerenderLinks) {
+  InstallTestBookmarkApp();
+  NavigateToLaunchingPage();
+
+  TestTabActionDoesNotNavigateOrOpenAppWindow(base::Bind(
+      [](content::WebContents* web_contents, const GURL& target_url) {
+        std::string script = base::StringPrintf(
+            "(() => {"
+            "const prerender_link = document.createElement('link');"
+            "prerender_link.rel = 'prerender';"
+            "prerender_link.href = '%s';"
+            "prerender_link.addEventListener('webkitprerenderstop',"
+            "() => window.domAutomationController.send(true));"
+            "document.body.appendChild(prerender_link);"
+            "})();",
+            target_url.spec().c_str());
+        bool result;
+        ASSERT_TRUE(content::ExecuteScriptAndExtractBool(web_contents, script,
+                                                         &result));
+        ASSERT_TRUE(result);
+      },
+      browser()->tab_strip_model()->GetActiveWebContents(),
+      embedded_test_server()->GetURL(kInScopeUrlPath)));
+}
+
+// Tests fetch calls don't open a new App window.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest, Fetch) {
+  InstallTestBookmarkApp();
+  NavigateToLaunchingPage();
+
+  TestTabActionDoesNotNavigateOrOpenAppWindow(base::Bind(
+      [](content::WebContents* web_contents, const GURL& target_url) {
+        std::string script = base::StringPrintf(
+            "(() => {"
+            "fetch('%s').then(response => {"
+            "  window.domAutomationController.send(response.ok);"
+            "});"
+            "})();",
+            target_url.spec().c_str());
+        bool result;
+        ASSERT_TRUE(content::ExecuteScriptAndExtractBool(web_contents, script,
+                                                         &result));
+        ASSERT_TRUE(result);
+      },
+      browser()->tab_strip_model()->GetActiveWebContents(),
+      embedded_test_server()->GetURL(kInScopeUrlPath)));
+}
+
+// Tests that clicking "Open link in incognito window" to an in-scope URL opens
+// an incognito window and not an app window.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest, OpenInIncognito) {
+  InstallTestBookmarkApp();
+  NavigateToLaunchingPage();
+
+  size_t num_browsers = chrome::GetBrowserCount(profile());
+  int num_tabs = browser()->tab_strip_model()->count();
+  content::WebContents* initial_tab =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  GURL initial_url = initial_tab->GetLastCommittedURL();
+
+  const GURL in_scope_url = embedded_test_server()->GetURL(kInScopeUrlPath);
+  ui_test_utils::UrlLoadObserver url_observer(
+      in_scope_url, content::NotificationService::AllSources());
+  content::ContextMenuParams params;
+  params.page_url = initial_url;
+  params.link_url = in_scope_url;
+  TestRenderViewContextMenu menu(initial_tab->GetMainFrame(), params);
+  menu.Init();
+  menu.ExecuteCommand(IDC_CONTENT_CONTEXT_OPENLINKOFFTHERECORD,
+                      0 /* event_flags */);
+  url_observer.Wait();
+
+  Browser* incognito_browser = chrome::FindLastActive();
+  EXPECT_EQ(incognito_browser->profile(), profile()->GetOffTheRecordProfile());
+  EXPECT_NE(browser(), incognito_browser);
+  EXPECT_EQ(in_scope_url, incognito_browser->tab_strip_model()
+                              ->GetActiveWebContents()
+                              ->GetLastCommittedURL());
+
+  EXPECT_EQ(num_browsers, chrome::GetBrowserCount(profile()));
+  EXPECT_EQ(num_tabs, browser()->tab_strip_model()->count());
+  EXPECT_EQ(initial_tab, browser()->tab_strip_model()->GetActiveWebContents());
+  EXPECT_EQ(initial_url, initial_tab->GetLastCommittedURL());
+}
+
+// Tests that clicking a link to an in-scope URL when in incognito does not open
+// an App window.
+IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest,
+                       InScopeUrlIncognito) {
+  InstallTestBookmarkApp();
+  Browser* incognito_browser = CreateIncognitoBrowser();
+  NavigateToLaunchingPage(incognito_browser);
+
+  const GURL in_scope_url = embedded_test_server()->GetURL(kInScopeUrlPath);
+  TestTabActionDoesNotOpenAppWindow(
+      incognito_browser, in_scope_url,
+      base::Bind(&ClickLinkAndWait,
+                 incognito_browser->tab_strip_model()->GetActiveWebContents(),
+                 in_scope_url, LinkTarget::SELF));
+}
+
 // Tests that clicking links inside a website for an installed app doesn't open
 // a new browser window.
 IN_PROC_BROWSER_TEST_F(BookmarkAppUrlRedirectorBrowserTest,
diff --git a/chrome/browser/extensions/extension_apitest.cc b/chrome/browser/extensions/extension_apitest.cc
index f6a6fd44..f38bccb 100644
--- a/chrome/browser/extensions/extension_apitest.cc
+++ b/chrome/browser/extensions/extension_apitest.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/ui/extensions/application_launch.h"
 #include "chrome/common/extensions/extension_process_policy.h"
 #include "chrome/test/base/ui_test_utils.h"
+#include "content/public/common/browser_side_navigation_policy.h"
 #include "content/public/common/content_switches.h"
 #include "extensions/browser/api/test/test_api.h"
 #include "extensions/browser/extension_registry.h"
@@ -46,6 +47,7 @@
 const char kTestWebSocketPort[] = "testWebSocketPort";
 const char kFtpServerPort[] = "ftpServer.port";
 const char kEmbeddedTestServerPort[] = "testServer.port";
+const char kBrowserSideNavigationEnabled[] = "browserSideNavigationEnabled";
 
 std::unique_ptr<net::test_server::HttpResponse> HandleServerRedirectRequest(
     const net::test_server::HttpRequest& request) {
@@ -159,6 +161,8 @@
   test_config_.reset(new base::DictionaryValue());
   test_config_->SetString(kTestDataDirectory,
                           net::FilePathToFileURL(test_data_dir_).spec());
+  test_config_->SetBoolean(kBrowserSideNavigationEnabled,
+                           content::IsBrowserSideNavigationEnabled());
   extensions::TestGetConfigFunction::set_test_config_state(
       test_config_.get());
 }
diff --git a/chrome/browser/extensions/extension_service.cc b/chrome/browser/extensions/extension_service.cc
index 0c27d53..66404cb 100644
--- a/chrome/browser/extensions/extension_service.cc
+++ b/chrome/browser/extensions/extension_service.cc
@@ -2062,6 +2062,9 @@
       registry_->terminated_extensions().GetByID(lowercase_id);
   registry_->RemoveTerminated(lowercase_id);
   if (extension) {
+    // TODO: This notification was already sent when the extension was
+    // unloaded as part of being terminated. But we send it again as observers
+    // may be tracking the terminated extension. See crbug.com/708230.
     content::NotificationService::current()->Notify(
         extensions::NOTIFICATION_EXTENSION_REMOVED,
         content::Source<Profile>(profile_),
diff --git a/chrome/browser/policy/profile_policy_connector.cc b/chrome/browser/policy/profile_policy_connector.cc
index 53c85a9..ada5cadc0 100644
--- a/chrome/browser/policy/profile_policy_connector.cc
+++ b/chrome/browser/policy/profile_policy_connector.cc
@@ -37,21 +37,6 @@
 
 namespace policy {
 
-namespace {
-
-std::string GetStoreManagementDomain(const CloudPolicyStore* policy_store) {
-  if (policy_store) {
-    CHECK(policy_store->is_initialized())
-        << "Cloud policy management domain must be "
-           "requested only after the policy system is fully initialized";
-    if (policy_store->is_managed() && policy_store->policy()->has_username())
-      return gaia::ExtractDomainName(policy_store->policy()->username());
-  }
-  return std::string();
-}
-
-}  // namespace
-
 ProfilePolicyConnector::ProfilePolicyConnector() {}
 
 ProfilePolicyConnector::~ProfilePolicyConnector() {}
@@ -128,6 +113,13 @@
       connector->SetUserPolicyDelegate(special_user_policy_provider_.get());
   }
 #endif
+
+#if defined(OS_CHROMEOS)
+  if (user && user->IsActiveDirectoryUser()) {
+    management_realm_ =
+        gaia::ExtractDomainName(user->GetAccountId().GetUserEmail());
+  }
+#endif
 }
 
 void ProfilePolicyConnector::InitForTesting(
@@ -164,8 +156,22 @@
 
 std::string ProfilePolicyConnector::GetManagementDomain() const {
   const CloudPolicyStore* actual_policy_store = GetActualPolicyStore();
-  if (actual_policy_store)
-    return GetStoreManagementDomain(actual_policy_store);
+  if (!actual_policy_store)
+    return std::string();
+  CHECK(actual_policy_store->is_initialized())
+      << "Cloud policy management domain must be "
+         "requested only after the policy system is fully initialized";
+  if (!actual_policy_store->is_managed())
+    return std::string();
+
+#if defined(OS_CHROMEOS)
+  if (!management_realm_.empty())
+    return management_realm_;
+#endif  // defined(OS_CHROMEOS)
+
+  if (actual_policy_store->policy()->has_username())
+    return gaia::ExtractDomainName(actual_policy_store->policy()->username());
+
   return std::string();
 }
 
diff --git a/chrome/browser/policy/profile_policy_connector.h b/chrome/browser/policy/profile_policy_connector.h
index f0efc439..975a0b4 100644
--- a/chrome/browser/policy/profile_policy_connector.h
+++ b/chrome/browser/policy/profile_policy_connector.h
@@ -83,6 +83,9 @@
   bool is_primary_user_ = false;
 
   std::unique_ptr<ConfigurationPolicyProvider> special_user_policy_provider_;
+
+  // Management realm for Active Directory users. Empty for other users.
+  std::string management_realm_;
 #endif  // defined(OS_CHROMEOS)
 
   std::unique_ptr<ConfigurationPolicyProvider>
diff --git a/chrome/browser/policy/profile_policy_connector_unittest.cc b/chrome/browser/policy/profile_policy_connector_unittest.cc
index 8a7d9ddb..278dae07 100644
--- a/chrome/browser/policy/profile_policy_connector_unittest.cc
+++ b/chrome/browser/policy/profile_policy_connector_unittest.cc
@@ -23,9 +23,16 @@
 #include "components/policy/core/common/schema_registry.h"
 #include "components/policy/policy_constants.h"
 #include "components/policy/proto/device_management_backend.pb.h"
+#include "components/signin/core/account_id/account_id.h"
+#include "components/user_manager/user.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+#if defined(OS_CHROMEOS)
+#include "chrome/browser/chromeos/login/users/fake_chrome_user_manager.h"
+#include "chrome/browser/chromeos/login/users/scoped_user_manager_enabler.h"
+#endif  // defined(OS_CHROMEOS)
+
 using testing::Return;
 using testing::_;
 
@@ -56,6 +63,12 @@
     cloud_policy_manager_->Shutdown();
   }
 
+  std::unique_ptr<user_manager::User> CreateRegularUser(
+      const AccountId& account_id) const {
+    return base::WrapUnique<user_manager::User>(
+        user_manager::User::CreateRegularUser(account_id));
+  }
+
   // Needs to be the first member.
   base::test::ScopedTaskEnvironment scoped_task_environment_;
   SchemaRegistry schema_registry_;
@@ -82,6 +95,33 @@
   connector.Shutdown();
 }
 
+#if defined(OS_CHROMEOS)
+TEST_F(ProfilePolicyConnectorTest, ManagedRealmForActiveDirectoryUsers) {
+  chromeos::ScopedUserManagerEnabler scoped_user_manager_enabler(
+      new chromeos::FakeChromeUserManager);
+  ProfilePolicyConnector connector;
+  const AccountId account_id =
+      AccountId::AdFromUserEmailObjGuid("user@realm.example", "obj-guid");
+  std::unique_ptr<user_manager::User> user = CreateRegularUser(account_id);
+  connector.Init(user.get(), &schema_registry_, cloud_policy_manager_.get(),
+                 &cloud_policy_store_, false);
+  cloud_policy_store_.policy_.reset(new enterprise_management::PolicyData());
+  cloud_policy_store_.policy_->set_state(
+      enterprise_management::PolicyData::ACTIVE);
+  EXPECT_TRUE(connector.IsManaged());
+  EXPECT_EQ(connector.GetManagementDomain(), "realm.example");
+
+  // Policy username does not override management realm for Active Directory
+  // user.
+  cloud_policy_store_.policy_->set_username("test@testdomain.com");
+  EXPECT_TRUE(connector.IsManaged());
+  EXPECT_EQ(connector.GetManagementDomain(), "realm.example");
+
+  // Cleanup.
+  connector.Shutdown();
+}
+#endif  // defined(OS_CHROMEOS)
+
 TEST_F(ProfilePolicyConnectorTest, IsProfilePolicy) {
   ProfilePolicyConnector connector;
   connector.Init(nullptr /* user */, &schema_registry_,
diff --git a/chrome/browser/printing/print_preview_message_handler.cc b/chrome/browser/printing/print_preview_message_handler.cc
index ae2ff22..a8f84dd 100644
--- a/chrome/browser/printing/print_preview_message_handler.cc
+++ b/chrome/browser/printing/print_preview_message_handler.cc
@@ -227,6 +227,7 @@
   if (handled)
     return true;
 
+  handled = true;
   IPC_BEGIN_MESSAGE_MAP(PrintPreviewMessageHandler, message)
     IPC_MESSAGE_HANDLER(PrintHostMsg_DidGetPreviewPageCount,
                         OnDidGetPreviewPageCount)
diff --git a/chrome/browser/ui/ash/session_controller_client.cc b/chrome/browser/ui/ash/session_controller_client.cc
index 168cc61..ee2af65 100644
--- a/chrome/browser/ui/ash/session_controller_client.cc
+++ b/chrome/browser/ui/ash/session_controller_client.cc
@@ -325,12 +325,11 @@
   SendUserSession(*added_user);
 }
 
-void SessionControllerClient::UserChangedChildStatus(User* user) {
-  SendUserSession(*user);
+void SessionControllerClient::OnUserImageChanged(const User& user) {
+  SendUserSession(user);
 }
 
-void SessionControllerClient::OnUserImageChanged(
-    const user_manager::User& user) {
+void SessionControllerClient::OnChildStatusChanged(const User& user) {
   SendUserSession(user);
 }
 
diff --git a/chrome/browser/ui/ash/session_controller_client.h b/chrome/browser/ui/ash/session_controller_client.h
index fd31b48..34f7e6c 100644
--- a/chrome/browser/ui/ash/session_controller_client.h
+++ b/chrome/browser/ui/ash/session_controller_client.h
@@ -80,10 +80,10 @@
   // user_manager::UserManager::UserSessionStateObserver:
   void ActiveUserChanged(const user_manager::User* active_user) override;
   void UserAddedToSession(const user_manager::User* added_user) override;
-  void UserChangedChildStatus(user_manager::User* user) override;
 
   // user_manager::UserManager::Observer
   void OnUserImageChanged(const user_manager::User& user) override;
+  void OnChildStatusChanged(const user_manager::User& user) override;
 
   // session_manager::SessionManagerObserver:
   void OnSessionStateChanged() override;
diff --git a/chrome/browser/ui/webui/chromeos/network_element_localized_strings_provider.cc b/chrome/browser/ui/webui/chromeos/network_element_localized_strings_provider.cc
index cf7e2b8..2411229 100644
--- a/chrome/browser/ui/webui/chromeos/network_element_localized_strings_provider.cc
+++ b/chrome/browser/ui/webui/chromeos/network_element_localized_strings_provider.cc
@@ -150,6 +150,9 @@
       {"OncVPN-ThirdPartyVPN-ProviderName",
        IDS_ONC_VPN_THIRD_PARTY_VPN_PROVIDER_NAME},
       {"OncVPN-Type", IDS_ONC_VPN_TYPE},
+      {"OncVPN-Type_L2TP-IPsec", IDS_ONC_VPN_TYPE_L2TP_IPSEC},
+      {"OncVPN-Type_OpenVPN", IDS_ONC_VPN_TYPE_OPENVPN},
+      {"OncVPN-Type_ARCVPN", IDS_ONC_VPN_TYPE_ARCVPN},
       {"OncWiFi-Frequency", IDS_ONC_WIFI_FREQUENCY},
       {"OncWiFi-Passphrase", IDS_ONC_WIFI_PASSWORD},
       {"OncWiFi-SSID", IDS_ONC_WIFI_SSID},
diff --git a/chrome/renderer/extensions/automation_internal_custom_bindings.cc b/chrome/renderer/extensions/automation_internal_custom_bindings.cc
index 42b314d..271c113 100644
--- a/chrome/renderer/extensions/automation_internal_custom_bindings.cc
+++ b/chrome/renderer/extensions/automation_internal_custom_bindings.cc
@@ -67,14 +67,14 @@
 // Adjust the bounding box of a node from local to global coordinates,
 // walking up the parent hierarchy to offset by frame offsets and
 // scroll offsets.
-static gfx::Rect ComputeGlobalNodeBounds(
-    TreeCache* cache,
-    ui::AXNode* node,
-    gfx::RectF local_bounds = gfx::RectF()) {
+static gfx::Rect ComputeGlobalNodeBounds(TreeCache* cache,
+                                         ui::AXNode* node,
+                                         gfx::RectF local_bounds = gfx::RectF(),
+                                         bool* offscreen = nullptr) {
   gfx::RectF bounds = local_bounds;
 
   while (node) {
-    bounds = cache->tree.RelativeToTreeBounds(node, bounds);
+    bounds = cache->tree.RelativeToTreeBounds(node, bounds, offscreen);
 
     TreeCache* previous_cache = cache;
     ui::AXNode* parent = cache->owner->GetParent(cache->tree.root(), &cache);
@@ -1004,8 +1004,10 @@
   gin::DataObjectBuilder state(isolate);
   uint32_t state_pos = 0, state_shifter = node->data().state;
   while (state_shifter) {
-    if (state_shifter & 1)
-      state.Set(ToString(static_cast<ui::AXState>(state_pos)), true);
+    if (state_pos != ui::AX_STATE_OFFSCREEN) {
+      if (state_shifter & 1)
+        state.Set(ToString(static_cast<ui::AXState>(state_pos)), true);
+    }
     state_shifter = state_shifter >> 1;
     state_pos++;
   }
@@ -1022,6 +1024,11 @@
   if (focused)
     state.Set("focused", true);
 
+  bool offscreen = false;
+  ComputeGlobalNodeBounds(cache, node, gfx::RectF(), &offscreen);
+  if (offscreen)
+    state.Set(ToString(ui::AX_STATE_OFFSCREEN), true);
+
   args.GetReturnValue().Set(state.Build());
 }
 
diff --git a/chrome/test/chromedriver/server/chromedriver_server.cc b/chrome/test/chromedriver/server/chromedriver_server.cc
index 8f940fa..0903de1 100644
--- a/chrome/test/chromedriver/server/chromedriver_server.cc
+++ b/chrome/test/chromedriver/server/chromedriver_server.cc
@@ -29,6 +29,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/task_scheduler/task_scheduler.h"
 #include "base/threading/thread.h"
 #include "base/threading/thread_local.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -335,7 +336,13 @@
     printf("Unable to initialize logging. Exiting...\n");
     return 1;
   }
+
+  base::TaskScheduler::CreateAndStartWithDefaultParams("ChromeDriver");
+
   RunServer(port, allow_remote, whitelisted_ips, url_base, adb_port,
             std::move(port_server));
+
+  // clean up
+  base::TaskScheduler::GetInstance()->Shutdown();
   return 0;
 }
diff --git a/chrome/test/data/extensions/api_test/networking_private/chromeos/test.js b/chrome/test/data/extensions/api_test/networking_private/chromeos/test.js
index c2fc323..771f1ad8 100644
--- a/chrome/test/data/extensions/api_test/networking_private/chromeos/test.js
+++ b/chrome/test/data/extensions/api_test/networking_private/chromeos/test.js
@@ -776,8 +776,10 @@
           assertEq(network_guid, result.GUID);
           var new_properties = {
             Priority: 1,
+            Type: 'VPN',
             VPN: {
-              Host: 'vpn.host1'
+              Host: 'vpn.host1',
+              Type: 'OpenVPN',
             }
           };
           chrome.networkingPrivate.setProperties(
diff --git a/chrome/test/data/extensions/api_test/webrequest/framework.js b/chrome/test/data/extensions/api_test/webrequest/framework.js
index 1519bbc..4aec309 100644
--- a/chrome/test/data/extensions/api_test/webrequest/framework.js
+++ b/chrome/test/data/extensions/api_test/webrequest/framework.js
@@ -13,6 +13,7 @@
 var frameIdMap;
 var testWebSocketPort;
 var testServerPort;
+var usingBrowserSideNavigation = false;
 var testServer = "www.a.com";
 var defaultScheme = "http";
 var eventsCaptured;
@@ -27,6 +28,13 @@
   'onCompleted': [],
   'onErrorOccurred': []
 };
+// Requests initiated by an extension or user interaction with the browser is a
+// BROWSER_INITIATED action. If the request was instead initiated by a website
+// or code run in the context of a website then it's WEB_INITIATED.
+const initiators = {
+  BROWSER_INITIATED: 2,
+  WEB_INITIATED: 3
+};
 
 // If true, don't bark on events that were not registered via expect().
 // These events are recorded in capturedUnexpectedData instead of
@@ -45,6 +53,7 @@
   chrome.test.getConfig(function(config) {
     testServerPort = config.testServer.port;
     testWebSocketPort = config.testWebSocketPort;
+    usingBrowserSideNavigation = config.browserSideNavigationEnabled;
     chrome.test.runTests(tests);
   });
 }
@@ -63,14 +72,49 @@
 
 // Returns an URL from the test server, fixing up the port. Must be called
 // from within a test case passed to runTests.
-function getServerURL(path, opt_host, opt_scheme) {
+function getServerURL(opt_path, opt_host, opt_scheme) {
   if (!testServerPort)
     throw new Error("Called getServerURL outside of runTests.");
   var host = opt_host || testServer;
   var scheme = opt_scheme || defaultScheme;
+  var path = opt_path || '';
   return scheme + "://" + host + ":" + testServerPort + "/" + path;
 }
 
+// Throws error if an invalid navigation type was presented.
+function validateNavigationType(navigationType) {
+  if (navigationType == undefined)
+    throw new Error("A navigation type must be defined.");
+  if(Object.values(initiators).indexOf(navigationType) === -1)
+    throw new Error("Unknown navigation type.");
+}
+
+// Similar to getURL without the path. If tests are run with
+// --enable-browser-side-navigation (PlzNavigate) browser initiated navigation
+// will have no initiator. The |navigationType| specifies if the navigation was
+// performed by the browser or the renderer.
+function getDomain(navigationType) {
+  validateNavigationType(navigationType);
+  if (navigationType == initiators.BROWSER_INITIATED &&
+      usingBrowserSideNavigation)
+    return undefined;
+  else
+    return getURL('').slice(0,-1);
+}
+
+// Similar to getServerURL without the path. If tests are run with
+// --enable-browser-side-navigation (PlzNavigate) browser initiated navigation
+// will have no initiator. The |navigationType| specifies if the navigation was
+// performed by the browser or the renderer.
+function getServerDomain(navigationType, opt_host, opt_scheme) {
+  validateNavigationType(navigationType);
+  if (navigationType == initiators.BROWSER_INITIATED &&
+      usingBrowserSideNavigation)
+    return undefined;
+  else
+    return getServerURL(undefined, opt_host, opt_scheme).slice(0, -1);
+}
+
 // Helper to advance to the next test only when the tab has finished loading.
 // This is because tabs.update can sometimes fail if the tab is in the middle
 // of a navigation (from the previous test), resulting in flakiness.
@@ -127,6 +171,10 @@
     if (!('type' in expectedEventData[i].details)) {
       expectedEventData[i].details.type = "main_frame";
     }
+    if ('initiator' in expectedEventData[i].details &&
+        expectedEventData[i].details.initiator == undefined) {
+      delete expectedEventData[i].details.initiator;
+    }
   }
 }
 
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_blocking.js b/chrome/test/data/extensions/api_test/webrequest/test_blocking.js
index 3cd5d9b..c084ad6a 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_blocking.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_blocking.js
@@ -44,7 +44,8 @@
           details: {
             type: "main_frame",
             url: getURL("complexLoad/b.html"),
-            frameUrl: getURL("complexLoad/b.html")
+            frameUrl: getURL("complexLoad/b.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           },
           retval: {cancel: true}
         },
@@ -54,7 +55,8 @@
           details: {
             url: getURL("complexLoad/b.html"),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -79,7 +81,8 @@
             method: "GET",
             type: "main_frame",
             url: getURLHttpSimpleLoad(),
-            frameUrl: getURLHttpSimpleLoad()
+            frameUrl: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval: {cancel: false}
         },
@@ -87,13 +90,15 @@
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
         },
         { label: "onSendHeaders",
           event: "onSendHeaders",
           details: {
-            url: getURLHttpSimpleLoad()
+            url: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -102,6 +107,7 @@
             url: getURLHttpSimpleLoad(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval: {cancel: true}
         },
@@ -111,7 +117,8 @@
           details: {
             url: getURLHttpSimpleLoad(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -136,14 +143,16 @@
             method: "GET",
             type: "main_frame",
             url: getURLHttpSimpleLoad(),
-            frameUrl: getURLHttpSimpleLoad()
+            frameUrl: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval: {requestHeaders: [{name: "User-Agent"}]}
         },
@@ -153,7 +162,8 @@
           event: "onSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -162,6 +172,7 @@
             url: getURLHttpSimpleLoad(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onResponseStarted",
@@ -172,6 +183,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -182,6 +194,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -208,14 +221,16 @@
             method: "GET",
             type: "main_frame",
             url: getURLHttpSimpleLoad(),
-            frameUrl: getURLHttpSimpleLoad()
+            frameUrl: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval: {foo: "bar"}
         },
@@ -224,7 +239,8 @@
           event: "onSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -233,6 +249,7 @@
             url: getURLHttpSimpleLoad(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onResponseStarted",
@@ -243,6 +260,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -253,6 +271,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -274,7 +293,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("complexLoad/a.html"),
-            frameUrl: getURL("complexLoad/a.html")
+            frameUrl: getURL("complexLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           },
           retval: {redirectUrl: getURL("simpleLoad/a.html")}
         },
@@ -286,6 +306,7 @@
             fromCache: false,
             statusLine: "HTTP/1.1 307 Internal Redirect",
             statusCode: 307,
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeRequest-2",
@@ -293,6 +314,7 @@
           details: {
             url: getURL("simpleLoad/a.html"),
             frameUrl: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onResponseStarted",
@@ -302,6 +324,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -312,6 +335,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -345,13 +369,15 @@
           event: "onBeforeRequest",
           details: {
             url: getURLEchoUserAgent(),
-            frameUrl: getURLEchoUserAgent()
+            frameUrl: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
           retval: {requestHeaders: [{name: "User-Agent", value: "FoobarUA"}]}
@@ -359,7 +385,8 @@
         { label: "onSendHeaders",
           event: "onSendHeaders",
           details: {
-            url: getURLEchoUserAgent()
+            url: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -368,6 +395,7 @@
             url: getURLEchoUserAgent(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onResponseStarted",
@@ -378,6 +406,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -388,6 +417,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -420,13 +450,15 @@
           event: "onBeforeRequest",
           details: {
             url: getURLEchoUserAgent(),
-            frameUrl: getURLEchoUserAgent()
+            frameUrl: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
           retval: {requestHeaders: [{name: "User-Agent",
@@ -435,7 +467,8 @@
         { label: "onSendHeaders",
           event: "onSendHeaders",
           details: {
-            url: getURLEchoUserAgent()
+            url: getURLEchoUserAgent(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -444,6 +477,7 @@
             url: getURLEchoUserAgent(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onResponseStarted",
@@ -454,6 +488,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -464,6 +499,7 @@
             statusCode: 200,
             ip: "127.0.0.1",
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -496,13 +532,15 @@
             method: "GET",
             type: "main_frame",
             url: getURLSetCookie(),
-            frameUrl: getURLSetCookie()
+            frameUrl: getURLSetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLSetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
         },
@@ -510,6 +548,7 @@
           event: "onSendHeaders",
           details: {
             url: getURLSetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -519,6 +558,7 @@
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval_function: function(name, details) {
             responseHeaders = details.responseHeaders;
@@ -544,6 +584,7 @@
             statusLine: "HTTP/1.1 200 OK",
             ip: "127.0.0.1",
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -555,6 +596,7 @@
             statusLine: "HTTP/1.1 200 OK",
             ip: "127.0.0.1",
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -587,13 +629,15 @@
             method: "GET",
             type: "main_frame",
             url: getURLNonUTF8SetCookie(),
-            frameUrl: getURLNonUTF8SetCookie()
+            frameUrl: getURLNonUTF8SetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLNonUTF8SetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
         },
@@ -601,6 +645,7 @@
           event: "onSendHeaders",
           details: {
             url: getURLNonUTF8SetCookie(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -610,6 +655,7 @@
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval_function: function(name, details) {
             responseHeaders = details.responseHeaders;
@@ -642,6 +688,7 @@
             statusLine: "HTTP/1.1 200 OK",
             ip: "127.0.0.1",
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted",
@@ -653,6 +700,7 @@
             statusLine: "HTTP/1.1 200 OK",
             ip: "127.0.0.1",
             responseHeadersExist: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -685,20 +733,23 @@
             method: "GET",
             type: "main_frame",
             url: getURLHttpSimpleLoad(),
-            frameUrl: getURLHttpSimpleLoad()
+            frameUrl: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Note: no requestHeaders because we don't ask for them.
           },
         },
         { label: "onSendHeaders",
           event: "onSendHeaders",
           details: {
-            url: getURLHttpSimpleLoad()
+            url: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -707,6 +758,7 @@
             url: getURLHttpSimpleLoad(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
           retval: {redirectUrl: getURL("simpleLoad/a.html")}
         },
@@ -719,6 +771,7 @@
             statusCode: 302,
             fromCache: false,
             ip: "127.0.0.1",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeRequest-2",
@@ -726,6 +779,7 @@
           details: {
             url: getURL("simpleLoad/a.html"),
             frameUrl: getURL("simpleLoad/a.html"),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onResponseStarted",
@@ -735,6 +789,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -745,6 +800,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -759,8 +815,8 @@
     navigateAndWait(getURLHttpSimpleLoad());
   },
 
-  // Checks that synchronous XHR requests from ourself are invisible to blocking
-  // handlers.
+  // Checks that synchronous XHR requests from ourself are invisible to
+  // blocking handlers.
   function syncXhrsFromOurselfAreInvisible() {
     expect(
       [  // events
@@ -768,7 +824,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("simpleLoad/a.html"),
-            frameUrl: getURL("simpleLoad/a.html")
+            frameUrl: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a-onResponseStarted",
@@ -778,6 +835,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -788,17 +846,19 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
-        // We do not see onBeforeRequest for the XHR request here because it is
-        // handled by a blocking handler.
+        // We do not see onBeforeRequest for the XHR request here because it
+        // is handled by a blocking handler.
         { label: "x-onSendHeaders",
           event: "onSendHeaders",
           details: {
             url: getURLHttpXHRData(),
             tabId: 1,
             type: "xmlhttprequest",
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "x-onResponseStarted",
@@ -811,6 +871,7 @@
             tabId: 1,
             type: "xmlhttprequest",
             ip: "127.0.0.1",
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -824,6 +885,7 @@
             tabId: 1,
             type: "xmlhttprequest",
             ip: "127.0.0.1",
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -831,7 +893,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("complexLoad/b.jpg"),
-            frameUrl: getURL("complexLoad/b.jpg")
+            frameUrl: getURL("complexLoad/b.jpg"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "b-onResponseStarted",
@@ -841,6 +904,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -851,6 +915,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -871,8 +936,8 @@
     });
   },
 
-  // Checks that asynchronous XHR requests from ourself are visible to blocking
-  // handlers.
+  // Checks that asynchronous XHR requests from ourself are visible to
+  // blocking handlers.
   function asyncXhrsFromOurselfAreVisible() {
     expect(
       [  // events
@@ -880,7 +945,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("simpleLoad/a.html"),
-            frameUrl: getURL("simpleLoad/a.html")
+            frameUrl: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a-onResponseStarted",
@@ -890,6 +956,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -900,6 +967,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -911,6 +979,7 @@
             tabId: 1,
             type: "xmlhttprequest",
             frameUrl: "unknown frame URL",
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         {
@@ -920,6 +989,7 @@
             url: getURLHttpXHRData(),
             tabId: 1,
             type: "xmlhttprequest",
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "x-onSendHeaders",
@@ -928,6 +998,7 @@
             url: getURLHttpXHRData(),
             tabId: 1,
             type: "xmlhttprequest",
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "x-onResponseStarted",
@@ -941,6 +1012,7 @@
             type: "xmlhttprequest",
             ip: "127.0.0.1",
             // Request to chrome-extension:// url has no IP.
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         {
@@ -952,6 +1024,7 @@
             type: "xmlhttprequest",
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "x-onCompleted",
@@ -965,13 +1038,15 @@
             type: "xmlhttprequest",
             ip: "127.0.0.1",
             // Request to chrome-extension:// url has no IP.
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onBeforeRequest",
           event: "onBeforeRequest",
           details: {
             url: getURL("complexLoad/b.jpg"),
-            frameUrl: getURL("complexLoad/b.jpg")
+            frameUrl: getURL("complexLoad/b.jpg"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "b-onResponseStarted",
@@ -981,6 +1056,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -991,6 +1067,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -1028,6 +1105,7 @@
       "&src=" + encodeURIComponent(requestedUrl));
   var redirectTarget = getServerURL(
       "extensions/api_test/webrequest/cors/redirect_target.gif", "domain.tld");
+  var initiator = getServerDomain(initiators.WEB_INITIATED);
   expect(
     [  // events
       { label: "onBeforeRequest-1",
@@ -1037,6 +1115,7 @@
           url: requestedUrl,
           // Frame URL unavailable because requests are filtered by type=image.
           frameUrl: "unknown frame URL",
+          initiator: initiator
         },
         retval: {redirectUrl: redirectTarget}
       },
@@ -1049,6 +1128,7 @@
           statusLine: "HTTP/1.1 307 Internal Redirect",
           statusCode: 307,
           fromCache: false,
+          initiator: initiator
         }
       },
       { label: "onBeforeRequest-2",
@@ -1058,6 +1138,7 @@
           url: redirectTarget,
           // Frame URL unavailable because requests are filtered by type=image.
           frameUrl: "unknown frame URL",
+          initiator: initiator
         },
       },
       {
@@ -1066,6 +1147,7 @@
         details: {
           type: "image",
           url: redirectTarget,
+          initiator: initiator
         }
       },
       {
@@ -1074,6 +1156,7 @@
         details: {
           type: "image",
           url: redirectTarget,
+          initiator: initiator
         }
       },
       {
@@ -1084,6 +1167,7 @@
           url: redirectTarget,
           statusLine: "HTTP/1.1 200 OK",
           statusCode: 200,
+          initiator: initiator
         }
       },
       { label: "onResponseStarted",
@@ -1095,6 +1179,7 @@
           statusCode: 200,
           ip: "127.0.0.1",
           statusLine: "HTTP/1.1 200 OK",
+          initiator: initiator
         }
       },
       { label: "onCompleted",
@@ -1106,6 +1191,7 @@
           statusCode: 200,
           ip: "127.0.0.1",
           statusLine: "HTTP/1.1 200 OK",
+          initiator: initiator
         }
       },
       // After the image loads, the test will load the following URL
@@ -1118,6 +1204,7 @@
           url: getServerURL("signal_that_image_loaded_successfully"),
           // Frame URL unavailable because requests are filtered by type=image.
           frameUrl: "unknown frame URL",
+          initiator: initiator
         },
         retval: {cancel: true}
       },
@@ -1128,6 +1215,7 @@
           url: getServerURL("signal_that_image_loaded_successfully"),
           fromCache: false,
           error: "net::ERR_BLOCKED_BY_CLIENT",
+          initiator: initiator
         }
       },
     ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_complex.js b/chrome/test/data/extensions/api_test/webrequest/test_complex.js
index a66ac3b2..383ab9f 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_complex.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_complex.js
@@ -22,7 +22,8 @@
           details: {
             type: "main_frame",
             url: getURL("complexLoad/a.html"),
-            frameUrl: getURL("complexLoad/a.html")
+            frameUrl: getURL("complexLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "b.html-onBeforeRequest",
@@ -33,6 +34,7 @@
             frameUrl: getURL("complexLoad/b.html"),
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b.jpg-onBeforeRequest",
@@ -43,6 +45,7 @@
             frameUrl: getURL("complexLoad/b.html"),
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.html-onResponseStarted",
@@ -53,6 +56,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -66,6 +70,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -79,6 +84,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -90,6 +96,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -103,6 +110,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -116,6 +124,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -141,7 +150,8 @@
           details: {
             type: "main_frame",
             url: getURL("complexLoad/a.html"),
-            frameUrl: getURL("complexLoad/a.html")
+            frameUrl: getURL("complexLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "b-onBeforeRequest",
@@ -153,6 +163,7 @@
             frameUrl: "unknown frame URL",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a-onResponseStarted",
@@ -163,6 +174,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -176,6 +188,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -187,6 +200,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -200,6 +214,7 @@
             statusLine: "HTTP/1.1 200 OK",
             frameId: 1,
             parentFrameId: 0,
+            initiator: getDomain(initiators.WEB_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -230,7 +245,8 @@
           details: {
             type: "main_frame",
             url: getURLHttpXHR(),
-            frameUrl: getURLHttpXHR()
+            frameUrl: getURLHttpXHR(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders-1",
@@ -238,6 +254,7 @@
           details: {
             type: "main_frame",
             url: getURLHttpXHR(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onSendHeaders-1",
@@ -245,6 +262,7 @@
           details: {
             type: "main_frame",
             url: getURLHttpXHR(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived-1",
@@ -254,6 +272,7 @@
             url: getURLHttpXHR(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onResponseStarted-1",
@@ -265,6 +284,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onCompleted-1",
@@ -276,6 +296,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a.js-onBeforeRequest",
@@ -283,7 +304,8 @@
           details: {
             type: "script",
             url: getURLHttpXHRJavaScript(),
-            frameUrl: getURLHttpXHR()
+            frameUrl: getURLHttpXHR(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.js-onBeforeSendHeaders",
@@ -291,6 +313,7 @@
           details: {
             type: "script",
             url: getURLHttpXHRJavaScript(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.js-onSendHeaders",
@@ -298,6 +321,7 @@
           details: {
             type: "script",
             url: getURLHttpXHRJavaScript(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.js-onHeadersReceived",
@@ -307,6 +331,7 @@
             url: getURLHttpXHRJavaScript(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.js-onResponseStarted",
@@ -318,6 +343,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "a.js-onCompleted",
@@ -329,6 +355,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onBeforeRequest-2",
@@ -336,7 +363,8 @@
           details: {
             type: "xmlhttprequest",
             url: getURLHttpXHRData(),
-            frameUrl: getURLHttpXHR()
+            frameUrl: getURLHttpXHR(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders-2",
@@ -344,6 +372,7 @@
           details: {
             type: "xmlhttprequest",
             url: getURLHttpXHRData(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onSendHeaders-2",
@@ -351,6 +380,7 @@
           details: {
             type: "xmlhttprequest",
             url: getURLHttpXHRData(),
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onHeadersReceived-2",
@@ -360,6 +390,7 @@
             url: getURLHttpXHRData(),
             statusLine: "HTTP/1.1 200 OK",
             statusCode: 200,
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onResponseStarted-2",
@@ -371,6 +402,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onCompleted-2",
@@ -382,6 +414,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         }
       ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_declarative1.js b/chrome/test/data/extensions/api_test/webrequest/test_declarative1.js
index 8a5b286..51ea16a 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_declarative1.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_declarative1.js
@@ -65,23 +65,31 @@
         event: "onBeforeRequest",
         details: {
           url: getURLOfHTMLWithThirdParty(),
-          frameUrl: getURLOfHTMLWithThirdParty()
+          frameUrl: getURLOfHTMLWithThirdParty(),
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         }
       },
       { label: "onBeforeSendHeaders",
         event: "onBeforeSendHeaders",
-        details: {url: getURLOfHTMLWithThirdParty()}
+        details: {
+          url: getURLOfHTMLWithThirdParty(),
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
+        }
       },
       { label: "onSendHeaders",
         event: "onSendHeaders",
-        details: {url: getURLOfHTMLWithThirdParty()}
+        details: {
+          url: getURLOfHTMLWithThirdParty(),
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
+        }
       },
       { label: "onHeadersReceived",
         event: "onHeadersReceived",
         details: {
           url: getURLOfHTMLWithThirdParty(),
           statusLine: "HTTP/1.1 200 OK",
-          statusCode: 200
+          statusCode: 200,
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         }
       },
       { label: "onResponseStarted",
@@ -91,7 +99,8 @@
           fromCache: false,
           ip: "127.0.0.1",
           statusCode: 200,
-          statusLine: "HTTP/1.1 200 OK"
+          statusLine: "HTTP/1.1 200 OK",
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         }
       },
       { label: "onCompleted",
@@ -101,7 +110,8 @@
           ip: "127.0.0.1",
           url: getURLOfHTMLWithThirdParty(),
           statusCode: 200,
-          statusLine: "HTTP/1.1 200 OK"
+          statusLine: "HTTP/1.1 200 OK",
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         }
       },
       { label: "img-onBeforeRequest",
@@ -109,7 +119,8 @@
         details: {
           type: "image",
           url: "http://non_existing_third_party.com/image.png",
-          frameUrl: getURLOfHTMLWithThirdParty()
+          frameUrl: getURLOfHTMLWithThirdParty(),
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         }
       },
       { label: "img-onErrorOccurred",
@@ -118,7 +129,8 @@
           error: "net::ERR_BLOCKED_BY_CLIENT",
           fromCache: false,
           type: "image",
-          url: "http://non_existing_third_party.com/image.png"
+          url: "http://non_existing_third_party.com/image.png",
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         }
       },
     ];
@@ -143,7 +155,8 @@
           details: {
             url: getURLHttpWithHeaders(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -179,19 +192,22 @@
           event: "onBeforeRequest",
           details: {
             url: getURLHttpWithHeaders(),
-            frameUrl: getURLHttpWithHeaders()
+            frameUrl: getURLHttpWithHeaders(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeSendHeaders",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpWithHeaders(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onSendHeaders",
           event: "onSendHeaders",
           details: {
             url: getURLHttpWithHeaders(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onHeadersReceived",
@@ -199,7 +215,8 @@
           details: {
             statusLine: "HTTP/1.1 200 OK",
             url: getURLHttpWithHeaders(),
-            statusCode: 200
+            statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onErrorOccurred",
@@ -207,7 +224,8 @@
           details: {
             url: getURLHttpWithHeaders(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -267,7 +285,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURLOfHTMLWithThirdParty(),
-            frameUrl: getURLOfHTMLWithThirdParty()
+            frameUrl: getURLOfHTMLWithThirdParty(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onErrorOccurred",
@@ -275,7 +294,8 @@
           details: {
             url: getURLOfHTMLWithThirdParty(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -304,7 +324,8 @@
           details: {
             type: "main_frame",
             url: getURLHttpComplex(),
-            frameUrl: getURLHttpComplex()
+            frameUrl: getURLHttpComplex(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onBeforeRedirect",
@@ -315,6 +336,7 @@
             fromCache: false,
             statusLine: "HTTP/1.1 307 Internal Redirect",
             statusCode: 307,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeRequest-b",
@@ -323,6 +345,7 @@
             type: "main_frame",
             url: getURLHttpSimple(),
             frameUrl: getURLHttpSimple(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onCompleted",
@@ -333,6 +356,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -360,6 +384,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         // We cannot wait for onCompleted signals because these are not sent
@@ -376,6 +401,7 @@
             statusLine: "HTTP/1.1 307 Internal Redirect",
             statusCode: 307,
             type: "image",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "onBeforeRedirect-2",
@@ -390,6 +416,7 @@
             statusLine: "HTTP/1.1 307 Internal Redirect",
             statusCode: 307,
             type: "sub_frame",
+            initiator: getServerDomain(initiators.WEB_INITIATED)
           }
         },
       ],
@@ -418,7 +445,8 @@
           details: {
             type: "main_frame",
             url: getURLHttpWithHeaders(),
-            frameUrl: getURLHttpWithHeaders()
+            frameUrl: getURLHttpWithHeaders(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onBeforeRedirect",
@@ -430,6 +458,7 @@
             statusCode: 302,
             fromCache: false,
             ip: "127.0.0.1",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "onBeforeRequest-b",
@@ -438,6 +467,7 @@
             type: "main_frame",
             url: getURLHttpSimple(),
             frameUrl: getURLHttpSimple(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           },
         },
         { label: "onCompleted",
@@ -448,6 +478,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -475,6 +506,7 @@
             fromCache: false,
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
@@ -498,7 +530,8 @@
           details: {
             url: getURLHttpSimple(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_declarative2.js b/chrome/test/data/extensions/api_test/webrequest/test_declarative2.js
index fd23a658..3d957b2 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_declarative2.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_declarative2.js
@@ -162,6 +162,7 @@
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
             ip: "127.0.0.1",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         }
       ],
@@ -263,7 +264,8 @@
           details: {
             url: getURLHttpSimple(),
             fromCache: false,
-            error: "net::ERR_BLOCKED_BY_CLIENT"
+            error: "net::ERR_BLOCKED_BY_CLIENT",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED)
           }
         },
       ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_newTab.js b/chrome/test/data/extensions/api_test/webrequest/test_newTab.js
index ab46ce1..e54cdfd5 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_newTab.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_newTab.js
@@ -12,7 +12,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("newTab/a.html"),
-            frameUrl: getURL("newTab/a.html")
+            frameUrl: getURL("newTab/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a-onResponseStarted",
@@ -22,6 +23,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED)
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -32,7 +34,8 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
-            // Request to chrome-extension:// url has no IP.
+            initiator: getDomain(initiators.BROWSER_INITIATED)
+           // Request to chrome-extension:// url has no IP.
           }
         },
         { label: "b-onBeforeRequest",
@@ -41,6 +44,7 @@
             url: getURL("newTab/b.html"),
             frameUrl: getURL("newTab/b.html"),
             tabId: 1,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onResponseStarted",
@@ -52,6 +56,7 @@
             statusLine: "HTTP/1.1 200 OK",
             // Request to chrome-extension:// url has no IP.
             tabId: 1,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onCompleted",
@@ -63,6 +68,7 @@
             statusLine: "HTTP/1.1 200 OK",
             // Request to chrome-extension:// url has no IP.
             tabId: 1,
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
       ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_osdd.js b/chrome/test/data/extensions/api_test/webrequest/test_osdd.js
index 1477772..ba33198 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_osdd.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_osdd.js
@@ -28,6 +28,7 @@
           // of type "other".
           frameUrl: 'unknown frame URL',
           tabId: 0,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -36,6 +37,7 @@
           type: 'other',
           url: getOSDDURL(),
           tabId: 0,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -44,6 +46,7 @@
           type: 'other',
           url: getOSDDURL(),
           tabId: 0,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -54,6 +57,7 @@
           tabId: 0,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -66,6 +70,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -78,6 +83,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_post.js b/chrome/test/data/extensions/api_test/webrequest/test_post.js
index dc8707b..f0a1731 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_post.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_post.js
@@ -27,7 +27,8 @@
             method: "GET",
             type: "main_frame",
             url: getURL(dirName + formFile),
-            frameUrl: getURL(dirName + formFile)
+            frameUrl: getURL(dirName + formFile),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a-onResponseStarted",
@@ -38,7 +39,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "main_frame",
-            url: getURL(dirName + formFile)
+            url: getURL(dirName + formFile),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "a-onCompleted",
@@ -49,7 +51,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "main_frame",
-            url: getURL(dirName + formFile)
+            url: getURL(dirName + formFile),
+            initiator: getDomain(initiators.BROWSER_INITIATED)
           }
         },
         { label: "s-onBeforeRequest",
@@ -58,7 +61,8 @@
             method: "GET",
             type: "script",
             url: getURL("requestBody/submit.js"),
-            frameUrl: getURL(dirName + formFile)
+            frameUrl: getURL(dirName + formFile),
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "s-onResponseStarted",
@@ -69,7 +73,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "script",
-            url: getURL("requestBody/submit.js")
+            url: getURL("requestBody/submit.js"),
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "s-onCompleted",
@@ -80,7 +85,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "script",
-            url: getURL("requestBody/submit.js")
+            url: getURL("requestBody/submit.js"),
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onBeforeRequest",
@@ -94,7 +100,8 @@
               formData: formData
             } : {
               raw: [{bytes: {}}] // ArrayBuffer
-            }
+            },
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onResponseStarted",
@@ -105,7 +112,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "main_frame",
-            url: getURL("simpleLoad/a.html")
+            url: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         },
         { label: "b-onCompleted",
@@ -116,7 +124,8 @@
             statusCode: 200,
             statusLine: "HTTP/1.1 200 OK",
             type: "main_frame",
-            url: getURL("simpleLoad/a.html")
+            url: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.WEB_INITIATED)
           }
         }
       ],
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_simple.js b/chrome/test/data/extensions/api_test/webrequest/test_simple.js
index 5a2218d..7038f99 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_simple.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_simple.js
@@ -20,7 +20,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("simpleLoad/a.html"),
-            frameUrl: getURL("simpleLoad/a.html")
+            frameUrl: getURL("simpleLoad/a.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "a-onResponseStarted",
@@ -30,6 +31,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED),
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -40,6 +42,7 @@
             statusCode: 200,
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getDomain(initiators.BROWSER_INITIATED),
             // Request to chrome-extension:// url has no IP.
           }
         },
@@ -58,21 +61,24 @@
           event: "onBeforeRequest",
           details: {
             url: getURLHttpSimpleLoadRedirect(),
-            frameUrl: getURLHttpSimpleLoadRedirect()
+            frameUrl: getURLHttpSimpleLoadRedirect(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onBeforeSendHeaders-1",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoadRedirect(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onSendHeaders-1",
           event: "onSendHeaders",
           details: {
             url: getURLHttpSimpleLoadRedirect(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onHeadersReceived-1",
@@ -81,7 +87,8 @@
             url: getURLHttpSimpleLoadRedirect(),
             responseHeadersExist: true,
             statusLine: "HTTP/1.1 301 Moved Permanently",
-            statusCode: 301
+            statusCode: 301,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onBeforeRedirect",
@@ -93,28 +100,32 @@
             responseHeadersExist: true,
             ip: "127.0.0.1",
             fromCache: false,
-            statusLine: "HTTP/1.1 301 Moved Permanently"
+            statusLine: "HTTP/1.1 301 Moved Permanently",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onBeforeRequest-2",
           event: "onBeforeRequest",
           details: {
             url: getURLHttpSimpleLoad(),
-            frameUrl: getURLHttpSimpleLoad()
+            frameUrl: getURLHttpSimpleLoad(),
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onBeforeSendHeaders-2",
           event: "onBeforeSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onSendHeaders-2",
           event: "onSendHeaders",
           details: {
             url: getURLHttpSimpleLoad(),
-            requestHeadersValid: true
+            requestHeadersValid: true,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onHeadersReceived-2",
@@ -123,7 +134,8 @@
             url: getURLHttpSimpleLoad(),
             responseHeadersExist: true,
             statusLine: "HTTP/1.1 200 OK",
-            statusCode: 200
+            statusCode: 200,
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onResponseStarted",
@@ -135,6 +147,7 @@
             ip: "127.0.0.1",
             fromCache: false,
             statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onCompleted",
@@ -145,7 +158,8 @@
             ip: "127.0.0.1",
             fromCache: false,
             responseHeadersExist: true,
-            statusLine: "HTTP/1.1 200 OK"
+            statusLine: "HTTP/1.1 200 OK",
+            initiator: getServerDomain(initiators.BROWSER_INITIATED),
           }
         }
       ],
@@ -167,7 +181,8 @@
           event: "onBeforeRequest",
           details: {
             url: getURL("does_not_exist.html"),
-            frameUrl: getURL("does_not_exist.html")
+            frameUrl: getURL("does_not_exist.html"),
+            initiator: getDomain(initiators.BROWSER_INITIATED),
           }
         },
         { label: "onErrorOccurred",
@@ -176,6 +191,7 @@
             url: getURL("does_not_exist.html"),
             fromCache: false,
             error: "net::ERR_FILE_NOT_FOUND",
+            initiator: getDomain(initiators.BROWSER_INITIATED),
             // Request to chrome-extension:// url has no IP.
           }
         },
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_types.js b/chrome/test/data/extensions/api_test/webrequest/test_types.js
index 48933454..013660f 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_types.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_types.js
@@ -70,6 +70,7 @@
           // tabId 0 = tab opened by test runner;
           // tabId 1 = this tab.
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -78,6 +79,7 @@
           type: 'stylesheet',
           url: getStyleURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -86,6 +88,7 @@
           type: 'stylesheet',
           url: getStyleURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -96,6 +99,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -108,6 +112,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -120,6 +125,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -228,6 +234,7 @@
           // tabId 0 = tab opened by test runner;
           // tabId 1 = this tab.
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -236,6 +243,7 @@
           type: 'font',
           url: getFontURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -244,6 +252,7 @@
           type: 'font',
           url: getFontURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -254,6 +263,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -266,6 +276,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -278,6 +289,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -297,6 +309,7 @@
           // tabId 0 = tab opened by test runner;
           // tabId 1 = this tab.
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -305,6 +318,7 @@
           type: 'script',
           url: getWorkerURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -313,6 +327,7 @@
           type: 'script',
           url: getWorkerURL(),
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -323,6 +338,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -335,6 +351,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -347,6 +364,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -372,6 +390,7 @@
           frameUrl: 'unknown frame URL',
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -382,6 +401,7 @@
           url: getPingURL(),
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -392,6 +412,7 @@
           url: getPingURL(),
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -404,6 +425,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -418,6 +440,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -432,6 +455,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -454,6 +478,7 @@
           frameUrl: 'unknown frame URL',
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -464,6 +489,7 @@
           url: getBeaconURL(),
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -474,6 +500,7 @@
           url: getBeaconURL(),
           frameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -486,6 +513,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -500,6 +528,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -514,6 +543,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -534,6 +564,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -545,6 +576,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -556,6 +588,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -572,6 +605,7 @@
           tabId: -1,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -587,6 +621,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -602,6 +637,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 200 OK',
           statusCode: 200,
+          initiator: getDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -629,6 +665,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -640,6 +677,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -651,6 +689,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onHeadersReceived',
@@ -664,6 +703,7 @@
           tabId: 1,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onResponseStarted',
@@ -679,6 +719,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       },
       { label: 'onCompleted',
@@ -694,6 +735,7 @@
           fromCache: false,
           statusLine: 'HTTP/1.1 404 Not Found',
           statusCode: 404,
+          initiator: getServerDomain(initiators.WEB_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_unload1.js b/chrome/test/data/extensions/api_test/webrequest/test_unload1.js
index d46ecdd..a2b4ad10 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_unload1.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_unload1.js
@@ -7,6 +7,7 @@
   // removes it.
   function insertSlowCrossOriginFrameAndRemove() {
     const url = getSlowURL('frame-in-extension-url');
+    const initiator = getServerDomain(initiators.BROWSER_INITIATED);
 
     expect([
       { label: 'onBeforeRequest',
@@ -18,6 +19,7 @@
           parentFrameId: 0,
           frameUrl: url,
           tabId: 1,
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -28,6 +30,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         },
       },
       { label: 'onSendHeaders',
@@ -38,6 +41,7 @@
           frameId: 1,
           parentFrameId: 0,
           tabId: 1,
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         },
       },
       { label: 'onErrorOccurred',
@@ -50,6 +54,7 @@
           tabId: 1,
           fromCache: false,
           error: 'net::ERR_ABORTED',
+          initiator: getServerDomain(initiators.BROWSER_INITIATED)
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_unload5.js b/chrome/test/data/extensions/api_test/webrequest/test_unload5.js
index 2562df0..be5afb2 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_unload5.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_unload5.js
@@ -10,6 +10,7 @@
   function startXMLHttpRequestAndRemoveFrame() {
     const hostname = 'slow-resourcetype-xhr-immediately-remove-frame';
     const url = getSlowURL(hostname);
+    const initiator = getServerDomain(initiators.WEB_INITIATED, hostname);
     const mainUrl = getPageWithFrame('empty.html', hostname);
 
     expect([
@@ -21,6 +22,7 @@
           frameId: 1,
           parentFrameId: 0,
           frameUrl: 'unknown frame URL',
+          initiator: initiator
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -30,6 +32,7 @@
           url,
           frameId: 1,
           parentFrameId: 0,
+          initiator: initiator
         },
       },
       { label: 'onSendHeaders',
@@ -39,6 +42,7 @@
           url,
           frameId: 1,
           parentFrameId: 0,
+          initiator: initiator
         },
       },
       { label: 'onErrorOccurred',
@@ -50,6 +54,7 @@
           parentFrameId: 0,
           fromCache: false,
           error: 'net::ERR_ABORTED',
+          initiator: initiator
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
@@ -76,6 +81,7 @@
   function startXMLHttpRequestAndRemoveTab() {
     const hostname = 'slow-resourcetype-xhr-immediately-remove-tab';
     const url = getSlowURL(hostname);
+    const initiator = getServerDomain(initiators.WEB_INITIATED, hostname);
     const mainUrl = getServerURL('empty.html', hostname);
 
     expect([
@@ -86,6 +92,7 @@
           url,
           frameUrl: 'unknown frame URL',
           tabId: 1,
+          initiator: initiator
         }
       },
       { label: 'onBeforeSendHeaders',
@@ -94,6 +101,7 @@
           type: 'xmlhttprequest',
           url,
           tabId: 1,
+          initiator: initiator
         },
       },
       { label: 'onSendHeaders',
@@ -102,6 +110,7 @@
           type: 'xmlhttprequest',
           url,
           tabId: 1,
+          initiator: initiator
         },
       },
       { label: 'onErrorOccurred',
@@ -112,6 +121,7 @@
           fromCache: false,
           error: 'net::ERR_ABORTED',
           tabId: 1,
+          initiator: initiator
         },
       }],
       [['onBeforeRequest', 'onBeforeSendHeaders', 'onSendHeaders',
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_unload6.js b/chrome/test/data/extensions/api_test/webrequest/test_unload6.js
index de78e2e..40d37e9 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_unload6.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_unload6.js
@@ -51,6 +51,7 @@
     const mainUrl = getServerURL('empty.html', hostname1);
     const frameUrl1 = getSlowURL(hostname1);
     const frameUrl2 = getSlowURL(hostname2);
+    const initiator = getServerDomain(initiators.WEB_INITIATED, hostname1)
 
     awaitOnErrorOccurred(2, function(results) {
       // The order of the URLs doesn't matter.
@@ -69,6 +70,7 @@
         tabId,
         type: 'sub_frame',
         fromCache: false,
+        initiator: initiator,
         error: 'net::ERR_ABORTED',
       }, {
         method: 'GET',
@@ -77,6 +79,7 @@
         tabId,
         type: 'sub_frame',
         fromCache: false,
+        initiator: initiator,
         error: 'net::ERR_ABORTED',
       }], results);
     });
@@ -113,6 +116,7 @@
         tabId,
         type: 'sub_frame',
         fromCache: false,
+        initiator: getServerDomain(initiators.WEB_INITIATED, hostname1),
         error: 'net::ERR_ABORTED',
       }], results);
     });
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_websocket.js b/chrome/test/data/extensions/api_test/webrequest/test_websocket.js
index 87c8198b..c260460 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_websocket.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_websocket.js
@@ -17,6 +17,7 @@
               type: 'websocket',
               // TODO(pkalinnikov): Figure out why the frame URL is unknown.
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onBeforeSendHeaders',
@@ -24,6 +25,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -31,6 +33,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onHeadersReceived',
@@ -40,6 +43,7 @@
               type: 'websocket',
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onResponseStarted',
@@ -51,6 +55,7 @@
               fromCache: false,
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onCompleted',
@@ -61,6 +66,7 @@
               fromCache: false,
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
         ],
@@ -86,6 +92,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
             retval: {cancel: true}
           },
@@ -96,6 +103,7 @@
               url: url,
               type: 'websocket',
               fromCache: false,
+              initiator: getDomain(initiators.WEB_INITIATED),
               error: 'net::ERR_BLOCKED_BY_CLIENT'
             }
           },
@@ -123,6 +131,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
             retval: {redirectUrl: redirectedUrl1}
           },
@@ -131,6 +140,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -138,6 +148,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onHeadersReceived',
@@ -147,6 +158,7 @@
               type: 'websocket',
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
             retval: {redirectUrl: redirectedUrl2}
           },
@@ -157,6 +169,7 @@
               type: 'websocket',
               ip: '127.0.0.1',
               fromCache: false,
+              initiator: getDomain(initiators.WEB_INITIATED),
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
             },
@@ -167,6 +180,7 @@
               url: url,
               type: 'websocket',
               fromCache: false,
+              initiator: getDomain(initiators.WEB_INITIATED),
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
             }
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_websocket_auth.js b/chrome/test/data/extensions/api_test/webrequest/test_websocket_auth.js
index b99416ca..79fc288 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_websocket_auth.js
+++ b/chrome/test/data/extensions/api_test/webrequest/test_websocket_auth.js
@@ -15,6 +15,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onBeforeSendHeaders',
@@ -22,6 +23,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -29,6 +31,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onHeadersReceived',
@@ -39,6 +42,7 @@
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
               responseHeadersExist: true,
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onAuthRequired',
@@ -53,6 +57,7 @@
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
               responseHeadersExist: true,
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onResponseStarted',
@@ -65,6 +70,7 @@
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
               responseHeadersExist: true,
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onErrorOccurred',
@@ -74,6 +80,7 @@
               type: 'websocket',
               fromCache: false,
               error: 'net::ERR_ABORTED',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
         ],
@@ -99,6 +106,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onBeforeSendHeaders',
@@ -106,6 +114,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -113,6 +122,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onHeadersReceived',
@@ -122,6 +132,7 @@
               type: 'websocket',
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onAuthRequired',
@@ -135,6 +146,7 @@
               challenger: {host: 'localhost', port: testWebSocketPort},
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onResponseStarted',
@@ -146,6 +158,7 @@
               ip: '127.0.0.1',
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onErrorOccurred',
@@ -155,6 +168,7 @@
               type: 'websocket',
               fromCache: false,
               error: 'net::ERR_ABORTED',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
         ],
@@ -178,6 +192,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onBeforeSendHeaders',
@@ -185,6 +200,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -192,6 +208,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onHeadersReceived',
@@ -201,6 +218,7 @@
               type: 'websocket',
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onAuthRequired',
@@ -214,6 +232,7 @@
               challenger: {host: 'localhost', port: testWebSocketPort},
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
             retval: {cancel: true}
           },
@@ -226,6 +245,7 @@
               ip: '127.0.0.1',
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onErrorOccurred',
@@ -235,6 +255,7 @@
               type: 'websocket',
               fromCache: false,
               error: 'net::ERR_ABORTED',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
         ],
@@ -258,6 +279,7 @@
               url: url,
               type: 'websocket',
               frameUrl: 'unknown frame URL',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onBeforeSendHeaders',
@@ -265,6 +287,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onSendHeaders',
@@ -272,6 +295,7 @@
             details: {
               url: url,
               type: 'websocket',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onHeadersReceived',
@@ -281,6 +305,7 @@
               type: 'websocket',
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
           { label: 'onAuthRequired',
@@ -294,6 +319,7 @@
               challenger: {host: 'localhost', port: testWebSocketPort},
               statusCode: 401,
               statusLine: 'HTTP/1.0 401 Unauthorized',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
             // Note: The test WebSocket server accepts only these credentials.
             retval: {authCredentials: {username: 'test', password: 'test'}}
@@ -307,6 +333,7 @@
               fromCache: false,
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             },
           },
           { label: 'onCompleted',
@@ -317,6 +344,7 @@
               fromCache: false,
               statusCode: 101,
               statusLine: 'HTTP/1.1 101 Switching Protocols',
+              initiator: getDomain(initiators.WEB_INITIATED)
             }
           },
         ],
diff --git a/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/background.js b/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/background.js
new file mode 100644
index 0000000..56fd3c2
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/background.js
@@ -0,0 +1,10 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+chrome.webRequest.onBeforeRequest.addListener((details) => {
+  chrome.test.sendMessage(
+      (details.initiator === undefined) ? 'NO_INITIATOR' : details.initiator);
+}, {urls: ['*://*/extensions/api_test/webrequest/xhr/data.json']}, []);
+
+chrome.test.sendMessage('ready');
diff --git a/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/manifest.json b/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/manifest.json
new file mode 100644
index 0000000..7ba8e971
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/webrequest_permissions/initiator/manifest.json
@@ -0,0 +1,15 @@
+{
+  "name": "webRequest permissions initiator",
+  "version": "1.0",
+  "manifest_version": 2,
+  "description": "Tests circumstances of exposing initiator in webRequest API.",
+  "permissions": [
+      "webRequest",
+      "*://*.example.com/*",
+      "*://*.example2.com/*",
+      "*://*.example3.com/*",
+      "*://*.example4.com/*"],
+  "background": {
+    "scripts": ["background.js"]
+  }
+}
diff --git a/components/feature_engagement/README.md b/components/feature_engagement/README.md
index f632bc1..51a4a2b 100644
--- a/components/feature_engagement/README.md
+++ b/components/feature_engagement/README.md
@@ -288,6 +288,7 @@
 {
   "availability": "{Comparator}",
   "session_rate": "{Comparator}",
+  "session_rate_impact": "{SessionRateImpact}",
   "event_used": "{EventConfig}",
   "event_trigger": "{EventConfig}",
   "event_???": "{EventConfig}",
@@ -308,6 +309,10 @@
         end user session.
     *   The value of the `Comparator` is a count of total In-Product Help
         displayed in the current end user session.
+*   `session_rate_impact`
+    *   Which other in-product help features showing the current IPH impacts.
+    *   By default, a feature impacts every other feature.
+    *   See `SessionRateImpact` below for details.
 *   `event_used`
     *   Relates to what the in-product help wants to highlight, i.e. teach the
         user about and increase usage of.
@@ -410,6 +415,33 @@
 <15
 ```
 
+### SessionRateImpact
+
+Format: ```[all|none|comma-separated list]```
+
+*   `all` means this feature impacts every other feature regarding their
+    `session_rate` calculations. This is the default.
+*   `none` means that this feature does not impact any other features regarding
+    the `session_rate`. This feature may therefore be shown an unlimited amount
+    of times, without making other features go over their `session_rate` config.
+*   `[comma-separated list]` means that this feature only impacts the particular
+    features listed. Use the `base::Feature` name of the feature in the list.
+    For features in the list, this feature will affect their `session_rate`
+    conditions, and for features not in the list, this feature will not affect
+    their `session_rate` calculations.
+    *   It is *NOT* valid to use the feature names `all` or `none`. They must
+        only be used alone with no comma, at which point they work as described
+        above.
+
+**Examples**
+
+```
+all
+none
+IPH_DownloadHome
+IPH_DonwloadPage,IPH_DownloadHome
+```
+
 ### Using Chrome Variations at runtime
 
 It is possible to test the whole backend from parsing the configuration,
diff --git a/components/feature_engagement/internal/chrome_variations_configuration.cc b/components/feature_engagement/internal/chrome_variations_configuration.cc
index c913e48..6a04161 100644
--- a/components/feature_engagement/internal/chrome_variations_configuration.cc
+++ b/components/feature_engagement/internal/chrome_variations_configuration.cc
@@ -8,6 +8,7 @@
 #include <memory>
 #include <string>
 #include <tuple>
+#include <utility>
 #include <vector>
 
 #include "base/logging.h"
@@ -31,10 +32,14 @@
 const char kComparatorTypeEqual[] = "==";
 const char kComparatorTypeNotEqual[] = "!=";
 
+const char kSessionRateImpactTypeAll[] = "all";
+const char kSessionRateImpactTypeNone[] = "none";
+
 const char kEventConfigUsedKey[] = "event_used";
 const char kEventConfigTriggerKey[] = "event_trigger";
 const char kEventConfigKeyPrefix[] = "event_";
 const char kSessionRateKey[] = "session_rate";
+const char kSessionRateImpactKey[] = "session_rate_impact";
 const char kAvailabilityKey[] = "availability";
 const char kIgnoredKeyPrefix[] = "x_";
 
@@ -49,7 +54,7 @@
 
 namespace {
 
-bool ParseComparatorSubstring(base::StringPiece definition,
+bool ParseComparatorSubstring(const base::StringPiece& definition,
                               Comparator* comparator,
                               ComparatorType type,
                               uint32_t type_len) {
@@ -64,7 +69,8 @@
   return true;
 }
 
-bool ParseComparator(base::StringPiece definition, Comparator* comparator) {
+bool ParseComparator(const base::StringPiece& definition,
+                     Comparator* comparator) {
   if (base::LowerCaseEqualsASCII(definition, kComparatorTypeAny)) {
     comparator->type = ANY;
     comparator->value = 0;
@@ -106,7 +112,8 @@
   return false;
 }
 
-bool ParseEventConfig(base::StringPiece definition, EventConfig* event_config) {
+bool ParseEventConfig(const base::StringPiece& definition,
+                      EventConfig* event_config) {
   // Support definitions with at least 4 tokens.
   auto tokens = base::SplitStringPiece(definition, ";", base::TRIM_WHITESPACE,
                                        base::SPLIT_WANT_ALL);
@@ -188,6 +195,72 @@
   return has_name && has_comparator && has_window && has_storage;
 }
 
+bool IsKnownFeature(const base::StringPiece& feature_name,
+                    const FeatureVector& features) {
+  for (const auto* feature : features) {
+    if (feature->name == feature_name.as_string())
+      return true;
+  }
+  return false;
+}
+
+bool ParseSessionRateImpact(const base::StringPiece& definition,
+                            SessionRateImpact* session_rate_impact,
+                            const base::Feature* this_feature,
+                            const FeatureVector& all_features) {
+  base::StringPiece trimmed_def =
+      base::TrimWhitespaceASCII(definition, base::TRIM_ALL);
+
+  if (trimmed_def.length() == 0)
+    return false;
+
+  if (base::LowerCaseEqualsASCII(trimmed_def, kSessionRateImpactTypeAll)) {
+    session_rate_impact->type = SessionRateImpact::Type::ALL;
+    return true;
+  }
+
+  if (base::LowerCaseEqualsASCII(trimmed_def, kSessionRateImpactTypeNone)) {
+    session_rate_impact->type = SessionRateImpact::Type::NONE;
+    return true;
+  }
+
+  auto parsed_feature_names = base::SplitStringPiece(
+      trimmed_def, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
+  if (parsed_feature_names.empty())
+    return false;
+
+  std::vector<std::string> affected_features;
+  for (const auto& feature_name : parsed_feature_names) {
+    if (feature_name.length() == 0) {
+      DVLOG(1) << "Empty feature name when parsing session_rate_impact "
+               << "for feature " << this_feature->name;
+      continue;
+    }
+    if (base::LowerCaseEqualsASCII(feature_name, kSessionRateImpactTypeAll) ||
+        base::LowerCaseEqualsASCII(feature_name, kSessionRateImpactTypeNone)) {
+      DVLOG(1) << "Illegal feature name when parsing session_rate_impact "
+               << "for feature " << this_feature->name << ": " << feature_name;
+      return false;
+    }
+    if (!IsKnownFeature(feature_name, all_features)) {
+      DVLOG(1) << "Unknown feature name found when parsing session_rate_impact "
+               << "for feature " << this_feature->name << ": " << feature_name;
+      stats::RecordConfigParsingEvent(
+          stats::ConfigParsingEvent::
+              FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE);
+      continue;
+    }
+    affected_features.push_back(feature_name.as_string());
+  }
+
+  if (affected_features.empty())
+    return false;
+
+  session_rate_impact->type = SessionRateImpact::Type::EXPLICIT;
+  session_rate_impact->affected_features = std::move(affected_features);
+  return true;
+}
+
 }  // namespace
 
 ChromeVariationsConfiguration::ChromeVariationsConfiguration() = default;
@@ -195,14 +268,15 @@
 ChromeVariationsConfiguration::~ChromeVariationsConfiguration() = default;
 
 void ChromeVariationsConfiguration::ParseFeatureConfigs(
-    FeatureVector features) {
+    const FeatureVector& features) {
   for (auto* feature : features) {
-    ParseFeatureConfig(feature);
+    ParseFeatureConfig(feature, features);
   }
 }
 
 void ChromeVariationsConfiguration::ParseFeatureConfig(
-    const base::Feature* feature) {
+    const base::Feature* feature,
+    const FeatureVector& all_features) {
   DCHECK(feature);
   DCHECK(configs_.find(feature->name) == configs_.end());
 
@@ -253,6 +327,16 @@
         continue;
       }
       config.session_rate = comparator;
+    } else if (key == kSessionRateImpactKey) {
+      SessionRateImpact impact;
+      if (!ParseSessionRateImpact(params[key], &impact, feature,
+                                  all_features)) {
+        stats::RecordConfigParsingEvent(
+            stats::ConfigParsingEvent::FAILURE_SESSION_RATE_IMPACT_PARSE);
+        ++parse_errors;
+        continue;
+      }
+      config.session_rate_impact = impact;
     } else if (key == kAvailabilityKey) {
       Comparator comparator;
       if (!ParseComparator(params[key], &comparator)) {
@@ -327,8 +411,16 @@
 }
 
 const Configuration::ConfigMap&
-ChromeVariationsConfiguration::GetRegisteredFeatures() const {
+ChromeVariationsConfiguration::GetRegisteredFeatureConfigs() const {
   return configs_;
 }
 
+const std::vector<std::string>
+ChromeVariationsConfiguration::GetRegisteredFeatures() const {
+  std::vector<std::string> features;
+  for (const auto& element : configs_)
+    features.push_back(element.first);
+  return features;
+}
+
 }  // namespace feature_engagement
diff --git a/components/feature_engagement/internal/chrome_variations_configuration.h b/components/feature_engagement/internal/chrome_variations_configuration.h
index cfdf79b..3196f5e 100644
--- a/components/feature_engagement/internal/chrome_variations_configuration.h
+++ b/components/feature_engagement/internal/chrome_variations_configuration.h
@@ -28,14 +28,16 @@
       const base::Feature& feature) const override;
   const FeatureConfig& GetFeatureConfigByName(
       const std::string& feature_name) const override;
-  const Configuration::ConfigMap& GetRegisteredFeatures() const override;
+  const Configuration::ConfigMap& GetRegisteredFeatureConfigs() const override;
+  const std::vector<std::string> GetRegisteredFeatures() const override;
 
   // Parses the variations configuration for all of the given |features| and
   // stores the result. It is only valid to call ParseFeatureConfig once.
-  void ParseFeatureConfigs(FeatureVector features);
+  void ParseFeatureConfigs(const FeatureVector& features);
 
  private:
-  void ParseFeatureConfig(const base::Feature* feature);
+  void ParseFeatureConfig(const base::Feature* feature,
+                          const FeatureVector& all_features);
 
   // The current configurations.
   ConfigMap configs_;
diff --git a/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc b/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc
index 7298374..1308b2b 100644
--- a/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc
+++ b/components/feature_engagement/internal/chrome_variations_configuration_unittest.cc
@@ -35,6 +35,14 @@
 const char kGroupName[] = "Group1";
 const char kConfigParseEventName[] = "InProductHelp.Config.ParsingEvent";
 
+SessionRateImpact CreateSessionRateImpactExplicit(
+    std::vector<std::string> affected_features) {
+  SessionRateImpact impact;
+  impact.type = SessionRateImpact::Type::EXPLICIT;
+  impact.affected_features = affected_features;
+  return impact;
+}
+
 class ChromeVariationsConfigurationTest : public ::testing::Test {
  public:
   ChromeVariationsConfigurationTest() : field_trials_(nullptr) {
@@ -70,7 +78,6 @@
     base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting();
   }
 
- protected:
   void SetFeatureParams(const base::Feature& feature,
                         std::map<std::string, std::string> params) {
     ASSERT_TRUE(
@@ -83,6 +90,7 @@
     EXPECT_EQ(params, actualParams);
   }
 
+ protected:
   void VerifyInvalid(const std::string& event_config) {
     std::map<std::string, std::string> foo_params;
     foo_params["event_used"] = "name:u;comparator:any;window:0;storage:1";
@@ -235,6 +243,268 @@
   histogram_tester.ExpectTotalCount(kConfigParseEventName, 1);
 }
 
+void RunSessionRateImpactTest(ChromeVariationsConfigurationTest* test,
+                              ChromeVariationsConfiguration* configuration,
+                              std::vector<const base::Feature*> features,
+                              std::string session_rate_impact_param_value,
+                              SessionRateImpact expected_impact,
+                              bool is_valid) {
+  std::map<std::string, std::string> foo_params;
+  foo_params["event_used"] = "name:eu;comparator:any;window:0;storage:360";
+  foo_params["event_trigger"] = "name:et;comparator:any;window:0;storage:360";
+  foo_params["session_rate_impact"] = session_rate_impact_param_value;
+  test->SetFeatureParams(kTestFeatureFoo, foo_params);
+
+  configuration->ParseFeatureConfigs(features);
+  FeatureConfig foo = configuration->GetFeatureConfig(kTestFeatureFoo);
+  EXPECT_EQ(is_valid, foo.valid);
+
+  FeatureConfig expected_foo;
+  expected_foo.valid = is_valid;
+  expected_foo.used = EventConfig("eu", Comparator(ANY, 0), 0, 360);
+  expected_foo.trigger = EventConfig("et", Comparator(ANY, 0), 0, 360);
+  expected_foo.session_rate_impact =
+      is_valid ? expected_impact : SessionRateImpact();
+  EXPECT_EQ(expected_foo, foo);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactAll) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(this, &configuration_, {&kTestFeatureFoo}, "all",
+                           SessionRateImpact(), true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactNone) {
+  base::HistogramTester histogram_tester;
+  SessionRateImpact impact;
+  impact.type = SessionRateImpact::Type::NONE;
+  RunSessionRateImpactTest(this, &configuration_, {&kTestFeatureFoo}, "none",
+                           impact, true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactExplicitSelf) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      this, &configuration_, {&kTestFeatureFoo}, "test_foo",
+      CreateSessionRateImpactExplicit({kTestFeatureFoo.name}),
+      true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactExplicitOther) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      this, &configuration_, {&kTestFeatureFoo, &kTestFeatureBar}, "test_bar",
+      CreateSessionRateImpactExplicit({kTestFeatureBar.name}),
+      true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  // bar has no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 1);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 2);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactExplicitMultiple) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      this, &configuration_,
+      {&kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux},
+      "test_bar,test_qux",
+      CreateSessionRateImpactExplicit(
+          {kTestFeatureBar.name, kTestFeatureQux.name}),
+      true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  // bar and qux have no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 2);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 3);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitAtLeastOneValidFeature) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      this, &configuration_,
+      {&kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux},
+      "test_foo,no_feature",
+      CreateSessionRateImpactExplicit(
+          {kTestFeatureBar.name, kTestFeatureQux.name}),
+      true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::
+                           FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE),
+      1);
+  // bar and qux have no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 2);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 4);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitAtLeastOneValidFeaturePlusEmpty) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      this, &configuration_,
+      {&kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux}, "test_foo, ",
+      CreateSessionRateImpactExplicit(
+          {kTestFeatureBar.name, kTestFeatureQux.name}),
+      true /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  // bar and qux have no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 2);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 3);
+}
+
+void TestInvalidSessionRateImpactParamValue(
+    ChromeVariationsConfigurationTest* test,
+    ChromeVariationsConfiguration* configuration,
+    std::string session_rate_impact_param_value,
+    uint32_t parse_errors,
+    uint32_t unknown_features) {
+  base::HistogramTester histogram_tester;
+  RunSessionRateImpactTest(
+      test, configuration,
+      {&kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux},
+      session_rate_impact_param_value, SessionRateImpact(),
+      false /* is_valid */);
+
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE), 1);
+  if (parse_errors > 0) {
+    histogram_tester.ExpectBucketCount(
+        kConfigParseEventName,
+        static_cast<int>(
+            stats::ConfigParsingEvent::FAILURE_SESSION_RATE_IMPACT_PARSE),
+        parse_errors);
+  }
+  if (unknown_features > 0) {
+    histogram_tester.ExpectBucketCount(
+        kConfigParseEventName,
+        static_cast<int>(stats::ConfigParsingEvent::
+                             FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE),
+        unknown_features);
+  }
+  // bar and qux has no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 2);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName,
+                                    3 + parse_errors + unknown_features);
+}
+
+TEST_F(ChromeVariationsConfigurationTest, SessionRateImpactExplicitEmpty) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "", 1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitEmptyWhitespace) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "   ", 1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitOnlySeparator) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, ",", 1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitOnlySeparatorWhitespace) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, " ,  ", 1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitOnlyUnknownFeature) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "not_feature",
+                                         1, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitOnlyMultipleUnknownFeatures) {
+  TestInvalidSessionRateImpactParamValue(
+      this, &configuration_, "not_feature,another_not_feature", 1, 2);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitOnlyUnknownFeaturePlusEmpty) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "not_feature, ",
+                                         1, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameAllFirst) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "all,test_foo",
+                                         1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameAllLast) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "test_foo,all",
+                                         1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameAllLastInvalidFirst) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_,
+                                         "not_feature,all", 1, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameNoneFirst) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "none,test_foo",
+                                         1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameNoneLast) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_, "test_foo,none",
+                                         1, 0);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameNoneLastInvalidFirst) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_,
+                                         "not_feature,none", 1, 1);
+}
+
+TEST_F(ChromeVariationsConfigurationTest,
+       SessionRateImpactExplicitInvalidFeatureNameFromCodeReview) {
+  TestInvalidSessionRateImpactParamValue(this, &configuration_,
+                                         "test_foo,all,none,mwahahaha", 1, 0);
+}
+
 TEST_F(ChromeVariationsConfigurationTest, WhitespaceIsValid) {
   std::map<std::string, std::string> foo_params;
   foo_params["event_used"] =
@@ -249,9 +519,12 @@
   foo_params["event_5"] = "name:e5;comparator: <=\r6 ;window:8;storage:390";
   foo_params["event_6"] = "name:e6;comparator:\n<=7;window:9;storage:400";
   foo_params["event_7"] = "name:e7;comparator:<=8\n;window:10;storage:410";
+  foo_params["session_rate_impact"] = " test_bar, test_qux ";
   SetFeatureParams(kTestFeatureFoo, foo_params);
 
-  std::vector<const base::Feature*> features = {&kTestFeatureFoo};
+  base::HistogramTester histogram_tester;
+  std::vector<const base::Feature*> features = {
+      &kTestFeatureFoo, &kTestFeatureBar, &kTestFeatureQux};
   configuration_.ParseFeatureConfigs(features);
 
   FeatureConfig foo = configuration_.GetFeatureConfig(kTestFeatureFoo);
@@ -277,7 +550,17 @@
       EventConfig("e6", Comparator(LESS_THAN_OR_EQUAL, 7), 9, 400));
   expected_foo.event_configs.insert(
       EventConfig("e7", Comparator(LESS_THAN_OR_EQUAL, 8), 10, 410));
+  expected_foo.session_rate_impact = CreateSessionRateImpactExplicit(
+      {kTestFeatureBar.name, kTestFeatureQux.name});
   EXPECT_EQ(expected_foo, foo);
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::SUCCESS), 1);
+  // bar and qux have no configuration.
+  histogram_tester.ExpectBucketCount(
+      kConfigParseEventName,
+      static_cast<int>(stats::ConfigParsingEvent::FAILURE_NO_FIELD_TRIAL), 2);
+  histogram_tester.ExpectTotalCount(kConfigParseEventName, 3);
 }
 
 TEST_F(ChromeVariationsConfigurationTest, IgnoresInvalidConfigKeys) {
diff --git a/components/feature_engagement/internal/condition_validator.h b/components/feature_engagement/internal/condition_validator.h
index 809f4579..a17e569c 100644
--- a/components/feature_engagement/internal/condition_validator.h
+++ b/components/feature_engagement/internal/condition_validator.h
@@ -9,6 +9,7 @@
 
 #include <ostream>
 #include <string>
+#include <vector>
 
 #include "base/macros.h"
 #include "components/feature_engagement/public/feature_list.h"
@@ -79,7 +80,10 @@
                                  uint32_t current_day) const = 0;
 
   // Must be called to notify that the |feature| is currently showing.
-  virtual void NotifyIsShowing(const base::Feature& feature) = 0;
+  virtual void NotifyIsShowing(
+      const base::Feature& feature,
+      const FeatureConfig& config,
+      const std::vector<std::string>& all_feature_names) = 0;
 
   // Must be called to notify that the |feature| is no longer showing.
   virtual void NotifyDismissed(const base::Feature& feature) = 0;
diff --git a/components/feature_engagement/internal/configuration.cc b/components/feature_engagement/internal/configuration.cc
index 048eb63d..a14c11a 100644
--- a/components/feature_engagement/internal/configuration.cc
+++ b/components/feature_engagement/internal/configuration.cc
@@ -7,8 +7,25 @@
 #include <string>
 
 #include "base/logging.h"
+#include "base/optional.h"
 
 namespace feature_engagement {
+namespace {
+std::ostream& operator<<(std::ostream& os, const SessionRateImpact::Type type) {
+  switch (type) {
+    case SessionRateImpact::Type::ALL:
+      return os << "ALL";
+    case SessionRateImpact::Type::NONE:
+      return os << "NONE";
+    case SessionRateImpact::Type::EXPLICIT:
+      return os << "EXPLICIT";
+    default:
+      // All cases should be covered.
+      NOTREACHED();
+      return os;
+  }
+}
+}  // namespace
 
 Comparator::Comparator() : type(ANY), value(0) {}
 
@@ -80,6 +97,36 @@
             << ", storage: " << event_config.storage << " }";
 }
 
+SessionRateImpact::SessionRateImpact()
+    : type(SessionRateImpact::Type::ALL), affected_features() {}
+
+SessionRateImpact::SessionRateImpact(const SessionRateImpact& other) = default;
+
+SessionRateImpact::~SessionRateImpact() = default;
+
+std::ostream& operator<<(std::ostream& os, const SessionRateImpact& impact) {
+  os << "{ type: " << impact.type << ", affected_features: ";
+  if (!impact.affected_features.has_value())
+    return os << "NO VALUE }";
+
+  os << "[";
+  bool first = true;
+  for (const auto& affected_feature : impact.affected_features.value()) {
+    if (first) {
+      first = false;
+      os << affected_feature;
+    } else {
+      os << ", " << affected_feature;
+    }
+  }
+  return os << "] }";
+}
+
+bool operator==(const SessionRateImpact& lhs, const SessionRateImpact& rhs) {
+  return std::tie(lhs.type, lhs.affected_features) ==
+         std::tie(rhs.type, rhs.affected_features);
+}
+
 FeatureConfig::FeatureConfig() : valid(false) {}
 
 FeatureConfig::FeatureConfig(const FeatureConfig& other) = default;
diff --git a/components/feature_engagement/internal/configuration.h b/components/feature_engagement/internal/configuration.h
index 891a62e..7b352f1 100644
--- a/components/feature_engagement/internal/configuration.h
+++ b/components/feature_engagement/internal/configuration.h
@@ -12,6 +12,7 @@
 #include <vector>
 
 #include "base/macros.h"
+#include "base/optional.h"
 
 namespace base {
 struct Feature;
@@ -80,6 +81,33 @@
 bool operator<(const EventConfig& lhs, const EventConfig& rhs);
 std::ostream& operator<<(std::ostream& os, const EventConfig& event_config);
 
+// A SessionRateImpact describes which features the |session_rate| of a given
+// FeatureConfig should affect. It can affect either |ALL| (default), |NONE|,
+// or an |EXPLICIT| list of the features. In the latter case, a list of affected
+// features is given as their base::Feature name.
+struct SessionRateImpact {
+ public:
+  enum class Type {
+    ALL = 0,      // Affects all other features.
+    NONE = 1,     // Affects no other features.
+    EXPLICIT = 2  // Affects only features in |affected_features|.
+  };
+
+  SessionRateImpact();
+  SessionRateImpact(const SessionRateImpact& other);
+  ~SessionRateImpact();
+
+  // Describes which features are impacted.
+  Type type;
+
+  // In the case of the Type |EXPLICIT|, this is the list of affected
+  // base::Feature names.
+  base::Optional<std::vector<std::string>> affected_features;
+};
+
+bool operator==(const SessionRateImpact& lhs, const SessionRateImpact& rhs);
+std::ostream& operator<<(std::ostream& os, const SessionRateImpact& impact);
+
 // A FeatureConfig contains all the configuration for a given feature.
 struct FeatureConfig {
  public:
@@ -106,6 +134,9 @@
   // comparison.
   Comparator session_rate;
 
+  // Which features the showing this in-product help impacts.
+  SessionRateImpact session_rate_impact;
+
   // Number of days the in-product help has been available must fit this
   // comparison.
   Comparator availability;
@@ -135,7 +166,10 @@
       const std::string& feature_name) const = 0;
 
   // Returns the immutable ConfigMap that contains all registered features.
-  virtual const ConfigMap& GetRegisteredFeatures() const = 0;
+  virtual const ConfigMap& GetRegisteredFeatureConfigs() const = 0;
+
+  // Returns the list of the names of all registred features.
+  virtual const std::vector<std::string> GetRegisteredFeatures() const = 0;
 
  protected:
   Configuration() = default;
diff --git a/components/feature_engagement/internal/editable_configuration.cc b/components/feature_engagement/internal/editable_configuration.cc
index cdce5386..24781436 100644
--- a/components/feature_engagement/internal/editable_configuration.cc
+++ b/components/feature_engagement/internal/editable_configuration.cc
@@ -36,9 +36,17 @@
   return it->second;
 }
 
-const Configuration::ConfigMap& EditableConfiguration::GetRegisteredFeatures()
-    const {
+const Configuration::ConfigMap&
+EditableConfiguration::GetRegisteredFeatureConfigs() const {
   return configs_;
 }
 
+const std::vector<std::string> EditableConfiguration::GetRegisteredFeatures()
+    const {
+  std::vector<std::string> features;
+  for (const auto& element : configs_)
+    features.push_back(element.first);
+  return features;
+}
+
 }  // namespace feature_engagement
diff --git a/components/feature_engagement/internal/editable_configuration.h b/components/feature_engagement/internal/editable_configuration.h
index b92b3da5..27164df 100644
--- a/components/feature_engagement/internal/editable_configuration.h
+++ b/components/feature_engagement/internal/editable_configuration.h
@@ -27,7 +27,8 @@
       const base::Feature& feature) const override;
   const FeatureConfig& GetFeatureConfigByName(
       const std::string& feature_name) const override;
-  const Configuration::ConfigMap& GetRegisteredFeatures() const override;
+  const Configuration::ConfigMap& GetRegisteredFeatureConfigs() const override;
+  const std::vector<std::string> GetRegisteredFeatures() const override;
 
   // Adds a new FeatureConfig to the current configurations. If it already
   // exists, the contents are replaced.
diff --git a/components/feature_engagement/internal/feature_config_condition_validator.cc b/components/feature_engagement/internal/feature_config_condition_validator.cc
index 2ec5b83..9a18a9f2d 100644
--- a/components/feature_engagement/internal/feature_config_condition_validator.cc
+++ b/components/feature_engagement/internal/feature_config_condition_validator.cc
@@ -4,6 +4,10 @@
 
 #include "components/feature_engagement/internal/feature_config_condition_validator.h"
 
+#include <algorithm>
+#include <string>
+#include <vector>
+
 #include "base/feature_list.h"
 #include "components/feature_engagement/internal/availability_model.h"
 #include "components/feature_engagement/internal/configuration.h"
@@ -14,7 +18,7 @@
 namespace feature_engagement {
 
 FeatureConfigConditionValidator::FeatureConfigConditionValidator()
-    : currently_showing_(false), times_shown_(0u) {}
+    : currently_showing_(false) {}
 
 FeatureConfigConditionValidator::~FeatureConfigConditionValidator() = default;
 
@@ -39,7 +43,8 @@
         EventConfigMeetsConditions(event_config, event_model, current_day);
   }
 
-  result.session_rate_ok = config.session_rate.MeetsCriteria(times_shown_);
+  result.session_rate_ok =
+      SessionRateMeetsConditions(config.session_rate, feature);
 
   result.availability_model_ready_ok = availability_model.IsReady();
 
@@ -50,12 +55,35 @@
 }
 
 void FeatureConfigConditionValidator::NotifyIsShowing(
-    const base::Feature& feature) {
+    const base::Feature& feature,
+    const FeatureConfig& config,
+    const std::vector<std::string>& all_feature_names) {
   DCHECK(!currently_showing_);
   DCHECK(base::FeatureList::IsEnabled(feature));
 
   currently_showing_ = true;
-  ++times_shown_;
+
+  switch (config.session_rate_impact.type) {
+    case SessionRateImpact::Type::ALL:
+      for (const std::string& feature_name : all_feature_names)
+        ++times_shown_for_feature_[feature_name];
+      break;
+    case SessionRateImpact::Type::NONE:
+      // Intentionally ignore, since no features should be impacted.
+      break;
+    case SessionRateImpact::Type::EXPLICIT:
+      DCHECK(config.session_rate_impact.affected_features.has_value());
+      for (const std::string& feature_name :
+           config.session_rate_impact.affected_features.value()) {
+        DCHECK(std::find(all_feature_names.begin(), all_feature_names.end(),
+                         feature_name) != all_feature_names.end());
+        ++times_shown_for_feature_[feature_name];
+      }
+      break;
+    default:
+      // All cases should be covered.
+      NOTREACHED();
+  }
 }
 
 void FeatureConfigConditionValidator::NotifyDismissed(
@@ -119,4 +147,15 @@
   return comparator.MeetsCriteria(days_available);
 }
 
+bool FeatureConfigConditionValidator::SessionRateMeetsConditions(
+    const Comparator session_rate,
+    const base::Feature& feature) const {
+  const auto it = times_shown_for_feature_.find(feature.name);
+  if (it == times_shown_for_feature_.end()) {
+    return session_rate.MeetsCriteria(0u);
+  } else {
+    return session_rate.MeetsCriteria(it->second);
+  }
+}
+
 }  // namespace feature_engagement
diff --git a/components/feature_engagement/internal/feature_config_condition_validator.h b/components/feature_engagement/internal/feature_config_condition_validator.h
index 949eaa7..4e74b385 100644
--- a/components/feature_engagement/internal/feature_config_condition_validator.h
+++ b/components/feature_engagement/internal/feature_config_condition_validator.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_FEATURE_ENGAGEMENT_INTERNAL_FEATURE_CONFIG_CONDITION_VALIDATOR_H_
 
 #include <stdint.h>
+#include <map>
 
 #include "base/macros.h"
 #include "components/feature_engagement/internal/condition_validator.h"
@@ -29,7 +30,10 @@
       const EventModel& event_model,
       const AvailabilityModel& availability_model,
       uint32_t current_day) const override;
-  void NotifyIsShowing(const base::Feature& feature) override;
+  void NotifyIsShowing(
+      const base::Feature& feature,
+      const FeatureConfig& config,
+      const std::vector<std::string>& all_feature_names) override;
   void NotifyDismissed(const base::Feature& feature) override;
 
  private:
@@ -42,11 +46,16 @@
                                    const AvailabilityModel& availability_model,
                                    uint32_t current_day) const;
 
+  bool SessionRateMeetsConditions(const Comparator session_rate,
+                                  const base::Feature& feature) const;
+
   // Whether in-product help is currently being shown.
   bool currently_showing_;
 
-  // Number of times in-product help has been shown within the current session.
-  uint32_t times_shown_;
+  // Stores how many times features that impact a given feature have been shown.
+  // By default, all features impact each other, but some features override this
+  // through the use of |session_rate_impact|.
+  std::map<std::string, uint32_t> times_shown_for_feature_;
 
   DISALLOW_COPY_AND_ASSIGN(FeatureConfigConditionValidator);
 };
diff --git a/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc b/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc
index 5f6420f8..ceba7ce 100644
--- a/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc
+++ b/components/feature_engagement/internal/feature_config_condition_validator_unittest.cc
@@ -25,6 +25,10 @@
                                     base::FEATURE_DISABLED_BY_DEFAULT};
 const base::Feature kTestFeatureBar{"test_bar",
                                     base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kTestFeatureQux{"test_qux",
+                                    base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kTestFeatureXyz{"test_xyz",
+                                    base::FEATURE_DISABLED_BY_DEFAULT};
 
 FeatureConfig GetValidFeatureConfig() {
   FeatureConfig config;
@@ -42,6 +46,14 @@
   return config;
 }
 
+SessionRateImpact CreateSessionRateImpactExplicit(
+    std::vector<std::string> affected_features) {
+  SessionRateImpact impact;
+  impact.type = SessionRateImpact::Type::EXPLICIT;
+  impact.affected_features = affected_features;
+  return impact;
+}
+
 class TestEventModel : public EventModel {
  public:
   TestEventModel() : ready_(true) {}
@@ -130,6 +142,13 @@
                                       availability_model_, 0);
   }
 
+  ConditionValidator::Result GetResultForDayZeroForFeature(
+      const base::Feature& feature,
+      const FeatureConfig& config) {
+    return validator_.MeetsConditions(feature, config, event_model_,
+                                      availability_model_, 0);
+  }
+
   TestEventModel event_model_;
   TestAvailabilityModel availability_model_;
   FeatureConfigConditionValidator validator_;
@@ -192,7 +211,8 @@
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {});
 
-  validator_.NotifyIsShowing(kTestFeatureBar);
+  validator_.NotifyIsShowing(kTestFeatureBar, FeatureConfig(),
+                             {kTestFeatureFoo.name, kTestFeatureBar.name});
   ConditionValidator::Result result =
       GetResultForDayZero(GetAcceptingFeatureConfig());
   EXPECT_FALSE(result.NoErrors());
@@ -290,29 +310,195 @@
 TEST_F(FeatureConfigConditionValidatorTest, SessionRate) {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {});
+  std::vector<std::string> all_feature_names = {kTestFeatureFoo.name,
+                                                kTestFeatureBar.name};
 
-  FeatureConfig config = GetAcceptingFeatureConfig();
-  config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig foo_config = GetAcceptingFeatureConfig();
+  foo_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig bar_config = GetAcceptingFeatureConfig();
 
-  EXPECT_TRUE(GetResultForDayZero(config).NoErrors());
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
 
-  validator_.NotifyIsShowing(kTestFeatureBar);
+  validator_.NotifyIsShowing(kTestFeatureBar, bar_config, all_feature_names);
   validator_.NotifyDismissed(kTestFeatureBar);
-  EXPECT_TRUE(GetResultForDayZero(config).NoErrors());
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
 
-  validator_.NotifyIsShowing(kTestFeatureBar);
+  validator_.NotifyIsShowing(kTestFeatureBar, bar_config, all_feature_names);
   validator_.NotifyDismissed(kTestFeatureBar);
-  ConditionValidator::Result result = GetResultForDayZero(config);
+  ConditionValidator::Result result = GetResultForDayZero(foo_config);
   EXPECT_FALSE(result.NoErrors());
   EXPECT_FALSE(result.session_rate_ok);
 
-  validator_.NotifyIsShowing(kTestFeatureBar);
+  validator_.NotifyIsShowing(kTestFeatureBar, bar_config, all_feature_names);
   validator_.NotifyDismissed(kTestFeatureBar);
-  result = GetResultForDayZero(config);
+  result = GetResultForDayZero(foo_config);
   EXPECT_FALSE(result.NoErrors());
   EXPECT_FALSE(result.session_rate_ok);
 }
 
+TEST_F(FeatureConfigConditionValidatorTest, SessionRateImpactAffectsNone) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {});
+  std::vector<std::string> all_feature_names = {kTestFeatureFoo.name,
+                                                kTestFeatureBar.name};
+
+  FeatureConfig foo_config = GetAcceptingFeatureConfig();
+  foo_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig affects_none_config = GetAcceptingFeatureConfig();
+  affects_none_config.session_rate_impact = SessionRateImpact();
+  affects_none_config.session_rate_impact.type = SessionRateImpact::Type::NONE;
+
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureBar, affects_none_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureBar);
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureBar, affects_none_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureBar);
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureBar, affects_none_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureBar);
+  EXPECT_TRUE(GetResultForDayZero(foo_config).NoErrors());
+}
+
+TEST_F(FeatureConfigConditionValidatorTest, SessionRateImpactAffectsExplicit) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      {kTestFeatureFoo, kTestFeatureBar, kTestFeatureQux}, {});
+  std::vector<std::string> all_feature_names = {
+      kTestFeatureFoo.name, kTestFeatureBar.name, kTestFeatureQux.name};
+
+  FeatureConfig foo_config = GetAcceptingFeatureConfig();
+  foo_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig bar_config = GetAcceptingFeatureConfig();
+  bar_config.session_rate = Comparator(LESS_THAN, 2u);
+
+  FeatureConfig affects_only_foo_config = GetAcceptingFeatureConfig();
+  affects_only_foo_config.session_rate_impact =
+      CreateSessionRateImpactExplicit({kTestFeatureFoo.name});
+
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureQux, affects_only_foo_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureQux);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureQux, affects_only_foo_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureQux);
+  ConditionValidator::Result result =
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config);
+  EXPECT_FALSE(result.NoErrors());
+  EXPECT_FALSE(result.session_rate_ok);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+}
+
+TEST_F(FeatureConfigConditionValidatorTest, SessionRateImpactAffectsSelf) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      {kTestFeatureFoo, kTestFeatureBar, kTestFeatureQux}, {});
+  std::vector<std::string> all_feature_names = {kTestFeatureFoo.name,
+                                                kTestFeatureBar.name};
+
+  FeatureConfig foo_config = GetAcceptingFeatureConfig();
+  foo_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig bar_config = GetAcceptingFeatureConfig();
+  bar_config.session_rate = Comparator(LESS_THAN, 2u);
+
+  FeatureConfig affects_only_foo_config = GetAcceptingFeatureConfig();
+  affects_only_foo_config.session_rate_impact =
+      CreateSessionRateImpactExplicit({kTestFeatureFoo.name});
+
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureFoo, affects_only_foo_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureFoo);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureFoo, affects_only_foo_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureFoo);
+  ConditionValidator::Result result =
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config);
+  EXPECT_FALSE(result.NoErrors());
+  EXPECT_FALSE(result.session_rate_ok);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+}
+
+TEST_F(FeatureConfigConditionValidatorTest,
+       SessionRateImpactAffectsExplicitMultipleFeatures) {
+  base::test::ScopedFeatureList scoped_feature_list;
+  scoped_feature_list.InitWithFeatures(
+      {kTestFeatureFoo, kTestFeatureBar, kTestFeatureQux, kTestFeatureXyz}, {});
+  std::vector<std::string> all_feature_names = {
+      kTestFeatureFoo.name, kTestFeatureBar.name, kTestFeatureQux.name,
+      kTestFeatureXyz.name};
+
+  FeatureConfig foo_config = GetAcceptingFeatureConfig();
+  foo_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig bar_config = GetAcceptingFeatureConfig();
+  bar_config.session_rate = Comparator(LESS_THAN, 2u);
+  FeatureConfig xyz_config = GetAcceptingFeatureConfig();
+  xyz_config.session_rate = Comparator(LESS_THAN, 2u);
+
+  FeatureConfig affects_foo_and_bar_config = GetAcceptingFeatureConfig();
+  affects_foo_and_bar_config.session_rate_impact =
+      CreateSessionRateImpactExplicit(
+          {kTestFeatureFoo.name, kTestFeatureBar.name});
+
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureXyz, xyz_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureQux, affects_foo_and_bar_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureQux);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureBar, bar_config).NoErrors());
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureXyz, xyz_config).NoErrors());
+
+  validator_.NotifyIsShowing(kTestFeatureQux, affects_foo_and_bar_config,
+                             all_feature_names);
+  validator_.NotifyDismissed(kTestFeatureQux);
+  ConditionValidator::Result foo_result =
+      GetResultForDayZeroForFeature(kTestFeatureFoo, foo_config);
+  EXPECT_FALSE(foo_result.NoErrors());
+  EXPECT_FALSE(foo_result.session_rate_ok);
+  ConditionValidator::Result bar_result =
+      GetResultForDayZeroForFeature(kTestFeatureFoo, bar_config);
+  EXPECT_FALSE(bar_result.NoErrors());
+  EXPECT_FALSE(bar_result.session_rate_ok);
+  EXPECT_TRUE(
+      GetResultForDayZeroForFeature(kTestFeatureXyz, xyz_config).NoErrors());
+}
+
 TEST_F(FeatureConfigConditionValidatorTest, Availability) {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitWithFeatures({kTestFeatureFoo, kTestFeatureBar}, {});
diff --git a/components/feature_engagement/internal/never_condition_validator.cc b/components/feature_engagement/internal/never_condition_validator.cc
index 038eedf..7b937cb2 100644
--- a/components/feature_engagement/internal/never_condition_validator.cc
+++ b/components/feature_engagement/internal/never_condition_validator.cc
@@ -19,7 +19,10 @@
   return ConditionValidator::Result(false);
 }
 
-void NeverConditionValidator::NotifyIsShowing(const base::Feature& feature) {}
+void NeverConditionValidator::NotifyIsShowing(
+    const base::Feature& feature,
+    const FeatureConfig& config,
+    const std::vector<std::string>& all_feature_names) {}
 
 void NeverConditionValidator::NotifyDismissed(const base::Feature& feature) {}
 
diff --git a/components/feature_engagement/internal/never_condition_validator.h b/components/feature_engagement/internal/never_condition_validator.h
index b915f838..4d7842c 100644
--- a/components/feature_engagement/internal/never_condition_validator.h
+++ b/components/feature_engagement/internal/never_condition_validator.h
@@ -31,7 +31,10 @@
       const EventModel& event_model,
       const AvailabilityModel& availability_model,
       uint32_t current_day) const override;
-  void NotifyIsShowing(const base::Feature& feature) override;
+  void NotifyIsShowing(
+      const base::Feature& feature,
+      const FeatureConfig& config,
+      const std::vector<std::string>& all_feature_names) override;
   void NotifyDismissed(const base::Feature& feature) override;
 
  private:
diff --git a/components/feature_engagement/internal/once_condition_validator.cc b/components/feature_engagement/internal/once_condition_validator.cc
index f3068f3..29d46fbb2e 100644
--- a/components/feature_engagement/internal/once_condition_validator.cc
+++ b/components/feature_engagement/internal/once_condition_validator.cc
@@ -34,7 +34,10 @@
   return result;
 }
 
-void OnceConditionValidator::NotifyIsShowing(const base::Feature& feature) {
+void OnceConditionValidator::NotifyIsShowing(
+    const base::Feature& feature,
+    const FeatureConfig& config,
+    const std::vector<std::string>& all_feature_names) {
   DCHECK(currently_showing_feature_.empty());
   DCHECK(shown_features_.find(feature.name) == shown_features_.end());
   shown_features_.insert(feature.name);
diff --git a/components/feature_engagement/internal/once_condition_validator.h b/components/feature_engagement/internal/once_condition_validator.h
index 0bafc34..c6721e3 100644
--- a/components/feature_engagement/internal/once_condition_validator.h
+++ b/components/feature_engagement/internal/once_condition_validator.h
@@ -44,7 +44,10 @@
       const EventModel& event_model,
       const AvailabilityModel& availability_model,
       uint32_t current_day) const override;
-  void NotifyIsShowing(const base::Feature& feature) override;
+  void NotifyIsShowing(
+      const base::Feature& feature,
+      const FeatureConfig& config,
+      const std::vector<std::string>& all_feature_names) override;
   void NotifyDismissed(const base::Feature& feature) override;
 
  private:
diff --git a/components/feature_engagement/internal/once_condition_validator_unittest.cc b/components/feature_engagement/internal/once_condition_validator_unittest.cc
index cdfc1a6..b0316df 100644
--- a/components/feature_engagement/internal/once_condition_validator_unittest.cc
+++ b/components/feature_engagement/internal/once_condition_validator_unittest.cc
@@ -72,7 +72,7 @@
                   .MeetsConditions(kTestFeatureFoo, kValidFeatureConfig,
                                    event_model_, availability_model_, 0u)
                   .NoErrors());
-  validator_.NotifyIsShowing(kTestFeatureFoo);
+  validator_.NotifyIsShowing(kTestFeatureFoo, FeatureConfig(), {""});
   ConditionValidator::Result result =
       validator_.MeetsConditions(kTestFeatureFoo, kValidFeatureConfig,
                                  event_model_, availability_model_, 0u);
@@ -125,7 +125,7 @@
 }
 
 TEST_F(OnceConditionValidatorTest, OnlyTriggerIfNothingElseIsShowing) {
-  validator_.NotifyIsShowing(kTestFeatureBar);
+  validator_.NotifyIsShowing(kTestFeatureBar, FeatureConfig(), {""});
   ConditionValidator::Result result =
       validator_.MeetsConditions(kTestFeatureFoo, kValidFeatureConfig,
                                  event_model_, availability_model_, 0u);
diff --git a/components/feature_engagement/internal/single_invalid_configuration.cc b/components/feature_engagement/internal/single_invalid_configuration.cc
index 5d06652..94a7010b 100644
--- a/components/feature_engagement/internal/single_invalid_configuration.cc
+++ b/components/feature_engagement/internal/single_invalid_configuration.cc
@@ -26,8 +26,13 @@
 }
 
 const Configuration::ConfigMap&
-SingleInvalidConfiguration::GetRegisteredFeatures() const {
+SingleInvalidConfiguration::GetRegisteredFeatureConfigs() const {
   return configs_;
 }
 
+const std::vector<std::string>
+SingleInvalidConfiguration::GetRegisteredFeatures() const {
+  return {};
+}
+
 }  // namespace feature_engagement
diff --git a/components/feature_engagement/internal/single_invalid_configuration.h b/components/feature_engagement/internal/single_invalid_configuration.h
index de6ce42c..82766e1a7 100644
--- a/components/feature_engagement/internal/single_invalid_configuration.h
+++ b/components/feature_engagement/internal/single_invalid_configuration.h
@@ -29,7 +29,8 @@
       const base::Feature& feature) const override;
   const FeatureConfig& GetFeatureConfigByName(
       const std::string& feature_name) const override;
-  const Configuration::ConfigMap& GetRegisteredFeatures() const override;
+  const Configuration::ConfigMap& GetRegisteredFeatureConfigs() const override;
+  const std::vector<std::string> GetRegisteredFeatures() const override;
 
  private:
   // The invalid configuration to always return.
diff --git a/components/feature_engagement/internal/stats.cc b/components/feature_engagement/internal/stats.cc
index 5cc79b2..2888247 100644
--- a/components/feature_engagement/internal/stats.cc
+++ b/components/feature_engagement/internal/stats.cc
@@ -56,7 +56,8 @@
   DCHECK(config);
 
   // Find which feature this event belongs to.
-  const Configuration::ConfigMap& features = config->GetRegisteredFeatures();
+  const Configuration::ConfigMap& features =
+      config->GetRegisteredFeatureConfigs();
   std::string feature_name;
   for (const auto& element : features) {
     const std::string fname = element.first;
diff --git a/components/feature_engagement/internal/stats.h b/components/feature_engagement/internal/stats.h
index 1a40220..0d8e983 100644
--- a/components/feature_engagement/internal/stats.h
+++ b/components/feature_engagement/internal/stats.h
@@ -20,6 +20,7 @@
 // Most of the fields maps to |ConditionValidator::Result|.
 // The failure reasons are not mutually exclusive.
 // Out-dated entries shouldn't be deleted but marked as obselete.
+// Keep this synced with the enum in //tools/metrics/histograms/enums.xml.
 enum class TriggerHelpUIResult {
   // The help UI is triggered.
   SUCCESS = 0,
@@ -64,6 +65,7 @@
 // Used in the metrics to track the configuration parsing event.
 // The failure reasons are not mutually exclusive.
 // Out-dated entries shouldn't be deleted but marked as obsolete.
+// Keep this synced with the enum in //tools/metrics/histograms/enums.xml.
 enum class ConfigParsingEvent {
   // The configuration is parsed correctly.
   SUCCESS = 0,
@@ -98,8 +100,14 @@
   // UnKnown key in configuration parameters.
   FAILURE_UNKNOWN_KEY = 10,
 
+  // Fails to parse the session rate impact.
+  FAILURE_SESSION_RATE_IMPACT_PARSE = 11,
+
+  // Fails to parse the session rate impact.
+  FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE = 12,
+
   // Last entry for the enum.
-  COUNT = 11,
+  COUNT = 13,
 };
 
 // Used in metrics to track database states. Each type will match to a suffix
diff --git a/components/feature_engagement/internal/tracker_impl.cc b/components/feature_engagement/internal/tracker_impl.cc
index 8dc508a..4b33b02 100644
--- a/components/feature_engagement/internal/tracker_impl.cc
+++ b/components/feature_engagement/internal/tracker_impl.cc
@@ -174,7 +174,9 @@
       feature, configuration_->GetFeatureConfig(feature), *event_model_,
       *availability_model_, time_provider_->GetCurrentDay());
   if (result.NoErrors()) {
-    condition_validator_->NotifyIsShowing(feature);
+    condition_validator_->NotifyIsShowing(
+        feature, configuration_->GetFeatureConfig(feature),
+        configuration_->GetRegisteredFeatures());
     FeatureConfig feature_config = configuration_->GetFeatureConfig(feature);
     DCHECK_NE("", feature_config.trigger.name);
     event_model_->IncrementEvent(feature_config.trigger.name,
diff --git a/components/onc/docs/onc_spec.md b/components/onc/docs/onc_spec.md
index 5abadf97..7fe473f 100644
--- a/components/onc/docs/onc_spec.md
+++ b/components/onc/docs/onc_spec.md
@@ -510,10 +510,7 @@
 
 * **Type**
     * (required) - **string**
-    * `Allowed values are` *IPsec*,
-        *L2TP-IPsec*,
-        *OpenVPN*, and
-        *ThirdPartyVPN*.
+    * `Allowed values are` *L2TP-IPsec*, *OpenVPN*, *ThirdPartyVPN*, *ARCVPN*
     * Type of the VPN.
 
 ## IPsec-based VPN types
diff --git a/components/user_manager/user.h b/components/user_manager/user.h
index 645cf88..ee68858 100644
--- a/components/user_manager/user.h
+++ b/components/user_manager/user.h
@@ -31,6 +31,10 @@
 class UserSessionManager;
 }
 
+namespace policy {
+class ProfilePolicyConnectorTest;
+}
+
 namespace user_manager {
 
 class UserManagerBase;
@@ -191,6 +195,7 @@
   friend class chromeos::FakeChromeUserManager;
   friend class chromeos::MockUserManager;
   friend class chromeos::UserAddingScreenTest;
+  friend class policy::ProfilePolicyConnectorTest;
   FRIEND_TEST_ALL_PREFIXES(UserTest, DeviceLocalAccountAffiliation);
   FRIEND_TEST_ALL_PREFIXES(UserTest, UserSessionInitialized);
 
diff --git a/components/user_manager/user_manager.cc b/components/user_manager/user_manager.cc
index a4f4282..27cea02 100644
--- a/components/user_manager/user_manager.cc
+++ b/components/user_manager/user_manager.cc
@@ -23,6 +23,8 @@
     const User& user,
     const gfx::ImageSkia& profile_image) {}
 
+void UserManager::Observer::OnChildStatusChanged(const User& user) {}
+
 void UserManager::UserSessionStateObserver::ActiveUserChanged(
     const User* active_user) {
 }
@@ -35,9 +37,6 @@
     const std::string& hash) {
 }
 
-void UserManager::UserSessionStateObserver::UserChangedChildStatus(User* user) {
-}
-
 UserManager::UserSessionStateObserver::~UserSessionStateObserver() {
 }
 
diff --git a/components/user_manager/user_manager.h b/components/user_manager/user_manager.h
index 0586d762e..e58a3f86 100644
--- a/components/user_manager/user_manager.h
+++ b/components/user_manager/user_manager.h
@@ -51,6 +51,9 @@
     virtual void OnUserProfileImageUpdated(const User& user,
                                            const gfx::ImageSkia& profile_image);
 
+    // Called when the child status of the given user has changed.
+    virtual void OnChildStatusChanged(const User& user);
+
    protected:
     virtual ~Observer();
   };
@@ -70,9 +73,6 @@
     // on account_id hash would be accessing up-to-date value.
     virtual void ActiveUserHashChanged(const std::string& hash);
 
-    // Called when child status has changed.
-    virtual void UserChangedChildStatus(User* user);
-
    protected:
     virtual ~UserSessionStateObserver();
   };
diff --git a/components/user_manager/user_manager_base.cc b/components/user_manager/user_manager_base.cc
index 6ed0497..961e3b73 100644
--- a/components/user_manager/user_manager_base.cc
+++ b/components/user_manager/user_manager_base.cc
@@ -1028,8 +1028,8 @@
   SaveUserType(user->GetAccountId(), is_child
                                          ? user_manager::USER_TYPE_CHILD
                                          : user_manager::USER_TYPE_REGULAR);
-  for (auto& observer : session_state_observer_list_)
-    observer.UserChangedChildStatus(user);
+  for (auto& observer : observer_list_)
+    observer.OnChildStatusChanged(*user);
 }
 
 void UserManagerBase::ResetProfileEverInitialized(const AccountId& account_id) {
diff --git a/content/browser/accessibility/accessibility_tree_formatter_blink.cc b/content/browser/accessibility/accessibility_tree_formatter_blink.cc
index fc64639..1c1c81a 100644
--- a/content/browser/accessibility/accessibility_tree_formatter_blink.cc
+++ b/content/browser/accessibility/accessibility_tree_formatter_blink.cc
@@ -138,7 +138,8 @@
   dict->SetInteger("boundsWidth", bounds.width());
   dict->SetInteger("boundsHeight", bounds.height());
 
-  gfx::Rect page_bounds = node.GetPageBoundsRect();
+  bool offscreen = false;
+  gfx::Rect page_bounds = node.GetPageBoundsRect(&offscreen);
   dict->SetInteger("pageBoundsX", page_bounds.x());
   dict->SetInteger("pageBoundsY", page_bounds.y());
   dict->SetInteger("pageBoundsWidth", page_bounds.width());
@@ -151,11 +152,16 @@
   for (int state_index = ui::AX_STATE_NONE;
        state_index <= ui::AX_STATE_LAST;
        ++state_index) {
+    if (state_index == ui::AX_STATE_OFFSCREEN)
+      continue;
     auto state = static_cast<ui::AXState>(state_index);
     if (node.HasState(state))
       dict->SetBoolean(ui::ToString(state), true);
   }
 
+  if (offscreen)
+    dict->SetBoolean(ui::ToString(ui::AX_STATE_OFFSCREEN), true);
+
   for (int attr_index = ui::AX_STRING_ATTRIBUTE_NONE;
        attr_index <= ui::AX_STRING_ATTRIBUTE_LAST;
        ++attr_index) {
diff --git a/content/browser/accessibility/browser_accessibility.cc b/content/browser/accessibility/browser_accessibility.cc
index ac0ffff..1612f40 100644
--- a/content/browser/accessibility/browser_accessibility.cc
+++ b/content/browser/accessibility/browser_accessibility.cc
@@ -335,8 +335,8 @@
   return RelativeToAbsoluteBounds(gfx::RectF(), true);
 }
 
-gfx::Rect BrowserAccessibility::GetPageBoundsRect() const {
-  return RelativeToAbsoluteBounds(gfx::RectF(), false);
+gfx::Rect BrowserAccessibility::GetPageBoundsRect(bool* offscreen) const {
+  return RelativeToAbsoluteBounds(gfx::RectF(), false, offscreen);
 }
 
 gfx::Rect BrowserAccessibility::GetPageBoundsForRange(int start, int len)
@@ -854,11 +854,12 @@
 
 gfx::Rect BrowserAccessibility::RelativeToAbsoluteBounds(
     gfx::RectF bounds,
-    bool frame_only) const {
+    bool frame_only,
+    bool* offscreen) const {
   const BrowserAccessibility* node = this;
   while (node) {
-    bounds =
-        node->manager()->ax_tree()->RelativeToTreeBounds(node->node(), bounds);
+    bounds = node->manager()->ax_tree()->RelativeToTreeBounds(
+        node->node(), bounds, offscreen);
 
     // On some platforms we need to unapply root scroll offsets.
     const BrowserAccessibility* root = node->manager()->GetRoot();
diff --git a/content/browser/accessibility/browser_accessibility.h b/content/browser/accessibility/browser_accessibility.h
index 9114604c..3ddedd3 100644
--- a/content/browser/accessibility/browser_accessibility.h
+++ b/content/browser/accessibility/browser_accessibility.h
@@ -152,7 +152,9 @@
 
   // Returns the bounds of this object in coordinates relative to the
   // page (specifically, the top-left corner of the topmost web contents).
-  gfx::Rect GetPageBoundsRect() const;
+  // Optionally updates |offscreen| to be true if the element is offscreen
+  // within its page.
+  gfx::Rect GetPageBoundsRect(bool* offscreen = nullptr) const;
 
   // Returns the bounds of the given range in coordinates relative to the
   // top-left corner of the overall web area. Only valid when the
@@ -166,8 +168,10 @@
   // (which is relative to its nearest scrollable ancestor) to
   // absolute bounds, either in page coordinates (when |frameOnly| is
   // false), or in frame coordinates (when |frameOnly| is true).
+  // Updates optional |offscreen| to be true if the node is offscreen.
   virtual gfx::Rect RelativeToAbsoluteBounds(gfx::RectF bounds,
-                                             bool frame_only) const;
+                                             bool frame_only,
+                                             bool* offscreen = nullptr) const;
 
   // This is to handle the cases such as ARIA textbox, where the value should
   // be calculated from the object's inner text.
diff --git a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
index fd2e3cd1..ecbb9a34 100644
--- a/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
+++ b/content/browser/accessibility/dump_accessibility_tree_browsertest.cc
@@ -1494,6 +1494,15 @@
   RunHtmlTest(FILE_PATH_LITERAL("object.html"));
 }
 
+IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest, AccessibilityOffscreen) {
+  RunHtmlTest(FILE_PATH_LITERAL("offscreen.html"));
+}
+
+IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest,
+                       AccessibilityOffscreenScroll) {
+  RunHtmlTest(FILE_PATH_LITERAL("offscreen-scroll.html"));
+}
+
 IN_PROC_BROWSER_TEST_F(DumpAccessibilityTreeTest, AccessibilityOptgroup) {
   RunHtmlTest(FILE_PATH_LITERAL("optgroup.html"));
 }
diff --git a/content/browser/download/download_item_impl_unittest.cc b/content/browser/download/download_item_impl_unittest.cc
index 8d00725..f682b55 100644
--- a/content/browser/download/download_item_impl_unittest.cc
+++ b/content/browser/download/download_item_impl_unittest.cc
@@ -19,6 +19,7 @@
 #include "base/feature_list.h"
 #include "base/files/file_util.h"
 #include "base/memory/ptr_util.h"
+#include "base/test/histogram_tester.h"
 #include "base/test/scoped_task_environment.h"
 #include "base/threading/thread.h"
 #include "content/browser/byte_stream.h"
@@ -65,6 +66,11 @@
 
 namespace {
 
+template <typename T>
+base::HistogramBase::Sample ToHistogramSample(T t) {
+  return static_cast<base::HistogramBase::Sample>(t);
+}
+
 class MockDelegate : public DownloadItemImplDelegate {
  public:
   MockDelegate() : DownloadItemImplDelegate() {
@@ -645,6 +651,7 @@
                         Property(&DownloadUrlParameters::offset, 1)),
                   _));
 
+  base::HistogramTester histogram_tester;
   item->DestinationObserverAsWeakPtr()->DestinationError(
       DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR, 1,
       std::unique_ptr<crypto::SecureHash>());
@@ -660,6 +667,14 @@
   // the automatic resumption was triggered.
   task_environment_.RunUntilIdle();
 
+  // Interrupt reason is recorded in auto resumption even when download is not
+  // finally interrupted.
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR),
+      1);
+
   // The download item is currently in RESUMING_INTERNAL state, which maps to
   // IN_PROGRESS.
   CleanupItem(item, nullptr, DownloadItem::IN_PROGRESS);
@@ -684,6 +699,7 @@
                            Property(&base::FilePath::empty, true)),
                   _));
 
+  base::HistogramTester histogram_tester;
   item->DestinationObserverAsWeakPtr()->DestinationError(
       DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE, 1,
       std::unique_ptr<crypto::SecureHash>());
@@ -692,8 +708,15 @@
   // Since the download is resumed automatically, the interrupt count doesn't
   // increase.
   ASSERT_EQ(0, observer.interrupt_count());
-  task_environment_.RunUntilIdle();
 
+  task_environment_.RunUntilIdle();
+  // Auto resumption will record interrupt reason even if download is not
+  // finally interrupted.
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_SERVER_NO_RANGE),
+      1);
   CleanupItem(item, nullptr, DownloadItem::IN_PROGRESS);
 }
 
@@ -707,6 +730,7 @@
 
   // Interrupt the download, using a restartable interrupt.
   EXPECT_CALL(*download_file, Cancel());
+  base::HistogramTester histogram_tester;
   item->DestinationObserverAsWeakPtr()->DestinationError(
       DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, 1,
       std::unique_ptr<crypto::SecureHash>());
@@ -714,8 +738,12 @@
   // Should not try to auto-resume.
   ASSERT_EQ(1, observer.interrupt_count());
   ASSERT_EQ(0, observer.resume_count());
-  task_environment_.RunUntilIdle();
 
+  task_environment_.RunUntilIdle();
+  histogram_tester.ExpectBucketCount("Download.InterruptedReason",
+                                     ToHistogramSample<DownloadInterruptReason>(
+                                         DOWNLOAD_INTERRUPT_REASON_FILE_FAILED),
+                                     1);
   CleanupItem(item, nullptr, DownloadItem::INTERRUPTED);
 }
 
@@ -744,6 +772,7 @@
                         Property(&DownloadUrlParameters::offset, 1)),
                   _));
 
+  base::HistogramTester histogram_tester;
   item->DestinationObserverAsWeakPtr()->DestinationError(
       DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH, 1,
       std::unique_ptr<crypto::SecureHash>());
@@ -754,6 +783,11 @@
   ASSERT_EQ(0, observer.resume_count());
 
   task_environment_.RunUntilIdle();
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_SERVER_CONTENT_LENGTH_MISMATCH),
+      1);
   CleanupItem(item, nullptr, DownloadItem::IN_PROGRESS);
 }
 
@@ -774,10 +808,16 @@
   EXPECT_CALL(*download_file, Cancel());
 
   // Complete download to trigger final rename.
+  base::HistogramTester histogram_tester;
   item->DestinationObserverAsWeakPtr()->DestinationCompleted(
       0, std::unique_ptr<crypto::SecureHash>());
-  task_environment_.RunUntilIdle();
 
+  task_environment_.RunUntilIdle();
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_FILE_BLOCKED),
+      1);
   ASSERT_TRUE(observer.CheckAndResetDownloadUpdated());
   // Should not try to auto-resume.
   ASSERT_EQ(1, observer.interrupt_count());
@@ -787,6 +827,7 @@
 }
 
 TEST_F(DownloadItemTest, AutomaticResumption_AttemptLimit) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   base::WeakPtr<DownloadDestinationObserver> as_observer(
       item->DestinationObserverAsWeakPtr());
@@ -854,6 +895,11 @@
     ::testing::Mock::VerifyAndClearExpectations(mock_download_file_ref);
   }
 
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR),
+      DownloadItemImpl::kMaxAutoResumeAttempts + 1);
   EXPECT_EQ(DownloadItem::INTERRUPTED, item->GetState());
   EXPECT_EQ(1, observer.interrupt_count());
   CleanupItem(item, nullptr, DownloadItem::INTERRUPTED);
@@ -1126,6 +1172,7 @@
   std::unique_ptr<MockRequestHandle> request_handle =
       base::MakeUnique<MockRequestHandle>();
 
+  base::HistogramTester histogram_tester;
   EXPECT_CALL(*file, Cancel());
   EXPECT_CALL(*request_handle, CancelRequest(_));
   EXPECT_CALL(*file, Initialize(_, _, _, _))
@@ -1151,11 +1198,17 @@
             item->GetLastReason());
   EXPECT_FALSE(item->GetTargetFilePath().empty());
   EXPECT_TRUE(item->GetFullPath().empty());
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED),
+      1);
 }
 
 // Handling of downloads initiated via a failed request. In this case, Start()
 // will get called with a DownloadCreateInfo with a non-zero interrupt_reason.
 TEST_F(DownloadItemTest, StartFailedDownload) {
+  base::HistogramTester histogram_tester;
   create_info()->result = DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED;
   DownloadItemImpl* item = CreateDownloadItem();
 
@@ -1182,6 +1235,12 @@
                                DOWNLOAD_INTERRUPT_REASON_NONE);
   task_environment_.RunUntilIdle();
 
+  // Interrupt reason carried in create info should be recorded.
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED),
+      1);
   EXPECT_EQ(target_path, item->GetTargetFilePath());
   CleanupItem(item, NULL, DownloadItem::INTERRUPTED);
 }
@@ -1228,6 +1287,7 @@
 TEST_F(DownloadItemTest, CallbackAfterInterruptedRename) {
   DownloadItemImpl* item = CreateDownloadItem();
   DownloadTargetCallback callback;
+  base::HistogramTester histogram_tester;
   MockDownloadFile* download_file = CallDownloadItemStart(item, &callback);
   base::FilePath final_path(
       base::FilePath(kDummyTargetPath).AppendASCII("foo.bar"));
@@ -1247,9 +1307,14 @@
   // All the callbacks should have happened by now.
   ::testing::Mock::VerifyAndClearExpectations(download_file);
   mock_delegate()->VerifyAndClearExpectations();
+  histogram_tester.ExpectBucketCount("Download.InterruptedReason",
+                                     ToHistogramSample<DownloadInterruptReason>(
+                                         DOWNLOAD_INTERRUPT_REASON_FILE_FAILED),
+                                     1);
 }
 
 TEST_F(DownloadItemTest, Interrupted) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   MockDownloadFile* download_file =
       DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
@@ -1269,11 +1334,16 @@
   item->Cancel(true);
   EXPECT_EQ(DownloadItem::CANCELLED, item->GetState());
   EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_USER_CANCELED, item->GetLastReason());
+
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(reason), 1);
 }
 
 // Destination errors that occur before the intermediate rename shouldn't cause
 // the download to be marked as interrupted until after the intermediate rename.
 TEST_F(DownloadItemTest, InterruptedBeforeIntermediateRename_Restart) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   DownloadTargetCallback callback;
   MockDownloadFile* download_file = CallDownloadItemStart(item, &callback);
@@ -1303,12 +1373,17 @@
   EXPECT_EQ(DownloadItem::INTERRUPTED, item->GetState());
   EXPECT_TRUE(item->GetFullPath().empty());
   EXPECT_EQ(final_path, item->GetTargetFilePath());
+  histogram_tester.ExpectBucketCount("Download.InterruptedReason",
+                                     ToHistogramSample<DownloadInterruptReason>(
+                                         DOWNLOAD_INTERRUPT_REASON_FILE_FAILED),
+                                     1);
 }
 
 // As above. But if the download can be resumed by continuing, then the
 // intermediate path should be retained when the download is interrupted after
 // the intermediate rename succeeds.
 TEST_F(DownloadItemTest, InterruptedBeforeIntermediateRename_Continue) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   DownloadTargetCallback callback;
   MockDownloadFile* download_file = CallDownloadItemStart(item, &callback);
@@ -1342,11 +1417,17 @@
   EXPECT_EQ(DownloadItem::INTERRUPTED, item->GetState());
   EXPECT_EQ(new_intermediate_path, item->GetFullPath());
   EXPECT_EQ(final_path, item->GetTargetFilePath());
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED),
+      1);
 }
 
 // As above. If the intermediate rename fails, then the interrupt reason should
 // be set to the file error and the intermediate path should be empty.
 TEST_F(DownloadItemTest, InterruptedBeforeIntermediateRename_Failed) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   DownloadTargetCallback callback;
   MockDownloadFile* download_file = CallDownloadItemStart(item, &callback);
@@ -1377,6 +1458,15 @@
   EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, item->GetLastReason());
   EXPECT_TRUE(item->GetFullPath().empty());
   EXPECT_EQ(final_path, item->GetTargetFilePath());
+
+  // Rename error will overwrite the previous network interrupt reason.
+  // TODO(xingliu): See if we should report both interrupted reasons or the
+  // first one, see https://crbug.com/769040.
+  histogram_tester.ExpectBucketCount("Download.InterruptedReason",
+                                     ToHistogramSample<DownloadInterruptReason>(
+                                         DOWNLOAD_INTERRUPT_REASON_FILE_FAILED),
+                                     1);
+  histogram_tester.ExpectTotalCount("Download.InterruptedReason", 1);
 }
 
 TEST_F(DownloadItemTest, Canceled) {
@@ -1476,6 +1566,7 @@
 }
 
 TEST_F(DownloadItemTest, DestinationError_NoRestartRequired) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   MockDownloadFile* download_file =
       DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
@@ -1501,9 +1592,15 @@
   EXPECT_EQ(
       std::string(std::begin(kHashOfTestData1), std::end(kHashOfTestData1)),
       item->GetHash());
+  histogram_tester.ExpectBucketCount(
+      "Download.InterruptedReason",
+      ToHistogramSample<DownloadInterruptReason>(
+          DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED),
+      1);
 }
 
 TEST_F(DownloadItemTest, DestinationError_RestartRequired) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   MockDownloadFile* download_file =
       DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
@@ -1527,9 +1624,14 @@
   EXPECT_EQ(DownloadItem::INTERRUPTED, item->GetState());
   EXPECT_EQ(DOWNLOAD_INTERRUPT_REASON_FILE_FAILED, item->GetLastReason());
   EXPECT_EQ(std::string(), item->GetHash());
+  histogram_tester.ExpectBucketCount("Download.InterruptedReason",
+                                     ToHistogramSample<DownloadInterruptReason>(
+                                         DOWNLOAD_INTERRUPT_REASON_FILE_FAILED),
+                                     1);
 }
 
 TEST_F(DownloadItemTest, DestinationCompleted) {
+  base::HistogramTester histogram_tester;
   DownloadItemImpl* item = CreateDownloadItem();
   MockDownloadFile* download_file =
       DoIntermediateRename(item, DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS);
@@ -1568,6 +1670,8 @@
   // target determination hasn't completed, hence the download item is stuck in
   // TARGET_PENDING.
   CleanupItem(item, download_file, DownloadItem::IN_PROGRESS);
+
+  histogram_tester.ExpectTotalCount("Download.InterruptedReason", 0);
 }
 
 TEST_F(DownloadItemTest, EnabledActionsForNormalDownload) {
diff --git a/content/browser/download/download_manager_impl.cc b/content/browser/download/download_manager_impl.cc
index 0eb0fb0..7742e3b 100644
--- a/content/browser/download/download_manager_impl.cc
+++ b/content/browser/download/download_manager_impl.cc
@@ -38,6 +38,7 @@
 #include "content/browser/renderer_host/render_view_host_impl.h"
 #include "content/browser/storage_partition_impl.h"
 #include "content/browser/web_contents/web_contents_impl.h"
+#include "content/common/throttling_url_loader.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/content_browser_client.h"
 #include "content/public/browser/download_interrupt_reasons.h"
@@ -118,7 +119,7 @@
                      params->callback()));
 }
 
-std::unique_ptr<UrlDownloader, BrowserThread::DeleteOnIOThread> BeginDownload(
+DownloadManagerImpl::UniqueUrlDownloadHandlerPtr BeginDownload(
     std::unique_ptr<DownloadUrlParameters> params,
     content::ResourceContext* resource_context,
     uint32_t download_id,
@@ -156,14 +157,13 @@
     return nullptr;
   }
 
-  return std::unique_ptr<UrlDownloader, BrowserThread::DeleteOnIOThread>(
+  return DownloadManagerImpl::UniqueUrlDownloadHandlerPtr(
       UrlDownloader::BeginDownload(download_manager, std::move(url_request),
                                    params->referrer(), false)
           .release());
 }
 
-std::unique_ptr<ResourceDownloader, BrowserThread::DeleteOnIOThread>
-BeginResourceDownload(
+DownloadManagerImpl::UniqueUrlDownloadHandlerPtr BeginResourceDownload(
     std::unique_ptr<DownloadUrlParameters> params,
     std::unique_ptr<ResourceRequest> request,
     scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter,
@@ -181,14 +181,38 @@
     return nullptr;
   }
 
-  return std::unique_ptr<ResourceDownloader, BrowserThread::DeleteOnIOThread>(
-      ResourceDownloader::BeginDownload(download_manager, std::move(params),
-                                        std::move(request),
-                                        url_loader_factory_getter, download_id,
-                                        false)
+  return DownloadManagerImpl::UniqueUrlDownloadHandlerPtr(
+      ResourceDownloader::BeginDownload(
+          download_manager, std::move(params), std::move(request),
+          url_loader_factory_getter, download_id, false)
           .release());
 }
 
+// Creates a ResourceDownloader to own the URLLoader and intercept the response,
+// and passes it back to the DownloadManager.
+void InterceptNavigationResponse(
+    base::OnceCallback<void(DownloadManagerImpl::UniqueUrlDownloadHandlerPtr)>
+        callback,
+    base::WeakPtr<DownloadManagerImpl> download_manager,
+    const scoped_refptr<ResourceResponse>& response,
+    mojo::ScopedDataPipeConsumerHandle consumer_handle,
+    const SSLStatus& ssl_status,
+    std::unique_ptr<ResourceRequest> resource_request,
+    std::unique_ptr<ThrottlingURLLoader> url_loader,
+    base::Optional<ResourceRequestCompletionStatus> completion_status) {
+  DCHECK_CURRENTLY_ON(BrowserThread::IO);
+  DownloadManagerImpl::UniqueUrlDownloadHandlerPtr resource_downloader(
+      ResourceDownloader::InterceptNavigationResponse(
+          download_manager, std::move(resource_request), response,
+          std::move(consumer_handle), ssl_status, std::move(url_loader),
+          std::move(completion_status))
+          .release());
+
+  BrowserThread::PostTask(
+      BrowserThread::UI, FROM_HERE,
+      base::BindOnce(std::move(callback), std::move(resource_downloader)));
+}
+
 class DownloadItemFactoryImpl : public DownloadItemFactory {
  public:
   DownloadItemFactoryImpl() {}
@@ -603,8 +627,7 @@
 }
 
 void DownloadManagerImpl::AddUrlDownloadHandler(
-    std::unique_ptr<UrlDownloadHandler, BrowserThread::DeleteOnIOThread>
-        downloader) {
+    UniqueUrlDownloadHandlerPtr downloader) {
   if (downloader)
     url_download_handlers_.push_back(std::move(downloader));
 }
@@ -665,6 +688,19 @@
   return DOWNLOAD_INTERRUPT_REASON_NONE;
 }
 
+NavigationURLLoader::NavigationInterceptionCB
+DownloadManagerImpl::GetNavigationInterceptionCB(
+    const scoped_refptr<ResourceResponse>& response,
+    mojo::ScopedDataPipeConsumerHandle consumer_handle,
+    const SSLStatus& ssl_status) {
+  return base::BindOnce(
+      &InterceptNavigationResponse,
+      base::BindOnce(&DownloadManagerImpl::AddUrlDownloadHandler,
+                     weak_factory_.GetWeakPtr()),
+      weak_factory_.GetWeakPtr(), response, std::move(consumer_handle),
+      ssl_status);
+}
+
 int DownloadManagerImpl::RemoveDownloadsByURLAndTime(
     const base::Callback<bool(const GURL&)>& url_filter,
     base::Time remove_begin,
diff --git a/content/browser/download/download_manager_impl.h b/content/browser/download/download_manager_impl.h
index 49ca8ed..14e66faf 100644
--- a/content/browser/download/download_manager_impl.h
+++ b/content/browser/download/download_manager_impl.h
@@ -22,11 +22,13 @@
 #include "base/synchronization/lock.h"
 #include "content/browser/download/download_item_impl_delegate.h"
 #include "content/browser/download/url_download_handler.h"
+#include "content/browser/loader/navigation_url_loader.h"
 #include "content/common/content_export.h"
 #include "content/public/browser/browser_thread.h"
 #include "content/public/browser/download_manager.h"
 #include "content/public/browser/download_manager_delegate.h"
 #include "content/public/browser/download_url_parameters.h"
+#include "content/public/browser/ssl_status.h"
 
 namespace net {
 class NetLog;
@@ -44,6 +46,8 @@
                                            private DownloadItemImplDelegate {
  public:
   using DownloadItemImplCreated = base::Callback<void(DownloadItemImpl*)>;
+  using UniqueUrlDownloadHandlerPtr =
+      std::unique_ptr<UrlDownloadHandler, BrowserThread::DeleteOnIOThread>;
 
   // Caller guarantees that |net_log| will remain valid
   // for the lifetime of DownloadManagerImpl (until Shutdown() is called).
@@ -143,6 +147,12 @@
       int render_frame_route_id,
       bool do_not_prompt_for_login);
 
+  // Returns the callback to intercept the navigation response.
+  NavigationURLLoader::NavigationInterceptionCB GetNavigationInterceptionCB(
+      const scoped_refptr<ResourceResponse>& response,
+      mojo::ScopedDataPipeConsumerHandle consumer_handle,
+      const SSLStatus& ssl_status);
+
  private:
   using DownloadSet = std::set<DownloadItem*>;
   using DownloadGuidMap = std::unordered_map<std::string, DownloadItemImpl*>;
@@ -199,9 +209,7 @@
   void ShowDownloadInShell(DownloadItemImpl* download) override;
   void DownloadRemoved(DownloadItemImpl* download) override;
 
-  void AddUrlDownloadHandler(
-      std::unique_ptr<UrlDownloadHandler, BrowserThread::DeleteOnIOThread>
-          downloader);
+  void AddUrlDownloadHandler(UniqueUrlDownloadHandlerPtr downloader);
 
   // Factory for creation of downloads items.
   std::unique_ptr<DownloadItemFactory> item_factory_;
@@ -240,9 +248,7 @@
 
   net::NetLog* net_log_;
 
-  std::vector<
-      std::unique_ptr<UrlDownloadHandler, BrowserThread::DeleteOnIOThread>>
-      url_download_handlers_;
+  std::vector<UniqueUrlDownloadHandlerPtr> url_download_handlers_;
 
   base::WeakPtrFactory<DownloadManagerImpl> weak_factory_;
 
diff --git a/content/browser/download/download_response_handler.cc b/content/browser/download/download_response_handler.cc
index 58cf742..2e1bc68 100644
--- a/content/browser/download/download_response_handler.cc
+++ b/content/browser/download/download_response_handler.cc
@@ -45,22 +45,24 @@
 
 }  // namespace
 
-DownloadResponseHandler::DownloadResponseHandler(DownloadUrlParameters* params,
-                                                 Delegate* delegate,
-                                                 bool is_parallel_request)
+DownloadResponseHandler::DownloadResponseHandler(
+    ResourceRequest* resource_request,
+    Delegate* delegate,
+    std::unique_ptr<DownloadSaveInfo> save_info,
+    bool is_parallel_request,
+    bool is_transient)
     : delegate_(delegate),
       started_(false),
-      save_info_(base::MakeUnique<DownloadSaveInfo>(params->GetSaveInfo())),
-      url_chain_(1, params->url()),
-      guid_(params->guid()),
-      method_(params->method()),
-      referrer_(params->referrer().url),
-      is_transient_(params->is_transient()),
+      save_info_(std::move(save_info)),
+      url_chain_(1, resource_request->url),
+      method_(resource_request->method),
+      referrer_(resource_request->referrer),
+      is_transient_(is_transient),
       has_strong_validators_(false) {
   if (!is_parallel_request)
     RecordDownloadCount(UNTHROTTLED_COUNT);
-  if (params->initiator().has_value())
-    origin_ = params->initiator().value().GetURL();
+  if (resource_request->request_initiator.has_value())
+    origin_ = resource_request->request_initiator.value().GetURL();
 }
 
 DownloadResponseHandler::~DownloadResponseHandler() = default;
@@ -118,7 +120,6 @@
   create_info->connection_info = head.connection_info;
   create_info->url_chain = url_chain_;
   create_info->referrer_url = referrer_;
-  create_info->guid = guid_;
   create_info->transient = is_transient_;
   create_info->response_headers = head.headers;
   create_info->offset = create_info->save_info->offset;
diff --git a/content/browser/download/download_response_handler.h b/content/browser/download/download_response_handler.h
index b080d1e..0881b1d 100644
--- a/content/browser/download/download_response_handler.h
+++ b/content/browser/download/download_response_handler.h
@@ -16,7 +16,6 @@
 
 namespace content {
 
-class DownloadUrlParameters;
 struct DownloadCreateInfo;
 struct DownloadSaveInfo;
 
@@ -35,9 +34,11 @@
     virtual void OnReceiveRedirect() = 0;
   };
 
-  DownloadResponseHandler(DownloadUrlParameters* params,
+  DownloadResponseHandler(ResourceRequest* resource_request,
                           Delegate* delegate,
-                          bool is_parallel_request);
+                          std::unique_ptr<DownloadSaveInfo> save_info,
+                          bool is_parallel_request,
+                          bool is_transient);
   ~DownloadResponseHandler() override;
 
   // mojom::URLLoaderClient
@@ -73,7 +74,6 @@
   // Information needed to create DownloadCreateInfo when the time comes.
   std::unique_ptr<DownloadSaveInfo> save_info_;
   std::vector<GURL> url_chain_;
-  std::string guid_;
   std::string method_;
   GURL referrer_;
   bool is_transient_;
diff --git a/content/browser/download/resource_downloader.cc b/content/browser/download/resource_downloader.cc
index e066986..2b8d74f5 100644
--- a/content/browser/download/resource_downloader.cc
+++ b/content/browser/download/resource_downloader.cc
@@ -5,7 +5,6 @@
 #include "content/browser/download/resource_downloader.h"
 
 #include "content/browser/download/download_utils.h"
-#include "content/browser/url_loader_factory_getter.h"
 #include "content/common/throttling_url_loader.h"
 #include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/web_contents.h"
@@ -45,60 +44,106 @@
 // static
 std::unique_ptr<ResourceDownloader> ResourceDownloader::BeginDownload(
     base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
-    std::unique_ptr<DownloadUrlParameters> download_url_parameters,
+    std::unique_ptr<DownloadUrlParameters> params,
     std::unique_ptr<ResourceRequest> request,
     scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter,
     uint32_t download_id,
     bool is_parallel_request) {
+  mojom::URLLoaderFactoryPtr* factory =
+      params->url().SchemeIs(url::kBlobScheme)
+          ? url_loader_factory_getter->GetBlobFactory()
+          : url_loader_factory_getter->GetNetworkFactory();
   auto downloader = base::MakeUnique<ResourceDownloader>(
-      delegate, std::move(download_url_parameters), url_loader_factory_getter,
-      download_id, is_parallel_request);
-  downloader->Start(std::move(request));
+      delegate, std::move(request),
+      base::MakeUnique<DownloadSaveInfo>(params->GetSaveInfo()), download_id,
+      params->guid(), is_parallel_request, params->is_transient());
+  downloader->Start(factory, std::move(params));
 
   return downloader;
 }
 
+// static
+std::unique_ptr<ResourceDownloader>
+ResourceDownloader::InterceptNavigationResponse(
+    base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
+    std::unique_ptr<ResourceRequest> resource_request,
+    const scoped_refptr<ResourceResponse>& response,
+    mojo::ScopedDataPipeConsumerHandle consumer_handle,
+    const SSLStatus& ssl_status,
+    std::unique_ptr<ThrottlingURLLoader> url_loader,
+    base::Optional<ResourceRequestCompletionStatus> completion_status) {
+  auto downloader = base::MakeUnique<ResourceDownloader>(
+      delegate, std::move(resource_request),
+      base::MakeUnique<DownloadSaveInfo>(), content::DownloadItem::kInvalidId,
+      std::string(), false, false);
+  downloader->InterceptResponse(std::move(url_loader), response,
+                                std::move(consumer_handle), ssl_status,
+                                std::move(completion_status));
+  return downloader;
+}
+
 ResourceDownloader::ResourceDownloader(
     base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
-    std::unique_ptr<DownloadUrlParameters> download_url_parameters,
-    scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter,
+    std::unique_ptr<ResourceRequest> resource_request,
+    std::unique_ptr<DownloadSaveInfo> save_info,
     uint32_t download_id,
-    bool is_parallel_request)
+    std::string guid,
+    bool is_parallel_request,
+    bool is_transient)
     : delegate_(delegate),
-      download_url_parameters_(std::move(download_url_parameters)),
-      url_loader_factory_getter_(url_loader_factory_getter),
-      response_handler_(download_url_parameters_.get(),
+      resource_request_(std::move(resource_request)),
+      response_handler_(resource_request_.get(),
                         this,
-                        is_parallel_request),
+                        std::move(save_info),
+                        is_parallel_request,
+                        is_transient),
       download_id_(download_id),
+      guid_(guid),
       weak_ptr_factory_(this) {}
 
 ResourceDownloader::~ResourceDownloader() = default;
 
-void ResourceDownloader::Start(std::unique_ptr<ResourceRequest> request) {
-  mojom::URLLoaderFactoryPtr* factory =
-      download_url_parameters_->url().SchemeIs(url::kBlobScheme)
-          ? url_loader_factory_getter_->GetBlobFactory()
-          : url_loader_factory_getter_->GetNetworkFactory();
+void ResourceDownloader::Start(
+    mojom::URLLoaderFactoryPtr* factory,
+    std::unique_ptr<DownloadUrlParameters> download_url_parameters) {
+  callback_ = download_url_parameters->callback();
   url_loader_ = ThrottlingURLLoader::CreateLoaderAndStart(
       factory->get(), std::vector<std::unique_ptr<URLLoaderThrottle>>(),
       0,  // routing_id
       0,  // request_id
       mojom::kURLLoadOptionSendSSLInfo | mojom::kURLLoadOptionSniffMimeType,
-      *(request.get()), &response_handler_,
-      download_url_parameters_->GetNetworkTrafficAnnotation());
+      *(resource_request_.get()), &response_handler_,
+      download_url_parameters->GetNetworkTrafficAnnotation());
   url_loader_->SetPriority(net::RequestPriority::IDLE,
                            0 /* intra_priority_value */);
 }
 
+void ResourceDownloader::InterceptResponse(
+    std::unique_ptr<ThrottlingURLLoader> url_loader,
+    const scoped_refptr<ResourceResponse>& response,
+    mojo::ScopedDataPipeConsumerHandle consumer_handle,
+    const SSLStatus& ssl_status,
+    base::Optional<ResourceRequestCompletionStatus> completion_status) {
+  url_loader_ = std::move(url_loader);
+  url_loader_->set_forwarding_client(&response_handler_);
+  net::SSLInfo info;
+  info.cert_status = ssl_status.cert_status;
+  response_handler_.OnReceiveResponse(response->head,
+                                      base::Optional<net::SSLInfo>(info),
+                                      mojom::DownloadedTempFilePtr());
+  response_handler_.OnStartLoadingResponseBody(std::move(consumer_handle));
+  if (completion_status.has_value())
+    response_handler_.OnComplete(completion_status.value());
+}
+
 void ResourceDownloader::OnResponseStarted(
     std::unique_ptr<DownloadCreateInfo> download_create_info,
     mojom::DownloadStreamHandlePtr stream_handle) {
   download_create_info->download_id = download_id_;
-  if (download_url_parameters_->render_process_host_id() >= 0) {
+  download_create_info->guid = guid_;
+  if (resource_request_->origin_pid >= 0) {
     download_create_info->request_handle.reset(new RequestHandle(
-        download_url_parameters_->render_process_host_id(),
-        download_url_parameters_->render_frame_host_routing_id()));
+        resource_request_->origin_pid, resource_request_->render_frame_id));
   }
   BrowserThread::PostTask(
       BrowserThread::UI, FROM_HERE,
@@ -106,7 +151,7 @@
                      delegate_, std::move(download_create_info),
                      base::MakeUnique<DownloadManager::InputStream>(
                          std::move(stream_handle)),
-                     download_url_parameters_->callback()));
+                     callback_));
 }
 
 void ResourceDownloader::OnReceiveRedirect() {
diff --git a/content/browser/download/resource_downloader.h b/content/browser/download/resource_downloader.h
index b5146e8f..6b00e687 100644
--- a/content/browser/download/resource_downloader.h
+++ b/content/browser/download/resource_downloader.h
@@ -7,13 +7,14 @@
 
 #include "content/browser/download/download_response_handler.h"
 #include "content/browser/download/url_download_handler.h"
+#include "content/browser/url_loader_factory_getter.h"
+#include "content/public/browser/ssl_status.h"
 #include "content/public/common/resource_request.h"
 #include "content/public/common/url_loader.mojom.h"
 
 namespace content {
 
 class ThrottlingURLLoader;
-class URLLoaderFactoryGetter;
 
 // Class for handing the download of a url.
 class ResourceDownloader : public UrlDownloadHandler,
@@ -28,12 +29,25 @@
       uint32_t download_id,
       bool is_parallel_request);
 
-  ResourceDownloader(
+  // Called to intercept a navigation response.
+  static std::unique_ptr<ResourceDownloader> InterceptNavigationResponse(
       base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
-      std::unique_ptr<DownloadUrlParameters> download_url_parameters,
-      scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter,
-      uint32_t download_id,
-      bool is_parallel_request);
+      std::unique_ptr<ResourceRequest> resource_request,
+      const scoped_refptr<ResourceResponse>& response,
+      mojo::ScopedDataPipeConsumerHandle consumer_handle,
+      const SSLStatus& ssl_status,
+      std::unique_ptr<ThrottlingURLLoader> url_loader,
+      base::Optional<ResourceRequestCompletionStatus> completion_status);
+
+  ResourceDownloader(base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
+                     std::unique_ptr<ResourceRequest> resource_request,
+                     std::unique_ptr<DownloadSaveInfo> save_info,
+                     uint32_t download_id,
+                     std::string guid,
+                     bool is_parallel_request,
+                     bool is_transient);
+  ResourceDownloader(base::WeakPtr<UrlDownloadHandler::Delegate> delegate,
+                     std::unique_ptr<ThrottlingURLLoader> url_loader);
   ~ResourceDownloader() override;
 
   // DownloadResponseHandler::Delegate
@@ -44,18 +58,24 @@
 
  private:
   // Helper method to start the network request.
-  void Start(std::unique_ptr<ResourceRequest> request);
+  void Start(mojom::URLLoaderFactoryPtr* factory,
+             std::unique_ptr<DownloadUrlParameters> download_url_parameters);
+
+  // Intercepts the navigation response and takes ownership of the |url_loader|.
+  void InterceptResponse(
+      std::unique_ptr<ThrottlingURLLoader> url_loader,
+      const scoped_refptr<ResourceResponse>& response,
+      mojo::ScopedDataPipeConsumerHandle consumer_handle,
+      const SSLStatus& ssl_status,
+      base::Optional<ResourceRequestCompletionStatus> completion_status);
 
   base::WeakPtr<UrlDownloadHandler::Delegate> delegate_;
 
   // URLLoader for sending out the request.
   std::unique_ptr<ThrottlingURLLoader> url_loader_;
 
-  // Parameters for constructing the ResourceRequest.
-  std::unique_ptr<DownloadUrlParameters> download_url_parameters_;
-
-  // Object for retrieving the URLLoaderFactory.
-  scoped_refptr<URLLoaderFactoryGetter> url_loader_factory_getter_;
+  // The ResourceRequest for this object.
+  std::unique_ptr<ResourceRequest> resource_request_;
 
   // Object for handing the server response.
   DownloadResponseHandler response_handler_;
@@ -64,6 +84,12 @@
   // download.
   uint32_t download_id_;
 
+  // GUID of the download, or empty if this is a new download.
+  std::string guid_;
+
+  // Callback to run after download starts.
+  DownloadUrlParameters::OnStartedCallback callback_;
+
   base::WeakPtrFactory<ResourceDownloader> weak_ptr_factory_;
 
   DISALLOW_COPY_AND_ASSIGN(ResourceDownloader);
diff --git a/content/browser/frame_host/navigation_request.cc b/content/browser/frame_host/navigation_request.cc
index ee570c4..b4ece03 100644
--- a/content/browser/frame_host/navigation_request.cc
+++ b/content/browser/frame_host/navigation_request.cc
@@ -13,6 +13,7 @@
 #include "content/browser/appcache/chrome_appcache_service.h"
 #include "content/browser/child_process_security_policy_impl.h"
 #include "content/browser/devtools/render_frame_devtools_agent_host.h"
+#include "content/browser/download/download_manager_impl.h"
 #include "content/browser/frame_host/debug_urls.h"
 #include "content/browser/frame_host/frame_tree.h"
 #include "content/browser/frame_host/frame_tree_node.h"
@@ -728,6 +729,8 @@
   response_ = response;
   body_ = std::move(body);
   handle_ = std::move(consumer_handle);
+  ssl_status_ = ssl_status;
+  is_download_ = is_download;
 
   subresource_loader_factory_info_ = std::move(subresource_loader_factory_info);
 
@@ -970,8 +973,23 @@
 
   // If the NavigationThrottles allowed the navigation to continue, have the
   // processing of the response resume in the network stack.
-  if (result.action() == NavigationThrottle::PROCEED)
+  if (result.action() == NavigationThrottle::PROCEED) {
+    // If this is a download, intercept the navigation response and pass it to
+    // DownloadManager, and cancel the navigation.
+    if (is_download_ &&
+        base::FeatureList::IsEnabled(features::kNetworkService)) {
+      BrowserContext* browser_context =
+          frame_tree_node_->navigator()->GetController()->GetBrowserContext();
+      DownloadManagerImpl* download_manager = static_cast<DownloadManagerImpl*>(
+          BrowserContext::GetDownloadManager(browser_context));
+      loader_->InterceptNavigation(
+          download_manager->GetNavigationInterceptionCB(
+              response_, std::move(handle_), ssl_status_));
+      OnRequestFailed(false, net::ERR_ABORTED, base::nullopt, false);
+      return;
+    }
     loader_->ProceedWithResponse();
+  }
 
   // Abort the request if needed. This includes requests that were blocked by
   // NavigationThrottles and requests that should not commit (e.g. downloads,
diff --git a/content/browser/frame_host/navigation_request.h b/content/browser/frame_host/navigation_request.h
index c78665b..f2ee387 100644
--- a/content/browser/frame_host/navigation_request.h
+++ b/content/browser/frame_host/navigation_request.h
@@ -316,6 +316,8 @@
   scoped_refptr<ResourceResponse> response_;
   std::unique_ptr<StreamHandle> body_;
   mojo::ScopedDataPipeConsumerHandle handle_;
+  SSLStatus ssl_status_;
+  bool is_download_;
 
   base::Closure on_start_checks_complete_closure_;
 
diff --git a/content/browser/frame_host/render_frame_host_impl.cc b/content/browser/frame_host/render_frame_host_impl.cc
index fff0f247..19ca15a 100644
--- a/content/browser/frame_host/render_frame_host_impl.cc
+++ b/content/browser/frame_host/render_frame_host_impl.cc
@@ -3372,6 +3372,19 @@
       !IsURLHandledByNetworkStack(common_params.url) ||
       FrameMsg_Navigate_Type::IsSameDocument(common_params.navigation_type) ||
       IsRendererDebugURL(common_params.url));
+
+  // TODO(arthursonzogni): Consider using separate methods and IPCs for
+  // javascript-url navigation. Excluding this case from the general one will
+  // prevent us from doing inappropriate things with javascript-url.
+  // See https://crbug.com/766149.
+  if (common_params.url.SchemeIs(url::kJavaScriptScheme)) {
+    Send(new FrameMsg_CommitNavigation(
+        routing_id_, ResourceResponseHead(), GURL(),
+        FrameMsg_CommitDataNetworkService_Params(), common_params,
+        request_params));
+    return;
+  }
+
   UpdatePermissionsForNavigation(common_params, request_params);
 
   // Get back to a clean state, in case we start a new navigation without
diff --git a/content/browser/loader/navigation_url_loader.h b/content/browser/loader/navigation_url_loader.h
index e015126..208fa14 100644
--- a/content/browser/loader/navigation_url_loader.h
+++ b/content/browser/loader/navigation_url_loader.h
@@ -7,8 +7,11 @@
 
 #include <memory>
 
+#include "base/callback.h"
 #include "base/macros.h"
+#include "base/optional.h"
 #include "content/common/content_export.h"
+#include "content/public/common/resource_request_completion_status.h"
 
 namespace content {
 
@@ -19,7 +22,9 @@
 class ResourceContext;
 class ServiceWorkerNavigationHandle;
 class StoragePartition;
+class ThrottlingURLLoader;
 struct NavigationRequestInfo;
+struct ResourceRequest;
 
 // PlzNavigate: The navigation logic's UI thread entry point into the resource
 // loading stack. It exposes an interface to control the request prior to
@@ -56,6 +61,20 @@
   // Called in response to OnResponseStarted to process the response.
   virtual void ProceedWithResponse() = 0;
 
+  // Callback to intercept the response from the URLLoader. Only used when
+  // network service is enabled. Args: the initial resource request,
+  // the URLLoader for sending the request, optional completion status if
+  // it has already been received.
+  using NavigationInterceptionCB =
+      base::OnceCallback<void(std::unique_ptr<ResourceRequest>,
+                              std::unique_ptr<ThrottlingURLLoader>,
+                              base::Optional<ResourceRequestCompletionStatus>)>;
+
+  // This method is called to intercept the url response. Caller is responsible
+  // for handling the URLLoader later on. The callback should be called on the
+  // same thread that URLLoader is constructed.
+  virtual void InterceptNavigation(NavigationInterceptionCB callback) = 0;
+
  protected:
   NavigationURLLoader() {}
 
diff --git a/content/browser/loader/navigation_url_loader_impl.cc b/content/browser/loader/navigation_url_loader_impl.cc
index 6371909..6b77e7f 100644
--- a/content/browser/loader/navigation_url_loader_impl.cc
+++ b/content/browser/loader/navigation_url_loader_impl.cc
@@ -85,6 +85,9 @@
       base::BindOnce(&NavigationURLLoaderImplCore::ProceedWithResponse, core_));
 }
 
+void NavigationURLLoaderImpl::InterceptNavigation(
+    NavigationURLLoader::NavigationInterceptionCB callback) {}
+
 void NavigationURLLoaderImpl::NotifyRequestRedirected(
     const net::RedirectInfo& redirect_info,
     const scoped_refptr<ResourceResponse>& response) {
diff --git a/content/browser/loader/navigation_url_loader_impl.h b/content/browser/loader/navigation_url_loader_impl.h
index 3349458..86f0e021 100644
--- a/content/browser/loader/navigation_url_loader_impl.h
+++ b/content/browser/loader/navigation_url_loader_impl.h
@@ -45,6 +45,7 @@
   // NavigationURLLoader implementation.
   void FollowRedirect() override;
   void ProceedWithResponse() override;
+  void InterceptNavigation(NavigationInterceptionCB callback) override;
 
  private:
   friend class NavigationURLLoaderImplCore;
diff --git a/content/browser/loader/navigation_url_loader_network_service.cc b/content/browser/loader/navigation_url_loader_network_service.cc
index 85ee1ec..7fb89fe 100644
--- a/content/browser/loader/navigation_url_loader_network_service.cc
+++ b/content/browser/loader/navigation_url_loader_network_service.cc
@@ -333,6 +333,17 @@
     Restart();
   }
 
+  // Navigation is intercepted, transfer the |resource_request_|, |url_loader_|
+  // and the |completion_status_| to the new owner. The new owner is
+  // responsible for handling all the mojom::URLLoaderClient callbacks from now
+  // on.
+  void InterceptNavigation(
+      NavigationURLLoader::NavigationInterceptionCB callback) {
+    std::move(callback).Run(std::move(resource_request_),
+                            std::move(url_loader_),
+                            std::move(completion_status_));
+  }
+
   // Ownership of the URLLoaderFactoryPtrInfo instance is transferred to the
   // caller.
   mojom::URLLoaderFactoryPtrInfo GetSubresourceURLLoaderFactory() {
@@ -418,6 +429,7 @@
       if (MaybeCreateLoaderForResponse(ResourceResponseHead()))
         return;
     }
+    completion_status_ = completion_status;
     BrowserThread::PostTask(
         BrowserThread::UI, FROM_HERE,
         base::BindOnce(&NavigationURLLoaderNetworkService::OnComplete, owner_,
@@ -479,6 +491,12 @@
   // URLLoaderClient::OnReceivedResponse() is called.
   bool received_response_ = false;
 
+  // The completion status if it has been received. This is needed to handle
+  // the case that the response is intercepted by download, and OnComplete() is
+  // already called while we are transferring the |url_loader_| and response
+  // body to download code.
+  base::Optional<ResourceRequestCompletionStatus> completion_status_;
+
   DISALLOW_COPY_AND_ASSIGN(URLLoaderRequestController);
 };
 
@@ -583,6 +601,15 @@
 
 void NavigationURLLoaderNetworkService::ProceedWithResponse() {}
 
+void NavigationURLLoaderNetworkService::InterceptNavigation(
+    NavigationURLLoader::NavigationInterceptionCB callback) {
+  BrowserThread::PostTask(
+      BrowserThread::IO, FROM_HERE,
+      base::BindOnce(&URLLoaderRequestController::InterceptNavigation,
+                     base::Unretained(request_controller_.get()),
+                     std::move(callback)));
+}
+
 void NavigationURLLoaderNetworkService::OnReceiveResponse(
     scoped_refptr<ResourceResponse> response,
     const base::Optional<net::SSLInfo>& ssl_info,
diff --git a/content/browser/loader/navigation_url_loader_network_service.h b/content/browser/loader/navigation_url_loader_network_service.h
index 9e7f5b9..a73b2048 100644
--- a/content/browser/loader/navigation_url_loader_network_service.h
+++ b/content/browser/loader/navigation_url_loader_network_service.h
@@ -38,6 +38,7 @@
   // NavigationURLLoader implementation:
   void FollowRedirect() override;
   void ProceedWithResponse() override;
+  void InterceptNavigation(NavigationInterceptionCB callback) override;
 
   void OnReceiveResponse(scoped_refptr<ResourceResponse> response,
                          const base::Optional<net::SSLInfo>& ssl_info,
diff --git a/content/browser/renderer_host/render_process_host_impl.cc b/content/browser/renderer_host/render_process_host_impl.cc
index bcdc434..c384508 100644
--- a/content/browser/renderer_host/render_process_host_impl.cc
+++ b/content/browser/renderer_host/render_process_host_impl.cc
@@ -182,6 +182,7 @@
 #include "ipc/ipc_logging.h"
 #include "media/base/media_switches.h"
 #include "media/media_features.h"
+#include "media/mojo/services/video_decode_perf_history.h"
 #include "mojo/edk/embedder/embedder.h"
 #include "mojo/public/cpp/bindings/associated_interface_ptr.h"
 #include "mojo/public/cpp/bindings/strong_binding.h"
@@ -1941,6 +1942,9 @@
                    storage_partition_impl_->GetBlobRegistry(), GetID()));
   }
 
+  registry->AddInterface(
+      base::Bind(&media::VideoDecodePerfHistory::BindRequest));
+
   ServiceManagerConnection* service_manager_connection =
       BrowserContext::GetServiceManagerConnectionFor(browser_context_);
   std::unique_ptr<ConnectionFilterImpl> connection_filter(
diff --git a/content/browser/service_worker/service_worker_script_url_loader.cc b/content/browser/service_worker/service_worker_script_url_loader.cc
index 865738bc..cde94a4 100644
--- a/content/browser/service_worker/service_worker_script_url_loader.cc
+++ b/content/browser/service_worker/service_worker_script_url_loader.cc
@@ -14,6 +14,7 @@
 #include "content/browser/service_worker/service_worker_version.h"
 #include "content/browser/service_worker/service_worker_write_to_cache_job.h"
 #include "content/browser/url_loader_factory_getter.h"
+#include "content/common/service_worker/service_worker_utils.h"
 #include "content/public/common/resource_response.h"
 #include "third_party/WebKit/common/mime_util/mime_util.h"
 
@@ -157,9 +158,6 @@
     return;
   }
 
-  // TODO(nhiroki): Check the path restriction.
-  // (See ServiceWorkerWriteToCacheJob::CheckPathRestriction())
-
   // TODO(nhiroki): Check the SSL certificate.
 
   if (resource_type_ == RESOURCE_TYPE_SERVICE_WORKER) {
@@ -170,6 +168,23 @@
           ResourceRequestCompletionStatus(net::ERR_INSECURE_RESPONSE));
       return;
     }
+
+    // Check the path restriction defined in the spec:
+    // https://w3c.github.io/ServiceWorker/#service-worker-script-response
+    const char kServiceWorkerAllowed[] = "Service-Worker-Allowed";
+    std::string service_worker_allowed;
+    bool has_header = response_head.headers->EnumerateHeader(
+        nullptr, kServiceWorkerAllowed, &service_worker_allowed);
+    std::string error_message;
+    if (!ServiceWorkerUtils::IsPathRestrictionSatisfied(
+            version_->scope(), request_url_,
+            has_header ? &service_worker_allowed : nullptr, &error_message)) {
+      // TODO(nhiroki): Report |error_message|.
+      CommitCompleted(
+          ResourceRequestCompletionStatus(net::ERR_INSECURE_RESPONSE));
+      return;
+    }
+
     version_->SetMainScriptHttpResponseInfo(*response_info);
   }
 
diff --git a/content/browser/service_worker/service_worker_script_url_loader_unittest.cc b/content/browser/service_worker/service_worker_script_url_loader_unittest.cc
index 688b2be..d2de0530 100644
--- a/content/browser/service_worker/service_worker_script_url_loader_unittest.cc
+++ b/content/browser/service_worker/service_worker_script_url_loader_unittest.cc
@@ -138,7 +138,7 @@
 
     mock_server_->AddResponse(GURL(kNormalScriptURL),
                               std::string("HTTP/1.1 200 OK\n"
-                                          "CONTENT-TYPE: text/javascript\n\n"),
+                                          "Content-Type: text/javascript\n\n"),
                               std::string("this body came from the network"));
 
     // Initialize URLLoaderFactory.
@@ -159,8 +159,7 @@
 
   // Sets up ServiceWorkerRegistration and ServiceWorkerVersion. This should be
   // called before DoRequest().
-  void SetUpRegistration(const GURL& script_url) {
-    GURL scope = script_url;
+  void SetUpRegistration(const GURL& script_url, const GURL& scope) {
     registration_ = base::MakeRefCounted<ServiceWorkerRegistration>(
         blink::mojom::ServiceWorkerRegistrationOptions(scope), 1L,
         helper_->context()->AsWeakPtr());
@@ -169,6 +168,12 @@
     version_->SetStatus(ServiceWorkerVersion::NEW);
   }
 
+  // Sets up ServiceWorkerRegistration and ServiceWorkerVersion with the default
+  // scope.
+  void SetUpRegistration(const GURL& script_url) {
+    SetUpRegistration(script_url, script_url.GetWithoutFilename());
+  }
+
   void DoRequest(const GURL& request_url) {
     DCHECK(registration_);
     DCHECK(version_);
@@ -271,7 +276,7 @@
   const GURL kEmptyScriptURL("https://example.com/empty.js");
   mock_server_->AddResponse(kEmptyScriptURL,
                             std::string("HTTP/1.1 200 OK\n"
-                                        "CONTENT-TYPE: text/javascript\n\n"),
+                                        "Content-Type: text/javascript\n\n"),
                             std::string());
   SetUpRegistration(kEmptyScriptURL);
   DoRequest(kEmptyScriptURL);
@@ -328,7 +333,7 @@
   const GURL kBadMimeTypeScriptURL("https://example.com/bad-mime-type.js");
   mock_server_->AddResponse(kBadMimeTypeScriptURL,
                             std::string("HTTP/1.1 200 OK\n"
-                                        "CONTENT-TYPE: text/css\n\n"),
+                                        "Content-Type: text/css\n\n"),
                             std::string("body with bad MIME type"));
   SetUpRegistration(kBadMimeTypeScriptURL);
   DoRequest(kBadMimeTypeScriptURL);
@@ -343,6 +348,55 @@
   EXPECT_FALSE(VerifyStoredResponse(kBadMimeTypeScriptURL));
 }
 
+TEST_F(ServiceWorkerScriptURLLoaderTest, Success_PathRestriction) {
+  // |kScope| is not under the default scope ("/out-of-scope/"), but the
+  // Service-Worker-Allowed header allows it.
+  const GURL kScriptURL("https://example.com/out-of-scope/normal.js");
+  const GURL kScope("https://example.com/in-scope/");
+  mock_server_->AddResponse(
+      kScriptURL,
+      std::string("HTTP/1.1 200 OK\n"
+                  "Content-Type: text/javascript\n"
+                  "Service-Worker-Allowed: /in-scope/\n\n"),
+      std::string());
+  SetUpRegistration(kScriptURL, kScope);
+  DoRequest(kScriptURL);
+  client_.RunUntilComplete();
+  EXPECT_EQ(net::OK, client_.completion_status().error_code);
+
+  // The client should have received the response.
+  EXPECT_TRUE(client_.has_received_response());
+  EXPECT_TRUE(client_.response_body().is_valid());
+  std::string response;
+  EXPECT_TRUE(mojo::common::BlockingCopyToString(
+      client_.response_body_release(), &response));
+  EXPECT_EQ(mock_server_->GetBody(kScriptURL), response);
+
+  // The response should also be stored in the storage.
+  EXPECT_TRUE(VerifyStoredResponse(kScriptURL));
+}
+
+TEST_F(ServiceWorkerScriptURLLoaderTest, Error_PathRestriction) {
+  // |kScope| is not under the default scope ("/out-of-scope/") and the
+  // Service-Worker-Allowed header is not specified.
+  const GURL kScriptURL("https://example.com/out-of-scope/normal.js");
+  const GURL kScope("https://example.com/in-scope/");
+  mock_server_->AddResponse(kScriptURL,
+                            std::string("HTTP/1.1 200 OK\n"
+                                        "Content-Type: text/javascript\n\n"),
+                            std::string());
+  SetUpRegistration(kScriptURL, kScope);
+  DoRequest(kScriptURL);
+  client_.RunUntilComplete();
+
+  // The request should be failed because the scope is not allowed.
+  EXPECT_EQ(net::ERR_INSECURE_RESPONSE, client_.completion_status().error_code);
+  EXPECT_FALSE(client_.has_received_response());
+
+  // The response shouldn't be stored in the storage.
+  EXPECT_FALSE(VerifyStoredResponse(kScriptURL));
+}
+
 TEST_F(ServiceWorkerScriptURLLoaderTest, Error_RedundantWorker) {
   GURL script_url(kNormalScriptURL);
   SetUpRegistration(script_url);
diff --git a/content/common/throttling_url_loader.h b/content/common/throttling_url_loader.h
index ec666656..e5711b9 100644
--- a/content/common/throttling_url_loader.h
+++ b/content/common/throttling_url_loader.h
@@ -73,6 +73,11 @@
   // Disconnects the client connection and releases the URLLoader.
   void DisconnectClient();
 
+  // Sets the forwarding client to receive all subsequent notifications.
+  void set_forwarding_client(mojom::URLLoaderClient* client) {
+    forwarding_client_ = client;
+  }
+
  private:
   class ForwardingThrottleDelegate;
 
diff --git a/content/public/app/mojo/content_browser_manifest.json b/content/public/app/mojo/content_browser_manifest.json
index aa4ea8c9..83093cd4 100644
--- a/content/public/app/mojo/content_browser_manifest.json
+++ b/content/public/app/mojo/content_browser_manifest.json
@@ -42,6 +42,7 @@
           "device::mojom::BatteryMonitor",
           "device::mojom::GamepadMonitor",
           "discardable_memory::mojom::DiscardableSharedMemoryManager",
+          "media::mojom::VideoDecodePerfHistory",
           "memory_coordinator::mojom::MemoryCoordinatorHandle",
           "metrics::mojom::SingleSampleMetricsProvider",
           "payments::mojom::PaymentManager",
diff --git a/content/test/data/accessibility/html/offscreen-expected-blink.txt b/content/test/data/accessibility/html/offscreen-expected-blink.txt
new file mode 100644
index 0000000..a372eb4
--- /dev/null
+++ b/content/test/data/accessibility/html/offscreen-expected-blink.txt
@@ -0,0 +1,4 @@
+rootWebArea
+++button name='Onscreen'
+++button offscreen name='Offscreen'
+<-- End-of-file -->
diff --git a/content/test/data/accessibility/html/offscreen-scroll-expected-blink.txt b/content/test/data/accessibility/html/offscreen-scroll-expected-blink.txt
new file mode 100644
index 0000000..1251327b
--- /dev/null
+++ b/content/test/data/accessibility/html/offscreen-scroll-expected-blink.txt
@@ -0,0 +1,4 @@
+rootWebArea scrollY=100
+++button offscreen name='Onscreen before scroll'
+++button name='Scrolled Button'
+<-- End-of-file -->
diff --git a/content/test/data/accessibility/html/offscreen-scroll.html b/content/test/data/accessibility/html/offscreen-scroll.html
new file mode 100644
index 0000000..76ef9fd
--- /dev/null
+++ b/content/test/data/accessibility/html/offscreen-scroll.html
@@ -0,0 +1,23 @@
+<!--
+@BLINK-ALLOW:scrollY=*
+@BLINK-ALLOW:offscreen
+@WAIT-FOR:Scrolled Button
+-->
+
+<html>
+<head>
+  <script>
+    window.onload = function(unused) {
+      setTimeout(function() {
+        window.scrollTo(0, 100);
+        document.getElementById('second').setAttribute('aria-label','Scrolled Button');
+      }, 100);
+    };
+  </script>
+</head>
+<body>
+  <button>Onscreen before scroll</button>
+  <div style="height:650px"></div>
+  <button id="second">Offscreen before scroll</button>
+</body>
+</html>
diff --git a/content/test/data/accessibility/html/offscreen.html b/content/test/data/accessibility/html/offscreen.html
new file mode 100644
index 0000000..3d843215
--- /dev/null
+++ b/content/test/data/accessibility/html/offscreen.html
@@ -0,0 +1,11 @@
+<!--
+@BLINK-ALLOW:offscreen
+-->
+
+<html>
+<body>
+<button>Onscreen</button>
+<div style="height:650px"></div>
+<button>Offscreen</button>
+</body>
+</html>
diff --git a/content/test/test_navigation_url_loader.cc b/content/test/test_navigation_url_loader.cc
index 367e0f90..f481a2f 100644
--- a/content/test/test_navigation_url_loader.cc
+++ b/content/test/test_navigation_url_loader.cc
@@ -38,6 +38,9 @@
   response_proceeded_ = true;
 }
 
+void TestNavigationURLLoader::InterceptNavigation(
+    NavigationURLLoader::NavigationInterceptionCB callback) {}
+
 void TestNavigationURLLoader::SimulateServerRedirect(const GURL& redirect_url) {
   net::RedirectInfo redirect_info;
   redirect_info.status_code = 302;
diff --git a/content/test/test_navigation_url_loader.h b/content/test/test_navigation_url_loader.h
index 8edcdfc..741e1054 100644
--- a/content/test/test_navigation_url_loader.h
+++ b/content/test/test_navigation_url_loader.h
@@ -36,6 +36,7 @@
   // NavigationURLLoader implementation.
   void FollowRedirect() override;
   void ProceedWithResponse() override;
+  void InterceptNavigation(NavigationInterceptionCB callback) override;
 
   NavigationRequestInfo* request_info() const { return request_info_.get(); }
 
diff --git a/extensions/browser/api/web_request/web_request_api.cc b/extensions/browser/api/web_request/web_request_api.cc
index 9078d58..5a56fe8 100644
--- a/extensions/browser/api/web_request/web_request_api.cc
+++ b/extensions/browser/api/web_request/web_request_api.cc
@@ -650,8 +650,8 @@
     event_details->SetRequestBody(request);
 
     initialize_blocked_requests |=
-        DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                      std::move(event_details));
+        DispatchEvent(browser_context, extension_info_map, request, listeners,
+                      navigation_ui_data, std::move(event_details));
   }
 
   if (!initialize_blocked_requests)
@@ -704,8 +704,8 @@
     event_details->SetRequestHeaders(*headers);
 
     initialize_blocked_requests |=
-        DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                      std::move(event_details));
+        DispatchEvent(browser_context, extension_info_map, request, listeners,
+                      navigation_ui_data, std::move(event_details));
   }
 
   if (!initialize_blocked_requests)
@@ -755,8 +755,8 @@
       CreateEventDetails(request, extra_info_spec));
   event_details->SetRequestHeaders(headers);
 
-  DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                std::move(event_details));
+  DispatchEvent(browser_context, extension_info_map, request, listeners,
+                navigation_ui_data, std::move(event_details));
 }
 
 int ExtensionWebRequestEventRouter::OnHeadersReceived(
@@ -793,8 +793,8 @@
     event_details->SetResponseHeaders(request, original_response_headers);
 
     initialize_blocked_requests |=
-        DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                      std::move(event_details));
+        DispatchEvent(browser_context, extension_info_map, request, listeners,
+                      navigation_ui_data, std::move(event_details));
   }
 
   if (!initialize_blocked_requests)
@@ -849,8 +849,8 @@
   event_details->SetResponseHeaders(request, request->response_headers());
   event_details->SetAuthInfo(auth_info);
 
-  if (DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                    std::move(event_details))) {
+  if (DispatchEvent(browser_context, extension_info_map, request, listeners,
+                    navigation_ui_data, std::move(event_details))) {
     BlockedRequest& blocked_request = blocked_requests_[request->identifier()];
     blocked_request.event = kOnAuthRequired;
     blocked_request.is_incognito |= IsIncognitoBrowserContext(browser_context);
@@ -896,8 +896,8 @@
   event_details->SetResponseSource(request);
   event_details->SetString(keys::kRedirectUrlKey, new_location.spec());
 
-  DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                std::move(event_details));
+  DispatchEvent(browser_context, extension_info_map, request, listeners,
+                navigation_ui_data, std::move(event_details));
 }
 
 void ExtensionWebRequestEventRouter::OnResponseStarted(
@@ -930,8 +930,8 @@
   event_details->SetResponseHeaders(request, request->response_headers());
   event_details->SetResponseSource(request);
 
-  DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                std::move(event_details));
+  DispatchEvent(browser_context, extension_info_map, request, listeners,
+                navigation_ui_data, std::move(event_details));
 }
 
 // Deprecated.
@@ -984,8 +984,8 @@
   event_details->SetResponseHeaders(request, request->response_headers());
   event_details->SetResponseSource(request);
 
-  DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                std::move(event_details));
+  DispatchEvent(browser_context, extension_info_map, request, listeners,
+                navigation_ui_data, std::move(event_details));
 }
 
 // Deprecated.
@@ -1055,8 +1055,8 @@
     event_details->SetBoolean(keys::kFromCache, request->was_cached());
   event_details->SetString(keys::kErrorKey, net::ErrorToString(net_error));
 
-  DispatchEvent(browser_context, request, listeners, navigation_ui_data,
-                std::move(event_details));
+  DispatchEvent(browser_context, extension_info_map, request, listeners,
+                navigation_ui_data, std::move(event_details));
 }
 
 void ExtensionWebRequestEventRouter::OnErrorOccurred(
@@ -1086,6 +1086,7 @@
 
 bool ExtensionWebRequestEventRouter::DispatchEvent(
     void* browser_context,
+    const InfoMap* extension_info_map,
     net::URLRequest* request,
     const RawListeners& listeners,
     ExtensionNavigationUIData* navigation_ui_data,
@@ -1130,7 +1131,8 @@
       IsResourceTypeFrame(info->GetResourceType())) {
     DCHECK(navigation_ui_data);
     event_details->SetFrameData(navigation_ui_data->frame_data());
-    DispatchEventToListeners(browser_context, std::move(listeners_to_dispatch),
+    DispatchEventToListeners(browser_context, extension_info_map,
+                             std::move(listeners_to_dispatch),
                              std::move(event_details));
   } else {
     // This Unretained is safe because the ExtensionWebRequestEventRouter
@@ -1138,6 +1140,7 @@
     event_details.release()->DetermineFrameDataOnIO(
         base::Bind(&ExtensionWebRequestEventRouter::DispatchEventToListeners,
                    base::Unretained(this), browser_context,
+                   base::RetainedRef(extension_info_map),
                    base::Passed(&listeners_to_dispatch)));
   }
 
@@ -1155,6 +1158,7 @@
 
 void ExtensionWebRequestEventRouter::DispatchEventToListeners(
     void* browser_context,
+    const InfoMap* extension_info_map,
     std::unique_ptr<ListenerIDs> listener_ids,
     std::unique_ptr<WebRequestEventDetails> event_details) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
@@ -1194,8 +1198,11 @@
 
     // Filter out the optional keys that this listener didn't request.
     std::unique_ptr<base::ListValue> args_filtered(new base::ListValue);
-    args_filtered->Append(
-        event_details->GetFilteredDict(listener->extra_info_spec));
+    void* cross_browser_context = GetCrossBrowserContext(browser_context);
+
+    args_filtered->Append(event_details->GetFilteredDict(
+        listener->extra_info_spec, extension_info_map,
+        listener->id.extension_id, (cross_browser_context != 0)));
 
     EventRouter::DispatchEventToSender(
         listener->ipc_sender.get(), browser_context, listener->id.extension_id,
diff --git a/extensions/browser/api/web_request/web_request_api.h b/extensions/browser/api/web_request/web_request_api.h
index 2abfae7..b493c71 100644
--- a/extensions/browser/api/web_request/web_request_api.h
+++ b/extensions/browser/api/web_request/web_request_api.h
@@ -405,6 +405,7 @@
   void ClearPendingCallbacks(const net::URLRequest* request);
 
   bool DispatchEvent(void* browser_context,
+                     const InfoMap* extension_info_map,
                      net::URLRequest* request,
                      const RawListeners& listener_ids,
                      ExtensionNavigationUIData* navigation_ui_data,
@@ -412,6 +413,7 @@
 
   void DispatchEventToListeners(
       void* browser_context,
+      const InfoMap* extension_info_map,
       std::unique_ptr<ListenerIDs> listener_ids,
       std::unique_ptr<WebRequestEventDetails> event_details);
 
diff --git a/extensions/browser/api/web_request/web_request_api_constants.cc b/extensions/browser/api/web_request/web_request_api_constants.cc
index 7b464ee..50b0688 100644
--- a/extensions/browser/api/web_request/web_request_api_constants.cc
+++ b/extensions/browser/api/web_request/web_request_api_constants.cc
@@ -43,6 +43,7 @@
 const char kAuthCredentialsKey[] = "authCredentials";
 const char kUsernameKey[] = "username";
 const char kPasswordKey[] = "password";
+const char kInitiatorKey[] = "initiator";
 
 const char kOnBeforeRedirectEvent[] = "webRequest.onBeforeRedirect";
 const char kOnBeforeSendHeadersEvent[] = "webRequest.onBeforeSendHeaders";
diff --git a/extensions/browser/api/web_request/web_request_api_constants.h b/extensions/browser/api/web_request/web_request_api_constants.h
index 4f89dad..cc33f1c 100644
--- a/extensions/browser/api/web_request/web_request_api_constants.h
+++ b/extensions/browser/api/web_request/web_request_api_constants.h
@@ -50,6 +50,7 @@
 extern const char kAuthCredentialsKey[];
 extern const char kUsernameKey[];
 extern const char kPasswordKey[];
+extern const char kInitiatorKey[];
 
 // Events.
 extern const char kOnAuthRequiredEvent[];
diff --git a/extensions/browser/api/web_request/web_request_event_details.cc b/extensions/browser/api/web_request/web_request_event_details.cc
index c89edeb..3ba38ec6 100644
--- a/extensions/browser/api/web_request/web_request_event_details.cc
+++ b/extensions/browser/api/web_request/web_request_event_details.cc
@@ -19,7 +19,9 @@
 #include "extensions/browser/api/web_request/upload_data_presenter.h"
 #include "extensions/browser/api/web_request/web_request_api_constants.h"
 #include "extensions/browser/api/web_request/web_request_api_helpers.h"
+#include "extensions/browser/api/web_request/web_request_permissions.h"
 #include "extensions/browser/api/web_request/web_request_resource_type.h"
+#include "extensions/common/permissions/permissions_data.h"
 #include "ipc/ipc_message.h"
 #include "net/base/auth.h"
 #include "net/base/upload_data_stream.h"
@@ -68,6 +70,8 @@
   dict_.SetString(keys::kTypeKey,
                   WebRequestResourceTypeToString(resource_type));
   dict_.SetString(keys::kUrlKey, request->url().spec());
+  if (request->initiator())
+    initiator_ = request->initiator();
 }
 
 WebRequestEventDetails::~WebRequestEventDetails() {}
@@ -201,7 +205,10 @@
 }
 
 std::unique_ptr<base::DictionaryValue> WebRequestEventDetails::GetFilteredDict(
-    int extra_info_spec) const {
+    int extra_info_spec,
+    const extensions::InfoMap* extension_info_map,
+    const extensions::ExtensionId& extension_id,
+    bool crosses_incognito) const {
   std::unique_ptr<base::DictionaryValue> result = dict_.CreateDeepCopy();
   if ((extra_info_spec & ExtraInfoSpec::REQUEST_BODY) && request_body_) {
     result->SetKey(keys::kRequestBodyKey, request_body_->Clone());
@@ -213,6 +220,17 @@
       response_headers_) {
     result->SetKey(keys::kResponseHeadersKey, response_headers_->Clone());
   }
+
+  // Only listeners with a permission for the initiator should recieve it.
+  if (extension_info_map && initiator_) {
+    int tab_id = -1;
+    dict_.GetInteger(keys::kTabIdKey, &tab_id);
+    if (WebRequestPermissions::CanExtensionAccessInitiator(
+            extension_info_map, extension_id, initiator_, tab_id,
+            crosses_incognito)) {
+      result->SetString(keys::kInitiatorKey, initiator_->Serialize());
+    }
+  }
   return result;
 }
 
diff --git a/extensions/browser/api/web_request/web_request_event_details.h b/extensions/browser/api/web_request/web_request_event_details.h
index 4172675..3c4661c 100644
--- a/extensions/browser/api/web_request/web_request_event_details.h
+++ b/extensions/browser/api/web_request/web_request_event_details.h
@@ -11,8 +11,11 @@
 #include "base/callback_forward.h"
 #include "base/gtest_prod_util.h"
 #include "base/macros.h"
+#include "base/optional.h"
 #include "base/values.h"
 #include "extensions/browser/extension_api_frame_id_map.h"
+#include "extensions/common/extension_id.h"
+#include "url/origin.h"
 
 namespace net {
 class AuthChallengeInfo;
@@ -23,6 +26,8 @@
 
 namespace extensions {
 
+class InfoMap;
+
 // This helper class is used to construct the details for a webRequest event
 // dictionary. Some keys are present on every event, others are only relevant
 // for a few events. And some keys must only be added to the event details if
@@ -118,10 +123,15 @@
   void DetermineFrameDataOnIO(const DeterminedFrameDataCallback& callback);
 
   // Create an event dictionary that contains all required keys, and also the
-  // extra keys as specified by the |extra_info_spec| filter.
+  // extra keys as specified by the |extra_info_spec| filter. If the listener
+  // this event will be dispatched to doesn't have permission for the initiator
+  // then the initiator will not be populated.
   // This can be called from any thread.
   std::unique_ptr<base::DictionaryValue> GetFilteredDict(
-      int extra_info_spec) const;
+      int extra_info_spec,
+      const InfoMap* extension_info_map,
+      const ExtensionId& extension_id,
+      bool crosses_incognito) const;
 
   // Get the internal dictionary, unfiltered. After this call, the internal
   // dictionary is empty.
@@ -149,6 +159,7 @@
   std::unique_ptr<base::DictionaryValue> request_body_;
   std::unique_ptr<base::ListValue> request_headers_;
   std::unique_ptr<base::ListValue> response_headers_;
+  base::Optional<url::Origin> initiator_;
 
   int extra_info_spec_;
 
diff --git a/extensions/browser/api/web_request/web_request_permissions.cc b/extensions/browser/api/web_request/web_request_permissions.cc
index ccfe22ba..0a1ddf5 100644
--- a/extensions/browser/api/web_request/web_request_permissions.cc
+++ b/extensions/browser/api/web_request/web_request_permissions.cc
@@ -10,6 +10,7 @@
 #include "chromeos/login/login_state.h"
 #include "content/public/browser/child_process_security_policy.h"
 #include "content/public/browser/resource_request_info.h"
+#include "extensions/browser/api/web_request/web_request_api_constants.h"
 #include "extensions/browser/extension_navigation_ui_data.h"
 #include "extensions/browser/guest_view/web_view/web_view_renderer_state.h"
 #include "extensions/browser/info_map.h"
@@ -211,3 +212,20 @@
 
   return access;
 }
+
+// static
+bool WebRequestPermissions::CanExtensionAccessInitiator(
+    const extensions::InfoMap* extension_info_map,
+    const extensions::ExtensionId extension_id,
+    const base::Optional<url::Origin>& initiator,
+    int tab_id,
+    bool crosses_incognito) {
+  PermissionsData::AccessType access = PermissionsData::ACCESS_ALLOWED;
+  if (initiator) {
+    access = CanExtensionAccessURL(
+        extension_info_map, extension_id, initiator->GetURL(), tab_id,
+        crosses_incognito, WebRequestPermissions::REQUIRE_HOST_PERMISSION,
+        base::nullopt);
+  }
+  return access == PermissionsData::ACCESS_ALLOWED;
+}
diff --git a/extensions/browser/api/web_request/web_request_permissions.h b/extensions/browser/api/web_request/web_request_permissions.h
index 74d9475..2d4c9d4 100644
--- a/extensions/browser/api/web_request/web_request_permissions.h
+++ b/extensions/browser/api/web_request/web_request_permissions.h
@@ -59,6 +59,13 @@
       HostPermissionsCheck host_permissions_check,
       const base::Optional<url::Origin>& initiator);
 
+  static bool CanExtensionAccessInitiator(
+      const extensions::InfoMap* extension_info_map,
+      const extensions::ExtensionId extension_id,
+      const base::Optional<url::Origin>& initiator,
+      int tab_id,
+      bool crosses_incognito);
+
  private:
   DISALLOW_IMPLICIT_CONSTRUCTORS(WebRequestPermissions);
 };
diff --git a/extensions/common/api/networking_onc.idl b/extensions/common/api/networking_onc.idl
index 58bf8289..c51bbcc 100644
--- a/extensions/common/api/networking_onc.idl
+++ b/extensions/common/api/networking_onc.idl
@@ -48,7 +48,7 @@
   };
 
   enum NetworkType {
-    All, Cellular, Ethernet, VPN, Wireless, WiFi, WiMAX
+    All, Cellular, Ethernet, Tether, VPN, Wireless, WiFi, WiMAX
   };
 
   enum ProxySettingsType {
@@ -559,7 +559,7 @@
     boolean? AutoConnect;
     // The VPN host.
     DOMString? Host;
-    // The VPN type.
+    // The VPN type. This can not be an enum because of 'L2TP-IPSec'.
     DOMString? Type;
   };
 
diff --git a/extensions/common/api/networking_private.idl b/extensions/common/api/networking_private.idl
index eb032487..63c6da2 100644
--- a/extensions/common/api/networking_private.idl
+++ b/extensions/common/api/networking_private.idl
@@ -600,6 +600,7 @@
     L2TPProperties? L2TP;
     OpenVPNProperties? OpenVPN;
     ThirdPartyVPNProperties? ThirdPartyVPN;
+    // 'Type' can not be an enum because of 'L2TP-IPSec'.
     DOMString? Type;
   };
 
diff --git a/extensions/common/api/test.json b/extensions/common/api/test.json
index 97dff284..94bddaf 100644
--- a/extensions/common/api/test.json
+++ b/extensions/common/api/test.json
@@ -60,6 +60,11 @@
                     "description": "The port on which the test WebSocket server is listening.",
                     "minimum": 0,
                     "maximum": 65535
+                  },
+                  "browserSideNavigationEnabled": {
+                    "type": "boolean",
+                    "optional": true,
+                    "description": "Whether navigation requets are initiated by the browser or renderer (PlzNavigate)."
                   }
                 }
               }
diff --git a/extensions/common/api/web_request.json b/extensions/common/api/web_request.json
index 110a6d3..9b21cb6 100644
--- a/extensions/common/api/web_request.json
+++ b/extensions/common/api/web_request.json
@@ -204,6 +204,7 @@
               },
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."}
             }
           }
@@ -246,6 +247,7 @@
               "frameId": {"type": "integer", "description": "The value 0 indicates that the request happens in the main frame; a positive value indicates the ID of a subframe in which the request happens. If the document of a (sub-)frame is loaded (<code>type</code> is <code>main_frame</code> or <code>sub_frame</code>), <code>frameId</code> indicates the ID of this frame, not the ID of the outer frame. Frame IDs are unique within a tab."},
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that are going to be sent out with this request."}
@@ -291,6 +293,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "requestHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP request headers that have been sent out with this request."}
             }
@@ -330,6 +333,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "statusLine": {"type": "string", "description": "HTTP status line of the response or the 'HTTP/0.9 200 OK' string for HTTP/0.9 responses (i.e., responses that lack a status line)."},
               "responseHeaders": {"$ref": "HttpHeaders", "optional": true, "description": "The HTTP response headers that have been received with this response."},
@@ -376,6 +380,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "scheme": {"type": "string", "description": "The authentication scheme, e.g. Basic or Digest."},
               "realm": {"type": "string", "description": "The authentication realm provided by the server, if there is one.", "optional": true},
@@ -435,6 +440,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
@@ -478,6 +484,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
@@ -522,6 +529,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
@@ -564,6 +572,7 @@
               "parentFrameId": {"type": "integer", "description": "ID of frame that wraps the frame which sent the request. Set to -1 if no parent frame exists."},
               "tabId": {"type": "integer", "description": "The ID of the tab in which the request takes place. Set to -1 if the request isn't related to a tab."},
               "type": {"$ref": "ResourceType", "description": "How the requested resource will be used."},
+              "initiator": {"type": "string", "optional": true, "description": "The origin where the request was initiated. This does not change through redirects."},
               "timeStamp": {"type": "number", "description": "The time when this signal is triggered, in milliseconds since the epoch."},
               "ip": {"type": "string", "optional": true, "description": "The server IP address that the request was actually sent to. Note that it may be a literal IPv6 address."},
               "fromCache": {"type": "boolean", "description": "Indicates if this response was fetched from disk cache."},
diff --git a/ipc/ipc_channel_proxy.cc b/ipc/ipc_channel_proxy.cc
index 49bfdbf..872a518 100644
--- a/ipc/ipc_channel_proxy.cc
+++ b/ipc/ipc_channel_proxy.cc
@@ -24,16 +24,6 @@
 #include "ipc/message_filter.h"
 #include "ipc/message_filter_router.h"
 
-// This is temporary to try to locate a core trampler.
-// TODO(bcwhite):  Remove when crbug/736675 is resolved.
-#if defined(OS_ANDROID)
-#include "base/metrics/statistics_recorder.h"
-#define VALIDATE_ALL_HISTOGRAMS(x) \
-  base::StatisticsRecorder::ValidateAllHistograms(static_cast<int>(x))
-#else
-#define VALIDATE_ALL_HISTOGRAMS(x)
-#endif
-
 namespace IPC {
 
 //------------------------------------------------------------------------------
@@ -314,8 +304,6 @@
   if (!listener_)
     return;
 
-  VALIDATE_ALL_HISTOGRAMS(message.type());
-
   OnDispatchConnected();
 
 #if BUILDFLAG(IPC_MESSAGE_LOG_ENABLED)
@@ -329,11 +317,9 @@
     logger->OnPreDispatchMessage(message);
 #endif
 
-  VALIDATE_ALL_HISTOGRAMS(message.type());
   listener_->OnMessageReceived(message);
   if (message.dispatch_error())
     listener_->OnBadMessageReceived(message);
-  VALIDATE_ALL_HISTOGRAMS(message.type());
 
 #if BUILDFLAG(IPC_MESSAGE_LOG_ENABLED)
   if (logger->Enabled())
diff --git a/media/blink/BUILD.gn b/media/blink/BUILD.gn
index b521394..041d988e 100644
--- a/media/blink/BUILD.gn
+++ b/media/blink/BUILD.gn
@@ -80,6 +80,7 @@
     "//media:shared_memory_support",
     "//media/mojo/interfaces",
     "//net",
+    "//services/service_manager/public/cpp:cpp",
     "//skia",
     "//third_party/WebKit/public:blink",
     "//ui/gfx",
diff --git a/media/blink/DEPS b/media/blink/DEPS
index 0fb853c9..96eff59 100644
--- a/media/blink/DEPS
+++ b/media/blink/DEPS
@@ -10,6 +10,7 @@
   "+mojo/public/cpp/bindings",
   "+net/base",
   "+net/http",
+  "+services/service_manager/public/cpp",
   "+third_party/WebKit/public/platform",
   "+third_party/WebKit/public/web",
 
diff --git a/media/blink/webmediacapabilitiesclient_impl.cc b/media/blink/webmediacapabilitiesclient_impl.cc
index d973d2f..21eec988 100644
--- a/media/blink/webmediacapabilitiesclient_impl.cc
+++ b/media/blink/webmediacapabilitiesclient_impl.cc
@@ -4,11 +4,15 @@
 
 #include "media/blink/webmediacapabilitiesclient_impl.h"
 
+#include "base/bind_helpers.h"
 #include "media/base/audio_codecs.h"
 #include "media/base/decode_capabilities.h"
 #include "media/base/mime_util.h"
 #include "media/base/video_codecs.h"
 #include "media/base/video_color_space.h"
+#include "mojo/public/cpp/bindings/associated_interface_ptr.h"
+#include "services/service_manager/public/cpp/connector.h"
+#include "third_party/WebKit/public/platform/Platform.h"
 #include "third_party/WebKit/public/platform/modules/media_capabilities/WebAudioConfiguration.h"
 #include "third_party/WebKit/public/platform/modules/media_capabilities/WebMediaCapabilitiesInfo.h"
 #include "third_party/WebKit/public/platform/modules/media_capabilities/WebMediaConfiguration.h"
@@ -16,19 +20,38 @@
 
 namespace media {
 
+void BindToHistoryService(mojom::VideoDecodePerfHistoryPtr* history_ptr) {
+  blink::Platform* platform = blink::Platform::Current();
+  service_manager::Connector* connector = platform->GetConnector();
+
+  connector->BindInterface(platform->GetBrowserServiceName(),
+                           mojo::MakeRequest(history_ptr));
+}
+
 WebMediaCapabilitiesClientImpl::WebMediaCapabilitiesClientImpl() = default;
 
 WebMediaCapabilitiesClientImpl::~WebMediaCapabilitiesClientImpl() = default;
 
+void VideoPerfInfoCallback(
+    std::unique_ptr<blink::WebMediaCapabilitiesQueryCallbacks> callbacks,
+    std::unique_ptr<blink::WebMediaCapabilitiesInfo> info,
+    bool is_smooth,
+    bool is_power_efficient) {
+  DCHECK(info->supported);
+  info->smooth = is_smooth;
+  info->power_efficient = is_power_efficient;
+  callbacks->OnSuccess(std::move(info));
+}
+
 void WebMediaCapabilitiesClientImpl::DecodingInfo(
     const blink::WebMediaConfiguration& configuration,
     std::unique_ptr<blink::WebMediaCapabilitiesQueryCallbacks> callbacks) {
   std::unique_ptr<blink::WebMediaCapabilitiesInfo> info(
       new blink::WebMediaCapabilitiesInfo());
 
-  bool audio_supported = true;
-  bool video_supported = true;
+  // TODO(chcunningham): split this up with helper methods.
 
+  bool audio_supported = true;
   if (configuration.audio_configuration) {
     const blink::WebAudioConfiguration& audio_config =
         configuration.audio_configuration.value();
@@ -40,8 +63,9 @@
                                &is_audio_codec_ambiguous, &audio_codec)) {
       // TODO(chcunningham): Replace this and other DVLOGs here with MEDIA_LOG.
       // MediaCapabilities may need its own tab in chrome://media-internals.
-      DVLOG(2) << __func__ << " Failed to parse audio codec string:"
-               << audio_config.codec.Ascii();
+      DVLOG(2) << __func__ << " Failed to parse audio contentType: "
+               << audio_config.mime_type.Ascii()
+               << "; codecs=" << audio_config.codec.Ascii();
       audio_supported = false;
     } else if (is_audio_codec_ambiguous) {
       DVLOG(2) << __func__ << " Invalid (ambiguous) audio codec string:"
@@ -53,39 +77,65 @@
     }
   }
 
-  if (configuration.video_configuration) {
-    const blink::WebVideoConfiguration& video_config =
-        configuration.video_configuration.value();
-    VideoCodec video_codec;
-    VideoCodecProfile video_profile;
-    uint8_t video_level;
-    VideoColorSpace video_color_space;
-    bool is_video_codec_ambiguous;
-
-    if (!ParseVideoCodecString(
-            video_config.mime_type.Ascii(), video_config.codec.Ascii(),
-            &is_video_codec_ambiguous, &video_codec, &video_profile,
-            &video_level, &video_color_space)) {
-      DVLOG(2) << __func__ << " Failed to parse video codec string:"
-               << video_config.codec.Ascii();
-      video_supported = false;
-    } else if (is_video_codec_ambiguous) {
-      DVLOG(2) << __func__ << " Invalid (ambiguous) video codec string:"
-               << video_config.codec.Ascii();
-      video_supported = false;
-    } else {
-      VideoConfig video_config = {video_codec, video_profile, video_level,
-                                  video_color_space};
-      video_supported = IsSupportedVideoConfig(video_config);
-    }
+  // No need to check video capabilities if video not included in configuration
+  // or when audio is already known to be unsupported.
+  if (!audio_supported || !configuration.video_configuration) {
+    // Supported audio-only configurations are always considered smooth and
+    // power efficient.
+    info->supported = info->smooth = info->power_efficient = audio_supported;
+    callbacks->OnSuccess(std::move(info));
+    return;
   }
 
-  info->supported = audio_supported && video_supported;
+  // Audio is supported and video configuration is provided in the query; all
+  // that remains is to check video support and performance.
+  bool video_supported;
+  DCHECK(audio_supported);
+  DCHECK(configuration.video_configuration);
+  const blink::WebVideoConfiguration& video_config =
+      configuration.video_configuration.value();
+  VideoCodec video_codec;
+  VideoCodecProfile video_profile;
+  uint8_t video_level;
+  VideoColorSpace video_color_space;
+  bool is_video_codec_ambiguous;
 
-  // TODO(chcunningham, mlamouri): real implementation for these.
-  info->smooth = info->power_efficient = info->supported;
+  if (!ParseVideoCodecString(
+          video_config.mime_type.Ascii(), video_config.codec.Ascii(),
+          &is_video_codec_ambiguous, &video_codec, &video_profile, &video_level,
+          &video_color_space)) {
+    DVLOG(2) << __func__ << " Failed to parse video contentType: "
+             << video_config.mime_type.Ascii()
+             << "; codecs=" << video_config.codec.Ascii();
+    video_supported = false;
+  } else if (is_video_codec_ambiguous) {
+    DVLOG(2) << __func__ << " Invalid (ambiguous) video codec string:"
+             << video_config.codec.Ascii();
+    video_supported = false;
+  } else {
+    video_supported = IsSupportedVideoConfig(
+        {video_codec, video_profile, video_level, video_color_space});
+  }
 
-  callbacks->OnSuccess(std::move(info));
+  // Return early for unsupported configurations.
+  if (!video_supported) {
+    info->supported = info->smooth = info->power_efficient = video_supported;
+    callbacks->OnSuccess(std::move(info));
+    return;
+  }
+
+  // Video is supported! Check its performance history.
+  info->supported = true;
+
+  if (!decode_history_ptr_.is_bound())
+    BindToHistoryService(&decode_history_ptr_);
+  DCHECK(decode_history_ptr_.is_bound());
+
+  decode_history_ptr_->GetPerfInfo(
+      video_profile, gfx::Size(video_config.width, video_config.height),
+      video_config.framerate,
+      base::BindOnce(&VideoPerfInfoCallback, std::move(callbacks),
+                     std::move(info)));
 }
 
 }  // namespace media
diff --git a/media/blink/webmediacapabilitiesclient_impl.h b/media/blink/webmediacapabilitiesclient_impl.h
index 97d3c0f..18ebc21 100644
--- a/media/blink/webmediacapabilitiesclient_impl.h
+++ b/media/blink/webmediacapabilitiesclient_impl.h
@@ -8,6 +8,7 @@
 #include "base/compiler_specific.h"
 #include "base/macros.h"
 #include "media/blink/media_blink_export.h"
+#include "media/mojo/interfaces/video_decode_perf_history.mojom.h"
 #include "third_party/WebKit/public/platform/modules/media_capabilities/WebMediaCapabilitiesClient.h"
 
 namespace media {
@@ -24,6 +25,8 @@
       std::unique_ptr<blink::WebMediaCapabilitiesQueryCallbacks>) override;
 
  private:
+  mojom::VideoDecodePerfHistoryPtr decode_history_ptr_;
+
   DISALLOW_COPY_AND_ASSIGN(WebMediaCapabilitiesClientImpl);
 };
 
diff --git a/media/mojo/BUILD.gn b/media/mojo/BUILD.gn
index 5d43865..92f91c6d 100644
--- a/media/mojo/BUILD.gn
+++ b/media/mojo/BUILD.gn
@@ -87,6 +87,7 @@
     "interfaces/video_frame_struct_traits_unittest.cc",
     "mojo_video_encode_accelerator_integration_test.cc",
     "services/mojo_audio_output_stream_unittest.cc",
+    "services/video_decode_perf_history_unittest.cc",
     "services/watch_time_recorder_unittest.cc",
   ]
 
diff --git a/media/mojo/interfaces/BUILD.gn b/media/mojo/interfaces/BUILD.gn
index 51e8c99..c22a23e 100644
--- a/media/mojo/interfaces/BUILD.gn
+++ b/media/mojo/interfaces/BUILD.gn
@@ -20,6 +20,7 @@
     "platform_verification.mojom",
     "provision_fetcher.mojom",
     "renderer.mojom",
+    "video_decode_perf_history.mojom",
     "video_decode_stats_recorder.mojom",
     "video_decoder.mojom",
     "video_encode_accelerator.mojom",
diff --git a/media/mojo/interfaces/video_decode_perf_history.mojom b/media/mojo/interfaces/video_decode_perf_history.mojom
new file mode 100644
index 0000000..9ee5747
--- /dev/null
+++ b/media/mojo/interfaces/video_decode_perf_history.mojom
@@ -0,0 +1,17 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module media.mojom;
+
+import "media/mojo/interfaces/media_types.mojom";
+import "ui/gfx/geometry/mojo/geometry.mojom";
+
+// This service will query the history of playback stats to evaluate how
+// a video stream with the given configuration will perform.
+interface VideoDecodePerfHistory {
+  // Parameters describe the stream characteristics for which we will return
+  // performance info.
+  GetPerfInfo(VideoCodecProfile profile, gfx.mojom.Size video_size,
+              int32 frames_per_sec) => (bool is_smooth, bool is_power_efficient);
+};
\ No newline at end of file
diff --git a/media/mojo/services/BUILD.gn b/media/mojo/services/BUILD.gn
index 13006ed..533ac35 100644
--- a/media/mojo/services/BUILD.gn
+++ b/media/mojo/services/BUILD.gn
@@ -16,6 +16,8 @@
     "gpu_mojo_media_client.h",
     "interface_factory_impl.cc",
     "interface_factory_impl.h",
+    "media_capabilities_database.cc",
+    "media_capabilities_database.h",
     "media_interface_provider.cc",
     "media_interface_provider.h",
     "media_mojo_export.h",
@@ -57,6 +59,8 @@
     "mojo_video_encode_accelerator_service.h",
     "test_mojo_media_client.cc",
     "test_mojo_media_client.h",
+    "video_decode_perf_history.cc",
+    "video_decode_perf_history.h",
     "video_decode_stats_recorder.cc",
     "video_decode_stats_recorder.h",
     "watch_time_recorder.cc",
diff --git a/media/mojo/services/media_capabilities_database.cc b/media/mojo/services/media_capabilities_database.cc
new file mode 100644
index 0000000..fd8e57e
--- /dev/null
+++ b/media/mojo/services/media_capabilities_database.cc
@@ -0,0 +1,18 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/mojo/services/media_capabilities_database.h"
+
+namespace media {
+
+MediaCapabilitiesDatabase::Entry::Entry(VideoCodecProfile codec_profile,
+                                        const gfx::Size& size,
+                                        int frame_rate)
+    : codec_profile_(codec_profile), size_(size), frame_rate_(frame_rate) {}
+
+MediaCapabilitiesDatabase::Info::Info(uint32_t frames_decoded,
+                                      uint32_t frames_dropped)
+    : frames_decoded(frames_decoded), frames_dropped(frames_dropped) {}
+
+}  // namespace media
\ No newline at end of file
diff --git a/media/mojo/services/media_capabilities_database.h b/media/mojo/services/media_capabilities_database.h
new file mode 100644
index 0000000..6d43e78
--- /dev/null
+++ b/media/mojo/services/media_capabilities_database.h
@@ -0,0 +1,64 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_MOJO_SERVICES_MEDIA_CAPABILITIES_DATABASE_H_
+#define MEDIA_MOJO_SERVICES_MEDIA_CAPABILITIES_DATABASE_H_
+
+#include <memory>
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "media/base/video_codecs.h"
+#include "media/mojo/services/media_mojo_export.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+
+// This defines the interface to be used by various media capabilities services
+// to store/retrieve decoding performance statistics.
+class MEDIA_MOJO_EXPORT MediaCapabilitiesDatabase {
+ public:
+  // Representation of the information used to identify a type of media
+  // playback.
+  class MEDIA_MOJO_EXPORT Entry {
+   public:
+    Entry(VideoCodecProfile codec_profile,
+          const gfx::Size& size,
+          int frame_rate);
+
+    VideoCodecProfile codec_profile() const { return codec_profile_; }
+    const gfx::Size& size() const { return size_; }
+    int frame_rate() const { return frame_rate_; }
+
+   private:
+    VideoCodecProfile codec_profile_;
+    gfx::Size size_;
+    int frame_rate_;
+  };
+
+  // Information saves to identify the capabilities related to a given |Entry|.
+  struct MEDIA_MOJO_EXPORT Info {
+    Info(uint32_t frames_decoded, uint32_t frames_dropped);
+    uint32_t frames_decoded;
+    uint32_t frames_dropped;
+  };
+
+  virtual ~MediaCapabilitiesDatabase() {}
+
+  // Adds `info` data into the database records associated with `entry`. The
+  // operation is asynchronous. The caller should be aware of potential race
+  // conditions when calling this method for the same `entry` very close to
+  // each others.
+  virtual void AppendInfoToEntry(const Entry& entry, const Info& info) = 0;
+
+  // Returns the `info` associated with `entry`. The `callback` will received
+  // the `info` in addition to a boolean signaling if the call was successful.
+  // `info` can be nullptr if there was no data associated with `entry`.
+  using GetInfoCallback = base::OnceCallback<void(bool, std::unique_ptr<Info>)>;
+  virtual void GetInfo(const Entry& entry, GetInfoCallback callback) = 0;
+};
+
+}  // namespace media
+
+#endif  // MEDIA_MOJO_SERVICES_MEDIA_CAPABILITIES_DATABASE_H_
\ No newline at end of file
diff --git a/media/mojo/services/video_decode_perf_history.cc b/media/mojo/services/video_decode_perf_history.cc
new file mode 100644
index 0000000..e51aefa
--- /dev/null
+++ b/media/mojo/services/video_decode_perf_history.cc
@@ -0,0 +1,124 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/mojo/services/video_decode_perf_history.h"
+#include "base/callback.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/stringprintf.h"
+#include "media/base/video_codecs.h"
+#include "mojo/public/cpp/bindings/strong_binding.h"
+
+namespace media {
+
+MediaCapabilitiesDatabase* g_database = nullptr;
+
+// static
+void VideoDecodePerfHistory::Initialize(
+    MediaCapabilitiesDatabase* db_instance) {
+  DVLOG(2) << __func__;
+  g_database = db_instance;
+}
+
+// static
+void VideoDecodePerfHistory::BindRequest(
+    mojom::VideoDecodePerfHistoryRequest request) {
+  DVLOG(2) << __func__;
+
+  // Single static instance should serve all requests.
+  static VideoDecodePerfHistory* instance = new VideoDecodePerfHistory();
+
+  instance->BindRequestInternal(std::move(request));
+}
+
+VideoDecodePerfHistory::VideoDecodePerfHistory() {
+  DVLOG(2) << __func__;
+}
+
+VideoDecodePerfHistory::~VideoDecodePerfHistory() {
+  DVLOG(2) << __func__;
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+}
+
+void VideoDecodePerfHistory::BindRequestInternal(
+    mojom::VideoDecodePerfHistoryRequest request) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  bindings_.AddBinding(this, std::move(request));
+}
+
+void VideoDecodePerfHistory::OnGotPerfInfo(
+    const MediaCapabilitiesDatabase::Entry& entry,
+    GetPerfInfoCallback mojo_cb,
+    bool database_success,
+    std::unique_ptr<MediaCapabilitiesDatabase::Info> info) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+  DCHECK(!mojo_cb.is_null());
+
+  bool is_power_efficient;
+  bool is_smooth;
+  double percent_dropped = 0;
+
+  if (info.get()) {
+    DCHECK(database_success);
+    percent_dropped =
+        static_cast<double>(info->frames_dropped) / info->frames_decoded;
+
+    // TODO(chcunningham): add statistics for power efficiency to database.
+    is_power_efficient = true;
+    is_smooth = percent_dropped <= kMaxSmoothDroppedFramesPercent;
+  } else {
+    // TODO(chcunningham/mlamouri): Refactor database API to give us nearby
+    // entry info whenever we don't have a perfect match. If higher
+    // resolutions/framerates are known to be smooth, we can report this as
+    // smooth. If lower resolutions/frames are known to be janky, we can assume
+    // this will be janky.
+
+    // No entry? Lets be optimistic.
+    is_power_efficient = true;
+    is_smooth = true;
+  }
+
+  DVLOG(3) << __func__
+           << base::StringPrintf(" profile:%s size:%s fps:%d --> ",
+                                 GetProfileName(entry.codec_profile()).c_str(),
+                                 entry.size().ToString().c_str(),
+                                 entry.frame_rate())
+           << (info.get()
+                   ? base::StringPrintf(
+                         "smooth:%d frames_decoded:%d pcnt_dropped:%f",
+                         is_smooth, info->frames_decoded, percent_dropped)
+                   : (database_success ? "no info" : "query FAILED"));
+
+  std::move(mojo_cb).Run(is_smooth, is_power_efficient);
+}
+
+void VideoDecodePerfHistory::GetPerfInfo(VideoCodecProfile profile,
+                                         const gfx::Size& natural_size,
+                                         int frame_rate,
+                                         GetPerfInfoCallback callback) {
+  DVLOG(3) << __func__;
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  DCHECK_NE(profile, VIDEO_CODEC_PROFILE_UNKNOWN);
+  DCHECK_GT(frame_rate, 0);
+  DCHECK(natural_size.width() > 0 && natural_size.height() > 0);
+
+  // TODO(chcunningham): Make this an error condition once the database impl
+  // has landed. Hardcoding results for now.
+  if (!g_database) {
+    DVLOG(2) << __func__ << " No database! Assuming smooth/efficient perf.";
+    std::move(callback).Run(true, true);
+    return;
+  }
+
+  MediaCapabilitiesDatabase::Entry db_entry(profile, natural_size, frame_rate);
+
+  // Unretained is safe because this is a leaky singleton.
+  g_database->GetInfo(
+      db_entry,
+      base::BindOnce(&VideoDecodePerfHistory::OnGotPerfInfo,
+                     base::Unretained(this), db_entry, std::move(callback)));
+}
+
+}  // namespace media
diff --git a/media/mojo/services/video_decode_perf_history.h b/media/mojo/services/video_decode_perf_history.h
new file mode 100644
index 0000000..6965648
--- /dev/null
+++ b/media/mojo/services/video_decode_perf_history.h
@@ -0,0 +1,87 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_MOJO_SERVICES_VIDEO_DECODE_PERF_HISTORY_H_
+#define MEDIA_MOJO_SERVICES_VIDEO_DECODE_PERF_HISTORY_H_
+
+#include <stdint.h>
+#include <memory>
+#include <string>
+
+#include "base/callback.h"
+#include "base/sequence_checker.h"
+#include "media/base/video_codecs.h"
+#include "media/mojo/interfaces/video_decode_perf_history.mojom.h"
+#include "media/mojo/services/media_capabilities_database.h"
+#include "media/mojo/services/media_mojo_export.h"
+#include "mojo/public/cpp/bindings/binding_set.h"
+#include "ui/gfx/geometry/size.h"
+
+namespace media {
+
+// This browser-process service helps render-process clients respond to
+// MediaCapabilities queries by looking up past performance for a given video
+// configuration.
+//
+// Smoothness and power efficiency are assessed by evaluating raw stats from the
+// MediaCapabilitiesDatabse.
+//
+// The object is lazily created upon the first call to BindRequest(). Future
+// calls will bind to the same instance.
+//
+// This class is not thread safe. All calls to BindRequest() and GetPerfInfo()
+// should be made on the same sequence (generally stemming from inbound Mojo
+// IPCs on the browser IO sequence).
+class MEDIA_MOJO_EXPORT VideoDecodePerfHistory
+    : public mojom::VideoDecodePerfHistory {
+ public:
+  // Provides |db_instance| for use once VideoDecodePerfHistory is lazily (upon
+  // first BindRequest) instantiated. Database lifetime should match/exceed that
+  // of the VideoDecodePerfHistory singleton.
+  static void Initialize(MediaCapabilitiesDatabase* db_instance);
+
+  // Bind the request to singleton instance.
+  static void BindRequest(mojom::VideoDecodePerfHistoryRequest request);
+
+  // mojom::VideoDecodePerfHistory implementation:
+  void GetPerfInfo(VideoCodecProfile profile,
+                   const gfx::Size& natural_size,
+                   int frame_rate,
+                   GetPerfInfoCallback callback) override;
+
+ private:
+  // Friends so it can create its own instances with mock database.
+  friend class VideoDecodePerfHistoryTest;
+
+  // Decode capabilities will be described as "smooth" whenever the percentage
+  // of dropped frames is less-than-or-equal-to this value. 10% chosen as a
+  // lenient value after manual testing.
+  static constexpr double kMaxSmoothDroppedFramesPercent = .10;
+
+  VideoDecodePerfHistory();
+  ~VideoDecodePerfHistory() override;
+
+  // Binds |request| to this instance.
+  void BindRequestInternal(mojom::VideoDecodePerfHistoryRequest request);
+
+  // Internal callback for |database_| queries. Will assess performance history
+  // from database and pass results on to |mojo_cb|.
+  void OnGotPerfInfo(const MediaCapabilitiesDatabase::Entry& entry,
+                     GetPerfInfoCallback mojo_cb,
+                     bool database_success,
+                     std::unique_ptr<MediaCapabilitiesDatabase::Info> info);
+
+  // Maps bindings from several render-processes to this single browser-process
+  // service.
+  mojo::BindingSet<mojom::VideoDecodePerfHistory> bindings_;
+
+  // Ensures all access to class members come on the same sequence.
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  DISALLOW_COPY_AND_ASSIGN(VideoDecodePerfHistory);
+};
+
+}  // namespace media
+
+#endif  // MEDIA_MOJO_SERVICES_VIDEO_DECODE_PERF_HISTORY_H_
\ No newline at end of file
diff --git a/media/mojo/services/video_decode_perf_history_unittest.cc b/media/mojo/services/video_decode_perf_history_unittest.cc
new file mode 100644
index 0000000..f35270a2
--- /dev/null
+++ b/media/mojo/services/video_decode_perf_history_unittest.cc
@@ -0,0 +1,142 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <map>
+#include <sstream>
+#include <string>
+
+#include "base/memory/ptr_util.h"
+#include "media/mojo/services/media_capabilities_database.h"
+#include "media/mojo/services/video_decode_perf_history.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace media {
+
+class FakeCapabilitiesDatabase : public MediaCapabilitiesDatabase {
+ public:
+  FakeCapabilitiesDatabase() = default;
+  ~FakeCapabilitiesDatabase() override {}
+
+  void AppendInfoToEntry(const Entry& entry, const Info& info) override {
+    std::string key = MakeEntryKey(entry);
+    if (entries_.find(key) == entries_.end()) {
+      entries_.emplace(std::make_pair(key, info));
+    } else {
+      const Info& known_info = entries_.at(key);
+      uint32_t frames_decoded = known_info.frames_decoded + info.frames_decoded;
+      uint32_t frames_dropped = known_info.frames_dropped + info.frames_dropped;
+      entries_.at(key) = Info(frames_decoded, frames_dropped);
+    }
+  }
+
+  void GetInfo(const Entry& entry, GetInfoCallback callback) override {
+    auto entry_it = entries_.find(MakeEntryKey(entry));
+    if (entry_it == entries_.end()) {
+      std::move(callback).Run(true, nullptr);
+    } else {
+      std::move(callback).Run(true, base::MakeUnique<Info>(entry_it->second));
+    }
+  }
+
+ private:
+  static std::string MakeEntryKey(const Entry& entry) {
+    std::stringstream ss;
+    ss << entry.codec_profile() << "|" << entry.size().ToString() << "|"
+       << entry.frame_rate();
+    return ss.str();
+  }
+
+  std::map<std::string, Info> entries_;
+};
+
+class VideoDecodePerfHistoryTest : public ::testing::Test {
+ public:
+  void SetUp() override {
+    // Sniff the database pointer so tests can inject entries.
+    database_ = base::MakeUnique<FakeCapabilitiesDatabase>();
+    VideoDecodePerfHistory::Initialize(database_.get());
+
+    // Make tests hermetic by creating a new instance. Cannot use unique_ptr
+    // here because the constructor/destructor are private and only accessible
+    // to this test via friending.
+    perf_history_ = new VideoDecodePerfHistory();
+  }
+
+  void TearDown() override {
+    // Avoid leaking between tests.
+    delete perf_history_;
+    perf_history_ = nullptr;
+  }
+
+  // Tests may set this as the callback for VideoDecodePerfHistory::GetPerfInfo
+  // to check the results of the call.
+  MOCK_METHOD2(MockPerfInfoCb, void(bool is_smooth, bool is_power_efficient));
+
+ protected:
+  using Entry = media::MediaCapabilitiesDatabase::Entry;
+  using Info = media::MediaCapabilitiesDatabase::Info;
+
+  static constexpr double kMaxSmoothDroppedFramesPercent =
+      VideoDecodePerfHistory::kMaxSmoothDroppedFramesPercent;
+
+  // The VideoDecodeStatsReporter being tested.
+  VideoDecodePerfHistory* perf_history_;
+
+  // The database |perf_history_| uses to store/query performance stats.
+  std::unique_ptr<FakeCapabilitiesDatabase> database_;
+};
+
+TEST_F(VideoDecodePerfHistoryTest, GetPerfInfo_Smooth) {
+  // Prepare database with 2 entries. The second entry has a higher framerate
+  // and a higher number of dropped frames such that it is "not smooth".
+  const VideoCodecProfile kKnownProfile = VP9PROFILE_PROFILE0;
+  const gfx::Size kKownSize(100, 200);
+  const int kSmoothFrameRate = 30;
+  const int kNotSmoothFrameRate = 90;
+  const int kFramesDecoded = 1000;
+  // Sets the ratio of dropped frames to barely qualify as smooth.
+  const int kSmoothFramesDropped =
+      kFramesDecoded * kMaxSmoothDroppedFramesPercent;
+  // Set the ratio of dropped frames to barely qualify as NOT smooth.
+  const int kNotSmoothFramesDropped =
+      kFramesDecoded * kMaxSmoothDroppedFramesPercent + 1;
+
+  // Add the entries.
+  database_->AppendInfoToEntry(
+      Entry(kKnownProfile, kKownSize, kSmoothFrameRate),
+      Info(kFramesDecoded, kSmoothFramesDropped));
+  database_->AppendInfoToEntry(
+      Entry(kKnownProfile, kKownSize, kNotSmoothFrameRate),
+      Info(kFramesDecoded, kNotSmoothFramesDropped));
+
+  // Verify perf history returns is_smooth = true for the smooth entry.
+  bool is_smooth = true;
+  bool is_power_efficient = true;
+  EXPECT_CALL(*this, MockPerfInfoCb(is_smooth, is_power_efficient));
+  perf_history_->GetPerfInfo(
+      kKnownProfile, kKownSize, kSmoothFrameRate,
+      base::BindOnce(&VideoDecodePerfHistoryTest::MockPerfInfoCb,
+                     base::Unretained(this)));
+
+  // Verify perf history returns is_smooth = false for the NOT smooth entry.
+  is_smooth = false;
+  EXPECT_CALL(*this, MockPerfInfoCb(is_smooth, is_power_efficient));
+  perf_history_->GetPerfInfo(
+      kKnownProfile, kKownSize, kNotSmoothFrameRate,
+      base::BindOnce(&VideoDecodePerfHistoryTest::MockPerfInfoCb,
+                     base::Unretained(this)));
+
+  // Verify perf history optimistically returns is_smooth = true when no entry
+  // can be found with the given configuration.
+  const VideoCodecProfile kUnknownProfile = VP9PROFILE_PROFILE2;
+  is_smooth = true;
+  EXPECT_CALL(*this, MockPerfInfoCb(is_smooth, is_power_efficient));
+  perf_history_->GetPerfInfo(
+      kUnknownProfile, kKownSize, kNotSmoothFrameRate,
+      base::BindOnce(&VideoDecodePerfHistoryTest::MockPerfInfoCb,
+                     base::Unretained(this)));
+}
+
+}  // namespace media
diff --git a/mojo/public/cpp/system/BUILD.gn b/mojo/public/cpp/system/BUILD.gn
index 6058fbfc..71156a5 100644
--- a/mojo/public/cpp/system/BUILD.gn
+++ b/mojo/public/cpp/system/BUILD.gn
@@ -27,8 +27,8 @@
     "buffer.h",
     "core.h",
     "data_pipe.h",
-    "data_pipe_string_writer.cc",
-    "data_pipe_string_writer.h",
+    "file_data_pipe_producer.cc",
+    "file_data_pipe_producer.g",
     "functions.h",
     "handle.h",
     "handle_signal_tracker.cc",
@@ -41,6 +41,8 @@
     "platform_handle.h",
     "simple_watcher.cc",
     "simple_watcher.h",
+    "string_data_pipe_producer.cc",
+    "string_data_pipe_producer.h",
     "system_export.h",
     "wait.cc",
     "wait.h",
diff --git a/mojo/public/cpp/system/file_data_pipe_producer.cc b/mojo/public/cpp/system/file_data_pipe_producer.cc
new file mode 100644
index 0000000..ab511e1f
--- /dev/null
+++ b/mojo/public/cpp/system/file_data_pipe_producer.cc
@@ -0,0 +1,215 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "mojo/public/cpp/system/file_data_pipe_producer.h"
+
+#include <algorithm>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/location.h"
+#include "base/memory/ptr_util.h"
+#include "base/memory/ref_counted_delete_on_sequence.h"
+#include "base/numerics/safe_conversions.h"
+#include "base/sequenced_task_runner.h"
+#include "base/synchronization/lock.h"
+#include "base/task_scheduler/post_task.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+#include "mojo/public/cpp/system/simple_watcher.h"
+
+namespace mojo {
+
+namespace {
+
+// No good reason not to attempt very large pipe transactions in case the data
+// pipe in use has a very large capacity available, so we default to trying
+// 64 MB chunks whenever a producer is writable.
+constexpr uint32_t kDefaultMaxReadSize = 64 * 1024 * 1024;
+
+}  // namespace
+
+class FileDataPipeProducer::FileSequenceState
+    : public base::RefCountedDeleteOnSequence<FileSequenceState> {
+ public:
+  using CompletionCallback =
+      base::OnceCallback<void(ScopedDataPipeProducerHandle producer,
+                              MojoResult result)>;
+
+  FileSequenceState(
+      ScopedDataPipeProducerHandle producer_handle,
+      scoped_refptr<base::SequencedTaskRunner> file_task_runner,
+      CompletionCallback callback,
+      scoped_refptr<base::SequencedTaskRunner> callback_task_runner)
+      : base::RefCountedDeleteOnSequence<FileSequenceState>(file_task_runner),
+        file_task_runner_(std::move(file_task_runner)),
+        callback_task_runner_(std::move(callback_task_runner)),
+        producer_handle_(std::move(producer_handle)),
+        callback_(std::move(callback)) {}
+
+  void Cancel() {
+    base::AutoLock lock(lock_);
+    is_cancelled_ = true;
+  }
+
+  void StartFromFile(base::File file) {
+    file_task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(&FileSequenceState::StartFromFileOnFileSequence, this,
+                       std::move(file)));
+  }
+
+  void StartFromPath(const base::FilePath& path) {
+    file_task_runner_->PostTask(
+        FROM_HERE,
+        base::BindOnce(&FileSequenceState::StartFromPathOnFileSequence, this,
+                       path));
+  }
+
+ private:
+  friend class base::DeleteHelper<FileSequenceState>;
+  friend class base::RefCountedDeleteOnSequence<FileSequenceState>;
+
+  ~FileSequenceState() = default;
+
+  void StartFromFileOnFileSequence(base::File file) {
+    file_ = std::move(file);
+    TransferSomeBytes();
+    if (producer_handle_.is_valid()) {
+      // If we didn't nail it all on the first transaction attempt, setup a
+      // watcher and complete the read asynchronously.
+      watcher_ = base::MakeUnique<SimpleWatcher>(
+          FROM_HERE, SimpleWatcher::ArmingPolicy::AUTOMATIC);
+      watcher_->Watch(producer_handle_.get(), MOJO_HANDLE_SIGNAL_WRITABLE,
+                      MOJO_WATCH_CONDITION_SATISFIED,
+                      base::Bind(&FileSequenceState::OnHandleReady, this));
+    }
+  }
+
+  void StartFromPathOnFileSequence(const base::FilePath& path) {
+    StartFromFileOnFileSequence(
+        base::File(path, base::File::FLAG_OPEN | base::File::FLAG_READ));
+  }
+
+  void OnHandleReady(MojoResult result, const HandleSignalsState& state) {
+    {
+      // Stop ourselves from doing redundant work if we've been cancelled from
+      // another thread. Note that we do not rely on this for any kind of thread
+      // safety concerns.
+      base::AutoLock lock(lock_);
+      if (is_cancelled_)
+        return;
+    }
+
+    if (result != MOJO_RESULT_OK) {
+      // Either the consumer pipe has been closed or something terrible
+      // happened. In any case, we'll never be able to write more data.
+      Finish(MOJO_RESULT_ABORTED);
+      return;
+    }
+
+    TransferSomeBytes();
+  }
+
+  void TransferSomeBytes() {
+    while (true) {
+      // Lock as much of the pipe as we can.
+      void* pipe_buffer;
+      uint32_t size = kDefaultMaxReadSize;
+      MojoResult result = producer_handle_->BeginWriteData(
+          &pipe_buffer, &size, MOJO_WRITE_DATA_FLAG_NONE);
+      if (result == MOJO_RESULT_SHOULD_WAIT)
+        return;
+      if (result != MOJO_RESULT_OK) {
+        Finish(MOJO_RESULT_ABORTED);
+        return;
+      }
+
+      // Attempt to read that many bytes from the file, directly into the data
+      // pipe.
+      DCHECK(base::IsValueInRangeForNumericType<int>(size));
+      int attempted_read_size = static_cast<int>(size);
+      int read_size = file_.ReadAtCurrentPos(static_cast<char*>(pipe_buffer),
+                                             attempted_read_size);
+      producer_handle_->EndWriteData(
+          read_size >= 0 ? static_cast<uint32_t>(read_size) : 0);
+
+      if (read_size < 0) {
+        Finish(MOJO_RESULT_ABORTED);
+        return;
+      }
+
+      if (read_size < attempted_read_size) {
+        // ReadAtCurrentPos makes a best effort to read all requested bytes. We
+        // reasonably assume if it fails to read what we ask for, we've hit EOF.
+        Finish(MOJO_RESULT_OK);
+        return;
+      }
+    }
+  }
+
+  void Finish(MojoResult result) {
+    watcher_.reset();
+    callback_task_runner_->PostTask(
+        FROM_HERE, base::BindOnce(std::move(callback_),
+                                  std::move(producer_handle_), result));
+  }
+
+  const scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
+  const scoped_refptr<base::SequencedTaskRunner> callback_task_runner_;
+
+  // State which is effectively owned and used only on the file sequence.
+  ScopedDataPipeProducerHandle producer_handle_;
+  base::File file_;
+  CompletionCallback callback_;
+  std::unique_ptr<SimpleWatcher> watcher_;
+
+  // Guards |is_cancelled_|.
+  base::Lock lock_;
+  bool is_cancelled_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(FileSequenceState);
+};
+
+FileDataPipeProducer::FileDataPipeProducer(
+    ScopedDataPipeProducerHandle producer)
+    : producer_(std::move(producer)), weak_factory_(this) {}
+
+FileDataPipeProducer::~FileDataPipeProducer() {
+  if (file_sequence_state_)
+    file_sequence_state_->Cancel();
+}
+
+void FileDataPipeProducer::WriteFromFile(base::File file,
+                                         CompletionCallback callback) {
+  InitializeNewRequest(std::move(callback));
+  file_sequence_state_->StartFromFile(std::move(file));
+}
+
+void FileDataPipeProducer::WriteFromPath(const base::FilePath& path,
+                                         CompletionCallback callback) {
+  InitializeNewRequest(std::move(callback));
+  file_sequence_state_->StartFromPath(path);
+}
+
+void FileDataPipeProducer::InitializeNewRequest(CompletionCallback callback) {
+  DCHECK(!file_sequence_state_);
+  auto file_task_runner = base::CreateSequencedTaskRunnerWithTraits(
+      {base::MayBlock(), base::TaskPriority::BACKGROUND});
+  file_sequence_state_ = new FileSequenceState(
+      std::move(producer_), file_task_runner,
+      base::BindOnce(&FileDataPipeProducer::OnWriteComplete,
+                     weak_factory_.GetWeakPtr(), std::move(callback)),
+      base::SequencedTaskRunnerHandle::Get());
+}
+
+void FileDataPipeProducer::OnWriteComplete(
+    CompletionCallback callback,
+    ScopedDataPipeProducerHandle producer,
+    MojoResult ready_result) {
+  producer_ = std::move(producer);
+  file_sequence_state_ = nullptr;
+  std::move(callback).Run(ready_result);
+}
+
+}  // namespace mojo
diff --git a/mojo/public/cpp/system/file_data_pipe_producer.h b/mojo/public/cpp/system/file_data_pipe_producer.h
new file mode 100644
index 0000000..15a808c
--- /dev/null
+++ b/mojo/public/cpp/system/file_data_pipe_producer.h
@@ -0,0 +1,76 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MOJO_PUBLIC_CPP_SYSTEM_FILE_DATA_PIPE_PRODUCER_H_
+#define MOJO_PUBLIC_CPP_SYSTEM_FILE_DATA_PIPE_PRODUCER_H_
+
+#include "base/callback_forward.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "mojo/public/cpp/system/data_pipe.h"
+#include "mojo/public/cpp/system/system_export.h"
+
+namespace mojo {
+
+// Helper class which takes ownership of a ScopedDataPipeProducerHandle and
+// assumes responsibility for feeding it the contents of a given file. This
+// takes care of waiting for pipe capacity as needed, and can notify callers
+// asynchronously when the operation is complete.
+//
+// Note that the FileDataPipeProducer must be kept alive until notified of
+// completion to ensure that all of the intended contents are written to the
+// pipe. Premature destruction may result in partial or total truncation of data
+// made available to the consumer.
+class MOJO_CPP_SYSTEM_EXPORT FileDataPipeProducer {
+ public:
+  using CompletionCallback = base::OnceCallback<void(MojoResult result)>;
+
+  // Constructs a new FileDataPipeProducer which will write data to |producer|.
+  explicit FileDataPipeProducer(ScopedDataPipeProducerHandle producer);
+  ~FileDataPipeProducer();
+
+  // Attempts to eventually write all of |file|'s contents to the pipe. Invokes
+  // |callback| asynchronously when done. Note that |callback| IS allowed to
+  // delete this FileDataPipeProducer.
+  //
+  // If the write is successful |result| will be |MOJO_RESULT_OK|. Otherwise
+  // (e.g. if the producer detects the consumer is closed and the pipe has no
+  // remaining capacity, or if file reads fail for any reason) |result| will be
+  // |MOJO_RESULT_ABORTED|.
+  //
+  // Note that if the FileDataPipeProducer is destroyed before |callback| can be
+  // invoked, |callback| is *never* invoked, and the write will be permanently
+  // interrupted (and the producer handle closed) after making potentially only
+  // partial progress.
+  //
+  // Multiple writes may be performed in sequence (each one after the last
+  // completes), but Write() must not be called before the |callback| for the
+  // previous call to Write() (if any) has returned.
+  void WriteFromFile(base::File file, CompletionCallback callback);
+
+  // Same as above but takes a FilePath instead of an opened File. Opens the
+  // file on an appropriate sequence and then proceeds as WriteFromFile() would.
+  void WriteFromPath(const base::FilePath& path, CompletionCallback callback);
+
+ private:
+  class FileSequenceState;
+
+  void InitializeNewRequest(CompletionCallback callback);
+  void OnWriteComplete(CompletionCallback callback,
+                       ScopedDataPipeProducerHandle producer,
+                       MojoResult result);
+
+  ScopedDataPipeProducerHandle producer_;
+  scoped_refptr<FileSequenceState> file_sequence_state_;
+  base::WeakPtrFactory<FileDataPipeProducer> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(FileDataPipeProducer);
+};
+
+}  // namespace mojo
+
+#endif  // MOJO_PUBLIC_CPP_SYSTEM_FILE_DATA_PIPE_PRODUCER_H_
diff --git a/mojo/public/cpp/system/data_pipe_string_writer.cc b/mojo/public/cpp/system/string_data_pipe_producer.cc
similarity index 85%
rename from mojo/public/cpp/system/data_pipe_string_writer.cc
rename to mojo/public/cpp/system/string_data_pipe_producer.cc
index 646eca6..34b25d60 100644
--- a/mojo/public/cpp/system/data_pipe_string_writer.cc
+++ b/mojo/public/cpp/system/string_data_pipe_producer.cc
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "mojo/public/cpp/system/data_pipe_string_writer.h"
+#include "mojo/public/cpp/system/string_data_pipe_producer.h"
 
 #include <algorithm>
 
@@ -55,16 +55,16 @@
 
 }  // namespace
 
-DataPipeStringWriter::DataPipeStringWriter(
+StringDataPipeProducer::StringDataPipeProducer(
     ScopedDataPipeProducerHandle producer)
     : producer_(std::move(producer)),
       watcher_(FROM_HERE, SimpleWatcher::ArmingPolicy::AUTOMATIC),
       weak_factory_(this) {}
 
-DataPipeStringWriter::~DataPipeStringWriter() = default;
+StringDataPipeProducer::~StringDataPipeProducer() = default;
 
-void DataPipeStringWriter::Write(const base::StringPiece& data,
-                                 CompletionCallback callback) {
+void StringDataPipeProducer::Write(const base::StringPiece& data,
+                                   CompletionCallback callback) {
   DCHECK(!callback_);
   callback_ = std::move(callback);
 
@@ -75,7 +75,7 @@
       WriteDataToProducerHandle(producer_.get(), data.data(), &size);
   if (result == MOJO_RESULT_OK && size == data.size()) {
     base::SequencedTaskRunnerHandle::Get()->PostTask(
-        FROM_HERE, base::BindOnce(&DataPipeStringWriter::InvokeCallback,
+        FROM_HERE, base::BindOnce(&StringDataPipeProducer::InvokeCallback,
                                   weak_factory_.GetWeakPtr(), MOJO_RESULT_OK));
   } else {
     // Copy whatever data didn't make the cut and try again when the pipe has
@@ -84,17 +84,17 @@
     data_view_ = data_;
     watcher_.Watch(producer_.get(), MOJO_HANDLE_SIGNAL_WRITABLE,
                    MOJO_WATCH_CONDITION_SATISFIED,
-                   base::Bind(&DataPipeStringWriter::OnProducerHandleReady,
+                   base::Bind(&StringDataPipeProducer::OnProducerHandleReady,
                               base::Unretained(this)));
   }
 }
 
-void DataPipeStringWriter::InvokeCallback(MojoResult result) {
+void StringDataPipeProducer::InvokeCallback(MojoResult result) {
   // May delete |this|.
   std::move(callback_).Run(result);
 }
 
-void DataPipeStringWriter::OnProducerHandleReady(
+void StringDataPipeProducer::OnProducerHandleReady(
     MojoResult ready_result,
     const HandleSignalsState& state) {
   bool failed = false;
diff --git a/mojo/public/cpp/system/data_pipe_string_writer.h b/mojo/public/cpp/system/string_data_pipe_producer.h
similarity index 69%
rename from mojo/public/cpp/system/data_pipe_string_writer.h
rename to mojo/public/cpp/system/string_data_pipe_producer.h
index f3b8e44..f35d8ff 100644
--- a/mojo/public/cpp/system/data_pipe_string_writer.h
+++ b/mojo/public/cpp/system/string_data_pipe_producer.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 MOJO_PUBLIC_CPP_SYSTEM_DATA_PIPE_STRING_WRITER_H_
-#define MOJO_PUBLIC_CPP_SYSTEM_DATA_PIPE_STRING_WRITER_H_
+#ifndef MOJO_PUBLIC_CPP_SYSTEM_STRING_DATA_PIPE_PRODUCER_H_
+#define MOJO_PUBLIC_CPP_SYSTEM_STRING_DATA_PIPE_PRODUCER_H_
 
 #include <string>
 
@@ -22,30 +22,31 @@
 // takes care of waiting for pipe capacity as needed, and can notify callers
 // asynchronously when the operation is complete.
 //
-// Note that the DataPipeStringWriter must be kept alive until notified of
+// Note that the StringDataPipeProducer must be kept alive until notified of
 // completion to ensure that all of the string's data is written to the pipe.
 // Premature destruction may result in partial or total truncation of data made
 // available to the consumer.
-class MOJO_CPP_SYSTEM_EXPORT DataPipeStringWriter {
+class MOJO_CPP_SYSTEM_EXPORT StringDataPipeProducer {
  public:
   using CompletionCallback = base::OnceCallback<void(MojoResult result)>;
 
-  // Constructs a new DataPipeStringWriter which will write data to |producer|.
-  explicit DataPipeStringWriter(ScopedDataPipeProducerHandle producer);
-  ~DataPipeStringWriter();
+  // Constructs a new StringDataPipeProducer which will write data to
+  // |producer|.
+  explicit StringDataPipeProducer(ScopedDataPipeProducerHandle producer);
+  ~StringDataPipeProducer();
 
   // Attempts to eventually write all of |data|. Invokes |callback|
   // asynchronously when done. Note that |callback| IS allowed to delete this
-  // DataPipeStringWriter.
+  // StringDataPipeProducer.
   //
   // If the write is successful |result| will be |MOJO_RESULT_OK|. Otherwise
   // (e.g. if the producer detects the consumer is closed and the pipe has no
   // remaining capacity) |result| will be |MOJO_RESULT_ABORTED|.
   //
-  // Note that if the DataPipeStringWriter is destroyed before |callback| can be
-  // invoked, |callback| is *never* invoked, and the write will be permanently
-  // interrupted (and the producer handle closed) after making potentially only
-  // partial progress.
+  // Note that if the StringDataPipeProducer is destroyed before |callback| can
+  // be invoked, |callback| is *never* invoked, and the write will be
+  // permanently interrupted (and the producer handle closed) after making
+  // potentially only partial progress.
   //
   // Multiple writes may be performed in sequence (each one after the last
   // completes), but Write() must not be called before the |callback| for the
@@ -62,11 +63,11 @@
   base::StringPiece data_view_;
   CompletionCallback callback_;
   SimpleWatcher watcher_;
-  base::WeakPtrFactory<DataPipeStringWriter> weak_factory_;
+  base::WeakPtrFactory<StringDataPipeProducer> weak_factory_;
 
-  DISALLOW_COPY_AND_ASSIGN(DataPipeStringWriter);
+  DISALLOW_COPY_AND_ASSIGN(StringDataPipeProducer);
 };
 
 }  // namespace mojo
 
-#endif  // MOJO_PUBLIC_CPP_SYSTEM_DATA_PIPE_STRING_WRITER_H_
+#endif  // MOJO_PUBLIC_CPP_SYSTEM_STRING_DATA_PIPE_PRODUCER_H_
diff --git a/mojo/public/cpp/system/tests/BUILD.gn b/mojo/public/cpp/system/tests/BUILD.gn
index 9c74b5e..f580601d 100644
--- a/mojo/public/cpp/system/tests/BUILD.gn
+++ b/mojo/public/cpp/system/tests/BUILD.gn
@@ -7,10 +7,11 @@
 
   sources = [
     "core_unittest.cc",
-    "data_pipe_string_writer_unittest.cc",
+    "file_data_pipe_producer_unittest.cc",
     "handle_signal_tracker_unittest.cc",
     "handle_signals_state_unittest.cc",
     "simple_watcher_unittest.cc",
+    "string_data_pipe_producer_unittest.cc",
     "wait_set_unittest.cc",
     "wait_unittest.cc",
   ]
diff --git a/mojo/public/cpp/system/tests/file_data_pipe_producer_unittest.cc b/mojo/public/cpp/system/tests/file_data_pipe_producer_unittest.cc
new file mode 100644
index 0000000..2ca9ba0
--- /dev/null
+++ b/mojo/public/cpp/system/tests/file_data_pipe_producer_unittest.cc
@@ -0,0 +1,214 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <algorithm>
+#include <string>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/macros.h"
+#include "base/memory/ptr_util.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/scoped_task_environment.h"
+#include "mojo/public/cpp/system/data_pipe.h"
+#include "mojo/public/cpp/system/file_data_pipe_producer.h"
+#include "mojo/public/cpp/system/simple_watcher.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace mojo {
+namespace {
+
+// Test helper. Reads a consumer handle, accumulating data into a string. Reads
+// until encountering an error (e.g. peer closure), at which point it invokes an
+// async callback.
+class DataPipeReader {
+ public:
+  explicit DataPipeReader(ScopedDataPipeConsumerHandle consumer_handle,
+                          size_t read_size,
+                          base::OnceClosure on_read_done)
+      : consumer_handle_(std::move(consumer_handle)),
+        read_size_(read_size),
+        on_read_done_(std::move(on_read_done)),
+        watcher_(FROM_HERE, SimpleWatcher::ArmingPolicy::AUTOMATIC) {
+    watcher_.Watch(
+        consumer_handle_.get(), MOJO_HANDLE_SIGNAL_READABLE,
+        MOJO_WATCH_CONDITION_SATISFIED,
+        base::Bind(&DataPipeReader::OnDataAvailable, base::Unretained(this)));
+  }
+  ~DataPipeReader() = default;
+
+  const std::string& data() const { return data_; }
+
+ private:
+  void OnDataAvailable(MojoResult result, const HandleSignalsState& state) {
+    if (result == MOJO_RESULT_OK) {
+      uint32_t size = static_cast<uint32_t>(read_size_);
+      std::vector<char> buffer(size, 0);
+      MojoResult read_result;
+      do {
+        read_result = consumer_handle_->ReadData(buffer.data(), &size,
+                                                 MOJO_READ_DATA_FLAG_NONE);
+        if (read_result == MOJO_RESULT_OK) {
+          std::copy(buffer.begin(), buffer.begin() + size,
+                    std::back_inserter(data_));
+        }
+      } while (read_result == MOJO_RESULT_OK);
+
+      if (read_result == MOJO_RESULT_SHOULD_WAIT)
+        return;
+    }
+
+    if (result != MOJO_RESULT_CANCELLED)
+      watcher_.Cancel();
+
+    std::move(on_read_done_).Run();
+  }
+
+  ScopedDataPipeConsumerHandle consumer_handle_;
+  const size_t read_size_;
+  base::OnceClosure on_read_done_;
+  SimpleWatcher watcher_;
+  std::string data_;
+
+  DISALLOW_COPY_AND_ASSIGN(DataPipeReader);
+};
+
+class FileDataPipeProducerTest : public testing::Test {
+ public:
+  FileDataPipeProducerTest() { CHECK(temp_dir_.CreateUniqueTempDir()); }
+
+  ~FileDataPipeProducerTest() override = default;
+
+ protected:
+  base::FilePath CreateTempFileWithContents(const std::string& contents) {
+    base::FilePath temp_file_path = temp_dir_.GetPath().AppendASCII(
+        base::StringPrintf("tmp%d", tmp_file_id_++));
+    base::File temp_file(temp_file_path,
+                         base::File::FLAG_CREATE | base::File::FLAG_WRITE);
+    int bytes_written = temp_file.WriteAtCurrentPos(
+        contents.data(), static_cast<int>(contents.size()));
+    CHECK_EQ(static_cast<int>(contents.size()), bytes_written);
+    return temp_file_path;
+  }
+
+  static void WriteFromFileThenCloseWriter(
+      std::unique_ptr<FileDataPipeProducer> producer,
+      base::File file) {
+    FileDataPipeProducer* raw_producer = producer.get();
+    raw_producer->WriteFromFile(
+        std::move(file),
+        base::BindOnce([](std::unique_ptr<FileDataPipeProducer> producer,
+                          MojoResult result) {},
+                       std::move(producer)));
+  }
+
+  static void WriteFromPathThenCloseWriter(
+      std::unique_ptr<FileDataPipeProducer> producer,
+      const base::FilePath& path) {
+    FileDataPipeProducer* raw_producer = producer.get();
+    raw_producer->WriteFromPath(
+        path, base::BindOnce([](std::unique_ptr<FileDataPipeProducer> producer,
+                                MojoResult result) {},
+                             std::move(producer)));
+  }
+
+ private:
+  base::test::ScopedTaskEnvironment task_environment_;
+  base::ScopedTempDir temp_dir_;
+  int tmp_file_id_ = 0;
+
+  DISALLOW_COPY_AND_ASSIGN(FileDataPipeProducerTest);
+};
+
+TEST_F(FileDataPipeProducerTest, WriteFromFile) {
+  const std::string kTestStringFragment = "Hello, world!";
+  constexpr size_t kNumRepetitions = 1000;
+  std::string test_string;
+  for (size_t i = 0; i < kNumRepetitions; ++i)
+    test_string += kTestStringFragment;
+
+  base::FilePath path = CreateTempFileWithContents(test_string);
+
+  base::RunLoop loop;
+  DataPipe pipe(16);
+  DataPipeReader reader(std::move(pipe.consumer_handle), 16,
+                        loop.QuitClosure());
+
+  base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+  WriteFromFileThenCloseWriter(
+      base::MakeUnique<FileDataPipeProducer>(std::move(pipe.producer_handle)),
+      std::move(file));
+  loop.Run();
+
+  EXPECT_EQ(test_string, reader.data());
+}
+
+TEST_F(FileDataPipeProducerTest, WriteFromPath) {
+  const std::string kTestStringFragment = "Hello, world!";
+  constexpr size_t kNumRepetitions = 1000;
+  std::string test_string;
+  for (size_t i = 0; i < kNumRepetitions; ++i)
+    test_string += kTestStringFragment;
+
+  base::FilePath path = CreateTempFileWithContents(test_string);
+
+  base::RunLoop loop;
+  DataPipe pipe(16);
+  DataPipeReader reader(std::move(pipe.consumer_handle), 16,
+                        loop.QuitClosure());
+
+  WriteFromPathThenCloseWriter(
+      base::MakeUnique<FileDataPipeProducer>(std::move(pipe.producer_handle)),
+      path);
+  loop.Run();
+
+  EXPECT_EQ(test_string, reader.data());
+}
+
+TEST_F(FileDataPipeProducerTest, TinyFile) {
+  const std::string kTestString = ".";
+  base::FilePath path = CreateTempFileWithContents(kTestString);
+  base::RunLoop loop;
+  DataPipe pipe(16);
+  DataPipeReader reader(std::move(pipe.consumer_handle), 16,
+                        loop.QuitClosure());
+  WriteFromPathThenCloseWriter(
+      base::MakeUnique<FileDataPipeProducer>(std::move(pipe.producer_handle)),
+      path);
+  loop.Run();
+
+  EXPECT_EQ(kTestString, reader.data());
+}
+
+TEST_F(FileDataPipeProducerTest, HugeFile) {
+  constexpr size_t kHugeFileSize = 100 * 1024 * 1024;
+  constexpr uint32_t kDataPipeSize = 512 * 1024;
+
+  std::string test_string(kHugeFileSize, 'a');
+  for (size_t i = 0; i + 3 < test_string.size(); i += 4) {
+    test_string[i + 1] = 'b';
+    test_string[i + 2] = 'c';
+    test_string[i + 3] = 'd';
+  }
+  base::FilePath path = CreateTempFileWithContents(test_string);
+
+  base::RunLoop loop;
+  DataPipe pipe(kDataPipeSize);
+  DataPipeReader reader(std::move(pipe.consumer_handle), kDataPipeSize,
+                        loop.QuitClosure());
+
+  WriteFromPathThenCloseWriter(
+      base::MakeUnique<FileDataPipeProducer>(std::move(pipe.producer_handle)),
+      path);
+  loop.Run();
+
+  EXPECT_EQ(test_string, reader.data());
+}
+
+}  // namespace
+}  // namespace mojo
diff --git a/mojo/public/cpp/system/tests/data_pipe_string_writer_unittest.cc b/mojo/public/cpp/system/tests/string_data_pipe_producer_unittest.cc
similarity index 70%
rename from mojo/public/cpp/system/tests/data_pipe_string_writer_unittest.cc
rename to mojo/public/cpp/system/tests/string_data_pipe_producer_unittest.cc
index 4bed947..61f5574 100644
--- a/mojo/public/cpp/system/tests/data_pipe_string_writer_unittest.cc
+++ b/mojo/public/cpp/system/tests/string_data_pipe_producer_unittest.cc
@@ -14,11 +14,12 @@
 #include "base/strings/string_piece.h"
 #include "base/test/scoped_task_environment.h"
 #include "mojo/public/cpp/system/data_pipe.h"
-#include "mojo/public/cpp/system/data_pipe_string_writer.h"
 #include "mojo/public/cpp/system/simple_watcher.h"
+#include "mojo/public/cpp/system/string_data_pipe_producer.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace mojo {
+namespace {
 
 // Test helper. Reads a consumer handle, accumulating data into a string. Reads
 // until encountering an error (e.g. peer closure), at which point it invokes an
@@ -72,102 +73,102 @@
   DISALLOW_COPY_AND_ASSIGN(DataPipeReader);
 };
 
-class DataPipeStringWriterTest : public testing::Test {
+class StringDataPipeProducerTest : public testing::Test {
  public:
-  DataPipeStringWriterTest() = default;
-  ~DataPipeStringWriterTest() override = default;
+  StringDataPipeProducerTest() = default;
+  ~StringDataPipeProducerTest() override = default;
 
  protected:
-  static void WriteStringThenCloseWriter(
-      std::unique_ptr<DataPipeStringWriter> writer,
+  static void WriteStringThenCloseProducer(
+      std::unique_ptr<StringDataPipeProducer> producer,
       const base::StringPiece& str) {
-    DataPipeStringWriter* raw_writer = writer.get();
-    raw_writer->Write(
-        str, base::BindOnce([](std::unique_ptr<DataPipeStringWriter> writer,
+    StringDataPipeProducer* raw_producer = producer.get();
+    raw_producer->Write(
+        str, base::BindOnce([](std::unique_ptr<StringDataPipeProducer> producer,
                                MojoResult result) {},
-                            std::move(writer)));
+                            std::move(producer)));
   }
 
-  static void WriteStringsThenCloseWriter(
-      std::unique_ptr<DataPipeStringWriter> writer,
+  static void WriteStringsThenCloseProducer(
+      std::unique_ptr<StringDataPipeProducer> producer,
       std::list<base::StringPiece> strings) {
-    DataPipeStringWriter* raw_writer = writer.get();
+    StringDataPipeProducer* raw_producer = producer.get();
     base::StringPiece str = strings.front();
     strings.pop_front();
-    raw_writer->Write(
+    raw_producer->Write(
         str, base::BindOnce(
-                 [](std::unique_ptr<DataPipeStringWriter> writer,
+                 [](std::unique_ptr<StringDataPipeProducer> producer,
                     std::list<base::StringPiece> strings, MojoResult result) {
                    if (!strings.empty())
-                     WriteStringsThenCloseWriter(std::move(writer),
-                                                 std::move(strings));
+                     WriteStringsThenCloseProducer(std::move(producer),
+                                                   std::move(strings));
                  },
-                 std::move(writer), std::move(strings)));
+                 std::move(producer), std::move(strings)));
   }
 
  private:
   base::test::ScopedTaskEnvironment task_environment_;
 
-  DISALLOW_COPY_AND_ASSIGN(DataPipeStringWriterTest);
+  DISALLOW_COPY_AND_ASSIGN(StringDataPipeProducerTest);
 };
 
-TEST_F(DataPipeStringWriterTest, EqualCapacity) {
+TEST_F(StringDataPipeProducerTest, EqualCapacity) {
   const std::string kTestString = "Hello, world!";
 
   base::RunLoop loop;
   mojo::DataPipe pipe(static_cast<uint32_t>(kTestString.size()));
   DataPipeReader reader(std::move(pipe.consumer_handle), loop.QuitClosure());
-  WriteStringThenCloseWriter(
-      base::MakeUnique<DataPipeStringWriter>(std::move(pipe.producer_handle)),
+  WriteStringThenCloseProducer(
+      base::MakeUnique<StringDataPipeProducer>(std::move(pipe.producer_handle)),
       kTestString);
   loop.Run();
 
   EXPECT_EQ(kTestString, reader.data());
 }
 
-TEST_F(DataPipeStringWriterTest, UnderCapacity) {
+TEST_F(StringDataPipeProducerTest, UnderCapacity) {
   const std::string kTestString = "Hello, world!";
 
   base::RunLoop loop;
   mojo::DataPipe pipe(static_cast<uint32_t>(kTestString.size() * 2));
   DataPipeReader reader(std::move(pipe.consumer_handle), loop.QuitClosure());
-  WriteStringThenCloseWriter(
-      base::MakeUnique<DataPipeStringWriter>(std::move(pipe.producer_handle)),
+  WriteStringThenCloseProducer(
+      base::MakeUnique<StringDataPipeProducer>(std::move(pipe.producer_handle)),
       kTestString);
   loop.Run();
 
   EXPECT_EQ(kTestString, reader.data());
 }
 
-TEST_F(DataPipeStringWriterTest, OverCapacity) {
+TEST_F(StringDataPipeProducerTest, OverCapacity) {
   const std::string kTestString = "Hello, world!";
 
   base::RunLoop loop;
   mojo::DataPipe pipe(static_cast<uint32_t>(kTestString.size() / 2));
   DataPipeReader reader(std::move(pipe.consumer_handle), loop.QuitClosure());
-  WriteStringThenCloseWriter(
-      base::MakeUnique<DataPipeStringWriter>(std::move(pipe.producer_handle)),
+  WriteStringThenCloseProducer(
+      base::MakeUnique<StringDataPipeProducer>(std::move(pipe.producer_handle)),
       kTestString);
   loop.Run();
 
   EXPECT_EQ(kTestString, reader.data());
 }
 
-TEST_F(DataPipeStringWriterTest, TinyPipe) {
+TEST_F(StringDataPipeProducerTest, TinyPipe) {
   const std::string kTestString = "Hello, world!";
 
   base::RunLoop loop;
   mojo::DataPipe pipe(1);
   DataPipeReader reader(std::move(pipe.consumer_handle), loop.QuitClosure());
-  WriteStringThenCloseWriter(
-      base::MakeUnique<DataPipeStringWriter>(std::move(pipe.producer_handle)),
+  WriteStringThenCloseProducer(
+      base::MakeUnique<StringDataPipeProducer>(std::move(pipe.producer_handle)),
       kTestString);
   loop.Run();
 
   EXPECT_EQ(kTestString, reader.data());
 }
 
-TEST_F(DataPipeStringWriterTest, MultipleWrites) {
+TEST_F(StringDataPipeProducerTest, MultipleWrites) {
   const std::string kTestString1 = "Hello, world!";
   const std::string kTestString2 = "There is a lot of data coming your way!";
   const std::string kTestString3 = "So many strings!";
@@ -176,8 +177,8 @@
   base::RunLoop loop;
   mojo::DataPipe pipe(4);
   DataPipeReader reader(std::move(pipe.consumer_handle), loop.QuitClosure());
-  WriteStringsThenCloseWriter(
-      base::MakeUnique<DataPipeStringWriter>(std::move(pipe.producer_handle)),
+  WriteStringsThenCloseProducer(
+      base::MakeUnique<StringDataPipeProducer>(std::move(pipe.producer_handle)),
       {kTestString1, kTestString2, kTestString3, kTestString4});
   loop.Run();
 
@@ -185,4 +186,5 @@
             reader.data());
 }
 
+}  // namespace
 }  // namespace mojo
diff --git a/net/BUILD.gn b/net/BUILD.gn
index dfdfb84..19c2dc5 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -4632,6 +4632,7 @@
     "base/mime_util_unittest.cc",
     "base/mock_network_change_notifier.cc",
     "base/mock_network_change_notifier.h",
+    "base/net_string_util_unittest.cc",
     "base/network_activity_monitor_unittest.cc",
     "base/network_change_notifier_unittest.cc",
     "base/network_change_notifier_win_unittest.cc",
diff --git a/net/DEPS b/net/DEPS
index 77c57f4..81a0949 100644
--- a/net/DEPS
+++ b/net/DEPS
@@ -35,6 +35,7 @@
 
   # Consolidated string functions that depend on icu.
   "net_string_util_icu\.cc": [
+    "+base/i18n/case_conversion.h",
     "+base/i18n/i18n_constants.h",
     "+base/i18n/icu_string_conversions.h",
     "+third_party/icu/source/common/unicode/ucnv.h"
diff --git a/net/android/java/src/org/chromium/net/NetStringUtil.java b/net/android/java/src/org/chromium/net/NetStringUtil.java
index 980b185..edbf4e0 100644
--- a/net/android/java/src/org/chromium/net/NetStringUtil.java
+++ b/net/android/java/src/org/chromium/net/NetStringUtil.java
@@ -12,6 +12,7 @@
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CodingErrorAction;
 import java.text.Normalizer;
+import java.util.Locale;
 
 /**
  * Utility functions for converting strings between formats when not built with
@@ -85,4 +86,19 @@
             return null;
         }
     }
+
+    /**
+     * Convert a string to uppercase.
+     * @param str String to convert.
+     * @return: String converted to uppercase using default locale,
+     * null on failure.
+     */
+    @CalledByNative
+    private static String toUpperCase(String str) {
+        try {
+            return str.toUpperCase(Locale.getDefault());
+        } catch (Exception e) {
+            return null;
+        }
+    }
 }
diff --git a/net/base/net_string_util.h b/net/base/net_string_util.h
index 8e959a0..fb9c0e15 100644
--- a/net/base/net_string_util.h
+++ b/net/base/net_string_util.h
@@ -8,6 +8,7 @@
 #include <string>
 
 #include "base/strings/string16.h"
+#include "net/base/net_export.h"
 
 // String conversion functions.  By default, they're implemented with ICU, but
 // when building with USE_ICU_ALTERNATIVES, they use platform functions instead.
@@ -37,6 +38,11 @@
                                      const char* charset,
                                      base::string16* output);
 
+// Converts |str| to uppercase using the default locale, and writes it to
+// |output|. On failure returns false and |output| is cleared.
+NET_EXPORT_PRIVATE bool ToUpper(const base::string16& str,
+                                base::string16* output);
+
 }  // namespace net
 
 #endif  // NET_BASE_NET_STRING_UTIL_H__
diff --git a/net/base/net_string_util_icu.cc b/net/base/net_string_util_icu.cc
index 525800c..7a3e96b54 100644
--- a/net/base/net_string_util_icu.cc
+++ b/net/base/net_string_util_icu.cc
@@ -4,6 +4,7 @@
 
 #include "net/base/net_string_util.h"
 
+#include "base/i18n/case_conversion.h"
 #include "base/i18n/i18n_constants.h"
 #include "base/i18n/icu_string_conversions.h"
 #include "base/strings/string_util.h"
@@ -59,4 +60,9 @@
                                output);
 }
 
+bool ToUpper(const base::string16& str, base::string16* output) {
+  *output = base::i18n::ToUpper(str);
+  return true;
+}
+
 }  // namespace net
diff --git a/net/base/net_string_util_icu_alternatives_android.cc b/net/base/net_string_util_icu_alternatives_android.cc
index 76d9326..736f41fd 100644
--- a/net/base/net_string_util_icu_alternatives_android.cc
+++ b/net/base/net_string_util_icu_alternatives_android.cc
@@ -113,4 +113,19 @@
   return true;
 }
 
+bool ToUpper(const base::string16& str, base::string16* output) {
+  output->clear();
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jstring> java_new_str(
+      env, env->NewString(str.data(), str.length()));
+  if (java_new_str.is_null())
+    return false;
+  ScopedJavaLocalRef<jstring> java_result =
+      android::Java_NetStringUtil_toUpperCase(env, java_new_str);
+  if (java_result.is_null())
+    return false;
+  *output = base::android::ConvertJavaStringToUTF16(java_result);
+  return true;
+}
+
 }  // namespace net
diff --git a/net/base/net_string_util_icu_alternatives_ios.mm b/net/base/net_string_util_icu_alternatives_ios.mm
index 1f6c0c0..0938434 100644
--- a/net/base/net_string_util_icu_alternatives_ios.mm
+++ b/net/base/net_string_util_icu_alternatives_ios.mm
@@ -73,4 +73,13 @@
   return false;
 }
 
+bool ToUpper(const base::string16& str, base::string16* output) {
+  base::ScopedCFTypeRef<CFStringRef> cfstring(base::SysUTF16ToCFStringRef(str));
+  base::ScopedCFTypeRef<CFMutableStringRef> mutable_cfstring(
+      CFStringCreateMutableCopy(kCFAllocatorDefault, 0, cfstring.get()));
+  CFStringUppercase(mutable_cfstring.get(), NULL);
+  *output = base::SysCFStringRefToUTF16(mutable_cfstring.get());
+  return true;
+}
+
 }  // namespace net
diff --git a/net/base/net_string_util_unittest.cc b/net/base/net_string_util_unittest.cc
new file mode 100644
index 0000000..c37ef9e
--- /dev/null
+++ b/net/base/net_string_util_unittest.cc
@@ -0,0 +1,45 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/base/net_string_util.h"
+
+#include "base/strings/string16.h"
+#include "base/strings/utf_string_conversions.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace net {
+
+TEST(NetStringUtilTest, ToUpperEmpty) {
+  base::string16 in;
+  base::string16 out;
+  base::string16 expected;
+  ASSERT_TRUE(ToUpper(in, &out));
+  ASSERT_EQ(expected, out);
+}
+
+TEST(NetStringUtilTest, ToUpperSingleChar) {
+  base::string16 in(base::WideToUTF16(L"a"));
+  base::string16 out;
+  base::string16 expected(base::WideToUTF16(L"A"));
+  ASSERT_TRUE(ToUpper(in, &out));
+  ASSERT_EQ(expected, out);
+}
+
+TEST(NetStringUtilTest, ToUpperSimple) {
+  base::string16 in(base::WideToUTF16(L"hello world"));
+  base::string16 out;
+  base::string16 expected(base::WideToUTF16(L"HELLO WORLD"));
+  ASSERT_TRUE(ToUpper(in, &out));
+  ASSERT_EQ(expected, out);
+}
+
+TEST(NetStringUtilTest, ToUpperAlreadyUpper) {
+  base::string16 in(base::WideToUTF16(L"HELLO WORLD"));
+  base::string16 out;
+  base::string16 expected(base::WideToUTF16(L"HELLO WORLD"));
+  ASSERT_TRUE(ToUpper(in, &out));
+  ASSERT_EQ(expected, out);
+}
+
+}  // namespace net
diff --git a/net/dns/dns_test_util.cc b/net/dns/dns_test_util.cc
index 2de2c69b..923a887 100644
--- a/net/dns/dns_test_util.cc
+++ b/net/dns/dns_test_util.cc
@@ -178,6 +178,10 @@
     return std::unique_ptr<DnsTransaction>(transaction);
   }
 
+  void AddEDNSOption(const OptRecordRdata::Opt& opt) override {
+    NOTREACHED() << "Not implemented";
+  }
+
   void CompleteDelayedTransactions() {
     DelayedTransactionList old_delayed_transactions;
     old_delayed_transactions.swap(delayed_transactions_);
diff --git a/net/dns/dns_transaction.cc b/net/dns/dns_transaction.cc
index 11ea820..899493b 100644
--- a/net/dns/dns_transaction.cc
+++ b/net/dns/dns_transaction.cc
@@ -48,11 +48,6 @@
 
 namespace {
 
-// Provide a common macro to simplify code and readability. We must use a
-// macro as the underlying HISTOGRAM macro creates static variables.
-#define DNS_HISTOGRAM(name, time) UMA_HISTOGRAM_CUSTOM_TIMES(name, time, \
-    base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromHours(1), 100)
-
 // Count labels in the fully-qualified name in DNS format.
 int CountLabels(const std::string& name) {
   size_t count = 0;
@@ -216,11 +211,11 @@
       return ERR_DNS_MALFORMED_RESPONSE;
     if (rv == OK) {
       DCHECK_EQ(STATE_NONE, next_state_);
-      DNS_HISTOGRAM("AsyncDNS.UDPAttemptSuccess",
-                    base::TimeTicks::Now() - start_time_);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.UDPAttemptSuccess",
+                                   base::TimeTicks::Now() - start_time_);
     } else if (rv != ERR_IO_PENDING) {
-      DNS_HISTOGRAM("AsyncDNS.UDPAttemptFail",
-                    base::TimeTicks::Now() - start_time_);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.UDPAttemptFail",
+                                   base::TimeTicks::Now() - start_time_);
     }
     return rv;
   }
@@ -389,11 +384,11 @@
     set_result(rv);
     if (rv == OK) {
       DCHECK_EQ(STATE_NONE, next_state_);
-      DNS_HISTOGRAM("AsyncDNS.TCPAttemptSuccess",
-                    base::TimeTicks::Now() - start_time_);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TCPAttemptSuccess",
+                                   base::TimeTicks::Now() - start_time_);
     } else if (rv != ERR_IO_PENDING) {
-      DNS_HISTOGRAM("AsyncDNS.TCPAttemptFail",
-                    base::TimeTicks::Now() - start_time_);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TCPAttemptFail",
+                                   base::TimeTicks::Now() - start_time_);
     }
     return rv;
   }
@@ -559,10 +554,12 @@
                      const std::string& hostname,
                      uint16_t qtype,
                      const DnsTransactionFactory::CallbackType& callback,
-                     const NetLogWithSource& net_log)
+                     const NetLogWithSource& net_log,
+                     const OptRecordRdata* opt_rdata)
       : session_(session),
         hostname_(hostname),
         qtype_(qtype),
+        opt_rdata_(opt_rdata),
         callback_(callback),
         net_log_(net_log),
         qnames_initial_size_(0),
@@ -708,7 +705,7 @@
     uint16_t id = session_->NextQueryId();
     std::unique_ptr<DnsQuery> query;
     if (attempts_.empty()) {
-      query.reset(new DnsQuery(id, qnames_.front(), qtype_));
+      query.reset(new DnsQuery(id, qnames_.front(), qtype_, opt_rdata_));
     } else {
       query = attempts_[0]->GetQuery()->CloneWithNewId(id);
     }
@@ -944,6 +941,7 @@
   scoped_refptr<DnsSession> session_;
   std::string hostname_;
   uint16_t qtype_;
+  const OptRecordRdata* opt_rdata_;
   // Cleared in DoCallback.
   DnsTransactionFactory::CallbackType callback_;
 
@@ -985,11 +983,19 @@
       const CallbackType& callback,
       const NetLogWithSource& net_log) override {
     return std::unique_ptr<DnsTransaction>(new DnsTransactionImpl(
-        session_.get(), hostname, qtype, callback, net_log));
+        session_.get(), hostname, qtype, callback, net_log, opt_rdata_.get()));
+  }
+
+  void AddEDNSOption(const OptRecordRdata::Opt& opt) override {
+    if (opt_rdata_ == nullptr)
+      opt_rdata_ = std::make_unique<OptRecordRdata>();
+
+    opt_rdata_->AddOpt(opt);
   }
 
  private:
   scoped_refptr<DnsSession> session_;
+  std::unique_ptr<OptRecordRdata> opt_rdata_;
 };
 
 }  // namespace
diff --git a/net/dns/dns_transaction.h b/net/dns/dns_transaction.h
index cd56fc4..5f6cd0a5c 100644
--- a/net/dns/dns_transaction.h
+++ b/net/dns/dns_transaction.h
@@ -13,6 +13,7 @@
 #include "base/callback_forward.h"
 #include "base/compiler_specific.h"
 #include "net/base/net_export.h"
+#include "net/dns/record_rdata.h"
 
 namespace net {
 
@@ -68,6 +69,10 @@
       const CallbackType& callback,
       const NetLogWithSource& net_log) WARN_UNUSED_RESULT = 0;
 
+  // The given EDNS0 option will be included in all DNS queries performed by
+  // transactions from this factory.
+  virtual void AddEDNSOption(const OptRecordRdata::Opt& opt) = 0;
+
   // Creates a DnsTransactionFactory which creates DnsTransactionImpl using the
   // |session|.
   static std::unique_ptr<DnsTransactionFactory> CreateFactory(
diff --git a/net/dns/dns_transaction_unittest.cc b/net/dns/dns_transaction_unittest.cc
index 0dc00f8b..c06e362 100644
--- a/net/dns/dns_transaction_unittest.cc
+++ b/net/dns/dns_transaction_unittest.cc
@@ -54,8 +54,9 @@
                 const char* dotted_name,
                 uint16_t qtype,
                 IoMode mode,
-                bool use_tcp)
-      : query_(new DnsQuery(id, DomainFromDot(dotted_name), qtype)),
+                bool use_tcp,
+                const OptRecordRdata* opt_rdata = nullptr)
+      : query_(new DnsQuery(id, DomainFromDot(dotted_name), qtype, opt_rdata)),
         use_tcp_(use_tcp) {
     if (use_tcp_) {
       std::unique_ptr<uint16_t> length(new uint16_t);
@@ -375,10 +376,11 @@
                            const uint8_t* response_data,
                            size_t response_length,
                            IoMode mode,
-                           bool use_tcp) {
+                           bool use_tcp,
+                           const OptRecordRdata* opt_rdata = nullptr) {
     CHECK(socket_factory_.get());
     std::unique_ptr<DnsSocketData> data(
-        new DnsSocketData(id, dotted_name, qtype, mode, use_tcp));
+        new DnsSocketData(id, dotted_name, qtype, mode, use_tcp, opt_rdata));
     data->AddResponseData(response_data, response_length, mode);
     AddSocketData(std::move(data));
   }
@@ -387,18 +389,20 @@
                                 const char* dotted_name,
                                 uint16_t qtype,
                                 const uint8_t* data,
-                                size_t data_length) {
-    AddQueryAndResponse(id, dotted_name, qtype, data, data_length, ASYNC,
-                        false);
+                                size_t data_length,
+                                const OptRecordRdata* opt_rdata = nullptr) {
+    AddQueryAndResponse(id, dotted_name, qtype, data, data_length, ASYNC, false,
+                        opt_rdata);
   }
 
   void AddSyncQueryAndResponse(uint16_t id,
                                const char* dotted_name,
                                uint16_t qtype,
                                const uint8_t* data,
-                               size_t data_length) {
+                               size_t data_length,
+                               const OptRecordRdata* opt_rdata = nullptr) {
     AddQueryAndResponse(id, dotted_name, qtype, data, data_length, SYNCHRONOUS,
-                        false);
+                        false, opt_rdata);
   }
 
   // Add expected query of |dotted_name| and |qtype| and no response.
@@ -491,6 +495,43 @@
   EXPECT_TRUE(helper0.Run(transaction_factory_.get()));
 }
 
+TEST_F(DnsTransactionTest, LookupWithEDNSOption) {
+  OptRecordRdata expected_opt_rdata;
+
+  const OptRecordRdata::Opt ednsOpt(123, "\xbe\xef");
+  transaction_factory_->AddEDNSOption(ednsOpt);
+  expected_opt_rdata.AddOpt(ednsOpt);
+
+  AddAsyncQueryAndResponse(0 /* id */, kT0HostName, kT0Qtype,
+                           kT0ResponseDatagram, arraysize(kT0ResponseDatagram),
+                           &expected_opt_rdata);
+
+  TransactionHelper helper0(kT0HostName, kT0Qtype, kT0RecordCount);
+  EXPECT_TRUE(helper0.Run(transaction_factory_.get()));
+}
+
+TEST_F(DnsTransactionTest, LookupWithMultipleEDNSOptions) {
+  OptRecordRdata expected_opt_rdata;
+
+  for (const auto& ednsOpt : {
+           // Two options with the same code, to check that both are included.
+           OptRecordRdata::Opt(1, "\xde\xad"),
+           OptRecordRdata::Opt(1, "\xbe\xef"),
+           // Try a different code and different length of data.
+           OptRecordRdata::Opt(2, "\xff"),
+       }) {
+    transaction_factory_->AddEDNSOption(ednsOpt);
+    expected_opt_rdata.AddOpt(ednsOpt);
+  }
+
+  AddAsyncQueryAndResponse(0 /* id */, kT0HostName, kT0Qtype,
+                           kT0ResponseDatagram, arraysize(kT0ResponseDatagram),
+                           &expected_opt_rdata);
+
+  TransactionHelper helper0(kT0HostName, kT0Qtype, kT0RecordCount);
+  EXPECT_TRUE(helper0.Run(transaction_factory_.get()));
+}
+
 // Concurrent lookup tests assume that DnsTransaction::Start immediately
 // consumes a socket from ClientSocketFactory.
 TEST_F(DnsTransactionTest, ConcurrentLookup) {
diff --git a/net/dns/host_resolver_impl.cc b/net/dns/host_resolver_impl.cc
index 38a7cdb..070f031 100644
--- a/net/dns/host_resolver_impl.cc
+++ b/net/dns/host_resolver_impl.cc
@@ -231,23 +231,30 @@
                         kSuffix, kSuffixLenTrimmed);
 }
 
-// Provide a common macro to simplify code and readability. We must use a
-// macro as the underlying HISTOGRAM macro creates static variables.
-#define DNS_HISTOGRAM(name, time) UMA_HISTOGRAM_CUSTOM_TIMES(name, time, \
-    base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromHours(1), 100)
-
 // A macro to simplify code and readability.
-#define DNS_HISTOGRAM_BY_PRIORITY(basename, priority, time) \
-  do { \
-    switch (priority) { \
-      case HIGHEST: DNS_HISTOGRAM(basename "_HIGHEST", time); break; \
-      case MEDIUM: DNS_HISTOGRAM(basename "_MEDIUM", time); break; \
-      case LOW: DNS_HISTOGRAM(basename "_LOW", time); break; \
-      case LOWEST: DNS_HISTOGRAM(basename "_LOWEST", time); break; \
-      case IDLE: DNS_HISTOGRAM(basename "_IDLE", time); break; \
-      case THROTTLED: DNS_HISTOGRAM(basename "_THROTTLED", time); break; \
-    } \
-    DNS_HISTOGRAM(basename, time); \
+#define DNS_HISTOGRAM_BY_PRIORITY(basename, priority, time)        \
+  do {                                                             \
+    switch (priority) {                                            \
+      case HIGHEST:                                                \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_HIGHEST", time);   \
+        break;                                                     \
+      case MEDIUM:                                                 \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_MEDIUM", time);    \
+        break;                                                     \
+      case LOW:                                                    \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_LOW", time);       \
+        break;                                                     \
+      case LOWEST:                                                 \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_LOWEST", time);    \
+        break;                                                     \
+      case IDLE:                                                   \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_IDLE", time);      \
+        break;                                                     \
+      case THROTTLED:                                              \
+        UMA_HISTOGRAM_LONG_TIMES_100(basename "_THROTTLED", time); \
+        break;                                                     \
+    }                                                              \
+    UMA_HISTOGRAM_LONG_TIMES_100(basename, time);                  \
   } while (0)
 
 // Record time from Request creation until a valid DNS response.
@@ -256,15 +263,15 @@
                      base::TimeDelta duration) {
   if (had_dns_config) {
     if (speculative) {
-      DNS_HISTOGRAM("AsyncDNS.TotalTime_speculative", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TotalTime_speculative", duration);
     } else {
-      DNS_HISTOGRAM("AsyncDNS.TotalTime", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TotalTime", duration);
     }
   } else {
     if (speculative) {
-      DNS_HISTOGRAM("DNS.TotalTime_speculative", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("DNS.TotalTime_speculative", duration);
     } else {
-      DNS_HISTOGRAM("DNS.TotalTime", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("DNS.TotalTime", duration);
     }
   }
 }
@@ -874,44 +881,48 @@
     if (error == OK) {
       if (had_non_speculative_request_) {
         category = RESOLVE_SUCCESS;
-        DNS_HISTOGRAM("DNS.ResolveSuccess", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSuccess", duration);
       } else {
         category = RESOLVE_SPECULATIVE_SUCCESS;
-        DNS_HISTOGRAM("DNS.ResolveSpeculativeSuccess", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSpeculativeSuccess", duration);
       }
 
       // Log DNS lookups based on |address_family|. This will help us determine
       // if IPv4 or IPv4/6 lookups are faster or slower.
       switch (key_.address_family) {
         case ADDRESS_FAMILY_IPV4:
-          DNS_HISTOGRAM("DNS.ResolveSuccess_FAMILY_IPV4", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSuccess_FAMILY_IPV4",
+                                       duration);
           break;
         case ADDRESS_FAMILY_IPV6:
-          DNS_HISTOGRAM("DNS.ResolveSuccess_FAMILY_IPV6", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSuccess_FAMILY_IPV6",
+                                       duration);
           break;
         case ADDRESS_FAMILY_UNSPECIFIED:
-          DNS_HISTOGRAM("DNS.ResolveSuccess_FAMILY_UNSPEC", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSuccess_FAMILY_UNSPEC",
+                                       duration);
           break;
       }
     } else {
       if (had_non_speculative_request_) {
         category = RESOLVE_FAIL;
-        DNS_HISTOGRAM("DNS.ResolveFail", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveFail", duration);
       } else {
         category = RESOLVE_SPECULATIVE_FAIL;
-        DNS_HISTOGRAM("DNS.ResolveSpeculativeFail", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveSpeculativeFail", duration);
       }
       // Log DNS lookups based on |address_family|. This will help us determine
       // if IPv4 or IPv4/6 lookups are faster or slower.
       switch (key_.address_family) {
         case ADDRESS_FAMILY_IPV4:
-          DNS_HISTOGRAM("DNS.ResolveFail_FAMILY_IPV4", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveFail_FAMILY_IPV4", duration);
           break;
         case ADDRESS_FAMILY_IPV6:
-          DNS_HISTOGRAM("DNS.ResolveFail_FAMILY_IPV6", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveFail_FAMILY_IPV6", duration);
           break;
         case ADDRESS_FAMILY_UNSPECIFIED:
-          DNS_HISTOGRAM("DNS.ResolveFail_FAMILY_UNSPEC", duration);
+          UMA_HISTOGRAM_LONG_TIMES_100("DNS.ResolveFail_FAMILY_UNSPEC",
+                                       duration);
           break;
       }
       UMA_HISTOGRAM_CUSTOM_ENUMERATION(kOSErrorsForGetAddrinfoHistogramName,
@@ -953,8 +964,9 @@
     // If first attempt didn't finish before retry attempt, then calculate stats
     // on how much time is saved by having spawned an extra attempt.
     if (!first_attempt_to_complete && is_first_attempt && !was_canceled()) {
-      DNS_HISTOGRAM("DNS.AttemptTimeSavedByRetry",
-                    base::TimeTicks::Now() - retry_attempt_finished_time_);
+      UMA_HISTOGRAM_LONG_TIMES_100(
+          "DNS.AttemptTimeSavedByRetry",
+          base::TimeTicks::Now() - retry_attempt_finished_time_);
     }
 
     if (was_canceled() || !first_attempt_to_complete) {
@@ -970,9 +982,9 @@
 
     base::TimeDelta duration = base::TimeTicks::Now() - start_time;
     if (error == OK)
-      DNS_HISTOGRAM("DNS.AttemptSuccessDuration", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("DNS.AttemptSuccessDuration", duration);
     else
-      DNS_HISTOGRAM("DNS.AttemptFailDuration", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("DNS.AttemptFailDuration", duration);
   }
 
   // Set on the task runner thread, read on the worker thread.
@@ -1111,18 +1123,19 @@
     DCHECK(transaction);
     base::TimeDelta duration = base::TimeTicks::Now() - start_time;
     if (net_error != OK) {
-      DNS_HISTOGRAM("AsyncDNS.TransactionFailure", duration);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TransactionFailure", duration);
       OnFailure(net_error, DnsResponse::DNS_PARSE_OK);
       return;
     }
 
-    DNS_HISTOGRAM("AsyncDNS.TransactionSuccess", duration);
+    UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TransactionSuccess", duration);
     switch (transaction->GetType()) {
       case dns_protocol::kTypeA:
-        DNS_HISTOGRAM("AsyncDNS.TransactionSuccess_A", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TransactionSuccess_A", duration);
         break;
       case dns_protocol::kTypeAAAA:
-        DNS_HISTOGRAM("AsyncDNS.TransactionSuccess_AAAA", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.TransactionSuccess_AAAA",
+                                     duration);
         break;
     }
 
@@ -1188,14 +1201,14 @@
                       bool success,
                       const AddressList& addr_list) {
     if (!success) {
-      DNS_HISTOGRAM("AsyncDNS.SortFailure",
-                    base::TimeTicks::Now() - start_time);
+      UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.SortFailure",
+                                   base::TimeTicks::Now() - start_time);
       OnFailure(ERR_DNS_SORT_ERROR, DnsResponse::DNS_PARSE_OK);
       return;
     }
 
-    DNS_HISTOGRAM("AsyncDNS.SortSuccess",
-                  base::TimeTicks::Now() - start_time);
+    UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.SortSuccess",
+                                 base::TimeTicks::Now() - start_time);
 
     // AddressSorter prunes unusable destinations.
     if (addr_list.empty()) {
@@ -1575,7 +1588,7 @@
     if (dns_task_error_ != OK) {
       base::TimeDelta duration = base::TimeTicks::Now() - start_time;
       if (net_error == OK) {
-        DNS_HISTOGRAM("AsyncDNS.FallbackSuccess", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.FallbackSuccess", duration);
         if ((dns_task_error_ == ERR_NAME_NOT_RESOLVED) &&
             ResemblesNetBIOSName(key_.hostname)) {
           UmaAsyncDnsResolveStatus(RESOLVE_STATUS_SUSPECT_NETBIOS);
@@ -1586,7 +1599,7 @@
                                     std::abs(dns_task_error_));
         resolver_->OnDnsTaskResolve(dns_task_error_);
       } else {
-        DNS_HISTOGRAM("AsyncDNS.FallbackFail", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.FallbackFail", duration);
         UmaAsyncDnsResolveStatus(RESOLVE_STATUS_FAIL);
       }
     }
@@ -1625,7 +1638,7 @@
   void OnDnsTaskFailure(const base::WeakPtr<DnsTask>& dns_task,
                         base::TimeDelta duration,
                         int net_error) {
-    DNS_HISTOGRAM("AsyncDNS.ResolveFail", duration);
+    UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.ResolveFail", duration);
 
     if (!dns_task)
       return;
@@ -1660,17 +1673,20 @@
       OnDnsTaskFailure(dns_task_->AsWeakPtr(), duration, net_error);
       return;
     }
-    DNS_HISTOGRAM("AsyncDNS.ResolveSuccess", duration);
+    UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.ResolveSuccess", duration);
     // Log DNS lookups based on |address_family|.
     switch (key_.address_family) {
       case ADDRESS_FAMILY_IPV4:
-        DNS_HISTOGRAM("AsyncDNS.ResolveSuccess_FAMILY_IPV4", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.ResolveSuccess_FAMILY_IPV4",
+                                     duration);
         break;
       case ADDRESS_FAMILY_IPV6:
-        DNS_HISTOGRAM("AsyncDNS.ResolveSuccess_FAMILY_IPV6", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.ResolveSuccess_FAMILY_IPV6",
+                                     duration);
         break;
       case ADDRESS_FAMILY_UNSPECIFIED:
-        DNS_HISTOGRAM("AsyncDNS.ResolveSuccess_FAMILY_UNSPEC", duration);
+        UMA_HISTOGRAM_LONG_TIMES_100("AsyncDNS.ResolveSuccess_FAMILY_UNSPEC",
+                                     duration);
         break;
     }
 
diff --git a/services/ui/main.cc b/services/ui/main.cc
index 4592379e..27c9ab5 100644
--- a/services/ui/main.cc
+++ b/services/ui/main.cc
@@ -8,7 +8,9 @@
 #include "services/ui/service.h"
 
 MojoResult ServiceMain(MojoHandle service_request_handle) {
-  service_manager::ServiceRunner runner(new ui::Service());
+  ui::Service* ui_service = new ui::Service;
+  ui_service->set_running_standalone(true);
+  service_manager::ServiceRunner runner(ui_service);
   runner.set_message_loop_type(base::MessageLoop::TYPE_UI);
   return runner.Run(service_request_handle);
 }
diff --git a/services/ui/service.cc b/services/ui/service.cc
index 5ff864e..9e5d510 100644
--- a/services/ui/service.cc
+++ b/services/ui/service.cc
@@ -186,7 +186,8 @@
     return false;
   }
 
-  ui::RegisterPathProvider();
+  if (running_standalone_)
+    ui::RegisterPathProvider();
 
   // Initialize resource bundle with 1x and 2x cursor bitmaps.
   ui::ResourceBundle::InitSharedInstanceWithPakFileRegion(
diff --git a/services/ui/service.h b/services/ui/service.h
index 968dfc57..d32960b 100644
--- a/services/ui/service.h
+++ b/services/ui/service.h
@@ -93,6 +93,9 @@
   explicit Service(const InProcessConfig* config = nullptr);
   ~Service() override;
 
+  // Call if the ui::Service is being run as a standalone process.
+  void set_running_standalone(bool value) { running_standalone_ = value; }
+
  private:
   // Holds InterfaceRequests received before the first WindowTreeHost Display
   // has been established.
@@ -227,6 +230,8 @@
 
   bool in_destructor_ = false;
 
+  bool running_standalone_ = false;
+
   DISALLOW_COPY_AND_ASSIGN(Service);
 };
 
diff --git a/testing/buildbot/filters/mojo.fyi.network_content_browsertests.filter b/testing/buildbot/filters/mojo.fyi.network_content_browsertests.filter
index 69d5a28..84fa2bf 100644
--- a/testing/buildbot/filters/mojo.fyi.network_content_browsertests.filter
+++ b/testing/buildbot/filters/mojo.fyi.network_content_browsertests.filter
@@ -1,23 +1,15 @@
 # http://crbug.com/715640
--BackgroundSyncBrowserTest*
--ServiceWorkerBlackBoxBrowserTest.Registration
--ServiceWorkerBrowserTest.CrossOriginFetchWithSaveData
--ServiceWorkerBrowserTest.CrossSiteTransfer
 -ServiceWorkerBrowserTest.FetchPageWithSaveData
--ServiceWorkerBrowserTest.FetchPageWithSaveDataPassThroughOnFetch
 -ServiceWorkerBrowserTest.ImportsBustMemcache
 -ServiceWorkerBrowserTest.Reload
 -ServiceWorkerBrowserTest.ResponseFromHTTPSServiceWorkerIsMarkedAsSecure
 -ServiceWorkerBrowserTest.ResponseFromHTTPServiceWorkerIsNotMarkedAsSecure
--ServiceWorkerDisableWebSecurityTest.UnregisterNoCrash
--ServiceWorkerDisableWebSecurityTest.UpdateNoCrash
 -ServiceWorkerNavigationPreloadTest.CanceledByInterceptor
 -ServiceWorkerNavigationPreloadTest.GetResponseText
 -ServiceWorkerNavigationPreloadTest.InvalidRedirect_InvalidLocation
 -ServiceWorkerNavigationPreloadTest.InvalidRedirect_MultiLocation
 -ServiceWorkerNavigationPreloadTest.NetworkError
 -ServiceWorkerNavigationPreloadTest.NetworkFallback
--ServiceWorkerNavigationPreloadTest.NotEnabled
 -ServiceWorkerNavigationPreloadTest.PreloadHeadersCustom
 -ServiceWorkerNavigationPreloadTest.PreloadHeadersSimple
 -ServiceWorkerNavigationPreloadTest.RedirectAndRespondWithNavigationPreload
@@ -25,13 +17,11 @@
 -ServiceWorkerNavigationPreloadTest.RespondWithNavigationPreloadWithMimeSniffing
 -ServiceWorkerNavigationPreloadTest.SetHeaderValue
 -ServiceWorkerV8CacheStrategiesAggressiveTest.V8CacheOnCacheStorage
--ServiceWorkerV8CacheStrategiesNoneTest.V8CacheOnCacheStorage
 -ServiceWorkerV8CacheStrategiesNormalTest.V8CacheOnCacheStorage
 -ServiceWorkerV8CacheStrategiesTest.V8CacheOnCacheStorage
 -ServiceWorkerVersionBrowserTest.ReadResourceFailure
 -ServiceWorkerVersionBrowserTest.ReadResourceFailure_WaitingWorker
 -ServiceWorkerVersionBrowserTest.ServiceWorkerScriptHeader
--ServiceWorkerVersionBrowserTest.StartNotFound
 
 # ServiceWorker restart needs to read installed scripts.
 # https://crbug.com/756312
diff --git a/testing/scripts/run_multiple_telemetry_benchmarks_as_googletest.py b/testing/scripts/run_multiple_telemetry_benchmarks_as_googletest.py
index 8a6b562..151f85d 100755
--- a/testing/scripts/run_multiple_telemetry_benchmarks_as_googletest.py
+++ b/testing/scripts/run_multiple_telemetry_benchmarks_as_googletest.py
@@ -100,7 +100,9 @@
 # This is not really a "script test" so does not need to manually add
 # any additional compile targets.
 def main_compile_targets(args):
-  json.dump([], args.output)
+  # Force compilation of the new isolate. Will change to telemetry_perf_tests
+  # once we switch the main isolate over to running this file.
+  json.dump(['telemetry_perf_tests_new'], args.output)
 
 
 if __name__ == '__main__':
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index c09dae2..1f58705 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -1330,7 +1330,7 @@
                 {
                     "name": "Enabled",
                     "params": {
-                        "engagement_threshold_for_flash": "64"
+                        "engagement_threshold_for_flash": "101"
                     },
                     "enable_features": [
                         "PreferHtmlOverPlugins"
diff --git a/third_party/WebKit/LayoutTests/PRESUBMIT.py b/third_party/WebKit/LayoutTests/PRESUBMIT.py
index 89ddada2..5b53b1a 100644
--- a/third_party/WebKit/LayoutTests/PRESUBMIT.py
+++ b/third_party/WebKit/LayoutTests/PRESUBMIT.py
@@ -72,10 +72,68 @@
     return results
 
 
+def _CheckTestExpectations(input_api, output_api):
+    local_paths = [f.LocalPath() for f in input_api.AffectedFiles()]
+    if any('LayoutTests' in path for path in local_paths):
+        lint_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
+            '..', 'Tools', 'Scripts', 'lint-test-expectations')
+        _, errs = input_api.subprocess.Popen(
+            [input_api.python_executable, lint_path],
+            stdout=input_api.subprocess.PIPE,
+            stderr=input_api.subprocess.PIPE).communicate()
+        if not errs:
+            return [output_api.PresubmitError(
+                "lint-test-expectations failed "
+                "to produce output; check by hand. ")]
+        if errs.strip() != 'Lint succeeded.':
+            return [output_api.PresubmitError(errs)]
+    return []
+
+
+def _CheckForJSTest(input_api, output_api):
+    """'js-test.js' is the past, 'testharness.js' is our glorious future"""
+    jstest_re = input_api.re.compile(r'resources/js-test.js')
+
+    def source_file_filter(path):
+        return input_api.FilterSourceFile(path,
+                                          white_list=[r'third_party/WebKit/LayoutTests/.*\.(html|js|php|pl|svg)$'])
+
+    errors = input_api.canned_checks._FindNewViolationsOfRule(
+        lambda _, x: not jstest_re.search(x), input_api, source_file_filter)
+    errors = ['  * %s' % violation for violation in errors]
+    if errors:
+        return [output_api.PresubmitPromptOrNotify(
+            '"resources/js-test.js" is deprecated; please write new layout '
+            'tests using the assertions in "resources/testharness.js" '
+            'instead, as these can be more easily upstreamed to Web Platform '
+            'Tests for cross-vendor compatibility testing. If you\'re not '
+            'already familiar with this framework, a tutorial is available at '
+            'https://darobin.github.io/test-harness-tutorial/docs/using-testharness.html'
+            '\n\n%s' % '\n'.join(errors))]
+    return []
+
+
+def _CheckForInvalidPreferenceError(input_api, output_api):
+    pattern = input_api.re.compile('Invalid name for preference: (.+)')
+    results = []
+
+    for f in input_api.AffectedFiles():
+        if not f.LocalPath().endswith('-expected.txt'):
+            continue
+        for line_num, line in f.ChangedContents():
+            error = pattern.search(line)
+            if error:
+                results.append(output_api.PresubmitError('Found an invalid preference %s in expected result %s:%s' % (error.group(1), f, line_num)))
+    return results
+
+
 def CheckChangeOnUpload(input_api, output_api):
     results = []
     results.extend(_CheckTestharnessResults(input_api, output_api))
     results.extend(_CheckFilesUsingEventSender(input_api, output_api))
+    results.extend(_CheckTestExpectations(input_api, output_api))
+    results.extend(_CheckForJSTest(input_api, output_api))
+    results.extend(_CheckForInvalidPreferenceError(input_api, output_api))
     return results
 
 
@@ -83,4 +141,5 @@
     results = []
     results.extend(_CheckTestharnessResults(input_api, output_api))
     results.extend(_CheckFilesUsingEventSender(input_api, output_api))
+    results.extend(_CheckTestExpectations(input_api, output_api))
     return results
diff --git a/third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json b/third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json
index 4efbabf5..7a4b02e 100644
--- a/third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json
+++ b/third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json
@@ -152198,6 +152198,14 @@
      }
     ]
    ],
+   "html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes-svg.tentative.html": [
+    [
+     "/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes-svg.tentative.html",
+     {
+      "timeout": "long"
+     }
+    ]
+   ],
    "html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html": [
     [
      "/html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html",
@@ -152206,6 +152214,22 @@
      }
     ]
    ],
+   "html/semantics/embedded-content/the-img-element/decode/image-decode-picture.html": [
+    [
+     "/html/semantics/embedded-content/the-img-element/decode/image-decode-picture.html",
+     {
+      "timeout": "long"
+     }
+    ]
+   ],
+   "html/semantics/embedded-content/the-img-element/decode/image-decode-svg.tentative.html": [
+    [
+     "/html/semantics/embedded-content/the-img-element/decode/image-decode-svg.tentative.html",
+     {
+      "timeout": "long"
+     }
+    ]
+   ],
    "html/semantics/embedded-content/the-img-element/decode/image-decode.html": [
     [
      "/html/semantics/embedded-content/the-img-element/decode/image-decode.html",
@@ -266414,15 +266438,27 @@
    "testharness"
   ],
   "html/semantics/embedded-content/the-img-element/decode/image-decode-iframe.html": [
-   "2632f69b005ea382ed91bfb71a7e0e2ee931a0c8",
+   "7fb6e548ca0d039496b35dadabd044274d0960be",
+   "testharness"
+  ],
+  "html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes-svg.tentative.html": [
+   "361ae415a6a41c7442c24f24898b51aef0e2270a",
    "testharness"
   ],
   "html/semantics/embedded-content/the-img-element/decode/image-decode-path-changes.html": [
-   "64dde3f7b1f9ed02e4433858f075123f18c092a4",
+   "34e5a2beebed1c95ab978258e3d93d9cefcd4d70",
+   "testharness"
+  ],
+  "html/semantics/embedded-content/the-img-element/decode/image-decode-picture.html": [
+   "0af39535cb047736d88948de962f876b1addbdbb",
+   "testharness"
+  ],
+  "html/semantics/embedded-content/the-img-element/decode/image-decode-svg.tentative.html": [
+   "9405efb65176096957438cbdcc59109b488d80e6",
    "testharness"
   ],
   "html/semantics/embedded-content/the-img-element/decode/image-decode.html": [
-   "02d38d593fb42587c3ef261c349f7c42db345005",
+   "5368b62bf6c950e8d57b16b36148e5695ce16fd8",
    "testharness"
   ],
   "html/semantics/embedded-content/the-img-element/delay-load-event-detached.html": [
diff --git a/third_party/WebKit/LayoutTests/reporting-observer/reporting-api.html b/third_party/WebKit/LayoutTests/reporting-observer/reporting-api.html
new file mode 100644
index 0000000..647ad935
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/reporting-observer/reporting-api.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<script src="../resources/testharness.js"></script>
+<script src="../resources/testharnessreport.js"></script>
+<script src="file:///gen/layout_test_data/mojo/public/js/mojo_bindings.js"></script>
+<script src="file:///gen/third_party/WebKit/public/platform/reporting.mojom.js"></script>
+<script>
+// Mock implementation of ReportingServiceProxy.
+// |promise| property is always a promise for the next report to be queued.
+class MockReportingServiceProxy {
+  constructor() {
+    this.bindingSet = new mojo.BindingSet(blink.mojom.ReportingServiceProxy);
+    this.resetPromise();
+  }
+
+  bind(handle) {
+    this.bindingSet.addBinding(this, handle);
+  }
+
+  resetPromise() {
+    this.promise = new Promise((resolve, reject) => {
+      this.resolve = resolve;
+    });
+  }
+
+  // Interface implementation.
+  async queueDeprecationReport(url, message, sourceFile, lineNumber) {
+    this.resolve([url, message, sourceFile, lineNumber]);
+    this.resetPromise();
+  }
+}
+
+// Make an instance and have it receive the request.
+var proxy = new MockReportingServiceProxy();
+var interceptor = new MojoInterfaceInterceptor(blink.mojom.ReportingServiceProxy.name);
+interceptor.oninterfacerequest = e => proxy.bind(e.handle);
+interceptor.start();
+
+promise_test(async () => {
+  let promise = proxy.promise;
+
+  // Use a deprecated feature.
+  window.webkitStorageInfo;
+
+  // Ensure the report is generated and routed to the mojo interface.
+  let [url, message, sourceFile, lineNumber] = await promise;
+  assert_true(url.url.endsWith("reporting-observer/reporting-api.html"));
+  assert_equals(typeof message, "string");
+  assert_true(sourceFile.endsWith("reporting-observer/reporting-api.html"));
+  assert_equals(typeof lineNumber, "number");
+}, "Deprecation report");
+</script>
diff --git a/third_party/WebKit/PRESUBMIT.py b/third_party/WebKit/PRESUBMIT.py
index 5cb3f74..03e199a 100644
--- a/third_party/WebKit/PRESUBMIT.py
+++ b/third_party/WebKit/PRESUBMIT.py
@@ -38,36 +38,6 @@
     return results
 
 
-def _CheckWatchlist(input_api, output_api):
-    """Check that the WATCHLIST file parses correctly."""
-    errors = []
-    for f in input_api.AffectedFiles():
-        if f.LocalPath() != 'WATCHLISTS':
-            continue
-        import StringIO
-        import logging
-        import watchlists
-
-        log_buffer = StringIO.StringIO()
-        log_handler = logging.StreamHandler(log_buffer)
-        log_handler.setFormatter(
-            logging.Formatter('%(levelname)s: %(message)s'))
-        logger = logging.getLogger()
-        logger.addHandler(log_handler)
-
-        wl = watchlists.Watchlists(input_api.change.RepositoryRoot())
-
-        logger.removeHandler(log_handler)
-        log_handler.flush()
-        log_buffer.flush()
-
-        if log_buffer.getvalue():
-            errors.append(output_api.PresubmitError(
-                'Cannot parse WATCHLISTS file, please resolve.',
-                log_buffer.getvalue().splitlines()))
-    return errors
-
-
 def _CommonChecks(input_api, output_api):
     """Checks common to both upload and commit."""
     # We should figure out what license checks we actually want to use.
@@ -78,29 +48,9 @@
         input_api, output_api, excluded_paths=_EXCLUDED_PATHS,
         maxlen=800, license_header=license_header))
     results.extend(_CheckForNonBlinkVariantMojomIncludes(input_api, output_api))
-    results.extend(_CheckTestExpectations(input_api, output_api))
-    results.extend(_CheckWatchlist(input_api, output_api))
     return results
 
 
-def _CheckTestExpectations(input_api, output_api):
-    local_paths = [f.LocalPath() for f in input_api.AffectedFiles()]
-    if any('LayoutTests' in path for path in local_paths):
-        lint_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
-            'Tools', 'Scripts', 'lint-test-expectations')
-        _, errs = input_api.subprocess.Popen(
-            [input_api.python_executable, lint_path],
-            stdout=input_api.subprocess.PIPE,
-            stderr=input_api.subprocess.PIPE).communicate()
-        if not errs:
-            return [output_api.PresubmitError(
-                "lint-test-expectations failed "
-                "to produce output; check by hand. ")]
-        if errs.strip() != 'Lint succeeded.':
-            return [output_api.PresubmitError(errs)]
-    return []
-
-
 def _CheckStyle(input_api, output_api):
     # Files that follow Chromium's coding style do not include capital letters.
     re_chromium_style_file = re.compile(r'\b[a-z_]+\.(cc|h)$')
@@ -148,51 +98,6 @@
     return []
 
 
-def _CheckForJSTest(input_api, output_api):
-    """'js-test.js' is the past, 'testharness.js' is our glorious future"""
-    jstest_re = input_api.re.compile(r'resources/js-test.js')
-
-    def source_file_filter(path):
-        return input_api.FilterSourceFile(path,
-                                          white_list=[r'third_party/WebKit/LayoutTests/.*\.(html|js|php|pl|svg)$'])
-
-    errors = input_api.canned_checks._FindNewViolationsOfRule(
-        lambda _, x: not jstest_re.search(x), input_api, source_file_filter)
-    errors = ['  * %s' % violation for violation in errors]
-    if errors:
-        return [output_api.PresubmitPromptOrNotify(
-            '"resources/js-test.js" is deprecated; please write new layout '
-            'tests using the assertions in "resources/testharness.js" '
-            'instead, as these can be more easily upstreamed to Web Platform '
-            'Tests for cross-vendor compatibility testing. If you\'re not '
-            'already familiar with this framework, a tutorial is available at '
-            'https://darobin.github.io/test-harness-tutorial/docs/using-testharness.html'
-            '\n\n%s' % '\n'.join(errors))]
-    return []
-
-def _CheckForFailInFile(input_api, f):
-    pattern = input_api.re.compile('^FAIL')
-    errors = []
-    for line_num, line in f.ChangedContents():
-        if pattern.match(line):
-            errors.append('    %s:%d %s' % (f.LocalPath(), line_num, line))
-    return errors
-
-
-def _CheckForInvalidPreferenceError(input_api, output_api):
-    pattern = input_api.re.compile('Invalid name for preference: (.+)')
-    results = []
-
-    for f in input_api.AffectedFiles():
-        if not f.LocalPath().endswith('-expected.txt'):
-            continue
-        for line_num, line in f.ChangedContents():
-            error = pattern.search(line)
-            if error:
-                results.append(output_api.PresubmitError('Found an invalid preference %s in expected result %s:%s' % (error.group(1), f, line_num)))
-    return results
-
-
 def _CheckForForbiddenNamespace(input_api, output_api):
     """Checks that Blink uses Chromium namespaces only in permitted code."""
     # This list is not exhaustive, but covers likely ones.
@@ -245,8 +150,6 @@
     results.extend(_CommonChecks(input_api, output_api))
     results.extend(_CheckStyle(input_api, output_api))
     results.extend(_CheckForPrintfDebugging(input_api, output_api))
-    results.extend(_CheckForJSTest(input_api, output_api))
-    results.extend(_CheckForInvalidPreferenceError(input_api, output_api))
     results.extend(_CheckForForbiddenNamespace(input_api, output_api))
     return results
 
diff --git a/third_party/WebKit/Source/core/frame/Deprecation.cpp b/third_party/WebKit/Source/core/frame/Deprecation.cpp
index 1c30a2e..6d0298d 100644
--- a/third_party/WebKit/Source/core/frame/Deprecation.cpp
+++ b/third_party/WebKit/Source/core/frame/Deprecation.cpp
@@ -9,6 +9,7 @@
 #include "core/frame/DeprecationReport.h"
 #include "core/frame/FrameConsole.h"
 #include "core/frame/LocalFrame.h"
+#include "core/frame/LocalFrameClient.h"
 #include "core/frame/Report.h"
 #include "core/frame/ReportingContext.h"
 #include "core/inspector/ConsoleMessage.h"
@@ -16,6 +17,8 @@
 #include "core/workers/WorkerOrWorkletGlobalScope.h"
 #include "platform/runtime_enabled_features.h"
 #include "public/platform/WebFeaturePolicyFeature.h"
+#include "public/platform/reporting.mojom-blink.h"
+#include "services/service_manager/public/cpp/interface_provider.h"
 
 namespace {
 
@@ -271,14 +274,22 @@
     return;
 
   Document* document = frame->GetDocument();
-  ReportingContext* reporting_context = ReportingContext::From(document);
-  if (!reporting_context->ObserverExists())
-    return;
 
-  // Send a deprecation report to any ReportingObservers.
-  ReportBody* body = new DeprecationReport(message, SourceLocation::Capture());
+  // Construct the deprecation report.
+  DeprecationReport* body =
+      new DeprecationReport(message, SourceLocation::Capture());
   Report* report = new Report("deprecation", document->Url().GetString(), body);
-  reporting_context->QueueReport(report);
+
+  // Send the deprecation report to any ReportingObservers.
+  ReportingContext* reporting_context = ReportingContext::From(document);
+  if (reporting_context->ObserverExists())
+    reporting_context->QueueReport(report);
+
+  // Send the deprecation report to the Reporting API.
+  mojom::blink::ReportingServiceProxyPtr service;
+  frame->Client()->GetInterfaceProvider()->GetInterface(&service);
+  service->QueueDeprecationReport(document->Url(), body->message(),
+                                  body->sourceFile(), body->lineNumber());
 }
 
 String Deprecation::DeprecationMessage(WebFeature feature) {
diff --git a/third_party/WebKit/Source/core/frame/DeprecationReport.h b/third_party/WebKit/Source/core/frame/DeprecationReport.h
index ee069e8..b51085ce 100644
--- a/third_party/WebKit/Source/core/frame/DeprecationReport.h
+++ b/third_party/WebKit/Source/core/frame/DeprecationReport.h
@@ -21,8 +21,10 @@
   ~DeprecationReport() override {}
 
   String message() const { return message_; }
-  String sourceFile() const { return location_->Url(); }
   long lineNumber() const { return location_->LineNumber(); }
+  String sourceFile() const {
+    return location_->Url().IsNull() ? "" : location_->Url();
+  }
 
   DEFINE_INLINE_VIRTUAL_TRACE() { ReportBody::Trace(visitor); }
 
diff --git a/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMapping.cpp b/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMapping.cpp
index af81ff14..5ae665d 100644
--- a/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMapping.cpp
+++ b/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMapping.cpp
@@ -1663,6 +1663,7 @@
     foreground_layer_->SetNeedsDisplay();
   }
   foreground_layer_->SetOffsetFromLayoutObject(foreground_offset);
+  foreground_layer_->SetShouldHitTest(true);
 
   // NOTE: there is some more configuring going on in
   // updateScrollingLayerGeometry().
diff --git a/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMappingTest.cpp b/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMappingTest.cpp
index ac50b10..f4d456d 100644
--- a/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMappingTest.cpp
+++ b/third_party/WebKit/Source/core/paint/compositing/CompositedLayerMappingTest.cpp
@@ -918,6 +918,9 @@
       static_cast<GraphicsLayerPaintingPhase>(
           kGraphicsLayerPaintForeground | kGraphicsLayerPaintOverflowContents),
       mapping->ForegroundLayer()->PaintingPhase());
+  // Regression test for crbug.com/767908: a foreground layer should also
+  // participates hit testing.
+  EXPECT_TRUE(mapping->ForegroundLayer()->GetShouldHitTestForTesting());
 
   Element* negative_composited_child =
       GetDocument().getElementById("negative-composited-child");
diff --git a/third_party/WebKit/Source/platform/graphics/GraphicsLayer.h b/third_party/WebKit/Source/platform/graphics/GraphicsLayer.h
index 6009278a..10c1ad5 100644
--- a/third_party/WebKit/Source/platform/graphics/GraphicsLayer.h
+++ b/third_party/WebKit/Source/platform/graphics/GraphicsLayer.h
@@ -183,6 +183,7 @@
   void SetIsRootForIsolatedGroup(bool);
 
   void SetShouldHitTest(bool);
+  bool GetShouldHitTestForTesting() { return should_hit_test_; }
 
   void SetFilters(CompositorFilterOperations);
   void SetBackdropFilters(CompositorFilterOperations);
diff --git a/third_party/blink/tools/move_blink_source.py b/third_party/blink/tools/move_blink_source.py
index 3a07e44..f8af13d 100755
--- a/third_party/blink/tools/move_blink_source.py
+++ b/third_party/blink/tools/move_blink_source.py
@@ -5,21 +5,8 @@
 
 """Tool to move Blink source from third_party/WebKit to third_party/blink.
 
-How to use:
-1. third_party/blink/tools/move_blink_source.py update --run
- (It would take a few minutes to complete this command.)
-2. git cl format
-3. git commit -a
-4. Land the commit
-
-5. third_party/blink/tools/move_blink_source.py move --git
- (It would take an hour to complete this command.)
-6. third_party/WebKit/Tools/Scripts/run-bindings-test --reset-results
-7. git commit -a
-8. Land the commit
-9. Pray for successful build!
-
-TODO(tkent): More automation.
+See https://docs.google.com/document/d/1l3aPv1Wx__SpRkdOhvJz8ciEGigNT3wFKv78XiuW0Tw/edit?usp=sharing#heading=h.o225wrxp242h
+for the details.
 """
 
 import argparse
@@ -157,7 +144,7 @@
               ('third_party/WebKit/Source', 'third_party/blink/renderer')]),
         ]
         for file_path, replacement_list in file_replacement_list:
-            self._update_single_file_content(file_path, replacement_list)
+            self._update_single_file_content(file_path, replacement_list, should_write=self._options.run)
 
     def move(self):
         _log.info('Planning renaming ...')
@@ -179,6 +166,10 @@
                 self._fs.move(self._fs.join(self._repo_root, src_from_repo),
                               self._fs.join(self._repo_root, dest_from_repo))
                 _log.info('[%d/%d] Moved %s', i + 1, len(file_pairs), src)
+        self._update_single_file_content(
+            'build/get_landmines.py',
+            [('\ndef main', '  print \'The Great Blink mv for source files (crbug.com/768828)\'\n\ndef main')])
+
 
     def _create_basename_maps(self, file_pairs):
         basename_map = {}
@@ -408,7 +399,7 @@
         return re.sub(r'#include\s+"(\w+\.h)"',
                       partial(self._replace_basename_only_include, subdir, source_path), content)
 
-    def _update_single_file_content(self, file_path, replace_list):
+    def _update_single_file_content(self, file_path, replace_list, should_write=True):
         full_path = self._fs.join(self._repo_root, file_path)
         original_content = self._fs.read_text_file(full_path)
         content = original_content
@@ -421,7 +412,7 @@
             else:
                 raise TypeError('A tuple or a function is expected.')
         if content != original_content:
-            if self._options.run:
+            if should_write:
                 self._fs.write_text_file(full_path, content)
             _log.info('Updated %s', file_path)
         else:
diff --git a/third_party/closure_compiler/externs/networking_private.js b/third_party/closure_compiler/externs/networking_private.js
index f6415091..5dd6aa6 100644
--- a/third_party/closure_compiler/externs/networking_private.js
+++ b/third_party/closure_compiler/externs/networking_private.js
@@ -678,6 +678,7 @@
  *   PaymentPortal: (!chrome.networkingPrivate.PaymentPortal|undefined),
  *   PRLVersion: (number|undefined),
  *   RoamingState: (string|undefined),
+ *   Scanning: (boolean|undefined),
  *   ServingOperator: (!chrome.networkingPrivate.CellularProviderProperties|undefined),
  *   SIMLockStatus: (!chrome.networkingPrivate.SIMLockStatus|undefined),
  *   SIMPresent: (boolean|undefined),
@@ -716,6 +717,7 @@
  *   PaymentPortal: (!chrome.networkingPrivate.PaymentPortal|undefined),
  *   PRLVersion: (number|undefined),
  *   RoamingState: (string|undefined),
+ *   Scanning: (boolean|undefined),
  *   ServingOperator: (!chrome.networkingPrivate.CellularProviderProperties|undefined),
  *   SIMLockStatus: (!chrome.networkingPrivate.SIMLockStatus|undefined),
  *   SIMPresent: (boolean|undefined),
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 5a09e2d70..50206b4 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -5757,6 +5757,8 @@
   <int value="8" label="FAILURE_SESSION_RATE_PARSE"/>
   <int value="9" label="FAILURE_AVAILABILITY_PARSE"/>
   <int value="10" label="FAILURE_UNKNOWN_KEY"/>
+  <int value="11" label="FAILURE_SESSION_RATE_IMPACT_PARSE"/>
+  <int value="12" label="FAILURE_SESSION_RATE_IMPACT_UNKNOWN_FEATURE"/>
 </enum>
 
 <enum name="ConnectionDiagnosticsIssue">
diff --git a/tools/perf/page_sets/data/key_desktop_move_cases.json b/tools/perf/page_sets/data/key_desktop_move_cases.json
index 40eafb5..ad5c0328 100644
--- a/tools/perf/page_sets/data/key_desktop_move_cases.json
+++ b/tools/perf/page_sets/data/key_desktop_move_cases.json
@@ -1,10 +1,10 @@
 {
     "archives": {
         "Maps": {
-            "DEFAULT": "key_desktop_move_cases_000.wpr"
+            "DEFAULT": "key_desktop_move_cases_000.wprgo"
         },
         "https://mail.google.com/mail/": {
-            "DEFAULT": "key_desktop_move_cases_002.wpr"
+            "DEFAULT": "key_desktop_move_cases_002.wprgo"
         }
     },
     "description": "Describes the Web Page Replay archives for a story set. Don't edit by hand! Use record_wpr for updating.",
diff --git a/tools/perf/page_sets/data/key_desktop_move_cases_000.wpr.sha1 b/tools/perf/page_sets/data/key_desktop_move_cases_000.wpr.sha1
deleted file mode 100644
index 79b4cb92..0000000
--- a/tools/perf/page_sets/data/key_desktop_move_cases_000.wpr.sha1
+++ /dev/null
@@ -1 +0,0 @@
-7982d4d8661f8e7bf33e4c4a68e41dd3b8dc7661
\ No newline at end of file
diff --git a/tools/perf/page_sets/data/key_desktop_move_cases_000.wprgo.sha1 b/tools/perf/page_sets/data/key_desktop_move_cases_000.wprgo.sha1
new file mode 100644
index 0000000..6223e1f
--- /dev/null
+++ b/tools/perf/page_sets/data/key_desktop_move_cases_000.wprgo.sha1
@@ -0,0 +1 @@
+26fe310f8a67bbbd91848cfbd260442285c91468
\ No newline at end of file
diff --git a/tools/perf/page_sets/data/key_desktop_move_cases_002.wpr.sha1 b/tools/perf/page_sets/data/key_desktop_move_cases_002.wpr.sha1
deleted file mode 100644
index 21eb8d3..0000000
--- a/tools/perf/page_sets/data/key_desktop_move_cases_002.wpr.sha1
+++ /dev/null
@@ -1 +0,0 @@
-2f03166ecdd325bacaf5ce2ab411d5f533d1b67a
\ No newline at end of file
diff --git a/tools/perf/page_sets/data/key_desktop_move_cases_002.wprgo.sha1 b/tools/perf/page_sets/data/key_desktop_move_cases_002.wprgo.sha1
new file mode 100644
index 0000000..3fa537cb
--- /dev/null
+++ b/tools/perf/page_sets/data/key_desktop_move_cases_002.wprgo.sha1
@@ -0,0 +1 @@
+ef581f08a71089465e81b6fe4bc72e6ddde33766
\ No newline at end of file
diff --git a/ui/accessibility/ax_tree.cc b/ui/accessibility/ax_tree.cc
index e7e9405..81584ab 100644
--- a/ui/accessibility/ax_tree.cc
+++ b/ui/accessibility/ax_tree.cc
@@ -166,7 +166,8 @@
 }
 
 gfx::RectF AXTree::RelativeToTreeBounds(const AXNode* node,
-                                        gfx::RectF bounds) const {
+                                        gfx::RectF bounds,
+                                        bool* offscreen) const {
   // If |bounds| is uninitialized, which is not the same as empty,
   // start with the node bounds.
   if (bounds.width() == 0 && bounds.height() == 0) {
@@ -175,12 +176,21 @@
     // If the node bounds is empty (either width or height is zero),
     // try to compute good bounds from the children.
     if (bounds.IsEmpty()) {
+      bool all_children_offscreen = true;
       for (size_t i = 0; i < node->children().size(); i++) {
         ui::AXNode* child = node->children()[i];
-        bounds.Union(GetTreeBounds(child));
+        bool temp_offscreen = false;
+        bounds.Union(GetTreeBounds(child, &temp_offscreen));
+        if (!temp_offscreen)
+          // At least one child is on screen.
+          all_children_offscreen = false;
       }
-      if (bounds.width() > 0 && bounds.height() > 0)
+      if (bounds.width() > 0 && bounds.height() > 0) {
+        // If all the children are offscreen, the node itself is offscreen.
+        if (offscreen != nullptr && all_children_offscreen)
+          *offscreen = true;
         return bounds;
+      }
     }
   } else {
     bounds.Offset(node->data().location.x(), node->data().location.y());
@@ -228,6 +238,8 @@
       if (!clipped.IsEmpty()) {
         // We can simply clip it to the container.
         bounds = clipped;
+        // No need to update |offscreen| if it is set, because it should be
+        // false by default.
       } else {
         // Totally offscreen. Find the nearest edge or corner.
         // Make the minimum dimension 1 instead of 0.
@@ -245,6 +257,8 @@
           bounds.set_y(0);
           bounds.set_height(1);
         }
+        if (offscreen != nullptr)
+          *offscreen |= true;
       }
     }
 
@@ -254,8 +268,8 @@
   return bounds;
 }
 
-gfx::RectF AXTree::GetTreeBounds(const AXNode* node) const {
-  return RelativeToTreeBounds(node, gfx::RectF());
+gfx::RectF AXTree::GetTreeBounds(const AXNode* node, bool* offscreen) const {
+  return RelativeToTreeBounds(node, gfx::RectF(), offscreen);
 }
 
 std::set<int32_t> AXTree::GetReverseRelations(AXIntAttribute attr,
diff --git a/ui/accessibility/ax_tree.h b/ui/accessibility/ax_tree.h
index 2a1f0f91..dda8f28 100644
--- a/ui/accessibility/ax_tree.h
+++ b/ui/accessibility/ax_tree.h
@@ -181,11 +181,20 @@
 
   // Convert any rectangle from the local coordinate space of one node in
   // the tree, to bounds in the coordinate space of the tree.
+  // If set, updates |offscreen| boolean to be true if the node is offscreen
+  // relative to its rootWebArea. Callers should initialize |offscreen|
+  // to false: this method may get called multiple times in a row and
+  // |offscreen| will be propagated.
   gfx::RectF RelativeToTreeBounds(const AXNode* node,
-                                  gfx::RectF node_bounds) const;
+                                  gfx::RectF node_bounds,
+                                  bool* offscreen = nullptr) const;
 
   // Get the bounds of a node in the coordinate space of the tree.
-  gfx::RectF GetTreeBounds(const AXNode* node) const;
+  // If set, updates |offscreen| boolean to be true if the node is offscreen
+  // relative to its rootWebArea. Callers should initialize |offscreen|
+  // to false: this method may get called multiple times in a row and
+  // |offscreen| will be propagated.
+  gfx::RectF GetTreeBounds(const AXNode* node, bool* offscreen = nullptr) const;
 
   // Given a node ID attribute (one where IsNodeIdIntAttribute is true),
   // and a destination node ID, return a set of all source node IDs that
diff --git a/ui/accessibility/ax_tree_unittest.cc b/ui/accessibility/ax_tree_unittest.cc
index 3430d85..8bf0d1f 100644
--- a/ui/accessibility/ax_tree_unittest.cc
+++ b/ui/accessibility/ax_tree_unittest.cc
@@ -44,6 +44,13 @@
                             bounds.y(), bounds.width(), bounds.height());
 }
 
+bool IsNodeOffscreen(const AXTree& tree, int32_t id) {
+  AXNode* node = tree.GetFromId(id);
+  bool result = false;
+  tree.GetTreeBounds(node, &result);
+  return result;
+}
+
 class FakeAXTreeDelegate : public AXTreeDelegate {
  public:
   FakeAXTreeDelegate()
@@ -827,6 +834,30 @@
   EXPECT_EQ("(100, 10) size (500 x 40)", GetBoundsAsString(tree, 2));
 }
 
+// If a node doesn't specify its location but at least one child does have
+// a location, it will be offscreen if all of its children are offscreen.
+TEST(AXTreeTest, EmptyNodeOffscreenWhenAllChildrenOffscreen) {
+  AXTreeUpdate tree_update;
+  tree_update.root_id = 1;
+  tree_update.nodes.resize(4);
+  tree_update.nodes[0].id = 1;
+  tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600);
+  tree_update.nodes[0].role = AX_ROLE_ROOT_WEB_AREA;
+  tree_update.nodes[0].child_ids.push_back(2);
+  tree_update.nodes[1].id = 2;
+  tree_update.nodes[1].location = gfx::RectF();  // Deliberately empty.
+  tree_update.nodes[1].child_ids.push_back(3);
+  tree_update.nodes[1].child_ids.push_back(4);
+  // Both children are offscreen
+  tree_update.nodes[2].id = 3;
+  tree_update.nodes[2].location = gfx::RectF(900, 10, 400, 20);
+  tree_update.nodes[3].id = 4;
+  tree_update.nodes[3].location = gfx::RectF(1000, 30, 400, 20);
+
+  AXTree tree(tree_update);
+  EXPECT_TRUE(IsNodeOffscreen(tree, 2));
+}
+
 // Test that getting the bounds of a node works when there's a transform.
 TEST(AXTreeTest, GetBoundsWithTransform) {
   AXTreeUpdate tree_update;
@@ -947,6 +978,37 @@
   EXPECT_EQ("(50, 599) size (150 x 1)", GetBoundsAsString(tree, 5));
 }
 
+TEST(AXTreeTest, GetBoundsUpdatesOffscreen) {
+  AXTreeUpdate tree_update;
+  tree_update.root_id = 1;
+  tree_update.nodes.resize(5);
+  tree_update.nodes[0].id = 1;
+  tree_update.nodes[0].location = gfx::RectF(0, 0, 800, 600);
+  tree_update.nodes[0].role = AX_ROLE_ROOT_WEB_AREA;
+  tree_update.nodes[0].child_ids.push_back(2);
+  tree_update.nodes[0].child_ids.push_back(3);
+  tree_update.nodes[0].child_ids.push_back(4);
+  tree_update.nodes[0].child_ids.push_back(5);
+  // Fully onscreen
+  tree_update.nodes[1].id = 2;
+  tree_update.nodes[1].location = gfx::RectF(10, 10, 150, 150);
+  // Cropped in the bottom right
+  tree_update.nodes[2].id = 3;
+  tree_update.nodes[2].location = gfx::RectF(700, 500, 150, 150);
+  // Offscreen on the top
+  tree_update.nodes[3].id = 4;
+  tree_update.nodes[3].location = gfx::RectF(50, -200, 150, 150);
+  // Offscreen on the bottom
+  tree_update.nodes[4].id = 5;
+  tree_update.nodes[4].location = gfx::RectF(50, 700, 150, 150);
+
+  AXTree tree(tree_update);
+  EXPECT_FALSE(IsNodeOffscreen(tree, 2));
+  EXPECT_FALSE(IsNodeOffscreen(tree, 3));
+  EXPECT_TRUE(IsNodeOffscreen(tree, 4));
+  EXPECT_TRUE(IsNodeOffscreen(tree, 5));
+}
+
 TEST(AXTreeTest, IntReverseRelations) {
   AXTreeUpdate initial_state;
   initial_state.root_id = 1;
diff --git a/ui/views/mus/aura_init.cc b/ui/views/mus/aura_init.cc
index 765ef0e..146c6f244 100644
--- a/ui/views/mus/aura_init.cc
+++ b/ui/views/mus/aura_init.cc
@@ -76,10 +76,11 @@
     const std::string& resource_file,
     const std::string& resource_file_200,
     scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
-    Mode mode) {
+    Mode mode,
+    bool register_path_provider) {
   std::unique_ptr<AuraInit> aura_init = base::WrapUnique(new AuraInit());
   if (!aura_init->Init(connector, identity, resource_file, resource_file_200,
-                       io_task_runner, mode)) {
+                       io_task_runner, mode, register_path_provider)) {
     aura_init.reset();
   }
   return aura_init;
@@ -90,7 +91,8 @@
                     const std::string& resource_file,
                     const std::string& resource_file_200,
                     scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
-                    Mode mode) {
+                    Mode mode,
+                    bool register_path_provider) {
   env_ = aura::Env::CreateInstance(
       (mode == Mode::AURA_MUS || mode == Mode::AURA_MUS_WINDOW_MANAGER)
           ? aura::Env::Mode::MUS
@@ -101,8 +103,10 @@
         base::WrapUnique(new MusClient(connector, identity, io_task_runner));
   }
   ui::MaterialDesignController::Initialize();
-  if (!InitializeResources(connector, resource_file, resource_file_200))
+  if (!InitializeResources(connector, resource_file, resource_file_200,
+                           register_path_provider)) {
     return false;
+  }
 
 // Initialize the skia font code to go ask fontconfig underneath.
 #if defined(OS_LINUX)
@@ -124,7 +128,8 @@
 
 bool AuraInit::InitializeResources(service_manager::Connector* connector,
                                    const std::string& resource_file,
-                                   const std::string& resource_file_200) {
+                                   const std::string& resource_file_200,
+                                   bool register_path_provider) {
   // Resources may have already been initialized (e.g. when 'chrome --mash' is
   // used to launch the current app).
   if (ui::ResourceBundle::HasSharedInstance())
@@ -146,7 +151,8 @@
   // Calling services will shutdown ServiceContext as appropriate.
   if (!loader.OpenFiles(std::move(directory), resource_paths))
     return false;
-  ui::RegisterPathProvider();
+  if (register_path_provider)
+    ui::RegisterPathProvider();
   base::File pak_file = loader.TakeFile(resource_file);
   base::File pak_file_2 = pak_file.Duplicate();
   ui::ResourceBundle::InitSharedInstanceWithPakFileRegion(
diff --git a/ui/views/mus/aura_init.h b/ui/views/mus/aura_init.h
index 2ba6fd2..68fa0fb 100644
--- a/ui/views/mus/aura_init.h
+++ b/ui/views/mus/aura_init.h
@@ -65,7 +65,8 @@
       const std::string& resource_file,
       const std::string& resource_file_200 = std::string(),
       scoped_refptr<base::SingleThreadTaskRunner> io_task_runner = nullptr,
-      Mode mode = Mode::UI);
+      Mode mode = Mode::UI,
+      bool register_path_provider = true);
 
   // Only valid if Mode::AURA_MUS was passed to constructor.
   MusClient* mus_client() { return mus_client_.get(); }
@@ -76,17 +77,18 @@
   // Returns true if AuraInit was able to successfully complete initialization.
   // If this returns false, then Aura is in an unusable state, and calling
   // services should shutdown.
-  bool Init(
-      service_manager::Connector* connector,
-      const service_manager::Identity& identity,
-      const std::string& resource_file,
-      const std::string& resource_file_200 = std::string(),
-      scoped_refptr<base::SingleThreadTaskRunner> io_task_runner = nullptr,
-      Mode mode = Mode::UI);
+  bool Init(service_manager::Connector* connector,
+            const service_manager::Identity& identity,
+            const std::string& resource_file,
+            const std::string& resource_file_200,
+            scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
+            Mode mode,
+            bool register_path_provider);
 
   bool InitializeResources(service_manager::Connector* connector,
                            const std::string& resource_file,
-                           const std::string& resource_file_200);
+                           const std::string& resource_file_200,
+                           bool register_path_provider);
 
 #if defined(OS_LINUX)
   sk_sp<font_service::FontLoader> font_loader_;
diff --git a/ui/webui/resources/cr_elements/chromeos/network/cr_onc_types.js b/ui/webui/resources/cr_elements/chromeos/network/cr_onc_types.js
index 4c517967..d93e0d6 100644
--- a/ui/webui/resources/cr_elements/chromeos/network/cr_onc_types.js
+++ b/ui/webui/resources/cr_elements/chromeos/network/cr_onc_types.js
@@ -120,6 +120,14 @@
 CrOnc.ProxySettingsType = chrome.networkingPrivate.ProxySettingsType;
 
 /** @enum {string} */
+CrOnc.VPNType = {
+  L2TP_IPSEC: 'L2TP-IPsec',
+  OPEN_VPN: 'OpenVPN',
+  THIRD_PARTY_VPN: 'ThirdPartyVPN',
+  ARCVPN: 'ARCVPN',
+};
+
+/** @enum {string} */
 CrOnc.Type = chrome.networkingPrivate.NetworkType;
 
 /** @enum {string} */
diff --git a/ui/webui/resources/cr_elements/cr_link_row/cr_link_row.html b/ui/webui/resources/cr_elements/cr_link_row/cr_link_row.html
index 8babeb5d..2fa42fb 100644
--- a/ui/webui/resources/cr_elements/cr_link_row/cr_link_row.html
+++ b/ui/webui/resources/cr_elements/cr_link_row/cr_link_row.html
@@ -48,6 +48,7 @@
       #labelWrapper {
         flex: 1;
         flex-basis: 0.000000001px;
+        text-align: start;
       }
 
       #outer {