diff --git a/DEPS b/DEPS
index 61965c4b..281e840 100644
--- a/DEPS
+++ b/DEPS
@@ -44,7 +44,7 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling V8
   # and whatever else without interference from each other.
-  'v8_revision': 'ba69da40dc9aade163504a99f088252c711b4bf4',
+  'v8_revision': 'c59a968ba3e44a7c7bd7359efcfb636a373e9339',
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling swarming_client
   # and whatever else without interference from each other.
@@ -202,7 +202,7 @@
     Var('chromium_git') + '/external/bidichecker/lib.git' + '@' + '97f2aa645b74c28c57eca56992235c79850fa9e0',
 
   'src/third_party/webgl/src':
-    Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '72eda82d069da578af04e5c4e8e411ae006b6a18',
+    Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + 'ca7121e53f7794a60ac2948c34c224ee921a8910',
 
   'src/third_party/webdriver/pylib':
     Var('chromium_git') + '/external/selenium/py.git' + '@' + '5fd78261a75fe08d27ca4835fb6c5ce4b42275bd',
diff --git a/ash/wm/lock_state_controller.cc b/ash/wm/lock_state_controller.cc
index f2382820..e781b23 100644
--- a/ash/wm/lock_state_controller.cc
+++ b/ash/wm/lock_state_controller.cc
@@ -496,9 +496,8 @@
 
   base::TimeDelta timeout =
       base::TimeDelta::FromMilliseconds(kLockFailTimeoutMs);
-  // Increase lock timeout for "daisy", see http://crbug.com/350628. (The boards
-  // "daisy_spring" and "daisy_skate" are fast thus using an exact string match
-  // instead of base::StartsWith().)
+  // TODO(derat): Remove this scaling after October 2017 when daisy (Samsung
+  // Chromebook XE303) is unsupported.
   if (base::SysInfo::GetStrippedReleaseBoard() == "daisy")
     timeout *= 2;
   lock_fail_timer_.Start(FROM_HERE, timeout, this,
diff --git a/base/metrics/histogram.cc b/base/metrics/histogram.cc
index 7780df528..7c2a6c2 100644
--- a/base/metrics/histogram.cc
+++ b/base/metrics/histogram.cc
@@ -39,6 +39,15 @@
 
 namespace {
 
+// A constant to be stored in the dummy field and later verified. This could
+// be either 32 or 64 bit but clang won't truncate the value without an error.
+// TODO(bcwhite): Remove this once crbug/736675 is fixed.
+#if defined(ARCH_CPU_64_BITS) && !defined(OS_NACL)
+constexpr uintptr_t kDummyValue = 0xFEEDC0DEDEADBEEF;
+#else
+constexpr uintptr_t kDummyValue = 0xDEADBEEF;
+#endif
+
 // TODO(asvitkine): Remove this after crbug/736675.
 char g_last_logged_histogram_name[256] = {0};
 
@@ -582,7 +591,7 @@
   // Temporary for https://crbug.com/736675.
   base::debug::ScopedCrashKey crash_key("bad_histogram", debug_string);
 #endif
-  CHECK(false) << debug_string;
+  // CHECK(false) << debug_string;
   debug::Alias(&bad_fields);
   return false;
 }
@@ -602,7 +611,7 @@
                      Sample minimum,
                      Sample maximum,
                      const BucketRanges* ranges)
-    : HistogramBase(name) {
+    : HistogramBase(name), dummy_(kDummyValue) {
   // TODO(bcwhite): Make this a DCHECK once crbug/734049 is resolved.
   CHECK(ranges) << name << ": " << minimum << "-" << maximum;
   unlogged_samples_.reset(new SampleVector(HashMetricName(name), ranges));
@@ -617,7 +626,7 @@
                      const DelayedPersistentAllocation& logged_counts,
                      HistogramSamples::Metadata* meta,
                      HistogramSamples::Metadata* logged_meta)
-    : HistogramBase(name) {
+    : HistogramBase(name), dummy_(kDummyValue) {
   // TODO(bcwhite): Make this a DCHECK once crbug/734049 is resolved.
   CHECK(ranges) << name << ": " << minimum << "-" << maximum;
   unlogged_samples_.reset(
diff --git a/base/metrics/histogram.h b/base/metrics/histogram.h
index f0aa7296..9515d0c 100644
--- a/base/metrics/histogram.h
+++ b/base/metrics/histogram.h
@@ -311,6 +311,13 @@
                              int64_t* sum,
                              ListValue* buckets) const override;
 
+  // This is a dummy field placed where corruption is frequently seen on
+  // current Android builds. The hope is that it will mitigate the problem
+  // sufficiently to continue with the M61 beta branch while investigation
+  // into the true problem continues.
+  // TODO(bcwhite): Remove this once crbug/736675 is fixed.
+  const uintptr_t dummy_;
+
   // Samples that have not yet been logged with SnapshotDelta().
   std::unique_ptr<SampleVectorBase> unlogged_samples_;
 
diff --git a/base/metrics/histogram_snapshot_manager.cc b/base/metrics/histogram_snapshot_manager.cc
index b38433b2..260fde9 100644
--- a/base/metrics/histogram_snapshot_manager.cc
+++ b/base/metrics/histogram_snapshot_manager.cc
@@ -45,13 +45,15 @@
 }
 
 void HistogramSnapshotManager::PrepareDelta(HistogramBase* histogram) {
-  histogram->ValidateHistogramContents(true, 0);
+  if (!histogram->ValidateHistogramContents(true, 0))
+    return;
   PrepareSamples(histogram, histogram->SnapshotDelta());
 }
 
 void HistogramSnapshotManager::PrepareFinalDelta(
     const HistogramBase* histogram) {
-  histogram->ValidateHistogramContents(true, 0);
+  if (!histogram->ValidateHistogramContents(true, 0))
+    return;
   PrepareSamples(histogram, histogram->SnapshotFinalDelta());
 }
 
diff --git a/base/sys_info.h b/base/sys_info.h
index 5f68df4..d435d58 100644
--- a/base/sys_info.h
+++ b/base/sys_info.h
@@ -116,17 +116,25 @@
   static bool GetLsbReleaseValue(const std::string& key, std::string* value);
 
   // Convenience function for GetLsbReleaseValue("CHROMEOS_RELEASE_BOARD",...).
-  // Returns "unknown" if CHROMEOS_RELEASE_BOARD is not set. Otherwise returns
-  // the full name of the board. WARNING: the returned value often differs in
-  // developer built system compared to devices that use the official version.
-  // E.g. for developer built version, the function could return 'glimmer' while
-  // for officially used versions it would be like 'glimmer-signed-mp-v4keys'.
-  // Use GetStrippedReleaseBoard() function if you need only the short name of
-  // the board (would be 'glimmer' in the case described above).
+  // Returns "unknown" if CHROMEOS_RELEASE_BOARD is not set. Otherwise, returns
+  // the full name of the board. Note that the returned value often differs
+  // between developers' systems and devices that use official builds. E.g. for
+  // a developer-built image, the function could return 'glimmer', while in an
+  // official build, it may be something like 'glimmer-signed-mp-v4keys'.
+  //
+  // NOTE: Strings returned by this function should be treated as opaque values
+  // within Chrome (e.g. for reporting metrics elsewhere). If you need to make
+  // Chrome behave differently for different Chrome OS devices, either directly
+  // check for the hardware feature that you care about (preferred) or add a
+  // command-line flag to Chrome and pass it from session_manager (based on
+  // whether a USE flag is set or not). See https://goo.gl/BbBkzg for more
+  // details.
   static std::string GetLsbReleaseBoard();
 
+  // DEPRECATED: Please see GetLsbReleaseBoard's comment.
   // Convenience function for GetLsbReleaseBoard() removing trailing "-signed-*"
   // if present. Returns "unknown" if CHROMEOS_RELEASE_BOARD is not set.
+  // TODO(derat): Delete this after October 2017.
   static std::string GetStrippedReleaseBoard();
 
   // Returns the creation time of /etc/lsb-release. (Used to get the date and
diff --git a/build/android/apk_operations.py b/build/android/apk_operations.py
index 0e37159d..2f695f4 100644
--- a/build/android/apk_operations.py
+++ b/build/android/apk_operations.py
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 import argparse
-import imp
+import json
 import logging
 import os
 import pipes
@@ -18,44 +18,26 @@
 from devil.android.sdk import adb_wrapper
 from devil.utils import run_tests_helper
 
+from incremental_install import installer
 from pylib import constants
 
 
-def _InstallApk(install_incremental, inc_install_script, devices_obj,
-                apk_to_install):
-  if install_incremental:
-    helper = apk_helper.ApkHelper(apk_to_install)
-    try:
-      install_wrapper = imp.load_source('install_wrapper', inc_install_script)
-    except IOError:
-      raise Exception('Incremental install script not found: %s\n' %
-                      inc_install_script)
-    params = install_wrapper.GetInstallParameters()
-
-    def install_incremental_apk(device):
-      from incremental_install import installer
-      installer.Install(device, helper, split_globs=params['splits'],
-                        native_libs=params['native_libs'],
-                        dex_files=params['dex_files'], permissions=None)
-    devices_obj.pMap(install_incremental_apk)
-  else:
-    # Install the regular apk on devices.
-    def install(device):
-      device.Install(apk_to_install)
-    devices_obj.pMap(install)
+def _InstallApk(apk, install_dict, devices_obj):
+  def install(device):
+    if install_dict:
+      installer.Install(device, install_dict, apk=apk)
+    else:
+      device.Install(apk)
+  devices_obj.pMap(install)
 
 
-def _UninstallApk(install_incremental, devices_obj, apk_package):
-  if install_incremental:
-    def uninstall_incremental_apk(device):
-      from incremental_install import installer
+def _UninstallApk(install_dict, devices_obj, apk_package):
+  def uninstall(device):
+    if install_dict:
       installer.Uninstall(device, apk_package)
-    devices_obj.pMap(uninstall_incremental_apk)
-  else:
-    # Uninstall the regular apk on devices.
-    def uninstall(device):
+    else:
       device.Uninstall(apk_package)
-    devices_obj.pMap(uninstall)
+  devices_obj.pMap(uninstall)
 
 
 def _LaunchUrl(devices_obj, input_args, device_args_file, url, apk_package):
@@ -160,8 +142,60 @@
   return os.path.join(constants.GetOutDirectory(), file_name)
 
 
-def Run(output_directory, apk_path, inc_apk_path, inc_install_script,
-         command_line_flags_file):
+def _SelectApk(apk_path, incremental_install_json_path, parser, args):
+  if apk_path and not os.path.exists(apk_path):
+    apk_path = None
+  if (incremental_install_json_path and
+      not os.path.exists(incremental_install_json_path)):
+    incremental_install_json_path = None
+
+  if args.incremental and args.non_incremental:
+    parser.error('--incremental and --non-incremental cannot both be used.')
+  elif args.non_incremental:
+    if not apk_path:
+      parser.error('Apk has not been built.')
+    incremental_install_json_path = None
+  elif args.incremental:
+    if not incremental_install_json_path:
+      parser.error('Incremental apk has not been built.')
+    apk_path = None
+
+  if args.command in ('install', 'run'):
+    if apk_path and incremental_install_json_path:
+      parser.error('Both incremental and non-incremental apks exist, please '
+                   'use --incremental or --non-incremental to select one.')
+    elif apk_path:
+      logging.info('Using the non-incremental apk.')
+    elif incremental_install_json_path:
+      logging.info('Using the incremental apk.')
+    else:
+      parser.error('Neither incremental nor non-incremental apk is built.')
+  return apk_path, incremental_install_json_path
+
+
+def _LoadDeviceCaches(devices):
+  for d in devices:
+    cache_path = _DeviceCachePath(d)
+    if os.path.exists(cache_path):
+      logging.info('Using device cache: %s', cache_path)
+      with open(cache_path) as f:
+        d.LoadCacheData(f.read())
+      # Delete the cached file so that any exceptions cause it to be cleared.
+      os.unlink(cache_path)
+    else:
+      logging.info('No cache present for device: %s', d)
+
+
+def _SaveDeviceCaches(devices):
+  for d in devices:
+    cache_path = _DeviceCachePath(d)
+    with open(cache_path, 'w') as f:
+      f.write(d.DumpCacheData())
+      logging.info('Wrote device cache: %s', cache_path)
+
+
+def Run(output_directory, apk_path, incremental_install_json_path,
+        command_line_flags_file):
   constants.SetOutputDirectory(output_directory)
 
   parser = argparse.ArgumentParser()
@@ -229,79 +263,33 @@
   if len(devices) > 1 and not args.all:
     raise Exception(_GenerateMissingAllFlagMessage(devices, devices_obj))
 
-  if args.incremental and args.non_incremental:
-    raise Exception('--incremental and --non-incremental cannot be set at the '
-                    'same time.')
-  install_incremental = False
-  active_apk = None
-  apk_package = None
   apk_name = os.path.basename(apk_path)
-  if apk_path and not os.path.exists(apk_path):
-    apk_path = None
+  apk_path, incremental_install_json_path = _SelectApk(
+      apk_path, incremental_install_json_path, parser, args)
+  install_dict = None
 
-  if args.non_incremental:
-    if apk_path:
-      active_apk = apk_path
-      logging.info('Use the non-incremental apk.')
-    else:
-      raise Exception("No regular apk is available.")
+  if incremental_install_json_path:
+    with open(incremental_install_json_path) as f:
+      install_dict = json.load(f)
+    apk = apk_helper.ToHelper(
+        os.path.join(output_directory, install_dict['apk_path']))
+  else:
+    apk = apk_helper.ToHelper(apk_path)
 
-  if inc_apk_path and not os.path.exists(inc_apk_path):
-    inc_apk_path = None
-
-  if args.incremental:
-    if inc_apk_path:
-      active_apk = inc_apk_path
-      install_incremental = True
-      logging.info('Use the incremental apk.')
-    else:
-      raise Exception("No incremental apk is available.")
-
-  if not args.incremental and not args.non_incremental and command in {
-      'install', 'run'}:
-    if apk_path and inc_apk_path:
-      raise Exception('Both incremental and non-incremental apks exist, please '
-                      'use --incremental or --non-incremental to select one.')
-    if not apk_path and not inc_apk_path:
-      raise Exception('Neither incremental nor non-incremental apk is '
-                      'available.')
-    if apk_path:
-      active_apk = apk_path
-      logging.info('Use the non-incremental apk.')
-    else:
-      active_apk = inc_apk_path
-      install_incremental = True
-      logging.info('Use the incremental apk.')
-
-  if apk_path is not None:
-    apk_package = apk_helper.GetPackageName(apk_path)
-  elif inc_apk_path is not None:
-    apk_package = apk_helper.GetPackageName(inc_apk_path)
+  apk_package = apk.GetPackageName()
 
   if use_cache:
-    for d in devices:
-      cache_path = _DeviceCachePath(d)
-      if os.path.exists(cache_path):
-        logging.info('Using device cache: %s', cache_path)
-        with open(cache_path) as f:
-          d.LoadCacheData(f.read())
-        # Delete the cached file so that any exceptions cause it to be cleared.
-        os.unlink(cache_path)
-      else:
-        logging.info('No cache present for device: %s', d)
+    _LoadDeviceCaches(devices)
 
   if command == 'install':
-    _InstallApk(install_incremental, inc_install_script, devices_obj,
-                active_apk)
+    _InstallApk(apk, install_dict, devices_obj)
   elif command == 'uninstall':
-    _UninstallApk(install_incremental, devices_obj, apk_package)
+    _UninstallApk(install_dict, devices_obj, apk_package)
   elif command == 'launch':
     _LaunchUrl(devices_obj, args.args, command_line_flags_file,
                args.url, apk_package)
   elif command == 'run':
-    _InstallApk(install_incremental, inc_install_script, devices_obj,
-                active_apk)
-    devices_obj.pFinish(None)
+    _InstallApk(apk, install_dict, devices_obj)
     _LaunchUrl(devices_obj, args.args, command_line_flags_file,
                args.url, apk_package)
   elif command == 'stop':
@@ -336,13 +324,6 @@
     flags = [adb_path, '-s', devices[0].adb.GetDeviceSerial(), 'logcat']
     os.execv(adb_path, flags)
 
-  # Wait for all threads to finish.
-  devices_obj.pFinish(None)
-
   # Save back to the cache.
   if use_cache:
-    for d in devices:
-      cache_path = _DeviceCachePath(d)
-      with open(cache_path, 'w') as f:
-        f.write(d.DumpCacheData())
-        logging.info('Wrote device cache: %s', cache_path)
+    _SaveDeviceCaches(devices)
diff --git a/build/android/gyp/create_apk_operations_script.py b/build/android/gyp/create_apk_operations_script.py
index 4d426f42..dddeb7a 100755
--- a/build/android/gyp/create_apk_operations_script.py
+++ b/build/android/gyp/create_apk_operations_script.py
@@ -24,10 +24,9 @@
       script_directory, p))
   sys.path.append(resolve(${APK_OPERATIONS_DIR}))
   import apk_operations
-  apk_operations.Run(output_directory=resolve(${OUTPUT_DIR}),
-                     apk_path=resolve(${APK_PATH}),
-                     inc_apk_path=resolve(${INC_APK_PATH}),
-                     inc_install_script=resolve(${INC_INSTALL_SCRIPT}),
+  apk_operations.Run(resolve(${OUTPUT_DIR}),
+                     resolve(${APK_PATH}),
+                     resolve(${INC_JSON_PATH}),
                      command_line_flags_file=${FLAGS_FILE})
 
 
@@ -41,8 +40,7 @@
   parser.add_argument('--script-output-path',
                       help='Output path for executable script.')
   parser.add_argument('--apk-path')
-  parser.add_argument('--incremental-apk-path')
-  parser.add_argument('--incremental-install-script')
+  parser.add_argument('--incremental-install-json-path')
   parser.add_argument('--command-line-flags-file')
   args = parser.parse_args(args)
 
@@ -59,8 +57,7 @@
         'APK_OPERATIONS_DIR': repr(apk_operations_dir),
         'OUTPUT_DIR': repr(relativize('.')),
         'APK_PATH': repr(relativize(args.apk_path)),
-        'INC_APK_PATH': repr(relativize(args.incremental_apk_path)),
-        'INC_INSTALL_SCRIPT': repr(relativize(args.incremental_install_script)),
+        'INC_JSON_PATH': repr(relativize(args.incremental_install_json_path)),
         'FLAGS_FILE': repr(args.command_line_flags_file),
     }
     script.write(SCRIPT_TEMPLATE.substitute(script_dict))
diff --git a/build/android/gyp/write_build_config.py b/build/android/gyp/write_build_config.py
index 63a8ee8..f3617ae 100755
--- a/build/android/gyp/write_build_config.py
+++ b/build/android/gyp/write_build_config.py
@@ -312,9 +312,9 @@
   parser.add_option('--apk-path', help='Path to the target\'s apk output.')
   parser.add_option('--incremental-apk-path',
                     help="Path to the target's incremental apk output.")
-  parser.add_option('--incremental-install-script-path',
+  parser.add_option('--incremental-install-json-path',
                     help="Path to the target's generated incremental install "
-                    "script.")
+                    "json.")
 
   parser.add_option('--tested-apk-config',
       help='Path to the build config of the tested apk (for an instrumentation '
@@ -472,8 +472,8 @@
     if options.type == 'android_apk':
       deps_info['apk_path'] = options.apk_path
       deps_info['incremental_apk_path'] = options.incremental_apk_path
-      deps_info['incremental_install_script_path'] = (
-          options.incremental_install_script_path)
+      deps_info['incremental_install_json_path'] = (
+          options.incremental_install_json_path)
       deps_info['enable_relocation_packing'] = options.enable_relocation_packing
 
   if options.type in ('java_binary', 'java_library', 'android_apk', 'dist_jar'):
diff --git a/build/android/incremental_install/create_install_script.py b/build/android/incremental_install/create_install_script.py
deleted file mode 100755
index 09004da..0000000
--- a/build/android/incremental_install/create_install_script.py
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/env python
-
-# 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.
-
-"""Creates a script to run an "_incremental" .apk."""
-
-import argparse
-import os
-import pprint
-import sys
-
-sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir))
-sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
-
-from pylib.constants import host_paths
-from util import build_utils
-
-
-SCRIPT_TEMPLATE = """\
-#!/usr/bin/env python
-#
-# This file was generated by:
-#     //build/android/incremental_install/create_install_script.py
-
-import os
-import subprocess
-import sys
-
-
-def _ResolvePath(path):
-  script_directory = os.path.dirname(__file__)
-  return os.path.abspath(os.path.join(script_directory, path))
-
-
-# Exported to allow test runner to be able to install incremental apks.
-def GetInstallParameters():
-  apk_path = {apk_path}
-  dex_files = {dex_files}
-  dont_even_try = {dont_even_try}
-  native_libs = {native_libs}
-  show_proguard_warning = {show_proguard_warning}
-  splits = {splits}
-
-  return dict(apk_path=_ResolvePath(apk_path),
-              dex_files=[_ResolvePath(p) for p in dex_files],
-              dont_even_try=dont_even_try,
-              native_libs=[_ResolvePath(p) for p in native_libs],
-              show_proguard_warning=show_proguard_warning,
-              splits=[_ResolvePath(p) for p in splits])
-
-
-def main():
-  output_directory = {output_directory}
-  cmd_path = {cmd_path}
-  params = GetInstallParameters()
-  cmd_args = [
-      _ResolvePath(cmd_path),
-      '--output-directory', _ResolvePath(output_directory),
-  ]
-  for native_lib in params['native_libs']:
-    cmd_args.extend(('--native_lib', native_lib))
-  for dex_path in params['dex_files']:
-    cmd_args.extend(('--dex-file', dex_path))
-  for split in params['splits']:
-    cmd_args.extend(('--split', split))
-  cmd_args.append(params['apk_path'])
-  if params['dont_even_try']:
-    cmd_args.extend(('--dont-even-try', params['dont_even_try']))
-  if params['show_proguard_warning']:
-    cmd_args.append('--show-proguard-warning')
-  return subprocess.call(cmd_args + sys.argv[1:])
-
-if __name__ == '__main__':
-  sys.exit(main())
-"""
-
-
-def _ParseArgs(args):
-  args = build_utils.ExpandFileArgs(args)
-  parser = argparse.ArgumentParser()
-  build_utils.AddDepfileOption(parser)
-  parser.add_argument('--script-output-path',
-                      help='Output path for executable script.',
-                      required=True)
-  parser.add_argument('--output-directory',
-                      help='Path to the root build directory.',
-                      default='.')
-  parser.add_argument('--apk-path',
-                      help='Path to the .apk to install.',
-                      required=True)
-  parser.add_argument('--split',
-                      action='append',
-                      dest='splits',
-                      default=[],
-                      help='A glob matching the apk splits. '
-                           'Can be specified multiple times.')
-  parser.add_argument('--native-libs',
-                      action='append',
-                      default=[],
-                      help='GYP-list of paths to native libraries. Can be '
-                      'repeated.')
-  parser.add_argument('--dex-file',
-                      action='append',
-                      default=[],
-                      dest='dex_files',
-                      help='List of dex files to include.')
-  parser.add_argument('--dex-file-list',
-                      help='GYP-list of dex files.')
-  parser.add_argument('--show-proguard-warning',
-                      action='store_true',
-                      default=False,
-                      help='Print a warning about proguard being disabled')
-  parser.add_argument('--dont-even-try',
-                      help='Prints this message and exits.')
-
-  options = parser.parse_args(args)
-  options.dex_files += build_utils.ParseGnList(options.dex_file_list)
-  all_libs = []
-  for gyp_list in options.native_libs:
-    all_libs.extend(build_utils.ParseGnList(gyp_list))
-  options.native_libs = all_libs
-  return options
-
-
-def main(args):
-  options = _ParseArgs(args)
-
-  def relativize(path):
-    script_dir = os.path.dirname(options.script_output_path)
-    return path and os.path.relpath(path, script_dir)
-
-  installer_path = os.path.join(host_paths.DIR_SOURCE_ROOT, 'build', 'android',
-                                'incremental_install', 'installer.py')
-  pformat = pprint.pformat
-  template_args = {
-      'cmd_path': pformat(relativize(installer_path)),
-      'apk_path': pformat(relativize(options.apk_path)),
-      'output_directory': pformat(relativize(options.output_directory)),
-      'native_libs': pformat([relativize(p) for p in options.native_libs]),
-      'dex_files': pformat([relativize(p) for p in options.dex_files]),
-      'dont_even_try': pformat(options.dont_even_try),
-      'show_proguard_warning': pformat(options.show_proguard_warning),
-      'splits': pformat([relativize(p) for p in options.splits]),
-  }
-
-  with open(options.script_output_path, 'w') as script:
-    script.write(SCRIPT_TEMPLATE.format(**template_args))
-
-  os.chmod(options.script_output_path, 0750)
-
-  if options.depfile:
-    build_utils.WriteDepfile(options.depfile, options.script_output_path)
-
-
-if __name__ == '__main__':
-  sys.exit(main(sys.argv[1:]))
diff --git a/build/android/incremental_install/installer.py b/build/android/incremental_install/installer.py
index 54abf76..92f86af 100755
--- a/build/android/incremental_install/installer.py
+++ b/build/android/incremental_install/installer.py
@@ -8,6 +8,7 @@
 
 import argparse
 import glob
+import json
 import logging
 import os
 import posixpath
@@ -83,31 +84,43 @@
   logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
 
 
-def Install(device, apk, split_globs=None, native_libs=None, dex_files=None,
-            enable_device_cache=False, use_concurrency=True,
-            show_proguard_warning=False, permissions=(),
-            allow_downgrade=True):
+def Install(device, install_json, apk=None, enable_device_cache=False,
+            use_concurrency=True, permissions=()):
   """Installs the given incremental apk and all required supporting files.
 
   Args:
-    device: A DeviceUtils instance.
-    apk: The path to the apk, or an ApkHelper instance.
-    split_globs: Glob patterns for any required apk splits (optional).
-    native_libs: List of app's native libraries (optional).
-    dex_files: List of .dex.jar files that comprise the app's Dalvik code.
+    device: A DeviceUtils instance (to install to).
+    install_json: Path to .json file or already parsed .json object.
+    apk: An existing ApkHelper instance for the apk (optional).
     enable_device_cache: Whether to enable on-device caching of checksums.
     use_concurrency: Whether to speed things up using multiple threads.
-    show_proguard_warning: Whether to print a warning about Proguard not being
-        enabled after installing.
     permissions: A list of the permissions to grant, or None to grant all
                  non-blacklisted permissions in the manifest.
   """
+  if isinstance(install_json, basestring):
+    with open(install_json) as f:
+      install_dict = json.load(f)
+  else:
+    install_dict = install_json
+
+  if install_dict.get('dont_even_try'):
+    raise Exception(install_dict['dont_even_try'])
+
   main_timer = time_profile.TimeProfile()
   install_timer = time_profile.TimeProfile()
   push_native_timer = time_profile.TimeProfile()
   push_dex_timer = time_profile.TimeProfile()
 
-  apk = apk_helper.ToHelper(apk)
+  def fix_path(p):
+    return os.path.normpath(os.path.join(constants.GetOutDirectory(), p))
+
+  if not apk:
+    apk = apk_helper.ToHelper(fix_path(install_dict['apk_path']))
+  split_globs = [fix_path(p) for p in install_dict['split_globs']]
+  native_libs = [fix_path(p) for p in install_dict['native_libs']]
+  dex_files = [fix_path(p) for p in install_dict['dex_files']]
+  show_proguard_warning = install_dict.get('show_proguard_warning')
+
   apk_package = apk.GetPackageName()
   device_incremental_dir = _GetDeviceIncrementalDir(apk_package)
 
@@ -119,11 +132,9 @@
       for split_glob in split_globs:
         splits.extend((f for f in glob.glob(split_glob)))
       device.InstallSplitApk(apk, splits, reinstall=True,
-                             allow_cached_props=True, permissions=permissions,
-                             allow_downgrade=allow_downgrade)
+                             allow_cached_props=True, permissions=permissions)
     else:
-      device.Install(apk, reinstall=True, permissions=permissions,
-                     allow_downgrade=allow_downgrade)
+      device.Install(apk, reinstall=True, permissions=permissions)
     install_timer.Stop(log=False)
 
   # Push .so and .dex files to the device (if they have changed).
@@ -227,23 +238,8 @@
 
 def main():
   parser = argparse.ArgumentParser()
-  parser.add_argument('apk_path',
-                      help='The path to the APK to install.')
-  parser.add_argument('--split',
-                      action='append',
-                      dest='splits',
-                      help='A glob matching the apk splits. '
-                           'Can be specified multiple times.')
-  parser.add_argument('--native_lib',
-                      dest='native_libs',
-                      help='Path to native library (repeatable)',
-                      action='append',
-                      default=[])
-  parser.add_argument('--dex-file',
-                      dest='dex_files',
-                      help='Path to dex files (repeatable)',
-                      action='append',
-                      default=[])
+  parser.add_argument('json_path',
+                      help='The path to the generated incremental apk .json.')
   parser.add_argument('-d', '--device', dest='device',
                       help='Target device for apk to install on.')
   parser.add_argument('--uninstall',
@@ -263,38 +259,21 @@
                       dest='cache',
                       help='Do not use cached information about what files are '
                            'currently on the target device.')
-  parser.add_argument('--show-proguard-warning',
-                      action='store_true',
-                      default=False,
-                      help='Print a warning about proguard being disabled')
-  parser.add_argument('--dont-even-try',
-                      help='Prints this message and exits.')
   parser.add_argument('-v',
                       '--verbose',
                       dest='verbose_count',
                       default=0,
                       action='count',
                       help='Verbose level (multiple times for more)')
-  parser.add_argument('--disable-downgrade',
-                      action='store_false',
-                      default=True,
-                      dest='allow_downgrade',
-                      help='Disable install of apk with lower version number'
-                           'than the version already on the device.')
 
   args = parser.parse_args()
 
   run_tests_helper.SetLogLevel(args.verbose_count)
-  constants.SetBuildType('Debug')
   if args.output_directory:
     constants.SetOutputDirectory(args.output_directory)
 
   devil_chromium.Initialize(output_directory=constants.GetOutDirectory())
 
-  if args.dont_even_try:
-    logging.fatal(args.dont_even_try)
-    return 1
-
   # Retries are annoying when commands fail for legitimate reasons. Might want
   # to enable them if this is ever used on bots though.
   device = device_utils.DeviceUtils.HealthyDevices(
@@ -302,15 +281,14 @@
       default_retries=0,
       enable_device_files_cache=True)[0]
 
-  apk = apk_helper.ToHelper(args.apk_path)
   if args.uninstall:
+    with open(args.json_path) as f:
+      install_dict = json.load(f)
+    apk = apk_helper.ToHelper(install_dict['apk_path'])
     Uninstall(device, apk.GetPackageName(), enable_device_cache=args.cache)
   else:
-    Install(device, apk, split_globs=args.splits, native_libs=args.native_libs,
-            dex_files=args.dex_files, enable_device_cache=args.cache,
-            use_concurrency=args.threading,
-            show_proguard_warning=args.show_proguard_warning,
-            allow_downgrade=args.allow_downgrade)
+    Install(device, args.json_path, enable_device_cache=args.cache,
+            use_concurrency=args.threading)
 
 
 if __name__ == '__main__':
diff --git a/build/android/incremental_install/write_installer_json.py b/build/android/incremental_install/write_installer_json.py
new file mode 100755
index 0000000..66ddd49d
--- /dev/null
+++ b/build/android/incremental_install/write_installer_json.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+# 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.
+
+"""Writes a .json file with the per-apk details for an incremental install."""
+
+import argparse
+import json
+import os
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
+
+from util import build_utils
+
+
+def _ParseArgs(args):
+  args = build_utils.ExpandFileArgs(args)
+  parser = argparse.ArgumentParser()
+  build_utils.AddDepfileOption(parser)
+  parser.add_argument('--output-path',
+                      help='Output path for .json file.',
+                      required=True)
+  parser.add_argument('--apk-path',
+                      help='Path to .apk relative to output directory.',
+                      required=True)
+  parser.add_argument('--split',
+                      action='append',
+                      dest='split_globs',
+                      default=[],
+                      help='A glob matching the apk splits. '
+                           'Can be specified multiple times.')
+  parser.add_argument('--native-libs-list',
+                      action='append',
+                      default=[],
+                      help='GN-list of paths to native libraries relative to '
+                           'output directory. Can be repeated.')
+  parser.add_argument('--dex-file',
+                      action='append',
+                      default=[],
+                      dest='dex_files',
+                      help='.dex file to include relative to output directory. '
+                           'Can be repeated')
+  parser.add_argument('--dex-file-list',
+                      help='GN-list of dex paths relative to output directory.')
+  parser.add_argument('--show-proguard-warning',
+                      action='store_true',
+                      default=False,
+                      help='Print a warning about proguard being disabled')
+  parser.add_argument('--dont-even-try',
+                      help='Prints the given message and exits.')
+
+  options = parser.parse_args(args)
+  options.dex_files += build_utils.ParseGnList(options.dex_file_list)
+  all_libs = []
+  for gn_list in options.native_libs_list:
+    all_libs.extend(build_utils.ParseGnList(gn_list))
+  options.native_libs_list = all_libs
+  return options
+
+
+def main(args):
+  options = _ParseArgs(args)
+
+  data = {
+      'apk_path': options.apk_path,
+      'native_libs': options.native_libs_list,
+      'dex_files': options.dex_files,
+      'dont_even_try': options.dont_even_try,
+      'show_proguard_warning': options.show_proguard_warning,
+      'split_globs': options.split_globs,
+  }
+
+  with open(options.output_path, 'w') as f:
+    json.dump(data, f, indent=2, sort_keys=True)
+
+  if options.depfile:
+    build_utils.WriteDepfile(options.depfile, options.output_path)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
diff --git a/build/android/pylib/gtest/gtest_test_instance.py b/build/android/pylib/gtest/gtest_test_instance.py
index 9e4e19a..bb1dbb9 100644
--- a/build/android/pylib/gtest/gtest_test_instance.py
+++ b/build/android/pylib/gtest/gtest_test_instance.py
@@ -281,14 +281,14 @@
         self._exe_dist_dir = exe_dist_dir
 
     incremental_part = ''
-    if args.test_apk_incremental_install_script:
+    if args.test_apk_incremental_install_json:
       incremental_part = '_incremental'
 
     apk_path = os.path.join(
         constants.GetOutDirectory(), '%s_apk' % self._suite,
         '%s-debug%s.apk' % (self._suite, incremental_part))
-    self._test_apk_incremental_install_script = (
-        args.test_apk_incremental_install_script)
+    self._test_apk_incremental_install_json = (
+        args.test_apk_incremental_install_json)
     if not os.path.exists(apk_path):
       self._apk_helper = None
     else:
@@ -423,8 +423,8 @@
     return self._suite
 
   @property
-  def test_apk_incremental_install_script(self):
-    return self._test_apk_incremental_install_script
+  def test_apk_incremental_install_json(self):
+    return self._test_apk_incremental_install_json
 
   @property
   def total_external_shards(self):
diff --git a/build/android/pylib/instrumentation/instrumentation_test_instance.py b/build/android/pylib/instrumentation/instrumentation_test_instance.py
index fc6a1a7..b6dd7c6 100644
--- a/build/android/pylib/instrumentation/instrumentation_test_instance.py
+++ b/build/android/pylib/instrumentation/instrumentation_test_instance.py
@@ -445,11 +445,11 @@
 
     self._additional_apks = []
     self._apk_under_test = None
-    self._apk_under_test_incremental_install_script = None
+    self._apk_under_test_incremental_install_json = None
     self._package_info = None
     self._suite = None
     self._test_apk = None
-    self._test_apk_incremental_install_script = None
+    self._test_apk_incremental_install_json = None
     self._test_jar = None
     self._test_package = None
     self._junit3_runner_class = None
@@ -536,12 +536,12 @@
 
     self._test_apk = apk_helper.ToHelper(test_apk_path)
 
-    self._apk_under_test_incremental_install_script = (
-        args.apk_under_test_incremental_install_script)
-    self._test_apk_incremental_install_script = (
-        args.test_apk_incremental_install_script)
+    self._apk_under_test_incremental_install_json = (
+        args.apk_under_test_incremental_install_json)
+    self._test_apk_incremental_install_json = (
+        args.test_apk_incremental_install_json)
 
-    if self._test_apk_incremental_install_script:
+    if self._test_apk_incremental_install_json:
       assert self._suite.endswith('_incremental')
       self._suite = self._suite[:-len('_incremental')]
 
@@ -700,8 +700,8 @@
     return self._apk_under_test
 
   @property
-  def apk_under_test_incremental_install_script(self):
-    return self._apk_under_test_incremental_install_script
+  def apk_under_test_incremental_install_json(self):
+    return self._apk_under_test_incremental_install_json
 
   @property
   def coverage_directory(self):
@@ -780,8 +780,8 @@
     return self._test_apk
 
   @property
-  def test_apk_incremental_install_script(self):
-    return self._test_apk_incremental_install_script
+  def test_apk_incremental_install_json(self):
+    return self._test_apk_incremental_install_json
 
   @property
   def test_jar(self):
diff --git a/build/android/pylib/local/device/local_device_gtest_run.py b/build/android/pylib/local/device/local_device_gtest_run.py
index 62ad71f5..4d0dfe0 100644
--- a/build/android/pylib/local/device/local_device_gtest_run.py
+++ b/build/android/pylib/local/device/local_device_gtest_run.py
@@ -14,6 +14,7 @@
 from devil.android import device_temp_file
 from devil.android import ports
 from devil.utils import reraiser_thread
+from incremental_install import installer
 from pylib import constants
 from pylib.base import base_test_result
 from pylib.gtest import gtest_test_instance
@@ -106,8 +107,8 @@
   def __init__(self, test_instance):
     self._activity = test_instance.activity
     self._apk_helper = test_instance.apk_helper
-    self._test_apk_incremental_install_script = (
-        test_instance.test_apk_incremental_install_script)
+    self._test_apk_incremental_install_json = (
+        test_instance.test_apk_incremental_install_json)
     self._package = test_instance.package
     self._runner = test_instance.runner
     self._permissions = test_instance.permissions
@@ -121,9 +122,9 @@
                           'chromium_tests_root')
 
   def Install(self, device):
-    if self._test_apk_incremental_install_script:
-      local_device_test_run.IncrementalInstall(device, self._apk_helper,
-          self._test_apk_incremental_install_script)
+    if self._test_apk_incremental_install_json:
+      installer.Install(device, self._test_apk_incremental_install_json,
+                        apk=self._apk_helper)
     else:
       device.Install(self._apk_helper, reinstall=True,
                      permissions=self._permissions)
diff --git a/build/android/pylib/local/device/local_device_instrumentation_test_run.py b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
index 14d97b31..51611f2 100644
--- a/build/android/pylib/local/device/local_device_instrumentation_test_run.py
+++ b/build/android/pylib/local/device/local_device_instrumentation_test_run.py
@@ -19,6 +19,7 @@
 from devil.android import flag_changer
 from devil.android.tools import system_app
 from devil.utils import reraiser_thread
+from incremental_install import installer
 from pylib import valgrind_tools
 from pylib.android import logdog_logcat_monitor
 from pylib.base import base_test_result
@@ -162,31 +163,30 @@
         return lambda: crash_handler.RetryOnSystemCrash(
             install_helper_internal, dev)
 
-      def incremental_install_helper(apk, script):
+      def incremental_install_helper(apk, json_path):
         @trace_event.traced("apk_path")
         def incremental_install_helper_internal(d, apk_path=apk.path):
           # pylint: disable=unused-argument
-          local_device_test_run.IncrementalInstall(
-              d, apk, script)
+          installer.Install(d, json_path, apk=apk)
         return lambda: crash_handler.RetryOnSystemCrash(
             incremental_install_helper_internal, dev)
 
       if self._test_instance.apk_under_test:
-        if self._test_instance.apk_under_test_incremental_install_script:
+        if self._test_instance.apk_under_test_incremental_install_json:
           steps.append(incremental_install_helper(
                            self._test_instance.apk_under_test,
                            self._test_instance.
-                               apk_under_test_incremental_install_script))
+                               apk_under_test_incremental_install_json))
         else:
           permissions = self._test_instance.apk_under_test.GetPermissions()
           steps.append(install_helper(self._test_instance.apk_under_test,
                                       permissions))
 
-      if self._test_instance.test_apk_incremental_install_script:
+      if self._test_instance.test_apk_incremental_install_json:
         steps.append(incremental_install_helper(
                          self._test_instance.test_apk,
                          self._test_instance.
-                             test_apk_incremental_install_script))
+                             test_apk_incremental_install_json))
       else:
         permissions = self._test_instance.test_apk.GetPermissions()
         steps.append(install_helper(self._test_instance.test_apk,
diff --git a/build/android/pylib/local/device/local_device_test_run.py b/build/android/pylib/local/device/local_device_test_run.py
index 92a9035..94e9c82 100644
--- a/build/android/pylib/local/device/local_device_test_run.py
+++ b/build/android/pylib/local/device/local_device_test_run.py
@@ -3,7 +3,6 @@
 # found in the LICENSE file.
 
 import fnmatch
-import imp
 import logging
 import posixpath
 import signal
@@ -26,28 +25,6 @@
   '  Your test may not have run.')
 
 
-def IncrementalInstall(device, apk_helper, installer_script):
-  """Performs an incremental install.
-
-  Args:
-    device: Device to install on.
-    apk_helper: ApkHelper instance for the _incremental.apk.
-    installer_script: Path to the installer script for the incremental apk.
-  """
-  try:
-    install_wrapper = imp.load_source('install_wrapper', installer_script)
-  except IOError:
-    raise Exception('Incremental install script not found: %s\n' %
-                    installer_script)
-  params = install_wrapper.GetInstallParameters()
-
-  from incremental_install import installer
-  installer.Install(device, apk_helper, split_globs=params['splits'],
-                    native_libs=params['native_libs'],
-                    dex_files=params['dex_files'],
-                    permissions=None)  # Auto-grant permissions from manifest.
-
-
 def SubstituteDeviceRoot(device_path, device_root):
   if not device_path:
     return device_root
diff --git a/build/android/test_runner.py b/build/android/test_runner.py
index ed0812b..01a0ec53 100755
--- a/build/android/test_runner.py
+++ b/build/android/test_runner.py
@@ -330,7 +330,7 @@
       dest='suite_name', nargs='+', metavar='SUITE_NAME', required=True,
       help='Executable name of the test suite to run.')
   parser.add_argument(
-      '--test-apk-incremental-install-script',
+      '--test-apk-incremental-install-json',
       type=os.path.realpath,
       help='Path to install script for the test apk.')
 
@@ -475,10 +475,10 @@
   # These arguments are suppressed from the help text because they should
   # only ever be specified by an intermediate script.
   parser.add_argument(
-      '--apk-under-test-incremental-install-script',
+      '--apk-under-test-incremental-install-json',
       help=argparse.SUPPRESS)
   parser.add_argument(
-      '--test-apk-incremental-install-script',
+      '--test-apk-incremental-install-json',
       type=os.path.realpath,
       help=argparse.SUPPRESS)
 
diff --git a/build/android/test_runner.pydeps b/build/android/test_runner.pydeps
index 4084940..1d03946 100644
--- a/build/android/test_runner.pydeps
+++ b/build/android/test_runner.pydeps
@@ -113,9 +113,15 @@
 ../../tools/swarming_client/libs/logdog/stream.py
 ../../tools/swarming_client/libs/logdog/streamname.py
 ../../tools/swarming_client/libs/logdog/varint.py
+../gn_helpers.py
 ../util/lib/common/chrome_test_server_spawner.py
 ../util/lib/common/unittest_util.py
 devil_chromium.py
+gyp/util/__init__.py
+gyp/util/build_utils.py
+gyp/util/md5_check.py
+incremental_install/__init__.py
+incremental_install/installer.py
 pylib/__init__.py
 pylib/android/__init__.py
 pylib/android/logdog_logcat_monitor.py
@@ -179,6 +185,7 @@
 pylib/utils/proguard.py
 pylib/utils/repo_utils.py
 pylib/utils/shared_preference_utils.py
+pylib/utils/time_profile.py
 pylib/valgrind_tools.py
 test_runner.py
 tombstones.py
diff --git a/build/config/android/internal_rules.gni b/build/config/android/internal_rules.gni
index 4228f64..8a8f6541 100644
--- a/build/config/android/internal_rules.gni
+++ b/build/config/android/internal_rules.gni
@@ -303,12 +303,12 @@
         _rebased_apk_path = rebase_path(invoker.apk_path, root_build_dir)
         _rebased_incremental_apk_path =
             rebase_path(invoker.incremental_apk_path, root_build_dir)
-        _rebased_incremental_install_script_path =
-            rebase_path(invoker.incremental_install_script_path, root_build_dir)
+        _rebased_incremental_install_json_path =
+            rebase_path(invoker.incremental_install_json_path, root_build_dir)
         _incremental_allowed =
             defined(invoker.incremental_allowed) && invoker.incremental_allowed
         args += [ "--apk-path=$_rebased_apk_path" ]
-        args += [ "--incremental-install-script-path=$_rebased_incremental_install_script_path" ]
+        args += [ "--incremental-install-json-path=$_rebased_incremental_install_json_path" ]
 
         assert(_rebased_incremental_apk_path != "")  # Mark as used.
         if (_incremental_allowed) {
@@ -644,13 +644,13 @@
     }
     if (_incremental_install) {
       test_runner_args += [
-        "--test-apk-incremental-install-script",
-        "@FileArg($_rebased_apk_build_config:deps_info:incremental_install_script_path)",
+        "--test-apk-incremental-install-json",
+        "@FileArg($_rebased_apk_build_config:deps_info:incremental_install_json_path)",
       ]
       if (defined(invoker.apk_under_test)) {
         test_runner_args += [
-          "--apk-under-test-incremental-install-script",
-          "@FileArg($_rebased_apk_under_test_build_config:deps_info:incremental_install_script_path)",
+          "--apk-under-test-incremental-install-json",
+          "@FileArg($_rebased_apk_under_test_build_config:deps_info:incremental_install_json_path)",
         ]
       }
       test_runner_args += [ "--fast-local-dev" ]
@@ -1197,7 +1197,7 @@
         (is_java_debug || dcheck_always_on)
 
     _desugar = defined(invoker.supports_android) && invoker.supports_android
-    
+
     _deps = []
     _previous_output_jar = _input_jar_path
 
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 648b14fe..a5fb140 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1647,16 +1647,8 @@
     _final_apk_path_no_ext = _final_apk_path_no_ext_list[0]
     assert(_final_apk_path_no_ext != "")  # Mark as used.
 
-    _install_script_name = "install_$_template_name"
-    if (defined(invoker.install_script_name)) {
-      _install_script_name = invoker.install_script_name
-    }
-    _incremental_install_script_path =
-        "${root_out_dir}/bin/${_install_script_name}"
-    if (!incremental_apk_by_default) {
-      _incremental_install_script_path =
-          "${_incremental_install_script_path}_incremental"
-    }
+    _incremental_install_json_path =
+        "$target_gen_dir/$target_name.incremental.json"
 
     _version_code = android_default_version_code
     if (defined(invoker.version_code)) {
@@ -1814,7 +1806,7 @@
       apk_path = _final_apk_path
       incremental_allowed = _incremental_allowed
       incremental_apk_path = "${_final_apk_path_no_ext}_incremental.apk"
-      incremental_install_script_path = _incremental_install_script_path
+      incremental_install_json_path = _incremental_install_json_path
       resources_zip = resources_zip_path
       build_config = _build_config
       android_manifest = _android_root_manifest
@@ -2400,28 +2392,27 @@
       }
     }
 
-    _create_incremental_script_rule_name =
-        "${_template_name}__incremental_script"
-    action(_create_incremental_script_rule_name) {
-      script = "//build/android/incremental_install/create_install_script.py"
+    _write_installer_json_rule_name = "${_template_name}__incremental_json"
+    action(_write_installer_json_rule_name) {
+      script = "//build/android/incremental_install/write_installer_json.py"
       depfile = "$target_gen_dir/$target_name.d"
       deps = [
         _native_libs_file_arg_dep,
       ]
 
       outputs = [
-        _incremental_install_script_path,
+        _incremental_install_json_path,
       ]
 
       _rebased_apk_path_no_ext =
           rebase_path(_final_apk_path_no_ext, root_build_dir)
-      _rebased_incremental_install_script_path =
-          rebase_path(_incremental_install_script_path, root_build_dir)
+      _rebased_incremental_install_json_path =
+          rebase_path(_incremental_install_json_path, root_build_dir)
       _rebased_depfile = rebase_path(depfile, root_build_dir)
       _dex_arg_key = "${_rebased_build_config}:final_dex:dependency_dex_files"
       args = [
         "--apk-path=${_rebased_apk_path_no_ext}_incremental.apk",
-        "--script-output-path=$_rebased_incremental_install_script_path",
+        "--output-path=$_rebased_incremental_install_json_path",
         "--dex-file=$_rebased_lib_dex_path",
         "--dex-file-list=@FileArg($_dex_arg_key)",
         "--depfile=$_rebased_depfile",
@@ -2465,6 +2456,8 @@
         args = [
           "--script-output-path",
           rebase_path(_generated_script, root_build_dir),
+          "--apk-path",
+          rebase_path(_final_apk_path, root_build_dir),
         ]
         if (defined(invoker.command_line_flags_file)) {
           args += [
@@ -2472,20 +2465,10 @@
             invoker.command_line_flags_file,
           ]
         }
-
         if (_incremental_allowed) {
           args += [
-            "--incremental-apk-path",
-            rebase_path("${_final_apk_path_no_ext}_incremental.apk",
-                        root_build_dir),
-            "--incremental-install-script",
-            rebase_path(_incremental_install_script_path, root_build_dir),
-          ]
-        }
-        if (!incremental_apk_by_default) {
-          args += [
-            "--apk-path",
-            rebase_path(_final_apk_path, root_build_dir),
+            "--incremental-install-json-path",
+            rebase_path(_incremental_install_json_path, root_build_dir),
           ]
         }
       }
@@ -2538,8 +2521,8 @@
         # actual target, but instead loads them at runtime, we need to explicitly
         # depend on them here.
         public_deps = [
-          ":${_create_incremental_script_rule_name}",
           ":${_template_name}__create_incremental",
+          ":${_write_installer_json_rule_name}",
           ":${java_target}",
         ]
 
@@ -2592,7 +2575,6 @@
     testonly = true
     _apk_target_name = "${target_name}__apk"
     _test_runner_target_name = "${target_name}__test_runner_script"
-    _install_script_name = "install_$target_name"
     _dist_ijar_path =
         "$root_build_dir/test.lib.java/" + invoker.apk_name + ".jar"
     _incremental_test_runner_target_name =
@@ -2643,7 +2625,6 @@
       deps = []
       data_deps = []
       forward_variables_from(invoker, "*")
-      install_script_name = _install_script_name
       deps += [ "//testing/android/broker:broker_java" ]
       data_deps += [
         "//build/android/pylib/device/commands",
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 0f8e166..e6aa4dc 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -12264,6 +12264,9 @@
       <message name="IDS_SCREEN_CAPTURE_NOTIFICATION_TEXT_2" desc="Text to be shown as a notification when screen capture is in progress.">
         Sharing screen
       </message>
+      <message name="IDS_VR_UNDER_DEVELOPMENT_NOTICE" desc="Text to be shown below the URL bar to announce that this is under development.">
+        This is an early release. Some features, like search and text entry, are not yet available.
+      </message>
     </if>
 
     <!-- VR specific strings -->
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 5d33e79..0429a755 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3204,7 +3204,10 @@
      flag_descriptions::kDisableNewVirtualKeyboardBehaviorName,
      flag_descriptions::kDisableNewVirtualKeyboardBehaviorDescription, kOsCrOS,
      SINGLE_DISABLE_VALUE_TYPE(switches::kDisableNewVirtualKeyboardBehavior)},
-#endif  // defined(OS_CHROMEOS)
+    {"enable-per-user-timezone", flag_descriptions::kEnablePerUserTimezoneName,
+     flag_descriptions::kEnablePerUserTimezoneDescription, kOsCrOS,
+     SINGLE_DISABLE_VALUE_TYPE(chromeos::switches::kDisablePerUserTimezone)},
+#endif  // OS_CHROMEOS
 
 #if !defined(OS_ANDROID)
     {"enable-picture-in-picture",
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index 1c1fe0f..89ddfc0 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -1859,8 +1859,6 @@
     "settings/device_settings_provider_unittest.cc",
     "settings/device_settings_service_unittest.cc",
     "settings/install_attributes_unittest.cc",
-    "settings/scoped_cros_settings_test_helper.cc",
-    "settings/scoped_cros_settings_test_helper.h",
     "settings/session_manager_operation_unittest.cc",
     "settings/shutdown_policy_handler_unittest.cc",
     "settings/stub_cros_settings_provider_unittest.cc",
diff --git a/chrome/browser/chromeos/extensions/info_private_api.cc b/chrome/browser/chromeos/extensions/info_private_api.cc
index 29bd74e2..c69c4ab2 100644
--- a/chrome/browser/chromeos/extensions/info_private_api.cc
+++ b/chrome/browser/chromeos/extensions/info_private_api.cc
@@ -326,6 +326,13 @@
   }
 
   if (property_name == kPropertyTimezone) {
+    if (chromeos::system::PerUserTimezoneEnabled()) {
+      return base::WrapUnique<base::Value>(
+          Profile::FromBrowserContext(context_)
+              ->GetPrefs()
+              ->GetUserPrefValue(prefs::kUserTimezone)
+              ->DeepCopy());
+    }
     // TODO(crbug.com/697817): Convert CrosSettings::Get to take a unique_ptr.
     return base::WrapUnique<base::Value>(
         chromeos::CrosSettings::Get()
@@ -363,8 +370,13 @@
   if (param_name == kPropertyTimezone) {
     std::string param_value;
     EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &param_value));
-    chromeos::CrosSettings::Get()->Set(chromeos::kSystemTimezone,
-                                       base::Value(param_value));
+    if (chromeos::system::PerUserTimezoneEnabled()) {
+      Profile::FromBrowserContext(context_)->GetPrefs()->SetString(
+          prefs::kUserTimezone, param_value);
+    } else {
+      chromeos::CrosSettings::Get()->Set(chromeos::kSystemTimezone,
+                                         base::Value(param_value));
+    }
   } else {
     const char* pref_name = GetBoolPrefNameForApiProperty(param_name.c_str());
     if (pref_name) {
diff --git a/chrome/browser/chromeos/extensions/info_private_apitest.cc b/chrome/browser/chromeos/extensions/info_private_apitest.cc
index 12c568e..c3c88b1 100644
--- a/chrome/browser/chromeos/extensions/info_private_apitest.cc
+++ b/chrome/browser/chromeos/extensions/info_private_apitest.cc
@@ -44,9 +44,7 @@
 IN_PROC_BROWSER_TEST_F(ChromeOSInfoPrivateTest, TestGetAndSet) {
   // Set the initial timezone different from what JS function
   // timezoneSetTest() will attempt to set.
-  base::Value initial_timezone("America/Los_Angeles");
-  chromeos::CrosSettings::Get()->Set(chromeos::kSystemTimezone,
-                                     initial_timezone);
+  profile()->GetPrefs()->SetString(prefs::kUserTimezone, "America/Los_Angeles");
 
   // Check that accessibility settings are set to default values.
   PrefService* prefs = profile()->GetPrefs();
diff --git a/chrome/browser/chromeos/login/screens/network_screen.cc b/chrome/browser/chromeos/login/screens/network_screen.cc
index 1d78fd4..006b4c7 100644
--- a/chrome/browser/chromeos/login/screens/network_screen.cc
+++ b/chrome/browser/chromeos/login/screens/network_screen.cc
@@ -21,6 +21,7 @@
 #include "chrome/browser/chromeos/login/screens/network_view.h"
 #include "chrome/browser/chromeos/login/ui/input_events_blocker.h"
 #include "chrome/browser/chromeos/login/wizard_controller.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chrome/browser/profiles/profile_manager.h"
 #include "chrome/browser/ui/webui/chromeos/login/l10n_util.h"
 #include "chrome/common/pref_names.h"
@@ -133,12 +134,11 @@
 }
 
 void NetworkScreen::SetTimezone(const std::string& timezone_id) {
-  std::string current_timezone_id;
-  CrosSettings::Get()->GetString(kSystemTimezone, &current_timezone_id);
-  if (current_timezone_id == timezone_id || timezone_id.empty())
+  if (timezone_id.empty())
     return;
+
   timezone_ = timezone_id;
-  CrosSettings::Get()->SetString(kSystemTimezone, timezone_id);
+  chromeos::system::SetSystemAndSigninScreenTimezone(timezone_id);
 }
 
 std::string NetworkScreen::GetTimezone() const {
diff --git a/chrome/browser/chromeos/login/ui/login_display_host_impl.cc b/chrome/browser/chromeos/login/ui/login_display_host_impl.cc
index 29c04248..d788e54e3 100644
--- a/chrome/browser/chromeos/login/ui/login_display_host_impl.cc
+++ b/chrome/browser/chromeos/login/ui/login_display_host_impl.cc
@@ -190,20 +190,29 @@
     chromeos::LoginDisplayHost* display_host) {
   TRACE_EVENT0("chromeos", "ShowLoginWizard::ShowLoginWizardFinish");
 
+  // Restore system timezone.
+  std::string timezone;
+  if (chromeos::system::PerUserTimezoneEnabled()) {
+    timezone = g_browser_process->local_state()->GetString(
+        prefs::kSigninScreenTimezone);
+  }
+
   if (ShouldShowSigninScreen(first_screen)) {
     display_host->StartSignInScreen(chromeos::LoginScreenContext());
   } else {
     display_host->StartWizard(first_screen);
 
     // Set initial timezone if specified by customization.
-    const std::string timezone_name = startup_manifest->initial_timezone();
-    VLOG(1) << "Initial time zone: " << timezone_name;
+    const std::string customization_timezone =
+        startup_manifest->initial_timezone();
+    VLOG(1) << "Initial time zone: " << customization_timezone;
     // Apply locale customizations only once to preserve whatever locale
     // user has changed to during OOBE.
-    if (!timezone_name.empty()) {
-      chromeos::system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
-          base::UTF8ToUTF16(timezone_name));
-    }
+    if (!customization_timezone.empty())
+      timezone = customization_timezone;
+  }
+  if (!timezone.empty()) {
+    chromeos::system::SetSystemAndSigninScreenTimezone(timezone);
   }
 }
 
diff --git a/chrome/browser/chromeos/login/users/chrome_user_manager_impl.cc b/chrome/browser/chromeos/login/users/chrome_user_manager_impl.cc
index 4e41a20..75f6af9 100644
--- a/chrome/browser/chromeos/login/users/chrome_user_manager_impl.cc
+++ b/chrome/browser/chromeos/login/users/chrome_user_manager_impl.cc
@@ -498,6 +498,7 @@
           multi_profile_user_controller_->StartObserving(profile);
         }
       }
+      system::UpdateSystemTimezone(profile);
       UpdateUserTimeZoneRefresher(profile);
       break;
     }
diff --git a/chrome/browser/chromeos/login/wizard_controller.cc b/chrome/browser/chromeos/login/wizard_controller.cc
index beca138..b4af5ef 100644
--- a/chrome/browser/chromeos/login/wizard_controller.cc
+++ b/chrome/browser/chromeos/login/wizard_controller.cc
@@ -67,6 +67,7 @@
 #include "chrome/browser/chromeos/policy/device_cloud_policy_manager_chromeos.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/system/device_disabling_manager.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chrome/browser/lifetime/application_lifetime.h"
 #include "chrome/browser/metrics/metrics_reporting_state.h"
 #include "chrome/browser/profiles/profile.h"
@@ -1502,9 +1503,7 @@
   if (!timezone->timeZoneId.empty()) {
     VLOG(1) << "Resolve TimeZone: setting timezone to '" << timezone->timeZoneId
             << "'";
-
-    system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
-        base::UTF8ToUTF16(timezone->timeZoneId));
+    chromeos::system::SetSystemAndSigninScreenTimezone(timezone->timeZoneId);
   }
 }
 
diff --git a/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc b/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
index d1a8418..3a583d6 100644
--- a/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
+++ b/chrome/browser/chromeos/policy/browser_policy_connector_chromeos.cc
@@ -36,6 +36,7 @@
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/settings/device_settings_service.h"
 #include "chrome/browser/chromeos/settings/install_attributes.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chrome/browser/policy/device_management_service_configuration.h"
 #include "chrome/common/pref_names.h"
 #include "chromeos/attestation/attestation_flow.h"
@@ -354,8 +355,7 @@
   if (chromeos::CrosSettings::Get()->GetString(chromeos::kSystemTimezonePolicy,
                                                &timezone) &&
       !timezone.empty()) {
-    chromeos::system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
-        base::UTF8ToUTF16(timezone));
+    chromeos::system::SetSystemAndSigninScreenTimezone(timezone);
   }
 }
 
diff --git a/chrome/browser/chromeos/preferences.cc b/chrome/browser/chromeos/preferences.cc
index c88005e..52e9b6c 100644
--- a/chrome/browser/chromeos/preferences.cc
+++ b/chrome/browser/chromeos/preferences.cc
@@ -25,8 +25,11 @@
 #include "chrome/browser/chromeos/login/session/user_session_manager.h"
 #include "chrome/browser/chromeos/net/wake_on_wifi_manager.h"
 #include "chrome/browser/chromeos/policy/proto/chrome_device_policy.pb.h"
+#include "chrome/browser/chromeos/profiles/profile_helper.h"
+#include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/system/input_device_settings.h"
 #include "chrome/browser/chromeos/system/timezone_resolver_manager.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chrome/browser/download/download_prefs.h"
 #include "chrome/browser/prefs/pref_service_syncable_util.h"
 #include "chrome/browser/ui/ash/ash_util.h"
@@ -34,6 +37,7 @@
 #include "chrome/common/chrome_features.h"
 #include "chrome/common/pref_names.h"
 #include "chromeos/chromeos_switches.h"
+#include "chromeos/settings/cros_settings_names.h"
 #include "chromeos/system/devicemode.h"
 #include "chromeos/system/statistics_provider.h"
 #include "chromeos/timezone/timezone_resolver.h"
@@ -116,6 +120,7 @@
   registry->RegisterBooleanPref(prefs::kAccessibilityMonoAudioEnabled,
                                 false);
   registry->RegisterStringPref(prefs::kLogoutStartedLast, std::string());
+  registry->RegisterStringPref(prefs::kSigninScreenTimezone, std::string());
   registry->RegisterBooleanPref(prefs::kResolveDeviceTimezoneByGeolocation,
                                 true);
   registry->RegisterIntegerPref(
@@ -353,6 +358,16 @@
 
   input_method::InputMethodSyncer::RegisterProfilePrefs(registry);
 
+  std::string current_timezone_id;
+  if (chromeos::CrosSettings::IsInitialized()) {
+    // In unit tests CrosSettings is not always initialized.
+    chromeos::CrosSettings::Get()->GetString(kSystemTimezone,
+                                             &current_timezone_id);
+  }
+  // |current_timezone_id| will be empty if CrosSettings doesn't know the
+  // timezone yet.
+  registry->RegisterStringPref(prefs::kUserTimezone, current_timezone_id);
+
   registry->RegisterBooleanPref(
       prefs::kResolveTimezoneByGeolocation, true,
       user_prefs::PrefRegistrySyncable::SYNCABLE_PREF);
@@ -432,6 +447,7 @@
                                  callback);
 
   pref_change_registrar_.Init(prefs);
+  pref_change_registrar_.Add(prefs::kUserTimezone, callback);
   pref_change_registrar_.Add(prefs::kResolveTimezoneByGeolocation, callback);
   pref_change_registrar_.Add(prefs::kUse24HourClock, callback);
   for (auto* remap_pref : kLanguageRemapPrefs)
@@ -727,6 +743,11 @@
         static_cast<WakeOnWifiManager::WakeOnWifiFeature>(features));
   }
 
+  if (pref_name == prefs::kUserTimezone &&
+      reason != REASON_ACTIVE_USER_CHANGED) {
+    system::UpdateSystemTimezone(ProfileHelper::Get()->GetProfileByUser(user_));
+  }
+
   if (pref_name == prefs::kResolveTimezoneByGeolocation &&
       reason != REASON_ACTIVE_USER_CHANGED) {
     const bool value = prefs_->GetBoolean(prefs::kResolveTimezoneByGeolocation);
diff --git a/chrome/browser/chromeos/printing/printer_info.h b/chrome/browser/chromeos/printing/printer_info.h
index 5b7403f..df7ca69 100644
--- a/chrome/browser/chromeos/printing/printer_info.h
+++ b/chrome/browser/chromeos/printing/printer_info.h
@@ -28,6 +28,7 @@
 void QueryIppPrinter(const std::string& host,
                      const int port,
                      const std::string& path,
+                     bool encrypted,
                      const PrinterInfoCallback& callback);
 
 }  // namespace chromeos
diff --git a/chrome/browser/chromeos/printing/printer_info_cups.cc b/chrome/browser/chromeos/printing/printer_info_cups.cc
index 27e6208..d5fd2c97 100644
--- a/chrome/browser/chromeos/printing/printer_info_cups.cc
+++ b/chrome/browser/chromeos/printing/printer_info_cups.cc
@@ -81,9 +81,10 @@
 std::unique_ptr<::printing::PrinterInfo> QueryPrinterImpl(
     const std::string& host,
     const int port,
-    const std::string& path) {
+    const std::string& path,
+    bool encrypted) {
   auto info = base::MakeUnique<::printing::PrinterInfo>();
-  if (!::printing::GetPrinterInfo(host, port, path, info.get())) {
+  if (!::printing::GetPrinterInfo(host, port, path, encrypted, info.get())) {
     LOG(ERROR) << "Could not retrieve printer info";
     return nullptr;
   }
@@ -125,6 +126,7 @@
 void QueryIppPrinter(const std::string& host,
                      const int port,
                      const std::string& path,
+                     bool encrypted,
                      const PrinterInfoCallback& callback) {
   DCHECK(!host.empty());
 
@@ -134,7 +136,7 @@
   base::PostTaskWithTraitsAndReplyWithResult(
       FROM_HERE,
       base::TaskTraits(base::TaskPriority::USER_VISIBLE, base::MayBlock()),
-      base::Bind(&QueryPrinterImpl, host, port, path),
+      base::Bind(&QueryPrinterImpl, host, port, path, encrypted),
       base::Bind(&OnPrinterQueried, callback));
 }
 
diff --git a/chrome/browser/chromeos/printing/printer_info_stub.cc b/chrome/browser/chromeos/printing/printer_info_stub.cc
index 6832229..85bd76b 100644
--- a/chrome/browser/chromeos/printing/printer_info_stub.cc
+++ b/chrome/browser/chromeos/printing/printer_info_stub.cc
@@ -12,6 +12,7 @@
 void QueryIppPrinter(const std::string& host,
                      const int port,
                      const std::string& path,
+                     bool encrypted,
                      const PrinterInfoCallback& callback) {
   DCHECK(!host.empty());
 
diff --git a/chrome/browser/chromeos/settings/system_settings_provider.cc b/chrome/browser/chromeos/settings/system_settings_provider.cc
index e636b91..02b9d11 100644
--- a/chrome/browser/chromeos/settings/system_settings_provider.cc
+++ b/chrome/browser/chromeos/settings/system_settings_provider.cc
@@ -7,6 +7,7 @@
 #include "base/strings/string16.h"
 #include "base/time/time.h"
 #include "base/values.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chromeos/login/login_state.h"
 #include "chromeos/settings/cros_settings_names.h"
 
@@ -20,6 +21,8 @@
   timezone_settings->AddObserver(this);
   timezone_value_.reset(
       new base::Value(timezone_settings->GetCurrentTimezoneID()));
+  per_user_timezone_enabled_value_.reset(
+      new base::Value(system::PerUserTimezoneEnabled()));
 }
 
 SystemSettingsProvider::~SystemSettingsProvider() {
@@ -41,11 +44,16 @@
     // This will call TimezoneChanged.
     system::TimezoneSettings::GetInstance()->SetTimezoneFromID(timezone_id);
   }
+  // kPerUserTimezoneEnabled is read-only.
 }
 
 const base::Value* SystemSettingsProvider::Get(const std::string& path) const {
   if (path == kSystemTimezone)
     return timezone_value_.get();
+
+  if (path == kPerUserTimezoneEnabled)
+    return per_user_timezone_enabled_value_.get();
+
   return NULL;
 }
 
@@ -56,7 +64,7 @@
 }
 
 bool SystemSettingsProvider::HandlesSetting(const std::string& path) const {
-  return path == kSystemTimezone;
+  return path == kSystemTimezone || path == kPerUserTimezoneEnabled;
 }
 
 void SystemSettingsProvider::TimezoneChanged(const icu::TimeZone& timezone) {
diff --git a/chrome/browser/chromeos/settings/system_settings_provider.h b/chrome/browser/chromeos/settings/system_settings_provider.h
index e61761f..c72b60f 100644
--- a/chrome/browser/chromeos/settings/system_settings_provider.h
+++ b/chrome/browser/chromeos/settings/system_settings_provider.h
@@ -39,6 +39,7 @@
   void DoSet(const std::string& path, const base::Value& in_value) override;
 
   std::unique_ptr<base::Value> timezone_value_;
+  std::unique_ptr<base::Value> per_user_timezone_enabled_value_;
 
   DISALLOW_COPY_AND_ASSIGN(SystemSettingsProvider);
 };
diff --git a/chrome/browser/chromeos/system/timezone_util.cc b/chrome/browser/chromeos/system/timezone_util.cc
index d01f60e..c7facbb 100644
--- a/chrome/browser/chromeos/system/timezone_util.cc
+++ b/chrome/browser/chromeos/system/timezone_util.cc
@@ -10,6 +10,7 @@
 #include <string>
 #include <utility>
 
+#include "base/command_line.h"
 #include "base/i18n/rtl.h"
 #include "base/i18n/unicodestring.h"
 #include "base/lazy_instance.h"
@@ -21,13 +22,18 @@
 #include "base/values.h"
 #include "chrome/browser/browser_process.h"
 #include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
+#include "chrome/browser/chromeos/policy/proto/chrome_device_policy.pb.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/system/timezone_resolver_manager.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
+#include "chromeos/chromeos_switches.h"
 #include "chromeos/settings/timezone_settings.h"
 #include "chromeos/timezone/timezone_request.h"
 #include "components/prefs/pref_service.h"
+#include "components/user_manager/user.h"
 #include "components/user_manager/user_manager.h"
 #include "third_party/icu/source/common/unicode/ures.h"
 #include "third_party/icu/source/common/unicode/utypes.h"
@@ -184,6 +190,42 @@
   return false;
 }
 
+bool IsTimezonePrefsManaged(const std::string& pref_name) {
+  DCHECK(pref_name == prefs::kUserTimezone ||
+         pref_name == prefs::kResolveTimezoneByGeolocation);
+
+  std::string policy_timezone;
+  if (chromeos::CrosSettings::Get()->GetString(chromeos::kSystemTimezonePolicy,
+                                               &policy_timezone) &&
+      !policy_timezone.empty()) {
+    return true;
+  }
+
+  const PrefService* local_state = g_browser_process->local_state();
+  if (!local_state->IsManagedPreference(
+          prefs::kSystemTimezoneAutomaticDetectionPolicy)) {
+    return false;
+  }
+
+  int resolve_policy_value =
+      local_state->GetInteger(prefs::kSystemTimezoneAutomaticDetectionPolicy);
+
+  switch (resolve_policy_value) {
+    case enterprise_management::SystemTimezoneProto::USERS_DECIDE:
+      return false;
+    case enterprise_management::SystemTimezoneProto::DISABLED:
+      // This only disables resolving.
+      return pref_name == prefs::kResolveTimezoneByGeolocation;
+    case enterprise_management::SystemTimezoneProto::IP_ONLY:
+    case enterprise_management::SystemTimezoneProto::SEND_WIFI_ACCESS_POINTS:
+    case enterprise_management::SystemTimezoneProto::SEND_ALL_LOCATION_INFO:
+      return true;
+  }
+  // Default for unknown policy value.
+  NOTREACHED() << "Unrecognized policy value: " << resolve_policy_value;
+  return true;
+}
+
 void ApplyTimeZone(const TimeZoneResponseData* timezone) {
   if (!g_browser_process->platform_part()
            ->GetTimezoneResolverManager()
@@ -195,10 +237,97 @@
     VLOG(1) << "Refresh TimeZone: setting timezone to '" << timezone->timeZoneId
             << "'";
 
-    chromeos::system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
-        base::UTF8ToUTF16(timezone->timeZoneId));
+    if (PerUserTimezoneEnabled()) {
+      const user_manager::UserManager* user_manager =
+          user_manager::UserManager::Get();
+      const user_manager::User* primary_user = user_manager->GetPrimaryUser();
+
+      if (primary_user) {
+        Profile* profile = ProfileHelper::Get()->GetProfileByUser(primary_user);
+        profile->GetPrefs()->SetString(prefs::kUserTimezone,
+                                       timezone->timeZoneId);
+        // chromeos::Preferences::ApplyPreferences() will automatically change
+        // system timezone because user is primary.
+      } else {
+        SetSystemAndSigninScreenTimezone(timezone->timeZoneId);
+      }
+    } else {
+      chromeos::system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
+          base::UTF8ToUTF16(timezone->timeZoneId));
+    }
   }
 }
 
+void UpdateSystemTimezone(Profile* profile) {
+  if (IsTimezonePrefsManaged(prefs::kUserTimezone)) {
+    VLOG(1) << "Ignoring user timezone change, because timezone is enterprise "
+               "managed.";
+    return;
+  }
+
+  const user_manager::UserManager* user_manager =
+      user_manager::UserManager::Get();
+  const user_manager::User* user =
+      ProfileHelper::Get()->GetUserByProfile(profile);
+
+  const AccountId owner(user_manager->GetOwnerAccountId());
+  const bool user_is_owner =
+      owner.is_valid() && (owner == user->GetAccountId());
+
+  const std::string value =
+      profile->GetPrefs()->GetString(prefs::kUserTimezone);
+  if (user_is_owner) {
+    g_browser_process->local_state()->SetString(prefs::kSigninScreenTimezone,
+                                                value);
+  }
+
+  if (user_manager->GetPrimaryUser() == user && PerUserTimezoneEnabled())
+    CrosSettings::Get()->SetString(kSystemTimezone, value);
+}
+
+void SetSystemAndSigninScreenTimezone(const std::string& timezone) {
+  if (timezone.empty())
+    return;
+
+  g_browser_process->local_state()->SetString(prefs::kSigninScreenTimezone,
+                                              timezone);
+
+  std::string current_timezone_id;
+  CrosSettings::Get()->GetString(kSystemTimezone, &current_timezone_id);
+  if (current_timezone_id != timezone) {
+    system::TimezoneSettings::GetInstance()->SetTimezoneFromID(
+        base::UTF8ToUTF16(timezone));
+  }
+}
+
+bool PerUserTimezoneEnabled() {
+  return !base::CommandLine::ForCurrentProcess()->HasSwitch(
+      switches::kDisablePerUserTimezone);
+}
+
+void SetTimezoneFromUI(Profile* profile, const std::string& timezone_id) {
+  if (!PerUserTimezoneEnabled()) {
+    CrosSettings::Get()->SetString(kSystemTimezone, timezone_id);
+    return;
+  }
+
+  if (ProfileHelper::IsSigninProfile(profile)) {
+    SetSystemAndSigninScreenTimezone(timezone_id);
+    return;
+  }
+
+  if (ProfileHelper::IsEphemeralUserProfile(profile)) {
+    CrosSettings::Get()->SetString(kSystemTimezone, timezone_id);
+    return;
+  }
+
+  Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
+  if (primary_profile && profile->IsSameProfile(primary_profile)) {
+    profile->GetPrefs()->SetString(prefs::kUserTimezone, timezone_id);
+  }
+  // Time zone UI should be blocked for non-primary users.
+  NOTREACHED();
+}
+
 }  // namespace system
 }  // namespace chromeos
diff --git a/chrome/browser/chromeos/system/timezone_util.h b/chrome/browser/chromeos/system/timezone_util.h
index 92d8ade..ee6e612 100644
--- a/chrome/browser/chromeos/system/timezone_util.h
+++ b/chrome/browser/chromeos/system/timezone_util.h
@@ -9,6 +9,8 @@
 
 #include "base/strings/string16.h"
 
+class Profile;
+
 namespace base {
 class ListValue;
 }
@@ -31,6 +33,29 @@
 // Apply TimeZone update from TimeZoneProvider.
 void ApplyTimeZone(const TimeZoneResponseData* timezone);
 
+// Returns true if given timezone preference is enterprise-managed.
+// Works for:
+// - prefs::kUserTimezone
+// - prefs::kResolveTimezoneByGeolocation
+bool IsTimezonePrefsManaged(const std::string& pref_name);
+
+// Updates system timezone from user profile data if needed.
+// This is called from chromeos::Preferences after updating profile
+// preferences to apply new value to system time zone.
+void UpdateSystemTimezone(Profile* profile);
+
+// Updates Local State preference prefs::kSigninScreenTimezone AND
+// also immediately sets system timezone (chromeos::system::TimezoneSettings).
+// This is called when there is no user session (i.e. OOBE and signin screen),
+// or when device policies are updated.
+void SetSystemAndSigninScreenTimezone(const std::string& timezone);
+
+// Returns true if per-user timezone preferences are enabled.
+bool PerUserTimezoneEnabled();
+
+// This is called from UI code to apply user-selected time zone.
+void SetTimezoneFromUI(Profile* profile, const std::string& timezone_id);
+
 }  // namespace system
 }  // namespace chromeos
 
diff --git a/chrome/browser/extensions/api/settings_private/prefs_util.cc b/chrome/browser/extensions/api/settings_private/prefs_util.cc
index 882be39..1749f60 100644
--- a/chrome/browser/extensions/api/settings_private/prefs_util.cc
+++ b/chrome/browser/extensions/api/settings_private/prefs_util.cc
@@ -40,6 +40,7 @@
 #include "chrome/browser/chromeos/policy/browser_policy_connector_chromeos.h"
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chromeos/settings/cros_settings_names.h"
 #include "ui/chromeos/events/pref_names.h"
 #endif
@@ -50,13 +51,23 @@
 bool IsPrivilegedCrosSetting(const std::string& pref_name) {
   if (!chromeos::CrosSettings::IsCrosSettings(pref_name))
     return false;
-  // kSystemTimezone should be changeable by all users.
-  if (pref_name == chromeos::kSystemTimezone)
-    return false;
-  // All other Cros settings are considered privileged and are either policy
+  if (!chromeos::system::PerUserTimezoneEnabled()) {
+    // kSystemTimezone should be changeable by all users.
+    if (pref_name == chromeos::kSystemTimezone)
+      return false;
+  }
+  // Cros settings are considered privileged and are either policy
   // controlled or owner controlled.
   return true;
 }
+
+bool IsCrosSettingReadOnly(const std::string& pref_name) {
+  if (chromeos::system::PerUserTimezoneEnabled()) {
+    // System timezone is never directly changable by user.
+    return pref_name == chromeos::kSystemTimezone;
+  }
+  return false;
+}
 #endif
 
 }  // namespace
@@ -302,9 +313,13 @@
 
   // Timezone settings.
   (*s_whitelist)[chromeos::kSystemTimezone] =
-      settings_private::PrefType::PREF_TYPE_BOOLEAN;
+      settings_private::PrefType::PREF_TYPE_STRING;
+  (*s_whitelist)[prefs::kUserTimezone] =
+      settings_private::PrefType::PREF_TYPE_STRING;
   (*s_whitelist)[::prefs::kResolveTimezoneByGeolocation] =
       settings_private::PrefType::PREF_TYPE_BOOLEAN;
+  (*s_whitelist)[chromeos::kPerUserTimezoneEnabled] =
+      settings_private::PrefType::PREF_TYPE_BOOLEAN;
 
   // Ash settings.
   (*s_whitelist)[::prefs::kEnableStylusTools] =
@@ -474,6 +489,21 @@
   }
 
 #if defined(OS_CHROMEOS)
+  // We first check for enterprise-managed, then for primary-user managed.
+  // Otherwise in multiprofile mode enterprise preference for the secondary
+  // user will appear primary-user-controlled, which looks strange, because
+  // primary user preference will be disabled with "enterprise controlled"
+  // status.
+  if (IsPrefEnterpriseManaged(name)) {
+    // Enterprise managed prefs are treated the same as device policy restricted
+    // prefs in the UI.
+    pref_object->controlled_by =
+        settings_private::ControlledBy::CONTROLLED_BY_DEVICE_POLICY;
+    pref_object->enforcement =
+        settings_private::Enforcement::ENFORCEMENT_ENFORCED;
+    return pref_object;
+  }
+
   if (IsPrefPrimaryUserControlled(name)) {
     pref_object->controlled_by =
         settings_private::ControlledBy::CONTROLLED_BY_PRIMARY_USER;
@@ -486,16 +516,6 @@
                             .GetUserEmail()));
     return pref_object;
   }
-
-  if (IsPrefEnterpriseManaged(name)) {
-    // Enterprise managed prefs are treated the same as device policy restricted
-    // prefs in the UI.
-    pref_object->controlled_by =
-        settings_private::ControlledBy::CONTROLLED_BY_DEVICE_POLICY;
-    pref_object->enforcement =
-        settings_private::Enforcement::ENFORCEMENT_ENFORCED;
-    return pref_object;
-  }
 #endif
 
   if (pref && pref->IsManaged()) {
@@ -681,16 +701,26 @@
 
 #if defined(OS_CHROMEOS)
 bool PrefsUtil::IsPrefEnterpriseManaged(const std::string& pref_name) {
-  if (IsPrivilegedCrosSetting(pref_name)) {
-    policy::BrowserPolicyConnectorChromeOS* connector =
-        g_browser_process->platform_part()->browser_policy_connector_chromeos();
-    if (connector->IsEnterpriseManaged())
-      return true;
+  policy::BrowserPolicyConnectorChromeOS* connector =
+      g_browser_process->platform_part()->browser_policy_connector_chromeos();
+  if (!connector->IsEnterpriseManaged())
+    return false;
+  if (IsPrivilegedCrosSetting(pref_name))
+    return true;
+  if (chromeos::system::PerUserTimezoneEnabled() &&
+      (pref_name == prefs::kUserTimezone ||
+       pref_name == prefs::kResolveTimezoneByGeolocation)) {
+    return chromeos::system::IsTimezonePrefsManaged(pref_name);
   }
   return false;
 }
 
 bool PrefsUtil::IsPrefOwnerControlled(const std::string& pref_name) {
+  // chromeos::kSystemTimezone is global display-only preference and
+  // it should appear as disabled, but not owned.
+  if (pref_name == chromeos::kSystemTimezone)
+    return false;
+
   if (IsPrivilegedCrosSetting(pref_name)) {
     if (!chromeos::ProfileHelper::IsOwnerProfile(profile_))
       return true;
@@ -699,13 +729,19 @@
 }
 
 bool PrefsUtil::IsPrefPrimaryUserControlled(const std::string& pref_name) {
-  if (pref_name == prefs::kWakeOnWifiDarkConnect) {
+  // chromeos::kSystemTimezone is read-only, but for the non-primary users
+  // it should have "primary user controlled" attribute.
+  if (pref_name == prefs::kWakeOnWifiDarkConnect ||
+      pref_name == prefs::kResolveTimezoneByGeolocation ||
+      pref_name == prefs::kUserTimezone ||
+      pref_name == chromeos::kSystemTimezone) {
     user_manager::UserManager* user_manager = user_manager::UserManager::Get();
     const user_manager::User* user =
         chromeos::ProfileHelper::Get()->GetUserByProfile(profile_);
-    if (user &&
-        user->GetAccountId() != user_manager->GetPrimaryUser()->GetAccountId())
+    if (user && user->GetAccountId() !=
+                    user_manager->GetPrimaryUser()->GetAccountId()) {
       return true;
+    }
   }
   return false;
 }
@@ -720,6 +756,11 @@
 }
 
 bool PrefsUtil::IsPrefUserModifiable(const std::string& pref_name) {
+#if defined(OS_CHROMEOS)
+  if (IsCrosSettingReadOnly(pref_name))
+    return false;
+#endif
+
   const PrefService::Preference* profile_pref =
       profile_->GetPrefs()->FindPreference(pref_name);
   if (profile_pref)
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 1663dcd..8cbe8de 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2081,6 +2081,11 @@
     "Enables Instant Tethering. Instant Tethering allows your nearby Google "
     "phone to share its Internet connection with this device.";
 
+const char kEnablePerUserTimezoneName[] = "Per-user time zone preferences.";
+const char kEnablePerUserTimezoneDescription[] =
+    "Chrome OS system timezone preference is stored and handled for each user "
+    "individually.";
+
 #endif  // defined(OS_CHROMEOS)
 
 #if defined(OS_CHROMEOS)
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 11e3ff4..90e0e25 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1317,6 +1317,9 @@
 extern const char kDisableNewVirtualKeyboardBehaviorName[];
 extern const char kDisableNewVirtualKeyboardBehaviorDescription[];
 
+extern const char kEnablePerUserTimezoneName[];
+extern const char kEnablePerUserTimezoneDescription[];
+
 extern const char kDisableSystemTimezoneAutomaticDetectionName[];
 extern const char kDisableSystemTimezoneAutomaticDetectionDescription[];
 
diff --git a/chrome/browser/resources/options/browser_options.html b/chrome/browser/resources/options/browser_options.html
index 3d2ce964..e299d65 100644
--- a/chrome/browser/resources/options/browser_options.html
+++ b/chrome/browser/resources/options/browser_options.html
@@ -329,7 +329,7 @@
           <select class="control"
               id="timezone-value-select"
               i18n-options="timezoneList" data-type="string"
-              pref="cros.system.timezone"
+              pref="settings.timezone"
               aria-labelledby="timezone-value-label"
               metric="Options_TimezoneSelect"></select>
         </div>
diff --git a/chrome/browser/resources/settings/date_time_page/date_time_page.html b/chrome/browser/resources/settings/date_time_page/date_time_page.html
index b40d202..edab66b 100644
--- a/chrome/browser/resources/settings/date_time_page/date_time_page.html
+++ b/chrome/browser/resources/settings/date_time_page/date_time_page.html
@@ -35,25 +35,61 @@
       <div id="timezoneGeolocateToggleLabel" class="start">
         $i18n{timeZoneGeolocation}
       </div>
-      <template is="dom-if" if="[[hasTimeZoneAutoDetectPolicy_]]" restamp>
-        <cr-policy-indicator indicator-type="devicePolicy"
-            icon-aria-label="$i18n{timeZoneGeolocation}">
-        </cr-policy-indicator>
+      <template is="dom-if" restamp
+          if="[[!prefs.cros.flags.per_user_timezone_enabled.value]]">
+        <template is="dom-if" if="[[hasTimeZoneAutoDetectPolicy_]]" restamp>
+          <cr-policy-indicator indicator-type="devicePolicy"
+              icon-aria-label="$i18n{timeZoneGeolocation}">
+          </cr-policy-indicator>
+        </template>
+        <paper-toggle-button
+            id="timeZoneAutoDetect"
+            aria-labelledby="timezoneGeolocateToggleLabel"
+            checked="[[timeZoneAutoDetect_]]"
+            disabled="[[hasTimeZoneAutoDetectPolicy_]]"
+            on-change="onTimeZoneAutoDetectChange_">
+        </paper-toggle-button>
       </template>
-      <paper-toggle-button
-          id="timeZoneAutoDetect"
-          aria-labelledby="timezoneGeolocateToggleLabel"
-          checked="[[timeZoneAutoDetect_]]"
-          disabled="[[hasTimeZoneAutoDetectPolicy_]]"
-          on-change="onTimeZoneAutoDetectChange_">
-      </paper-toggle-button>
+      <template is="dom-if" restamp
+          if="[[prefs.cros.flags.per_user_timezone_enabled.value]]">
+        <settings-toggle-button
+            pref="{{prefs.settings.resolve_timezone_by_geolocation}}"
+            id="timeZoneAutoDetect"
+            aria-labelledby="timezoneGeolocateToggleLabel">
+        </settings-toggle-button>
+      </template>
     </div>
     <div class="settings-box continuation embedded">
-      <settings-dropdown-menu pref="{{prefs.cros.system.timezone}}"
-          label="$i18n{timeZone}"
-          menu-options="[[timeZoneList_]]"
-          disabled="[[timeZoneAutoDetect_]]">
-      </settings-dropdown-menu>
+      <template is="dom-if" restamp
+          if="[[!prefs.cros.flags.per_user_timezone_enabled.value]]">
+        <settings-dropdown-menu pref="{{prefs.cros.system.timezone}}"
+            label="$i18n{timeZone}"
+            menu-options="[[timeZoneList_]]"
+            disabled="[[timeZoneAutoDetect_]]">
+        </settings-dropdown-menu>
+      </template>
+      <template is="dom-if" restamp
+          if="[[prefs.cros.flags.per_user_timezone_enabled.value]]">
+        <template is="dom-if" if="[[!isUserTimeZoneSelectorHidden_(
+               prefs.settings.timezone,
+               prefs.settings.resolve_timezone_by_geolocation.value)]]" restamp>
+          <settings-dropdown-menu id="userTimeZoneSelector"
+              pref="{{prefs.settings.timezone}}"
+              label="$i18n{timeZone}"
+              menu-options="[[timeZoneList_]]">
+          </settings-dropdown-menu>
+        </template>
+        <template is="dom-if" if="[[isUserTimeZoneSelectorHidden_(
+               prefs.settings.timezone,
+               prefs.settings.resolve_timezone_by_geolocation.value)]]" restamp>
+          <settings-dropdown-menu id="systemTimezoneSelector"
+              pref="{{prefs.cros.system.timezone}}"
+              label="$i18n{timeZone}"
+              menu-options="[[timeZoneList_]]"
+              disabled>
+          </settings-dropdown-menu>
+        </template>
+      </template>
     </div>
     <div class="settings-box">
       <settings-toggle-button class="start"
diff --git a/chrome/browser/resources/settings/date_time_page/date_time_page.js b/chrome/browser/resources/settings/date_time_page/date_time_page.js
index 3b004f2..ef37894 100644
--- a/chrome/browser/resources/settings/date_time_page/date_time_page.js
+++ b/chrome/browser/resources/settings/date_time_page/date_time_page.js
@@ -90,7 +90,9 @@
   },
 
   observers: [
-    'maybeGetTimeZoneList_(' +
+    'maybeGetTimeZoneListPerUser_(' +
+        'prefs.settings.timezone.value, timeZoneAutoDetect_)',
+    'maybeGetTimeZoneListPerSystem_(' +
         'prefs.cros.system.timezone.value, timeZoneAutoDetect_)',
   ],
 
@@ -174,24 +176,54 @@
 
   /**
    * Fetches the list of time zones if necessary.
+   * @param {boolean=} perUserTimeZoneMode Expected value of per-user time zone.
    * @private
    */
-  maybeGetTimeZoneList_: function() {
+  maybeGetTimeZoneList_: function(perUserTimeZoneMode) {
+    if (typeof(perUserTimeZoneMode) !== 'undefined') {
+      /* This method is called as observer. Skip if if current mode does not
+       * match expected.
+       */
+      if (perUserTimeZoneMode !=
+          this.getPref('cros.flags.per_user_timezone_enabled').value) {
+        return;
+      }
+    }
     // Only fetch the list once.
     if (this.timeZoneList_.length > 1 || !CrSettingsPrefs.isInitialized)
       return;
 
     // If auto-detect is enabled, we only need the current time zone.
-    if (this.timeZoneAutoDetect_ &&
-        this.getPref('cros.system.timezone').value ==
-            this.timeZoneList_[0].value) {
-      return;
+    if (this.timeZoneAutoDetect_) {
+      var isPerUserTimezone =
+          this.getPref('cros.flags.per_user_timezone_enabled').value;
+      if (this.timeZoneList_[0].value ==
+          (isPerUserTimezone ? this.getPref('settings.timezone').value :
+                               this.getPref('cros.system.timezone').value)) {
+        return;
+      }
     }
 
     cr.sendWithPromise('getTimeZones').then(this.setTimeZoneList_.bind(this));
   },
 
   /**
+   * Prefs observer for Per-user time zone enabled mode.
+   * @private
+   */
+  maybeGetTimeZoneListPerUser_: function() {
+    this.maybeGetTimeZoneList_(true);
+  },
+
+  /**
+   * Prefs observer for Per-user time zone disabled mode.
+   * @private
+   */
+  maybeGetTimeZoneListPerSystem_: function() {
+    this.maybeGetTimeZoneList_(false);
+  },
+
+  /**
    * Converts the C++ response into an array of menu options.
    * @param {!Array<!Array<string>>} timeZones C++ time zones response.
    * @private
@@ -204,4 +236,17 @@
       };
     });
   },
+
+  /**
+   * Computes visibility of user timezone preference.
+   * @param {?chrome.settingsPrivate.PrefObject} prefUserTimezone
+   *     pref.settings.timezone
+   * @param {boolean} prefResolveValue
+   *     prefs.settings.resolve_timezone_by_geolocation.value
+   * @private
+   */
+  isUserTimeZoneSelectorHidden_: function(prefUserTimezone, prefResolveValue) {
+    return (prefUserTimezone && prefUserTimezone.controlledBy != null) ||
+        prefResolveValue;
+  },
 });
diff --git a/chrome/browser/ui/webui/chromeos/set_time_ui.cc b/chrome/browser/ui/webui/chromeos/set_time_ui.cc
index 98871678..bc44b7e 100644
--- a/chrome/browser/ui/webui/chromeos/set_time_ui.cc
+++ b/chrome/browser/ui/webui/chromeos/set_time_ui.cc
@@ -22,6 +22,7 @@
 #include "chromeos/dbus/system_clock_client.h"
 #include "chromeos/login/login_state.h"
 #include "chromeos/settings/timezone_settings.h"
+#include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui.h"
 #include "content/public/browser/web_ui_data_source.h"
 #include "content/public/browser/web_ui_message_handler.h"
@@ -63,6 +64,10 @@
     web_ui()->CallJavascriptFunctionUnsafe("settime.TimeSetter.updateTime");
   }
 
+  // UI actually shows real device timezone, but only allows changing the user
+  // timezone. If user timezone settings are different from system, this means
+  // that user settings are overriden and must be disabled. (And we will still
+  // show the actual device timezone.)
   // system::TimezoneSettings::Observer:
   void TimezoneChanged(const icu::TimeZone& timezone) override {
     base::Value timezone_id(system::TimezoneSettings::GetTimezoneID(timezone));
@@ -94,7 +99,10 @@
       return;
     }
 
-    CrosSettings::Get()->SetString(kSystemTimezone, timezone_id);
+    Profile* profile = Profile::FromBrowserContext(
+        web_ui()->GetWebContents()->GetBrowserContext());
+    DCHECK(profile);
+    system::SetTimezoneFromUI(profile, timezone_id);
   }
 
   DISALLOW_COPY_AND_ASSIGN(SetTimeMessageHandler);
diff --git a/chrome/browser/ui/webui/options/chromeos/core_chromeos_options_handler.cc b/chrome/browser/ui/webui/options/chromeos/core_chromeos_options_handler.cc
index 71db18f..201f458 100644
--- a/chrome/browser/ui/webui/options/chromeos/core_chromeos_options_handler.cc
+++ b/chrome/browser/ui/webui/options/chromeos/core_chromeos_options_handler.cc
@@ -26,6 +26,7 @@
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/proxy_cros_settings_parser.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
+#include "chrome/browser/chromeos/system/timezone_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/ash/session_controller_client.h"
 #include "chrome/browser/ui/webui/chromeos/ui_account_tweaks.h"
@@ -47,20 +48,34 @@
 
 namespace {
 
-// List of settings that should be changeable by all users.
-const char* kNonPrivilegedSettings[] = {
+// List of settings that are not changeable by users.
+const char* kReadOnlySettings[] = {
     kSystemTimezone
 };
 
 // List of settings that should only be changeable by the primary user.
 const char* kPrimaryUserSettings[] = {
     prefs::kWakeOnWifiDarkConnect,
+    prefs::kUserTimezone,
+    prefs::kResolveTimezoneByGeolocation
 };
 
 // Returns true if |pref| can be controlled (e.g. by policy or owner).
 bool IsSettingPrivileged(const std::string& pref) {
-  const char** end = kNonPrivilegedSettings + arraysize(kNonPrivilegedSettings);
-  return std::find(kNonPrivilegedSettings, end, pref) == end;
+  if (!chromeos::system::PerUserTimezoneEnabled()) {
+    return pref != kSystemTimezone;
+  }
+  // All the other Cros Settings are controlled.
+  return true;
+}
+
+// Returns true if |pref| is modifiable from UI.
+bool IsSettingWritable(const std::string& pref) {
+  if (!system::PerUserTimezoneEnabled())
+    return true;
+
+  const char** end = kReadOnlySettings + arraysize(kReadOnlySettings);
+  return std::find(kReadOnlySettings, end, pref) == end;
 }
 
 // Returns true if |pref| is shared (controlled by the primary user).
@@ -200,8 +215,14 @@
       return value;
     dict->SetString("controlledBy", "shared");
     dict->SetBoolean("disabled", true);
-    dict->SetBoolean("value", primary_profile->GetPrefs()->GetBoolean(
-        pref_name));
+    if (system::PerUserTimezoneEnabled()) {
+      const PrefService::Preference* pref =
+          primary_profile->GetPrefs()->FindPreference(pref_name);
+      dict->Set("value", base::MakeUnique<base::Value>(*pref->GetValue()));
+    } else {
+      dict->SetBoolean("value",
+                       primary_profile->GetPrefs()->GetBoolean(pref_name));
+    }
     return value;
   }
 
@@ -218,7 +239,7 @@
     dict->Set("value", base::MakeUnique<base::Value>(*pref_value));
 
   std::string controlled_by;
-  if (IsSettingPrivileged(pref_name)) {
+  if (system::PerUserTimezoneEnabled() || IsSettingPrivileged(pref_name)) {
     policy::BrowserPolicyConnectorChromeOS* connector =
         g_browser_process->platform_part()->browser_policy_connector_chromeos();
     if (connector->IsEnterpriseManaged())
@@ -226,9 +247,12 @@
     else if (!ProfileHelper::IsOwnerProfile(profile))
       controlled_by = "owner";
   }
-  dict->SetBoolean("disabled", !controlled_by.empty());
   if (!controlled_by.empty())
     dict->SetString("controlledBy", controlled_by);
+
+  // Read-only setting is always disabled.
+  dict->SetBoolean("disabled",
+                   !controlled_by.empty() || !IsSettingWritable(pref_name));
   return std::move(dict);
 }
 
@@ -262,6 +286,10 @@
   }
   if (!CrosSettings::IsCrosSettings(pref_name))
     return ::options::CoreOptionsHandler::SetPref(pref_name, value, metric);
+  if (!IsSettingWritable(pref_name)) {
+    NOTREACHED() << pref_name;
+    return;
+  }
   OwnerSettingsServiceChromeOS* service =
       OwnerSettingsServiceChromeOS::FromWebUI(web_ui());
   if (service && service->HandlesSetting(pref_name))
diff --git a/chrome/browser/ui/webui/settings/chromeos/cups_printers_handler.cc b/chrome/browser/ui/webui/settings/chromeos/cups_printers_handler.cc
index 2d8a809f..96389b0 100644
--- a/chrome/browser/ui/webui/settings/chromeos/cups_printers_handler.cc
+++ b/chrome/browser/ui/webui/settings/chromeos/cups_printers_handler.cc
@@ -54,6 +54,8 @@
 const char kIppsScheme[] = "ipps";
 
 const int kIppPort = 631;
+// IPPS commonly uses the HTTPS port despite the spec saying it should use the
+// IPP port.
 const int kIppsPort = 443;
 
 // These values are written to logs.  New enum values can be added, but existing
@@ -311,21 +313,23 @@
   url::ParseStandardURL(uri_ptr, printer_uri.length(), &parsed);
   base::StringPiece host(&printer_uri[parsed.host.begin], parsed.host.len);
 
+  bool encrypted = printer_protocol != kIppScheme;
   int port = ParsePort(uri_ptr, parsed.port);
+  // Port not specified.
   if (port == url::SpecialPort::PORT_UNSPECIFIED ||
       port == url::SpecialPort::PORT_INVALID) {
-    // imply port from protocol
     if (printer_protocol == kIppScheme) {
       port = kIppPort;
     } else if (printer_protocol == kIppsScheme) {
-      // ipps is ipp over https so it uses the https port.
       port = kIppsPort;
     } else {
+      // Port was not defined explicitly and scheme is not recognized.  Cannot
+      // infer a port number.
       NOTREACHED() << "Unrecognized protocol. Port was not set.";
     }
   }
 
-  QueryIppPrinter(host.as_string(), port, printer_queue,
+  QueryIppPrinter(host.as_string(), port, printer_queue, encrypted,
                   base::Bind(&CupsPrintersHandler::OnPrinterInfo,
                              weak_factory_.GetWeakPtr(), callback_id));
 }
diff --git a/chrome/browser/vr/BUILD.gn b/chrome/browser/vr/BUILD.gn
index 985ad1c..3e17b74 100644
--- a/chrome/browser/vr/BUILD.gn
+++ b/chrome/browser/vr/BUILD.gn
@@ -65,6 +65,8 @@
     "elements/system_indicator.h",
     "elements/system_indicator_texture.cc",
     "elements/system_indicator_texture.h",
+    "elements/text.cc",
+    "elements/text.h",
     "elements/textured_element.cc",
     "elements/textured_element.h",
     "elements/transience_manager.cc",
diff --git a/chrome/browser/vr/elements/text.cc b/chrome/browser/vr/elements/text.cc
new file mode 100644
index 0000000..c8909ec
--- /dev/null
+++ b/chrome/browser/vr/elements/text.cc
@@ -0,0 +1,86 @@
+// 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/vr/elements/text.h"
+
+#include "base/memory/ptr_util.h"
+#include "cc/paint/skia_paint_canvas.h"
+#include "chrome/browser/vr/elements/ui_texture.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/gfx/canvas.h"
+#include "ui/gfx/font_list.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/vector2d.h"
+#include "ui/gfx/render_text.h"
+
+namespace vr {
+
+class TextTexture : public UiTexture {
+ public:
+  TextTexture(int resource_id, float font_height, float text_width)
+      : resource_id_(resource_id),
+        font_height_(font_height),
+        text_width_(text_width) {}
+  ~TextTexture() override {}
+
+ private:
+  gfx::Size GetPreferredTextureSize(int width) const override {
+    return gfx::Size(width, width);
+  }
+
+  gfx::SizeF GetDrawnSize() const override { return size_; }
+
+  void Draw(SkCanvas* sk_canvas, const gfx::Size& texture_size) override;
+
+  gfx::SizeF size_;
+  int resource_id_;
+  // These widths are in meters.
+  float font_height_;
+  float text_width_;
+  // TODO(vollick): this should be parameterized.
+  SkColor text_color_ = 0xFF363636;
+
+  DISALLOW_COPY_AND_ASSIGN(TextTexture);
+};
+
+Text::Text(int maximum_width_pixels,
+           float font_height_meters,
+           float text_width_meters,
+           int resource_id)
+    : TexturedElement(maximum_width_pixels),
+      texture_(base::MakeUnique<TextTexture>(resource_id,
+                                             font_height_meters,
+                                             text_width_meters)) {}
+Text::~Text() {}
+
+UiTexture* Text::GetTexture() const {
+  return texture_.get();
+}
+
+void TextTexture::Draw(SkCanvas* sk_canvas, const gfx::Size& texture_size) {
+  cc::SkiaPaintCanvas paint_canvas(sk_canvas);
+  gfx::Canvas gfx_canvas(&paint_canvas, 1.0f);
+  gfx::Canvas* canvas = &gfx_canvas;
+
+  gfx::FontList fonts;
+  auto text = l10n_util::GetStringUTF16(resource_id_);
+  float pixels_per_meter = texture_size.width() / text_width_;
+  int pixel_font_height = static_cast<int>(font_height_ * pixels_per_meter);
+  GetFontList(pixel_font_height, text, &fonts);
+  gfx::Rect text_bounds(texture_size.width(), 0);
+
+  std::vector<std::unique_ptr<gfx::RenderText>> lines =
+      // TODO(vollick): if this subsumes all text, then we should probably move
+      // this function into this class.
+      PrepareDrawStringRect(text, fonts, text_color_, &text_bounds,
+                            kTextAlignmentCenter, kWrappingBehaviorWrap);
+  // Draw the text.
+  for (auto& render_text : lines)
+    render_text->Draw(canvas);
+
+  // Note, there is no padding here whatsoever.
+  size_ = gfx::SizeF(text_bounds.size());
+}
+
+}  // namespace vr
diff --git a/chrome/browser/vr/elements/text.h b/chrome/browser/vr/elements/text.h
new file mode 100644
index 0000000..9935cb7
--- /dev/null
+++ b/chrome/browser/vr/elements/text.h
@@ -0,0 +1,34 @@
+// 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_VR_ELEMENTS_TEXT_H_
+#define CHROME_BROWSER_VR_ELEMENTS_TEXT_H_
+
+#include <memory>
+
+#include "chrome/browser/vr/elements/textured_element.h"
+
+namespace vr {
+
+class TextTexture;
+class UiTexture;
+
+class Text : public TexturedElement {
+ public:
+  Text(int maximum_width_pixels,
+       float font_height_meters,
+       float text_width_meters,
+       int resource_id);
+  ~Text() override;
+
+ private:
+  UiTexture* GetTexture() const override;
+
+  std::unique_ptr<TextTexture> texture_;
+  DISALLOW_COPY_AND_ASSIGN(Text);
+};
+
+}  // namespace vr
+
+#endif  // CHROME_BROWSER_VR_ELEMENTS_TEXT_H_
diff --git a/chrome/browser/vr/elements/ui_element_debug_id.h b/chrome/browser/vr/elements/ui_element_debug_id.h
index 2574541a..04c6ff6f 100644
--- a/chrome/browser/vr/elements/ui_element_debug_id.h
+++ b/chrome/browser/vr/elements/ui_element_debug_id.h
@@ -41,6 +41,7 @@
   kBackgroundRight,
   kBackgroundTop,
   kBackgroundBottom,
+  kUnderDevelopmentNotice,
 };
 
 }  // namespace vr
diff --git a/chrome/browser/vr/ui_scene_manager.cc b/chrome/browser/vr/ui_scene_manager.cc
index 54f20e8..c5b0a2e 100644
--- a/chrome/browser/vr/ui_scene_manager.cc
+++ b/chrome/browser/vr/ui_scene_manager.cc
@@ -20,6 +20,7 @@
 #include "chrome/browser/vr/elements/screen_dimmer.h"
 #include "chrome/browser/vr/elements/splash_screen_icon.h"
 #include "chrome/browser/vr/elements/system_indicator.h"
+#include "chrome/browser/vr/elements/text.h"
 #include "chrome/browser/vr/elements/transient_url_bar.h"
 #include "chrome/browser/vr/elements/ui_element.h"
 #include "chrome/browser/vr/elements/ui_element_debug_id.h"
@@ -134,6 +135,14 @@
 // Tiny distance to offset textures that should appear in the same plane.
 static constexpr float kTextureOffset = 0.01;
 
+static constexpr float kUnderDevelopmentNoticeFontHeightM =
+    0.02f * kUrlBarDistance;
+static constexpr float kUnderDevelopmentNoticeHeightM = 0.1f * kUrlBarDistance;
+static constexpr float kUnderDevelopmentNoticeWidthM = 0.44f * kUrlBarDistance;
+static constexpr float kUnderDevelopmentNoticeVerticalOffsetM =
+    0.5f * kUnderDevelopmentNoticeHeightM + kUrlBarHeight;
+static constexpr float kUnderDevelopmentNoticeRotationRad = -0.19;
+
 enum DrawPhase : int {
   kPhaseBackground = 0,
   kPhaseFloorCeiling,
@@ -166,6 +175,7 @@
   CreateExitPrompt();
   CreateToasts();
   CreateSplashScreen();
+  CreateUnderDevelopmentNotice();
 
   ConfigureScene();
 }
@@ -333,6 +343,24 @@
   scene_->AddUiElement(std::move(icon));
 }
 
+void UiSceneManager::CreateUnderDevelopmentNotice() {
+  std::unique_ptr<Text> text = base::MakeUnique<Text>(
+      512, kUnderDevelopmentNoticeFontHeightM, kUnderDevelopmentNoticeWidthM,
+      IDS_VR_UNDER_DEVELOPMENT_NOTICE);
+  text->set_debug_id(kUnderDevelopmentNotice);
+  text->set_id(AllocateId());
+  text->set_draw_phase(kPhaseForeground);
+  text->set_hit_testable(false);
+  text->SetSize(kUnderDevelopmentNoticeWidthM, kUnderDevelopmentNoticeHeightM);
+  text->SetTranslate(0, -kUnderDevelopmentNoticeVerticalOffsetM, 0);
+  text->SetRotate(1, 0, 0, kUnderDevelopmentNoticeRotationRad);
+  text->SetEnabled(true);
+  text->set_y_anchoring(YAnchoring::YBOTTOM);
+  url_bar_->AddChild(text.get());
+  control_elements_.push_back(text.get());
+  scene_->AddUiElement(std::move(text));
+}
+
 void UiSceneManager::CreateBackground() {
 
   // Background solid-color panels.
diff --git a/chrome/browser/vr/ui_scene_manager.h b/chrome/browser/vr/ui_scene_manager.h
index 2fb94de..31767477 100644
--- a/chrome/browser/vr/ui_scene_manager.h
+++ b/chrome/browser/vr/ui_scene_manager.h
@@ -77,6 +77,7 @@
   void CreateSystemIndicators();
   void CreateContentQuad(ContentInputDelegate* delegate);
   void CreateSplashScreen();
+  void CreateUnderDevelopmentNotice();
   void CreateBackground();
   void CreateUrlBar();
   void CreateTransientUrlBar();
diff --git a/chrome/browser/vr/ui_scene_manager_unittest.cc b/chrome/browser/vr/ui_scene_manager_unittest.cc
index a8a36bda..d2cb8000 100644
--- a/chrome/browser/vr/ui_scene_manager_unittest.cc
+++ b/chrome/browser/vr/ui_scene_manager_unittest.cc
@@ -32,9 +32,10 @@
     kBackgroundFront, kBackgroundLeft,   kBackgroundBack, kBackgroundRight,
     kBackgroundTop,   kBackgroundBottom, kCeiling,        kFloor};
 std::set<UiElementDebugId> kElementsVisibleInBrowsing = {
-    kBackgroundFront, kBackgroundLeft,   kBackgroundBack, kBackgroundRight,
-    kBackgroundTop,   kBackgroundBottom, kCeiling,        kFloor,
-    kContentQuad,     kBackplane,        kUrlBar};
+    kBackgroundFront, kBackgroundLeft, kBackgroundBack,
+    kBackgroundRight, kBackgroundTop,  kBackgroundBottom,
+    kCeiling,         kFloor,          kContentQuad,
+    kBackplane,       kUrlBar,         kUnderDevelopmentNotice};
 std::set<UiElementDebugId> kElementsVisibleWithExitPrompt = {
     kBackgroundFront, kBackgroundLeft,     kBackgroundBack, kBackgroundRight,
     kBackgroundTop,   kBackgroundBottom,   kCeiling,        kFloor,
diff --git a/chrome/browser/web_applications/update_shortcut_worker_win.cc b/chrome/browser/web_applications/update_shortcut_worker_win.cc
index 34a1923..9d3ae82 100644
--- a/chrome/browser/web_applications/update_shortcut_worker_win.cc
+++ b/chrome/browser/web_applications/update_shortcut_worker_win.cc
@@ -86,10 +86,6 @@
 
   if (unprocessed_icons_.empty()) {
     // No app icon. Just use the favicon from WebContents.
-    // TODO(mgiuca): This Image is passed to the FILE thread while still being
-    // used on the UI thread. This is not thread-safe and needs to be fixed.
-    // Remove this thread-check disable. https://crbug.com/596348.
-    shortcut_info_->favicon.DisableThreadChecking();
     UpdateShortcuts();
     return;
   }
diff --git a/chrome/browser/web_applications/web_app.cc b/chrome/browser/web_applications/web_app.cc
index 2417537a..6f2a856 100644
--- a/chrome/browser/web_applications/web_app.cc
+++ b/chrome/browser/web_applications/web_app.cc
@@ -236,7 +236,11 @@
                                           web_contents->GetTitle()) :
       app_info.title;
   info->description = app_info.description;
-  info->favicon.Add(favicon_driver->GetFavicon());
+  // Even though GetFavicon returns a gfx::Image, we *deliberately* call
+  // AsImageSkia to get the internal ImageSkia, then construct a new gfx::Image.
+  // This ensures the ShortcutInfo's favicon does not share a backing store with
+  // |web_contents|, which would not be thread safe. https://crbug.com/596348.
+  info->favicon.Add(favicon_driver->GetFavicon().AsImageSkia());
 
   Profile* profile =
       Profile::FromBrowserContext(web_contents->GetBrowserContext());
diff --git a/chrome/browser/web_applications/web_app.h b/chrome/browser/web_applications/web_app.h
index 695af275..7a341c88 100644
--- a/chrome/browser/web_applications/web_app.h
+++ b/chrome/browser/web_applications/web_app.h
@@ -131,7 +131,9 @@
     ShortcutInfoCallback;
 
 #if defined(TOOLKIT_VIEWS)
-// Extracts shortcut info of the given WebContents.
+// Extracts shortcut info of the given WebContents. The result's |favicon|
+// member does *not* share a backing store with |web_contents| (so it is
+// safe to use it on another thread).
 std::unique_ptr<ShortcutInfo> GetShortcutInfoForTab(
     content::WebContents* web_contents);
 #endif
diff --git a/chrome/common/pref_names.cc b/chrome/common/pref_names.cc
index 6719125..3b455db41 100644
--- a/chrome/common/pref_names.cc
+++ b/chrome/common/pref_names.cc
@@ -544,6 +544,9 @@
 // A boolean pref set to true if time should be displayed in 24-hour clock.
 const char kUse24HourClock[] = "settings.clock.use_24hour_clock";
 
+// A string pref containing Timezone ID for this user.
+const char kUserTimezone[] = "settings.timezone";
+
 // This setting disables manual timezone selection and starts periodic timezone
 // refresh.
 const char kResolveTimezoneByGeolocation[] =
@@ -2034,6 +2037,10 @@
 const char kDebuggingFeaturesRequested[] = "DebuggingFeaturesRequested";
 
 #if defined(OS_CHROMEOS)
+// This setting controls initial device timezone that is used before user
+// session started. It is controlled by device owner.
+const char kSigninScreenTimezone[] = "settings.signin_screen_timezone";
+
 // This setting starts periodic timezone refresh when not in user session.
 // (user session is controlled by user profile preference
 // kResolveTimezoneByGeolocation
diff --git a/chrome/common/pref_names.h b/chrome/common/pref_names.h
index 0bfb05b..f4928d6 100644
--- a/chrome/common/pref_names.h
+++ b/chrome/common/pref_names.h
@@ -208,6 +208,7 @@
 extern const char kMouseSensitivity[];
 extern const char kTouchpadSensitivity[];
 extern const char kUse24HourClock[];
+extern const char kUserTimezone[];
 extern const char kResolveTimezoneByGeolocation[];
 // TODO(yusukes): Change "kLanguageABC" to "kABC". The current form is too long
 // to remember and confusing. The prefs are actually for input methods and i18n
@@ -758,6 +759,7 @@
 extern const char kDebuggingFeaturesRequested[];
 
 #if defined(OS_CHROMEOS)
+extern const char kSigninScreenTimezone[];
 extern const char kResolveDeviceTimezoneByGeolocation[];
 extern const char kSystemTimezoneAutomaticDetectionPolicy[];
 #endif  // defined(OS_CHROMEOS)
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 780ebd46..efc312a35 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2936,6 +2936,8 @@
     "//testing/scripts/common.py",
     "//testing/xvfb.py",
     "//testing/scripts/run_telemetry_as_googletest.py",
+    # For smoke testing run_telemetry_benchmark_as_googletest
+    "//testing/scripts/run_telemetry_benchmark_as_googletest.py",
   ]
 
   if (enable_package_mash_services) {
diff --git a/chrome/test/data/webui/settings/date_time_page_tests.js b/chrome/test/data/webui/settings/date_time_page_tests.js
index 7a48d13..d6375374 100644
--- a/chrome/test/data/webui/settings/date_time_page_tests.js
+++ b/chrome/test/data/webui/settings/date_time_page_tests.js
@@ -13,6 +13,15 @@
             value: 'Westeros/Kings_Landing',
           },
         },
+        flags: {
+          // TODO(alemate): This test should be run for both values of this
+          // option.
+          per_user_timezone_enabled: {
+            key: 'cros.flags.per_user_timezone_enabled',
+            type: chrome.settingsPrivate.PrefType.BOOLEAN,
+            value: true,
+          },
+        },
       },
       settings: {
         clock: {
@@ -27,10 +36,41 @@
           type: chrome.settingsPrivate.PrefType.BOOLEAN,
           value: true,
         },
+        timezone: {
+          key: 'settings.timezone',
+          type: chrome.settingsPrivate.PrefType.STRING,
+          value: 'Westeros/Kings_Landing',
+        },
       },
     };
   }
 
+  function updatePrefsWithPolicy(prefs, managed, valueFromPolicy) {
+    var prefsCopy = JSON.parse(JSON.stringify(prefs));
+    if (managed) {
+      prefsCopy.settings.resolve_timezone_by_geolocation.controlledBy =
+          chrome.settingsPrivate.ControlledBy.USER_POLICY;
+      prefsCopy.settings.resolve_timezone_by_geolocation.enforcement =
+          chrome.settingsPrivate.Enforcement.ENFORCED;
+      prefsCopy.settings.resolve_timezone_by_geolocation.value =
+          valueFromPolicy;
+      prefsCopy.settings.timezone.controlledBy =
+          chrome.settingsPrivate.ControlledBy.USER_POLICY;
+      prefsCopy.settings.timezone.enforcement =
+          chrome.settingsPrivate.Enforcement.ENFORCED;
+    } else {
+      prefsCopy.settings.resolve_timezone_by_geolocation.controlledBy =
+          undefined;
+      prefsCopy.settings.resolve_timezone_by_geolocation.enforcement =
+          undefined;
+      // Auto-resolve defaults to true.
+      prefsCopy.settings.resolve_timezone_by_geolocation.value = true;
+      prefsCopy.settings.timezone.controlledBy = undefined;
+      prefsCopy.settings.timezone.enforcement = undefined;
+    }
+    return prefsCopy;
+  }
+
   /**
    * Sets up fakes and creates the date time element.
    * @param {!Object} prefs
@@ -57,10 +97,13 @@
     loadTimeData.data = data;
 
     var dateTime = document.createElement('settings-date-time-page');
-    dateTime.prefs = prefs;
+    dateTime.prefs =
+        updatePrefsWithPolicy(prefs, hasPolicy, opt_autoDetectPolicyValue);
     CrSettingsPrefs.setInitialized();
 
     document.body.appendChild(dateTime);
+    cr.webUIListenerCallback(
+        'time-zone-auto-detect-policy', hasPolicy, opt_autoDetectPolicyValue);
     return dateTime;
   }
 
@@ -110,14 +153,24 @@
       PolymerTest.clearBody();
     });
 
-    function verifyAutoDetectSetting(autoDetect) {
-      assertEquals(autoDetect, dateTime.$$('settings-dropdown-menu').disabled);
-      assertEquals(autoDetect, dateTime.$.timeZoneAutoDetect.checked);
+    function verifyAutoDetectSetting(autoDetect, managed) {
+      Polymer.dom.flush();
+      var selector = dateTime.$$('#userTimeZoneSelector');
+      var selectorHidden = selector ? selector.hidden : true;
+      assertEquals(managed || autoDetect, selectorHidden);
+
+      var checkButton = dateTime.$$('#timeZoneAutoDetect');
+      var checkButtonChecked = checkButton ? checkButton.checked : false;
+      if (!managed)
+        assertEquals(autoDetect, checkButtonChecked);
     }
 
     function verifyPolicy(policy) {
       Polymer.dom.flush();
-      var indicator = dateTime.$$('cr-policy-indicator');
+      var indicator =
+          dateTime.$$('#timeZoneAutoDetect').$$('cr-policy-pref-indicator');
+      if (indicator && indicator.style.display == 'none')
+        indicator = null;
 
       if (policy) {
         assertTrue(!!indicator);
@@ -127,17 +180,31 @@
         assertFalse(!!indicator);
       }
 
-      assertEquals(policy, dateTime.$.timeZoneAutoDetect.disabled);
+      assertEquals(
+          policy, dateTime.$$('#timeZoneAutoDetect').$$('#control').disabled);
     }
 
     function verifyTimeZonesPopulated(populated) {
-      var dropdown = dateTime.$$('settings-dropdown-menu');
+      Polymer.dom.flush();
+      var userTimezoneDropdown = dateTime.$$('#userTimeZoneSelector');
+      var systemTimezoneDropdown = dateTime.$$('#systemTimezoneSelector');
+
+      var dropdown =
+          userTimezoneDropdown ? userTimezoneDropdown : systemTimezoneDropdown;
       if (populated)
         assertEquals(fakeTimeZones.length, dropdown.menuOptions.length);
       else
         assertEquals(1, dropdown.menuOptions.length);
     }
 
+    function updatePolicy(dateTime, managed, valueFromPolicy) {
+      dateTime.prefs =
+          updatePrefsWithPolicy(dateTime.prefs, managed, valueFromPolicy);
+      cr.webUIListenerCallback(
+          'time-zone-auto-detect-policy', managed, valueFromPolicy);
+      Polymer.dom.flush();
+    }
+
     test('auto-detect on', function(done) {
       var prefs = getFakePrefs();
       dateTime = initializeDateTime(prefs, false);
@@ -145,13 +212,13 @@
       assertTrue(dateTimePageReadyCalled);
       assertFalse(getTimeZonesCalled);
 
-      verifyAutoDetectSetting(true);
+      verifyAutoDetectSetting(true, false);
       verifyTimeZonesPopulated(false);
       verifyPolicy(false);
 
       // Disable auto-detect.
-      MockInteractions.tap(dateTime.$.timeZoneAutoDetect);
-      verifyAutoDetectSetting(false);
+      MockInteractions.tap(dateTime.$$('#timeZoneAutoDetect').$$('#control'));
+      verifyAutoDetectSetting(false, false);
       assertTrue(getTimeZonesCalled);
 
       setTimeout(function() {
@@ -161,22 +228,21 @@
     });
 
     test('auto-detect off', function(done) {
-      var prefs = getFakePrefs();
-      prefs.settings.resolve_timezone_by_geolocation.value = false;
-      dateTime = initializeDateTime(prefs, false);
-      cr.webUIListenerCallback('time-zone-auto-detect-policy', false);
+      dateTime = initializeDateTime(getFakePrefs(), false);
+      dateTime.set(
+          'prefs.settings.resolve_timezone_by_geolocation.value', false);
 
       assertTrue(dateTimePageReadyCalled);
       assertTrue(getTimeZonesCalled);
 
-      verifyAutoDetectSetting(false);
+      verifyAutoDetectSetting(false, false);
       verifyPolicy(false);
 
       setTimeout(function() {
         verifyTimeZonesPopulated(true);
 
         // Enable auto-detect.
-        MockInteractions.tap(dateTime.$.timeZoneAutoDetect);
+        MockInteractions.tap(dateTime.$$('#timeZoneAutoDetect').$$('#control'));
         verifyAutoDetectSetting(true);
         done();
       });
@@ -184,25 +250,25 @@
 
     test('auto-detect forced on', function(done) {
       var prefs = getFakePrefs();
-      prefs.settings.resolve_timezone_by_geolocation.value = false;
       dateTime = initializeDateTime(prefs, true, true);
-      cr.webUIListenerCallback('time-zone-auto-detect-policy', true, true);
+      dateTime.set(
+          'prefs.settings.resolve_timezone_by_geolocation.value', false);
 
       assertTrue(dateTimePageReadyCalled);
       assertFalse(getTimeZonesCalled);
 
-      verifyAutoDetectSetting(true);
+      verifyAutoDetectSetting(true, true);
       verifyTimeZonesPopulated(false);
       verifyPolicy(true);
 
       // Cannot disable auto-detect.
-      MockInteractions.tap(dateTime.$.timeZoneAutoDetect);
-      verifyAutoDetectSetting(true);
+      MockInteractions.tap(dateTime.$$('#timeZoneAutoDetect').$$('#control'));
+      verifyAutoDetectSetting(true, true);
       assertFalse(getTimeZonesCalled);
 
       // Update the policy: force auto-detect off.
-      cr.webUIListenerCallback('time-zone-auto-detect-policy', true, false);
-      verifyAutoDetectSetting(false);
+      updatePolicy(dateTime, true, false);
+      verifyAutoDetectSetting(false, true);
       verifyPolicy(true);
 
       assertTrue(getTimeZonesCalled);
@@ -215,32 +281,30 @@
     test('auto-detect forced off', function(done) {
       var prefs = getFakePrefs();
       dateTime = initializeDateTime(prefs, true, false);
-      cr.webUIListenerCallback('time-zone-auto-detect-policy', true, false);
 
       assertTrue(dateTimePageReadyCalled);
       assertTrue(getTimeZonesCalled);
 
-      verifyAutoDetectSetting(false);
+      verifyAutoDetectSetting(false, true);
       verifyPolicy(true);
 
       setTimeout(function() {
         verifyTimeZonesPopulated(true);
 
         // Remove the policy so user's preference takes effect.
-        cr.webUIListenerCallback('time-zone-auto-detect-policy', false);
-        verifyAutoDetectSetting(true);
+        updatePolicy(dateTime, false);
+        verifyAutoDetectSetting(true, false);
         verifyPolicy(false);
 
         // User can disable auto-detect.
-        MockInteractions.tap(dateTime.$.timeZoneAutoDetect);
-        verifyAutoDetectSetting(false);
+        MockInteractions.tap(dateTime.$$('#timeZoneAutoDetect').$$('#control'));
+        verifyAutoDetectSetting(false, false);
         done();
       });
     });
 
     test('set date and time button', function() {
       dateTime = initializeDateTime(getFakePrefs(), false);
-      cr.webUIListenerCallback('time-zone-auto-detect-policy', false);
 
       var showSetDateTimeUICalled = false;
       registerMessageCallback('showSetDateTimeUI', null, function() {
@@ -248,7 +312,7 @@
         showSetDateTimeUICalled = true;
       });
 
-      var setDateTimeButton = dateTime.$.setDateTime;
+      var setDateTimeButton = dateTime.$$('#setDateTime');
       assertEquals(0, setDateTimeButton.offsetHeight);
 
       // Make the date and time editable.
diff --git a/chromeos/chromeos_switches.cc b/chromeos/chromeos_switches.cc
index b1d2ffa4..e900198 100644
--- a/chromeos/chromeos_switches.cc
+++ b/chromeos/chromeos_switches.cc
@@ -522,6 +522,9 @@
 const char kEnterpriseEnableLicenseTypeSelection[] =
     "enterprise-enable-license-type-selection";
 
+// Disables per-user timezone.
+const char kDisablePerUserTimezone[] = "disable-per-user-timezone";
+
 bool WakeOnWifiEnabled() {
   return !base::CommandLine::ForCurrentProcess()->HasSwitch(kDisableWakeOnWifi);
 }
diff --git a/chromeos/chromeos_switches.h b/chromeos/chromeos_switches.h
index 701cfe4..598ceb0 100644
--- a/chromeos/chromeos_switches.h
+++ b/chromeos/chromeos_switches.h
@@ -149,6 +149,7 @@
 CHROMEOS_EXPORT extern const char kCrosGaiaApiV1[];
 CHROMEOS_EXPORT extern const char kVoiceInteractionLocales[];
 CHROMEOS_EXPORT extern const char kEnterpriseEnableLicenseTypeSelection[];
+CHROMEOS_EXPORT extern const char kDisablePerUserTimezone[];
 
 // Returns true if the system should wake in response to wifi traffic.
 CHROMEOS_EXPORT bool WakeOnWifiEnabled();
diff --git a/chromeos/settings/cros_settings_names.cc b/chromeos/settings/cros_settings_names.cc
index e203881..34a8ebd 100644
--- a/chromeos/settings/cros_settings_names.cc
+++ b/chromeos/settings/cros_settings_names.cc
@@ -245,4 +245,7 @@
 const char kDeviceLoginScreenInputMethods[] =
     "cros.device_login_screen_input_methods";
 
+// A boolean pref that matches enable-per-user-time-zone chrome://flags value.
+const char kPerUserTimezoneEnabled[] = "cros.flags.per_user_timezone_enabled";
+
 }  // namespace chromeos
diff --git a/chromeos/settings/cros_settings_names.h b/chromeos/settings/cros_settings_names.h
index 0034a20e..06dac28d 100644
--- a/chromeos/settings/cros_settings_names.h
+++ b/chromeos/settings/cros_settings_names.h
@@ -119,6 +119,8 @@
 CHROMEOS_EXPORT extern const char kDeviceLoginScreenLocales[];
 CHROMEOS_EXPORT extern const char kDeviceLoginScreenInputMethods[];
 
+CHROMEOS_EXPORT extern const char kPerUserTimezoneEnabled[];
+
 }  // namespace chromeos
 
 #endif  // CHROMEOS_SETTINGS_CROS_SETTINGS_NAMES_H_
diff --git a/components/metrics/drive_metrics_provider_linux.cc b/components/metrics/drive_metrics_provider_linux.cc
index 194e1ee..0ba3a9c 100644
--- a/components/metrics/drive_metrics_provider_linux.cc
+++ b/components/metrics/drive_metrics_provider_linux.cc
@@ -34,6 +34,8 @@
 bool DriveMetricsProvider::HasSeekPenalty(const base::FilePath& path,
                                           bool* has_seek_penalty) {
 #if defined(OS_CHROMEOS)
+  // TODO(derat): Remove special-casing after October 2017 when parrot (Acer C7
+  // Chromebook) is unsupported.
   std::string board = base::SysInfo::GetStrippedReleaseBoard();
   // There are "parrot", "parrot_ivb" and "parrot_freon" boards that have
   // devices with rotating disks. All other ChromeOS devices have SSDs.
diff --git a/content/child/runtime_features.cc b/content/child/runtime_features.cc
index 7f5b9ed..6e98c09 100644
--- a/content/child/runtime_features.cc
+++ b/content/child/runtime_features.cc
@@ -333,6 +333,9 @@
   WebRuntimeFeatures::EnableMojoBlobs(
       base::FeatureList::IsEnabled(features::kMojoBlobs));
 
+  WebRuntimeFeatures::EnableNetworkService(
+      base::FeatureList::IsEnabled(features::kNetworkService));
+
   if (base::FeatureList::IsEnabled(features::kGamepadExtensions))
     WebRuntimeFeatures::EnableGamepadExtensions(true);
 
diff --git a/content/test/gpu/gpu_tests/webgl_conformance_revision.txt b/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
index 223e1a0..ad668dcd 100644
--- a/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
+++ b/content/test/gpu/gpu_tests/webgl_conformance_revision.txt
@@ -1,3 +1,3 @@
 # AUTOGENERATED FILE - DO NOT EDIT
 # SEE roll_webgl_conformance.py
-Current webgl revision 72eda82d069da578af04e5c4e8e411ae006b6a18
+Current webgl revision ca7121e53f7794a60ac2948c34c224ee921a8910
diff --git a/ios/build/xcode_install_wrapper.py b/ios/build/xcode_install_wrapper.py
deleted file mode 100755
index b7b53fcb..0000000
--- a/ios/build/xcode_install_wrapper.py
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env python
-#
-# 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.
-
-"""Wrapper script to install Xcode. For use on the bots.
-
-Installing Xcode requires sudo. This script will be whitelisted in /etc/sudoers
-so that we can easily update Xcode on our bots.
-"""
-
-import argparse
-import hashlib
-import logging
-import os
-import subprocess
-import sys
-
-
-# List of sha256 hashes of Xcode package file
-# Contents/Resources/Packages/XcodeSystemResources.pkg
-WHITELISTED_PACKAGES = [
-  # Xcode versions 8A218a-1, 8A218a-2
-  '3d948c4bd7c8941478a60aece3cb98ef936a57a4cc1c8f8d5f7eef70d0ebbad1',
-  # Xcode versions 8B62-1, 8C1002-1
-  '21ed3271af2ac7d67c6c09a9481a1fd5bb886950990365bb936dde7585e09061',
-  # Xcode versions 8E2002-1
-  'ff2377d3976f7acf2adfe5ca91c2e6cc59dd112647efa0bf39e9d9decd2ee1b3',
-  # Xcode versions 9M136h-1
-  '2ec798b123bcfa7f8c0e5618c193ecd26ddce87f2e27f6b5d2fb0720926e5464',
-  # Xcode versions 9M137d-1
-  '6c5f4a2fd6dc477f8f06ccd6f6119da540dd5fe3a6036357dbe9c13d611fc366',
-]
-
-def main():
-  logging.basicConfig(level=logging.DEBUG)
-
-  parser = argparse.ArgumentParser(
-      description='Wrapper script to install Xcode.')
-  parser.add_argument(
-      '--package-path',
-      help='Path to Xcode package to install.',
-      required=True)
-  args = parser.parse_args()
-
-  if not os.path.isfile(args.package_path):
-    logging.critical('Path %s is not a file.' % args.package_path)
-    return 1
-
-  sha256 = hashlib.sha256()
-  with open(args.package_path, 'rb') as f:
-    for chunk in iter(lambda: f.read(8192), b''):
-      sha256.update(chunk)
-  sha256_hash = sha256.hexdigest()
-
-  if sha256_hash not in WHITELISTED_PACKAGES:
-    logging.critical('Attempted to install a non-whitelisted Xcode package.')
-    return 1
-
-  pipe = subprocess.Popen(
-      ['/usr/sbin/installer', '-pkg', args.package_path, '-target', '/'],
-      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-  stdout, stderr = pipe.communicate()
-
-  for line in stdout.splitlines():
-    logging.debug(line)
-  for line in stderr.splitlines():
-    logging.error(line)
-
-  return pipe.returncode
-
-
-if __name__ == '__main__':
-  sys.exit(main())
diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc
index f0f2e762..ced6324 100644
--- a/net/http/http_cache_transaction.cc
+++ b/net/http/http_cache_transaction.cc
@@ -2051,9 +2051,18 @@
   if (result > 0) {
     read_offset_ += result;
   } else if (result == 0) {  // End of file.
-    RecordHistograms();
-    cache_->DoneReadingFromEntry(entry_, this);
-    entry_ = NULL;
+    // TODO(shivanisha@), ideally it should not happen that |this| is not a
+    // reader but referenced from entry in another field, but it seems to be
+    // happening in some edge case (crbug.com/752774). Thus not invoking
+    // DoneReadingFromEntry here.
+    if (entry_->writer == this) {
+      DoneWritingToEntry(true);
+    } else {
+      RecordHistograms();
+      cache_->DoneWithEntry(entry_, this, false /* process_cancel */,
+                            partial_ != nullptr);
+      entry_ = NULL;
+    }
   } else {
     return OnCacheReadError(result, false);
   }
diff --git a/printing/backend/cups_jobs.cc b/printing/backend/cups_jobs.cc
index 80daed7..18c303a9 100644
--- a/printing/backend/cups_jobs.cc
+++ b/printing/backend/cups_jobs.cc
@@ -395,6 +395,8 @@
   ippAddStrings(request.get(), IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
                 kRequestedAttributes, num_attributes, nullptr, attributes);
 
+  DCHECK_EQ(ippValidateAttributes(request.get()), 1);
+
   auto response = WrapIpp(cupsDoRequest(http, request.release(), rp.c_str()));
   *status = ippGetStatusCode(response.get());
 
@@ -427,12 +429,14 @@
 bool GetPrinterInfo(const std::string& address,
                     const int port,
                     const std::string& resource,
+                    bool encrypted,
                     PrinterInfo* printer_info) {
   base::ThreadRestrictions::AssertIOAllowed();
 
-  ScopedHttpPtr http = ScopedHttpPtr(
-      httpConnect2(address.data(), port, nullptr, AF_INET,
-                   HTTP_ENCRYPTION_IF_REQUESTED, 0, 200, nullptr));
+  ScopedHttpPtr http = ScopedHttpPtr(httpConnect2(
+      address.data(), port, nullptr, AF_INET,
+      encrypted ? HTTP_ENCRYPTION_REQUIRED : HTTP_ENCRYPTION_IF_REQUESTED, 0,
+      200, nullptr));
   if (!http) {
     LOG(WARNING) << "Could not connect to host";
     return false;
diff --git a/printing/backend/cups_jobs.h b/printing/backend/cups_jobs.h
index 8c0e4d4b..c2606eb 100644
--- a/printing/backend/cups_jobs.h
+++ b/printing/backend/cups_jobs.h
@@ -153,10 +153,12 @@
 void ParsePrinterStatus(ipp_t* response, PrinterStatus* printer_status);
 
 // Queries the printer at |address| on |port| with a Get-Printer-Attributes
-// request to populate |printer_info|.  Returns false if the request failed.
+// request to populate |printer_info|. If |encrypted| is true, request is made
+// using ipps, otherwise, ipp is used. Returns false if the request failed.
 bool PRINTING_EXPORT GetPrinterInfo(const std::string& address,
                                     const int port,
                                     const std::string& resource,
+                                    bool encrypted,
                                     PrinterInfo* printer_info);
 
 // Attempts to retrieve printer status using connection |http| for |printer_id|.
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 19d9833b..7f2de04 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -13650,14 +13650,28 @@
           "can_use_on_swarming_builders": true,
           "dimension_sets": [
             {
-              "os": "Mac-10.12"
+              "gpu": "8086:0d26",
+              "hidpi": "1",
+              "os": "Mac-10.11.6"
+            },
+            {
+              "gpu": "none",
+              "os": "Mac-10.9.5"
+            },
+            {
+              "gpu": "none",
+              "os": "Mac-10.10.5"
+            },
+            {
+              "gpu": "none",
+              "os": "Mac-10.11.6"
             },
             {
               "gpu": "8086:0a2e",
-              "os": "Mac-10.12"
+              "os": "Mac-10.12.5"
             }
           ],
-          "shards": 8
+          "shards": 2
         }
       }
     ]
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-animation-responsive-expected.html b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-transform-responsive-expected.html
similarity index 100%
rename from third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-animation-responsive-expected.html
rename to third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-transform-responsive-expected.html
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-animation-responsive.html b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-transform-responsive.html
similarity index 100%
rename from third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-animation-responsive.html
rename to third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-transform-responsive.html
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive-expected.html b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive-expected.html
new file mode 100644
index 0000000..853c7b5
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive-expected.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<style>
+#target {
+  position: absolute;
+  width: 100px;
+  height: 100px;
+  background: blue;
+  translate: calc(50vw - 50px) calc(50vh - 50px);
+}
+</style>
+This box should be located in the center of the page.
+<div id="target"></div>
+<script>
+if (window.testRunner)  {
+  testRunner.useUnfortunateSynchronousResizeMode();
+}
+window.resizeTo(window.innerWidth / 2, window.innerHeight / 2);
+</script>
\ No newline at end of file
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive.html b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive.html
new file mode 100644
index 0000000..2bc9f10
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/animations/responsive/viewport-unit-translate-responsive.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<style>
+#target {
+  position: absolute;
+  width: 100px;
+  height: 100px;
+  background: blue;
+}
+</style>
+This box should be located in the center of the page.
+<div id="target"></div>
+<script>
+if (window.testRunner) {
+  testRunner.useUnfortunateSynchronousResizeMode();
+  testRunner.waitUntilDone();
+}
+function waitForCompositor() {
+  return document.body.animate({opacity: [1, 1]}, 1).finished;
+}
+
+var position = 'calc(50vw - 50px) calc(50vh - 50px)';
+var animation = target.animate({translate: [position, position]}, 1e10);
+animation.ready.then(() => {
+  window.resizeTo(window.innerWidth / 2, window.innerHeight / 2);
+}).then(waitForCompositor).then(() => {
+  if (window.testRunner) {
+    testRunner.notifyDone();
+  }
+});
+</script>
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation-expected.html b/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation-expected.html
new file mode 100644
index 0000000..8cbd2baa
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation-expected.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<style>
+#container {
+  position: absolute;
+  top: 0;
+  font-size: 10px;
+}
+.target {
+  width: 40px;
+  height: 40px;
+  border-top: solid;
+  border-left: solid;
+  margin-bottom: 20px;
+}
+</style>
+<div id="container"></div>
+<div id="footer"></div>
+<script>
+'use strict';
+internals.setZoomFactor(2);
+
+[
+  '10px 10px 10px',
+  'none', // Composited animations fail to zoom the last expectation correctly. ):
+].forEach(translation => {
+  var text = document.createElement('div');
+  text.textContent = translation;
+  container.appendChild(text);
+
+  var target = document.createElement('div');
+  target.classList.add('target');
+  container.appendChild(target);
+  target.animate([
+    {translate: translation},
+    {translate: translation},
+  ], 1e8);
+});
+
+// We must wait a frame to let compositor animations render.
+if (window.testRunner)
+  testRunner.waitUntilDone();
+
+function waitForCompositor() {
+  return footer.animate({opacity: ['1', '1']}, 1).finished;
+}
+
+requestAnimationFrame(() => {
+  requestAnimationFrame(() => {
+    waitForCompositor().then(() => {
+      if (window.testRunner)
+        testRunner.notifyDone();
+    });
+  });
+});
+</script>
diff --git a/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation.html b/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation.html
new file mode 100644
index 0000000..3b8b5a31
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/animations/responsive/zoom-responsive-translate-animation.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<style>
+#container {
+  position: absolute;
+  top: 0;
+  font-size: 10px;
+}
+.target {
+  width: 40px;
+  height: 40px;
+  border-top: solid;
+  border-left: solid;
+  margin-bottom: 20px;
+}
+</style>
+<div id="container"></div>
+<div id="footer"></div>
+<script>
+'use strict';
+[
+  '10px 10px 10px',
+  'none', // Composited animations fail to zoom the last expectation correctly. ):
+].forEach(translation => {
+  var text = document.createElement('div');
+  text.textContent = translation;
+  container.appendChild(text);
+
+  var target = document.createElement('div');
+  target.classList.add('target');
+  container.appendChild(target);
+  target.animate([
+    {translate: translation},
+    {translate: translation},
+  ], 1e8);
+});
+
+if (window.testRunner)
+  testRunner.waitUntilDone();
+
+function waitForCompositor() {
+  return footer.animate({opacity: ['1', '1']}, 1).finished;
+}
+
+requestAnimationFrame(() => {
+  requestAnimationFrame(() => {
+    internals.setZoomFactor(2);
+    requestAnimationFrame(() => {
+      requestAnimationFrame(() => {
+        waitForCompositor().then(() => {
+          if (window.testRunner)
+            testRunner.notifyDone();
+        });
+      });
+    });
+  });
+});
+</script>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/resources/interpolation-testcommon.js b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/resources/interpolation-testcommon.js
new file mode 100644
index 0000000..5ab5551
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/resources/interpolation-testcommon.js
@@ -0,0 +1,54 @@
+'use strict';
+function test_interpolation(settings, expectations) {
+  // Returns a timing function that at 0.5 evaluates to progress.
+  function timingFunction(progress) {
+    if (progress === 0)
+      return 'steps(1, end)';
+    if (progress === 1)
+      return 'steps(1, start)';
+    var y = (8 * progress - 1) / 6;
+    return 'cubic-bezier(0, ' + y + ', 1, ' + y + ')';
+  }
+
+  test(function(){
+    assert_true(CSS.supports(settings.property, settings.from), 'Value "' + settings.from + '" is supported by ' + settings.property);
+    assert_true(CSS.supports(settings.property, settings.to), 'Value "' + settings.to + '" is supported by ' + settings.property);
+  }, '"' + settings.from + '" and "' + settings.to + '" are valid ' + settings.property + ' values');
+
+  for (var i = 0; i < expectations.length; ++i) {
+    var progress = expectations[i].at;
+    var expectation = expectations[i].expect;
+    var animationId = 'anim' + i;
+    var targetId = 'target' + i;
+    var referenceId = 'reference' + i;
+
+    test(function(){
+      assert_true(CSS.supports(settings.property, expectation), 'Value "' + expectation + '" is supported by ' + settings.property);
+
+      var stylesheet = document.createElement('style');
+      stylesheet.textContent =
+        '#' + targetId + ' {\n' +
+        '  animation: 2s ' + timingFunction(progress) + ' -1s paused ' + animationId + ';\n' +
+        '}\n' +
+        '@keyframes ' + animationId + ' {\n' +
+        '  0% { ' + settings.property + ': ' + settings.from + '; }\n' +
+        '  100% { ' + settings.property + ': ' + settings.to + '; }\n' +
+        '}\n' +
+        '#' + referenceId + ' {\n' +
+        '  ' + settings.property + ': ' + expectation + ';\n' +
+        '}\n';
+      document.head.appendChild(stylesheet);
+
+      var target = document.createElement('div');
+      target.id = targetId;
+      document.body.appendChild(target);
+
+      var reference = document.createElement('div');
+      reference.id = referenceId;
+      document.body.appendChild(reference);
+      reference.style = '';
+
+      assert_equals(getComputedStyle(target)[settings.property], getComputedStyle(reference)[settings.property]);
+    }, 'Animation between "' + settings.from + '" and "' + settings.to + '" at progress ' + progress);
+  }
+}
diff --git a/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/rotate-interpolation.html b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/rotate-interpolation.html
new file mode 100644
index 0000000..ec96ba0b
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/rotate-interpolation.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>rotate interpolation</title>
+    <link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+    <link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-rotate">
+    <meta name="assert" content="rotate supports animation.">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="resources/interpolation-testcommon.js"></script>
+  </head>
+  <body>
+    <script>
+      test_interpolation({
+        property: 'rotate',
+        from: '100deg',
+        to: '180deg',
+      }, [
+        {at: -1, expect: '20deg'},
+        {at: 0, expect: '100deg'},
+        {at: 0.125, expect: '110deg'},
+        {at: 0.875, expect: '170deg'},
+        {at: 1, expect: '180deg'},
+        {at: 2, expect: '260deg'}
+      ]);
+
+      test_interpolation({
+        property: 'rotate',
+        from: '45deg',
+        to: '-1 1 0 60deg',
+      }, [
+        {at: -1, expect: '0.447214 -0.447214 0.774597 104.478deg'},
+        {at: 0, expect: '45deg'},
+        {at: 0.125, expect: '-0.136456 0.136456 0.981203 40.6037deg'},
+        {at: 0.875, expect: '-0.70246 0.70246 0.114452 53.1994deg'},
+        {at: 1, expect: '-1 1 0 60deg'},
+        {at: 2, expect: '-0.637897 0.637897 -0.431479 124.975deg'}
+      ]);
+
+      test_interpolation({
+        property: 'rotate',
+        from: 'none',
+        to: '7 -8 9 400grad',
+      }, [
+        {at: -1, expect: '7 -8 9 -400grad'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: '7 -8 9 50grad'},
+        {at: 0.875, expect: '7 -8 9 350grad'},
+        {at: 1, expect: '7 -8 9 400grad'},
+        {at: 2, expect: '7 -8 9 800grad'}
+      ]);
+
+      test_interpolation({
+        property: 'rotate',
+        from: 'none',
+        to: 'none',
+      }, [
+        {at: -1, expect: 'none'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: 'none'},
+        {at: 0.875, expect: 'none'},
+        {at: 1, expect: 'none'},
+        {at: 2, expect: 'none'}
+      ]);
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/scale-interpolation.html b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/scale-interpolation.html
new file mode 100644
index 0000000..da25d3a
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/scale-interpolation.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>scale interpolation</title>
+    <link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+    <link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-scale">
+    <meta name="assert" content="scale supports animation.">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="resources/interpolation-testcommon.js"></script>
+  </head>
+  <body>
+    <script>
+      test_interpolation({
+        property: 'scale',
+        from: '2 30 400',
+        to: '10 110 1200',
+      }, [
+        {at: -1, expect: '-6 -50 -400'},
+        {at: 0, expect: '2 30 400'},
+        {at: 0.125, expect: '3 40 500'},
+        {at: 0.875, expect: '9 100 1100'},
+        {at: 1, expect: '10 110 1200'},
+        {at: 2, expect: '18 190 2000'}
+      ]);
+
+      test_interpolation({
+        property: 'scale',
+        from: '26 17 9',
+        to: '2',
+      }, [
+        {at: -1, expect: '50 33 17'},
+        {at: 0, expect: '26 17 9'},
+        {at: 0.125, expect: '23 15 8'},
+        {at: 0.875, expect: '5 3 2'},
+        {at: 1, expect: '2'},
+        {at: 2, expect: '-22 -15 -7'}
+      ]);
+
+      test_interpolation({
+        property: 'scale',
+        from: 'none',
+        to: '4 3 2',
+      }, [
+        {at: -1, expect: '-2 -1 0'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: '1.375 1.25 1.125'},
+        {at: 0.875, expect: '3.625 2.75 1.875'},
+        {at: 1, expect: '4 3 2'},
+        {at: 2, expect: '7 5 3'}
+      ]);
+
+      test_interpolation({
+        property: 'scale',
+        from: 'none',
+        to: 'none',
+      }, [
+        {at: -1, expect: 'none'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: 'none'},
+        {at: 0.875, expect: 'none'},
+        {at: 1, expect: 'none'},
+        {at: 2, expect: 'none'}
+      ]);
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/translate-interpolation.html b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/translate-interpolation.html
new file mode 100644
index 0000000..d5d47f7
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/css/css-transforms-2/animation/translate-interpolation.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>translate interpolation</title>
+    <link rel="author" title="Eric Willigers" href="mailto:ericwilligers@chromium.org">
+    <link rel="help" href="https://drafts.csswg.org/css-transforms-2/#propdef-translate">
+    <meta name="assert" content="translate supports <length> and <percentage> animation.">
+    <script src="/resources/testharness.js"></script>
+    <script src="/resources/testharnessreport.js"></script>
+    <script src="resources/interpolation-testcommon.js"></script>
+    <style>
+      body {
+        width: 500px;
+        height: 500px;
+      }
+      div {
+        width: 10px;
+        height: 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <script>
+      test_interpolation({
+        property: 'translate',
+        from: '220px 240px 260px',
+        to: '300px 400px 500px',
+      }, [
+        {at: -1, expect: '140px 80px 20px'},
+        {at: 0, expect: '220px 240px 260px'},
+        {at: 0.125, expect: '230px 260px 290px'},
+        {at: 0.875, expect: '290px 380px 470px'},
+        {at: 1, expect: '300px 400px 500px'},
+        {at: 2, expect: '380px 560px 740px'}
+      ]);
+
+      test_interpolation({
+        property: 'translate',
+        from: '480px 400px 320px',
+        to: '240% 160%',
+      }, [
+        {at: -1, expect: 'calc(960px - 240%) calc(800px - 160%) 640px'},
+        {at: 0, expect: '480px 400px 320px'},
+        {at: 0.125, expect: 'calc(420px + 30%) calc(350px + 20%) 280px'},
+        {at: 0.875, expect: 'calc(210% + 60px) calc(140% + 50px) 40px'},
+        {at: 1, expect: '240% 160%'},
+        {at: 2, expect: 'calc(480% - 480px) calc(320% - 400px) -320px'}
+      ]);
+
+      test_interpolation({
+        property: 'translate',
+        from: 'none',
+        to: '8px 80% 800px',
+      }, [
+        {at: -1, expect: '-8px -80% -800px'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: '1px 10% 100px'},
+        {at: 0.875, expect: '7px 70% 700px'},
+        {at: 1, expect: '8px 80% 800px'},
+        {at: 2, expect: '16px 160% 1600px'}
+      ]);
+
+      test_interpolation({
+        property: 'translate',
+        from: 'none',
+        to: 'none',
+      }, [
+        {at: -1, expect: 'none'},
+        {at: 0, expect: 'none'},
+        {at: 0.125, expect: 'none'},
+        {at: 0.875, expect: 'none'},
+        {at: 1, expect: 'none'},
+        {at: 2, expect: 'none'}
+      ]);
+    </script>
+  </body>
+</html>
diff --git a/third_party/WebKit/LayoutTests/mojo/bind-interface.html b/third_party/WebKit/LayoutTests/mojo/bind-interface.html
index eaa08c3..26c4867 100644
--- a/third_party/WebKit/LayoutTests/mojo/bind-interface.html
+++ b/third_party/WebKit/LayoutTests/mojo/bind-interface.html
@@ -77,25 +77,41 @@
   return Promise.reject();
 }, "interface interceptors are mutually exclusive");
 
-test(() => {
+test(async t => {
   const kTestInterfaceName = "foo::mojom::Bar";
-  let interceptedHandle = null;
 
+  // First check that the interceptor can be started and intercepts requests.
   let interceptor = new MojoInterfaceInterceptor(kTestInterfaceName);
-  interceptor.oninterfacerequest = e => { interceptedHandle = e.handle; };
+  let promise = new Promise(resolve => {
+    interceptor.oninterfacerequest = e => {
+      resolve(e.handle);
+    };
+  });
   interceptor.start();
 
-  let {handle0, handle1} = Mojo.createMessagePipe();
-  Mojo.bindInterface(kTestInterfaceName, handle0);
-  interceptor.stop();
-
+  let pipe = Mojo.createMessagePipe();
+  Mojo.bindInterface(kTestInterfaceName, pipe.handle0);
+  let interceptedHandle = await promise;
   assert_true(interceptedHandle instanceof MojoHandle);
   interceptedHandle.close();
-  interceptedHandle = null;
+  pipe.handle1.close();
 
-  Mojo.bindInterface(kTestInterfaceName, handle1);
-  assert_equals(interceptedHandle, null);
-  handle1.close();
+  // Stop the interceptor and make another request.
+  interceptor.stop();
+
+  pipe = Mojo.createMessagePipe();
+  interceptor.oninterfacerequest = t.step_func(() => {
+    assert_unreached("unexpected 'interfacerequest' event after stop");
+  });
+  promise = new Promise(resolve => {
+    let watcher = pipe.handle1.watch({peerClosed: true}, () => {
+      watcher.cancel();  // Necessary to avoid a DCHECK when handle1 is closed.
+      resolve();
+    });
+  });
+  Mojo.bindInterface(kTestInterfaceName, pipe.handle0);
+  await promise;
+  pipe.handle1.close();
 
   interceptor = new MojoInterfaceInterceptor(kTestInterfaceName);
   interceptor.oninterfacerequest = e => {};
diff --git a/third_party/WebKit/Source/core/animation/css/CSSAnimations.cpp b/third_party/WebKit/Source/core/animation/css/CSSAnimations.cpp
index 09352d9..1e9bcec 100644
--- a/third_party/WebKit/Source/core/animation/css/CSSAnimations.cpp
+++ b/third_party/WebKit/Source/core/animation/css/CSSAnimations.cpp
@@ -265,7 +265,8 @@
 
     bool update_compositor_keyframes = false;
     if ((transform_zoom_changed || was_viewport_resized) &&
-        keyframe_effect->Affects(PropertyHandle(CSSPropertyTransform)) &&
+        (keyframe_effect->Affects(PropertyHandle(CSSPropertyTransform)) ||
+         keyframe_effect->Affects(PropertyHandle(CSSPropertyTranslate))) &&
         keyframe_effect->SnapshotAllCompositorKeyframes(element, style,
                                                         parent_style)) {
       update_compositor_keyframes = true;
diff --git a/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.cpp b/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.cpp
index 3e930b7..e570adee 100644
--- a/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.cpp
+++ b/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.cpp
@@ -7,6 +7,7 @@
 #include "bindings/core/v8/ExceptionState.h"
 #include "core/dom/Document.h"
 #include "core/dom/ExecutionContext.h"
+#include "core/dom/TaskRunnerHelper.h"
 #include "core/frame/LocalDOMWindow.h"
 #include "core/frame/LocalFrame.h"
 #include "core/frame/LocalFrameClient.h"
@@ -14,6 +15,7 @@
 #include "core/mojo/test/MojoInterfaceRequestEvent.h"
 #include "core/workers/WorkerGlobalScope.h"
 #include "core/workers/WorkerThread.h"
+#include "platform/WebTaskRunner.h"
 #include "platform/bindings/ScriptState.h"
 #include "platform/wtf/text/StringUTF8Adaptor.h"
 #include "services/service_manager/public/cpp/interface_provider.h"
@@ -118,6 +120,20 @@
 
 void MojoInterfaceInterceptor::OnInterfaceRequest(
     mojo::ScopedMessagePipeHandle handle) {
+  // Execution of JavaScript may be forbidden in this context as this method is
+  // called synchronously by the InterfaceProvider. Dispatching of the
+  // 'interfacerequest' event is therefore scheduled to take place in the next
+  // microtask. This also more closely mirrors the behavior when an interface
+  // request is being satisfied by another process.
+  TaskRunnerHelper::Get(TaskType::kMicrotask, GetExecutionContext())
+      ->PostTask(
+          BLINK_FROM_HERE,
+          WTF::Bind(&MojoInterfaceInterceptor::DispatchInterfaceRequestEvent,
+                    WrapPersistent(this), WTF::Passed(std::move(handle))));
+}
+
+void MojoInterfaceInterceptor::DispatchInterfaceRequestEvent(
+    mojo::ScopedMessagePipeHandle handle) {
   DispatchEvent(MojoInterfaceRequestEvent::Create(
       MojoHandle::Create(mojo::ScopedHandle::From(std::move(handle)))));
 }
diff --git a/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.h b/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.h
index ff73826..9877b32a 100644
--- a/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.h
+++ b/third_party/WebKit/Source/core/mojo/test/MojoInterfaceInterceptor.h
@@ -65,6 +65,7 @@
 
   service_manager::InterfaceProvider* GetInterfaceProvider() const;
   void OnInterfaceRequest(mojo::ScopedMessagePipeHandle);
+  void DispatchInterfaceRequestEvent(mojo::ScopedMessagePipeHandle);
 
   const String interface_name_;
   bool started_ = false;
diff --git a/third_party/WebKit/Source/core/xmlhttprequest/XMLHttpRequest.cpp b/third_party/WebKit/Source/core/xmlhttprequest/XMLHttpRequest.cpp
index efea764..9376e9a 100644
--- a/third_party/WebKit/Source/core/xmlhttprequest/XMLHttpRequest.cpp
+++ b/third_party/WebKit/Source/core/xmlhttprequest/XMLHttpRequest.cpp
@@ -1083,7 +1083,9 @@
 
   // When responseType is set to "blob", we redirect the downloaded data to a
   // file-handle directly.
-  downloading_to_file_ = GetResponseTypeCode() == kResponseTypeBlob;
+  // TODO: implement this for network service code path. http://crbug.com/754493
+  if (!RuntimeEnabledFeatures::NetworkServiceEnabled())
+    downloading_to_file_ = GetResponseTypeCode() == kResponseTypeBlob;
   if (downloading_to_file_) {
     request.SetDownloadToFile(true);
     resource_loader_options.data_buffering_policy = kDoNotBufferData;
diff --git a/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5 b/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5
index 80cba1e0..43d271e3 100644
--- a/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5
+++ b/third_party/WebKit/Source/platform/RuntimeEnabledFeatures.json5
@@ -710,6 +710,9 @@
       name: "NetInfoRtt",
       status: "stable",
     },
+    {
+      name: "NetworkService",
+    },
     // Not a web exposed feature, enabled from the command line.
     {
       name: "NewRemotePlaybackPipeline",
diff --git a/third_party/WebKit/Source/platform/exported/WebRuntimeFeatures.cpp b/third_party/WebKit/Source/platform/exported/WebRuntimeFeatures.cpp
index 5af69d5c..d2eac73 100644
--- a/third_party/WebKit/Source/platform/exported/WebRuntimeFeatures.cpp
+++ b/third_party/WebKit/Source/platform/exported/WebRuntimeFeatures.cpp
@@ -208,6 +208,10 @@
   RuntimeEnabledFeatures::SetNetInfoDownlinkMaxEnabled(enable);
 }
 
+void WebRuntimeFeatures::EnableNetworkService(bool enable) {
+  RuntimeEnabledFeatures::SetNetworkServiceEnabled(enable);
+}
+
 void WebRuntimeFeatures::EnableOffMainThreadFetch(bool enable) {
   RuntimeEnabledFeatures::SetOffMainThreadFetchEnabled(enable);
 }
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
index 0d7ebea..844aa27 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
@@ -798,15 +798,11 @@
             scroll_node.main_thread_scrolling_reasons);
 
   auto* layer = ContentLayerAt(0);
+  auto transform_node_index = layer->transform_tree_index();
+  EXPECT_EQ(transform_node_index, transform_node.id);
   auto scroll_node_index = layer->scroll_tree_index();
   EXPECT_EQ(scroll_node_index, scroll_node.id);
 
-  // Only one content layer, and the first child layer is the dummy layer for
-  // the transform node.
-  const cc::Layer* transform_node_layer = RootLayer()->children()[0].get();
-  auto transform_node_index = transform_node_layer->transform_tree_index();
-  EXPECT_EQ(transform_node_index, transform_node.id);
-
   EXPECT_EQ(0u, scroll_client.did_scroll_count);
   // TODO(pdr): The PaintArtifactCompositor should set the scrolling content
   // bounds so the Layer is scrollable. This call should be removed.
@@ -1715,31 +1711,6 @@
             ElementIdToTransformNodeIndex(transform->GetCompositorElementId()));
 }
 
-TEST_F(PaintArtifactCompositorTestWithPropertyTrees,
-       TransformNodeHasOwningLayerId) {
-  RefPtr<TransformPaintPropertyNode> transform =
-      TransformPaintPropertyNode::Create(
-          TransformPaintPropertyNode::Root(), TransformationMatrix().Rotate(90),
-          FloatPoint3D(100, 100, 0), false, 0, kCompositingReason3DTransform);
-
-  TestPaintArtifact artifact;
-  artifact
-      .Chunk(transform, ClipPaintPropertyNode::Root(),
-             EffectPaintPropertyNode::Root())
-      .RectDrawing(FloatRect(100, 100, 200, 100), Color::kBlack);
-  Update(artifact.Build());
-
-  // Only one content layer, and the first child layer is the dummy layer for
-  // the transform node.
-  ASSERT_EQ(1u, ContentLayerCount());
-  const cc::Layer* transform_node_layer = RootLayer()->children()[0].get();
-  const cc::TransformNode* cc_transform_node =
-      GetPropertyTrees().transform_tree.Node(
-          transform_node_layer->transform_tree_index());
-  auto transform_node_index = transform_node_layer->transform_tree_index();
-  EXPECT_EQ(transform_node_index, cc_transform_node->id);
-}
-
 TEST_F(PaintArtifactCompositorTestWithPropertyTrees, EffectWithElementId) {
   RefPtr<EffectPaintPropertyNode> effect =
       CreateSampleEffectNodeWithElementId();
@@ -1826,7 +1797,7 @@
 
   // Two content layers for the differentiated rect drawings and three dummy
   // layers for each of the transform, clip and effect nodes.
-  EXPECT_EQ(5u, RootLayer()->children().size());
+  EXPECT_EQ(2u, RootLayer()->children().size());
   int sequence_number = GetPropertyTrees().sequence_number;
   EXPECT_GT(sequence_number, 0);
   for (auto layer : RootLayer()->children()) {
@@ -1835,7 +1806,7 @@
 
   Update(artifact.Build());
 
-  EXPECT_EQ(5u, RootLayer()->children().size());
+  EXPECT_EQ(2u, RootLayer()->children().size());
   sequence_number++;
   EXPECT_EQ(sequence_number, GetPropertyTrees().sequence_number);
   for (auto layer : RootLayer()->children()) {
@@ -1844,7 +1815,7 @@
 
   Update(artifact.Build());
 
-  EXPECT_EQ(5u, RootLayer()->children().size());
+  EXPECT_EQ(2u, RootLayer()->children().size());
   sequence_number++;
   EXPECT_EQ(sequence_number, GetPropertyTrees().sequence_number);
   for (auto layer : RootLayer()->children()) {
@@ -2344,27 +2315,26 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //                l2
-  //      l1 [ mask_effect_0 ]
-  // (l0) [ mask_isolation_0 ]
-  // [          e0           ]
-  // One content layer, one dummy, one clip mask.
-  ASSERT_EQ(3u, RootLayer()->children().size());
+  //           l1
+  // l0 [ mask_effect_0 ]
+  // [ mask_isolation_0 ]
+  // [        e0        ]
+  // One content layer, one clip mask.
+  ASSERT_EQ(2u, RootLayer()->children().size());
   ASSERT_EQ(1u, ContentLayerCount());
   ASSERT_EQ(1u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[2].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[1].get();
 
+  constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
@@ -2403,44 +2373,40 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //                        l4
-  //      l1 (l2) l3 [ mask_effect_0 ]
-  // (l0) [     mask_isolation_0     ]
-  // [              e0               ]
-  // Two content layers, two dummy, one clip mask.
-  ASSERT_EQ(5u, RootLayer()->children().size());
+  //              l2
+  // l0 l1 [ mask_effect_0 ]
+  // [  mask_isolation_0   ]
+  // [         e0          ]
+  // Two content layers, one clip mask.
+  ASSERT_EQ(3u, RootLayer()->children().size());
   ASSERT_EQ(2u, ContentLayerCount());
   ASSERT_EQ(1u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* dummy_t1 = RootLayer()->children()[2].get();
-  const cc::Layer* content1 = RootLayer()->children()[3].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[4].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* content1 = RootLayer()->children()[1].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[2].get();
 
   constexpr int t0_id = 1;
+  constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
   EXPECT_EQ(t0_id, content0->transform_tree_index());
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
   ASSERT_EQ(e0_id, mask_isolation_0.parent_id);
   EXPECT_EQ(SkBlendMode::kSrcOver, mask_isolation_0.blend_mode);
 
-  int t1_id = dummy_t1->transform_tree_index();
+  EXPECT_EQ(ContentLayerAt(1), content1);
+  int t1_id = content1->transform_tree_index();
   const cc::TransformNode& cc_t1 =
       *GetPropertyTrees().transform_tree.Node(t1_id);
   ASSERT_EQ(t0_id, cc_t1.parent_id);
-
-  EXPECT_EQ(ContentLayerAt(1), content1);
-  EXPECT_EQ(t1_id, content1->transform_tree_index());
   EXPECT_EQ(c1_id, content1->clip_tree_index());
   EXPECT_EQ(mask_isolation_0_id, content1->effect_tree_index());
 
@@ -2480,45 +2446,37 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //                     l3                        l6
-  //      l1 (l2) [ mask_effect_0 ]    l5 [   mask_effect_1   ]
-  // (l0) [   mask_isolation_0    ] l4 [   mask_isolation_1   ]
-  // [                          e0                            ]
-  // Three content layers, two dummy, two clip mask.
-  ASSERT_EQ(7u, RootLayer()->children().size());
+  //           l1                      l4
+  // l0 [ mask_effect_0 ]    l3 [ mask_effect_1 ]
+  // [ mask_isolation_0 ] l2 [ mask_isolation_1 ]
+  // [                    e0                    ]
+  // Three content layers, two clip mask.
+  ASSERT_EQ(5u, RootLayer()->children().size());
   ASSERT_EQ(3u, ContentLayerCount());
   ASSERT_EQ(2u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* dummy_t1 = RootLayer()->children()[2].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[3].get();
-  const cc::Layer* content1 = RootLayer()->children()[4].get();
-  const cc::Layer* content2 = RootLayer()->children()[5].get();
-  const cc::Layer* clip_mask1 = RootLayer()->children()[6].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[1].get();
+  const cc::Layer* content1 = RootLayer()->children()[2].get();
+  const cc::Layer* content2 = RootLayer()->children()[3].get();
+  const cc::Layer* clip_mask1 = RootLayer()->children()[4].get();
 
   constexpr int t0_id = 1;
   constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
   EXPECT_EQ(t0_id, content0->transform_tree_index());
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
   ASSERT_EQ(e0_id, mask_isolation_0.parent_id);
   EXPECT_EQ(SkBlendMode::kSrcOver, mask_isolation_0.blend_mode);
 
-  int t1_id = dummy_t1->transform_tree_index();
-  const cc::TransformNode& cc_t1 =
-      *GetPropertyTrees().transform_tree.Node(t1_id);
-  ASSERT_EQ(t0_id, cc_t1.parent_id);
-
   EXPECT_EQ(SynthesizedClipLayerAt(0), clip_mask0);
   EXPECT_EQ(gfx::Size(300, 200), clip_mask0->bounds());
   EXPECT_EQ(t0_id, clip_mask0->transform_tree_index());
@@ -2530,7 +2488,10 @@
   EXPECT_EQ(SkBlendMode::kDstIn, mask_effect_0.blend_mode);
 
   EXPECT_EQ(ContentLayerAt(1), content1);
-  EXPECT_EQ(t1_id, content1->transform_tree_index());
+  int t1_id = content1->transform_tree_index();
+  const cc::TransformNode& cc_t1 =
+      *GetPropertyTrees().transform_tree.Node(t1_id);
+  ASSERT_EQ(t0_id, cc_t1.parent_id);
   EXPECT_EQ(c0_id, content1->clip_tree_index());
   EXPECT_EQ(e0_id, content1->effect_tree_index());
 
@@ -2579,43 +2540,39 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //         (l2) l3           l5
-  //      l1 [ e1  ] l4 [ mask_effect_0 ]
-  // (l0) [      mask_isolation_0       ]
-  // [               e0                 ]
-  // Three content layers, two dummy, one clip mask.
-  ASSERT_EQ(6u, RootLayer()->children().size());
+  //      l1             l3
+  // l0 [ e1 ] l2 [ mask_effect_0 ]
+  // [      mask_isolation_0      ]
+  // [             e0             ]
+  // Three content layers, one clip mask.
+  ASSERT_EQ(4u, RootLayer()->children().size());
   ASSERT_EQ(3u, ContentLayerCount());
   ASSERT_EQ(1u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* dummy_e1 = RootLayer()->children()[2].get();
-  const cc::Layer* content1 = RootLayer()->children()[3].get();
-  const cc::Layer* content2 = RootLayer()->children()[4].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[5].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* content1 = RootLayer()->children()[1].get();
+  const cc::Layer* content2 = RootLayer()->children()[2].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[3].get();
 
+  constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
   ASSERT_EQ(e0_id, mask_isolation_0.parent_id);
   EXPECT_EQ(SkBlendMode::kSrcOver, mask_isolation_0.blend_mode);
 
-  int e1_id = dummy_e1->effect_tree_index();
-  const cc::EffectNode& cc_e1 = *GetPropertyTrees().effect_tree.Node(e1_id);
-  ASSERT_EQ(mask_isolation_0_id, cc_e1.parent_id);
-
   EXPECT_EQ(ContentLayerAt(1), content1);
   EXPECT_EQ(c1_id, content1->clip_tree_index());
-  EXPECT_EQ(e1_id, content1->effect_tree_index());
+  int e1_id = content1->effect_tree_index();
+  const cc::EffectNode& cc_e1 = *GetPropertyTrees().effect_tree.Node(e1_id);
+  ASSERT_EQ(mask_isolation_0_id, cc_e1.parent_id);
 
   EXPECT_EQ(ContentLayerAt(2), content2);
   EXPECT_EQ(c1_id, content2->clip_tree_index());
@@ -2658,33 +2615,31 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //                                          l5
-  //                l2              l4 [ mask_effect_1 ]           l7
-  //      l1 [ mask_effect_0 ] (l3) [ mask_isolation_1 ] l6 [ mask_effect_2 ]
-  // (l0) [ mask_isolation_0 ][           e1           ][ mask_isolation_2  ]
-  // [                                  e0                                  ]
-  // Three content layers, two dummy, three clip mask.
-  ASSERT_EQ(8u, RootLayer()->children().size());
+  //                               l3
+  //           l1        l2 [ mask_effect_1 ]           l5
+  // l0 [ mask_effect_0 ][ mask_isolation_1 ] l4 [ mask_effect_2 ]
+  // [ mask_isolation_0 ][        e1        ][ mask_isolation_2  ]
+  // [                            e0                             ]
+  // Three content layers, three clip mask.
+  ASSERT_EQ(6u, RootLayer()->children().size());
   ASSERT_EQ(3u, ContentLayerCount());
   ASSERT_EQ(3u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[2].get();
-  const cc::Layer* dummy_e1 = RootLayer()->children()[3].get();
-  const cc::Layer* content1 = RootLayer()->children()[4].get();
-  const cc::Layer* clip_mask1 = RootLayer()->children()[5].get();
-  const cc::Layer* content2 = RootLayer()->children()[6].get();
-  const cc::Layer* clip_mask2 = RootLayer()->children()[7].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[1].get();
+  const cc::Layer* content1 = RootLayer()->children()[2].get();
+  const cc::Layer* clip_mask1 = RootLayer()->children()[3].get();
+  const cc::Layer* content2 = RootLayer()->children()[4].get();
+  const cc::Layer* clip_mask2 = RootLayer()->children()[5].get();
 
+  constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
@@ -2700,18 +2655,16 @@
   ASSERT_EQ(mask_isolation_0_id, mask_effect_0.parent_id);
   EXPECT_EQ(SkBlendMode::kDstIn, mask_effect_0.blend_mode);
 
-  int e1_id = dummy_e1->effect_tree_index();
-  const cc::EffectNode& cc_e1 = *GetPropertyTrees().effect_tree.Node(e1_id);
-  ASSERT_EQ(e0_id, cc_e1.parent_id);
-
   EXPECT_EQ(ContentLayerAt(1), content1);
   EXPECT_EQ(c1_id, content1->clip_tree_index());
   int mask_isolation_1_id = content1->effect_tree_index();
   const cc::EffectNode& mask_isolation_1 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_1_id);
   EXPECT_NE(mask_isolation_0_id, mask_isolation_1_id);
-  ASSERT_EQ(e1_id, mask_isolation_1.parent_id);
   EXPECT_EQ(SkBlendMode::kSrcOver, mask_isolation_1.blend_mode);
+  int e1_id = mask_isolation_1.parent_id;
+  const cc::EffectNode& cc_e1 = *GetPropertyTrees().effect_tree.Node(e1_id);
+  ASSERT_EQ(e0_id, cc_e1.parent_id);
 
   EXPECT_EQ(SynthesizedClipLayerAt(1), clip_mask1);
   EXPECT_EQ(gfx::Size(300, 200), clip_mask1->bounds());
@@ -2767,32 +2720,30 @@
   Update(artifact.Build());
 
   // Expectation in effect stack diagram:
-  //                l2        (l3) l4       l5                   l7
-  //      l1 [ mask_effect_0 ][ e1  ][ mask_effect_1 ] l6 [ mask_effect_2 ]
-  // (l0) [ mask_isolation_0 ][   mask_isolation_1   ][ mask_isolation_2  ]
-  // [                                 e0                                 ]
-  // Three content layers, two dummy, three clip mask.
-  ASSERT_EQ(8u, RootLayer()->children().size());
+  //           l1          l2         l3                   l5
+  // l0 [ mask_effect_0 ][ e1 ][ mask_effect_1 ] l4 [ mask_effect_2 ]
+  // [ mask_isolation_0 ][  mask_isolation_1   ][ mask_isolation_2  ]
+  // [                              e0                              ]
+  // Three content layers, three clip mask.
+  ASSERT_EQ(6u, RootLayer()->children().size());
   ASSERT_EQ(3u, ContentLayerCount());
   ASSERT_EQ(3u, SynthesizedClipLayerCount());
 
-  const cc::Layer* dummy_c1 = RootLayer()->children()[0].get();
-  const cc::Layer* content0 = RootLayer()->children()[1].get();
-  const cc::Layer* clip_mask0 = RootLayer()->children()[2].get();
-  const cc::Layer* dummy_e1 = RootLayer()->children()[3].get();
-  const cc::Layer* content1 = RootLayer()->children()[4].get();
-  const cc::Layer* clip_mask1 = RootLayer()->children()[5].get();
-  const cc::Layer* content2 = RootLayer()->children()[6].get();
-  const cc::Layer* clip_mask2 = RootLayer()->children()[7].get();
+  const cc::Layer* content0 = RootLayer()->children()[0].get();
+  const cc::Layer* clip_mask0 = RootLayer()->children()[1].get();
+  const cc::Layer* content1 = RootLayer()->children()[2].get();
+  const cc::Layer* clip_mask1 = RootLayer()->children()[3].get();
+  const cc::Layer* content2 = RootLayer()->children()[4].get();
+  const cc::Layer* clip_mask2 = RootLayer()->children()[5].get();
 
+  constexpr int c0_id = 1;
   constexpr int e0_id = 1;
 
-  int c1_id = dummy_c1->clip_tree_index();
-  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
-  ASSERT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
-
   EXPECT_EQ(ContentLayerAt(0), content0);
-  EXPECT_EQ(c1_id, content0->clip_tree_index());
+  int c1_id = content0->clip_tree_index();
+  const cc::ClipNode& cc_c1 = *GetPropertyTrees().clip_tree.Node(c1_id);
+  EXPECT_EQ(gfx::RectF(50, 50, 300, 200), cc_c1.clip);
+  ASSERT_EQ(c0_id, cc_c1.parent_id);
   int mask_isolation_0_id = content0->effect_tree_index();
   const cc::EffectNode& mask_isolation_0 =
       *GetPropertyTrees().effect_tree.Node(mask_isolation_0_id);
@@ -2808,7 +2759,9 @@
   ASSERT_EQ(mask_isolation_0_id, mask_effect_0.parent_id);
   EXPECT_EQ(SkBlendMode::kDstIn, mask_effect_0.blend_mode);
 
-  int e1_id = dummy_e1->effect_tree_index();
+  EXPECT_EQ(ContentLayerAt(1), content1);
+  EXPECT_EQ(c1_id, content1->clip_tree_index());
+  int e1_id = content1->effect_tree_index();
   const cc::EffectNode& cc_e1 = *GetPropertyTrees().effect_tree.Node(e1_id);
   EXPECT_EQ(SkBlendMode::kSrcOver, cc_e1.blend_mode);
   int mask_isolation_1_id = cc_e1.parent_id;
@@ -2818,10 +2771,6 @@
   ASSERT_EQ(e0_id, mask_isolation_1.parent_id);
   EXPECT_EQ(SkBlendMode::kMultiply, mask_isolation_1.blend_mode);
 
-  EXPECT_EQ(ContentLayerAt(1), content1);
-  EXPECT_EQ(c1_id, content1->clip_tree_index());
-  EXPECT_EQ(e1_id, content1->effect_tree_index());
-
   EXPECT_EQ(SynthesizedClipLayerAt(1), clip_mask1);
   EXPECT_EQ(gfx::Size(300, 200), clip_mask1->bounds());
   EXPECT_EQ(c1_id, clip_mask1->clip_tree_index());
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.cpp b/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.cpp
index ec6d920..7a1159a 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.cpp
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.cpp
@@ -160,7 +160,6 @@
   if (it != transform_node_map_.end())
     return it->value;
 
-  scoped_refptr<cc::Layer> dummy_layer = cc::Layer::Create();
   int parent_id = EnsureCompositorTransformNode(transform_node->Parent());
   int id = GetTransformTree().Insert(cc::TransformNode(), parent_id);
 
@@ -179,12 +178,6 @@
       transform_node->FlattensInheritedTransform();
   compositor_node.sorting_context_id = transform_node->RenderingContextId();
 
-  root_layer_->AddChild(dummy_layer);
-  dummy_layer->SetTransformTreeIndex(id);
-  dummy_layer->SetClipTreeIndex(kSecondaryRootNodeId);
-  dummy_layer->SetEffectTreeIndex(kSecondaryRootNodeId);
-  dummy_layer->SetScrollTreeIndex(kRealRootNodeId);
-  dummy_layer->set_property_tree_sequence_number(sequence_number_);
   CompositorElementId compositor_element_id =
       transform_node->GetCompositorElementId();
   if (compositor_element_id) {
@@ -213,7 +206,6 @@
   if (it != clip_node_map_.end())
     return it->value;
 
-  scoped_refptr<cc::Layer> dummy_layer = cc::Layer::Create();
   int parent_id = EnsureCompositorClipNode(clip_node->Parent());
   int id = GetClipTree().Insert(cc::ClipNode(), parent_id);
 
@@ -224,13 +216,6 @@
       EnsureCompositorTransformNode(clip_node->LocalTransformSpace());
   compositor_node.clip_type = cc::ClipNode::ClipType::APPLIES_LOCAL_CLIP;
 
-  root_layer_->AddChild(dummy_layer);
-  dummy_layer->SetTransformTreeIndex(compositor_node.transform_id);
-  dummy_layer->SetClipTreeIndex(id);
-  dummy_layer->SetEffectTreeIndex(kSecondaryRootNodeId);
-  dummy_layer->SetScrollTreeIndex(kRealRootNodeId);
-  dummy_layer->set_property_tree_sequence_number(sequence_number_);
-
   auto result = clip_node_map_.Set(clip_node, id);
   DCHECK(result.is_new_entry);
   GetClipTree().set_needs_update(true);
@@ -573,12 +558,6 @@
   SkBlendMode used_blend_mode = SynthesizeCcEffectsForClipsIfNeeded(
       next_effect->OutputClip(), next_effect->BlendMode(), newly_built);
 
-  // We currently create dummy layers to host effect nodes and corresponding
-  // render surfaces. This should be removed once cc implements better support
-  // for freestanding property trees.
-  scoped_refptr<cc::Layer> dummy_layer = next_effect->EnsureDummyLayer();
-  root_layer_->AddChild(dummy_layer);
-
   int output_clip_id = EnsureCompositorClipNode(next_effect->OutputClip());
 
   cc::EffectNode& effect_node = *GetEffectTree().Node(
@@ -629,12 +608,6 @@
   current_effect_ = next_effect;
   current_clip_ = next_effect->OutputClip();
 
-  dummy_layer->set_property_tree_sequence_number(sequence_number_);
-  dummy_layer->SetTransformTreeIndex(kSecondaryRootNodeId);
-  dummy_layer->SetClipTreeIndex(output_clip_id);
-  dummy_layer->SetEffectTreeIndex(effect_node.id);
-  dummy_layer->SetScrollTreeIndex(kRealRootNodeId);
-
   return true;
 }
 
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.h b/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.h
index 02ccc54..24de727 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.h
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PropertyTreeManager.h
@@ -138,9 +138,8 @@
   cc::PropertyTrees& property_trees_;
 
   // The special layer which is the parent of every other layers.
-  // We currently add dummy layers as required by cc property nodes, but this
-  // may change in the future. We also generate clip mask layers for clips
-  // that can't be rendered by pure cc clip nodes.
+  // This is where clip mask layers we generated for synthesized clips are
+  // appended into.
   cc::Layer* root_layer_;
 
   // Maps from Blink-side property tree nodes to cc property node indices.
diff --git a/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.cpp b/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.cpp
index 7dc69931..40bd19d 100644
--- a/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.cpp
+++ b/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.cpp
@@ -26,13 +26,6 @@
   return result;
 }
 
-cc::Layer* EffectPaintPropertyNode::EnsureDummyLayer() const {
-  if (dummy_layer_)
-    return dummy_layer_.get();
-  dummy_layer_ = cc::Layer::Create();
-  return dummy_layer_.get();
-}
-
 String EffectPaintPropertyNode::ToString() const {
   return String::Format(
       "parent=%p localTransformSpace=%p outputClip=%p opacity=%f filter=%s "
diff --git a/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.h b/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.h
index d3c4001..07ceeba4 100644
--- a/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.h
+++ b/third_party/WebKit/Source/platform/graphics/paint/EffectPaintPropertyNode.h
@@ -100,8 +100,6 @@
   // |inputRect|. The rects are in the space of localTransformSpace.
   FloatRect MapRect(const FloatRect& input_rect) const;
 
-  cc::Layer* EnsureDummyLayer() const;
-
 #if DCHECK_IS_ON()
   // The clone function is used by FindPropertiesNeedingUpdate.h for recording
   // an effect node before it has been updated, to later detect changes.
@@ -185,13 +183,6 @@
   SkBlendMode blend_mode_;
   // === End of effects ===
 
-  // TODO(trchen): Remove the dummy layer.
-  // The main purpose of the dummy layer is to maintain a permanent identity
-  // to associate with cc::RenderSurfaceImpl for damage tracking. This shall
-  // be removed in favor of a stable ID once cc::LayerImpl no longer owns
-  // RenderSurfaceImpl.
-  mutable scoped_refptr<cc::Layer> dummy_layer_;
-
   CompositingReasons direct_compositing_reasons_;
   CompositorElementId compositor_element_id_;
 
diff --git a/third_party/WebKit/public/platform/WebRuntimeFeatures.h b/third_party/WebKit/public/platform/WebRuntimeFeatures.h
index 0d3f6e05..3fbb6a5a2 100644
--- a/third_party/WebKit/public/platform/WebRuntimeFeatures.h
+++ b/third_party/WebKit/public/platform/WebRuntimeFeatures.h
@@ -100,6 +100,7 @@
   BLINK_PLATFORM_EXPORT static void EnableMojoBlobs(bool);
   BLINK_PLATFORM_EXPORT static void EnableNavigatorContentUtils(bool);
   BLINK_PLATFORM_EXPORT static void EnableNetInfoDownlinkMax(bool);
+  BLINK_PLATFORM_EXPORT static void EnableNetworkService(bool);
   BLINK_PLATFORM_EXPORT static void EnableNotificationConstructor(bool);
   BLINK_PLATFORM_EXPORT static void EnableNotificationContentImage(bool);
   BLINK_PLATFORM_EXPORT static void EnableNotifications(bool);
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 206ff96..aa2061b 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -23435,6 +23435,7 @@
   <int value="330138076" label="enable-clear-browsing-data-counters"/>
   <int value="332391072" label="cs-contextual-cards-bar-integration"/>
   <int value="334802038" label="OfflinePreviews:disabled"/>
+  <int value="339671131" label="disable-per-user-timezone"/>
   <int value="346711293" label="enable-save-password-bubble"/>
   <int value="348854923" label="v8-cache-strategies-for-cache-storage"/>
   <int value="352191859" label="disabled-new-style-notification"/>
diff --git a/tools/perf/scripts_smoke_unittest.py b/tools/perf/scripts_smoke_unittest.py
index 1307515..c6d0fa6 100644
--- a/tools/perf/scripts_smoke_unittest.py
+++ b/tools/perf/scripts_smoke_unittest.py
@@ -2,9 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import json
 import os
 import subprocess
 import sys
+from telemetry.testing import options_for_unittests
 import unittest
 
 
@@ -55,3 +57,28 @@
       self.skipTest('small_profile_extender is missing')
     self.assertEquals(return_code, 0, stdout)
     self.assertIn('kraken', stdout)
+
+  def testRunTelemetryBenchmarkAsGoogletest(self):
+    options = options_for_unittests.GetCopy()
+    browser_type = options.browser_type
+    return_code, stdout = self.RunPerfScript(
+        '../../testing/scripts/run_telemetry_benchmark_as_googletest.py '
+        'run_benchmark dummy_benchmark.stable_benchmark_1 --browser=%s '
+        '--isolated-script-test-output=output.json '
+        '--isolated-script-test-chartjson-output=chartjson_output.json '
+        '--output-format=chartjson' % browser_type)
+    self.assertEquals(return_code, 0, stdout)
+    try:
+      with open('../../tools/perf/output.json') as f:
+        self.assertIsNotNone(
+            json.load(f), 'json_test_results should be populated: ' + stdout)
+      os.remove('../../tools/perf/output.json')
+    except IOError as e:
+      self.fail('json_test_results should be populated: ' + stdout + str(e))
+    try:
+      with open('../../tools/perf/chartjson_output.json') as f:
+        self.assertIsNotNone(
+            json.load(f), 'chartjson should be populated: ' + stdout)
+      os.remove('../../tools/perf/chartjson_output.json')
+    except IOError as e:
+      self.fail('chartjson should be populated: ' + stdout + str(e))
diff --git a/ui/compositor/compositor.cc b/ui/compositor/compositor.cc
index fe79595..687b813 100644
--- a/ui/compositor/compositor.cc
+++ b/ui/compositor/compositor.cc
@@ -588,10 +588,9 @@
   active_locks_.push_back(lock.get());
 
   bool should_extend_timeout = false;
-  if ((scheduled_timeout_.is_null() || allow_locks_to_extend_timeout_) &&
-      !timeout.is_zero()) {
+  if ((was_empty || allow_locks_to_extend_timeout_) && !timeout.is_zero()) {
     const base::TimeTicks time_to_timeout = base::TimeTicks::Now() + timeout;
-    // For the first lock, scheduled_timeout_.is_null is true,
+    // For the first lock, scheduled_timeout.is_null is true,
     // |time_to_timeout| will always larger than |scheduled_timeout_|. And it
     // is ok to invalidate the weakptr of |lock_timeout_weak_ptr_factory_|.
     if (time_to_timeout > scheduled_timeout_) {
diff --git a/ui/compositor/compositor.h b/ui/compositor/compositor.h
index 6babbc1c..b01b4116 100644
--- a/ui/compositor/compositor.h
+++ b/ui/compositor/compositor.h
@@ -340,12 +340,9 @@
   void RemoveAnimationObserver(CompositorAnimationObserver* observer);
   bool HasAnimationObserver(const CompositorAnimationObserver* observer) const;
 
-  // Creates a compositor lock. If the timeout is null, then no timeout is used.
-  // If multiple locks exist simultaneously, the timeout behavior is as follows:
-  //   1. if allow_locks_to_extend_timeouts has been set, all locks time out at
-  //      the latest chronological timeout value provided;
-  //   2. otherwise, all locks time out at the time specified by the first lock
-  //      that was created with a non-zero timeout value.
+  // Creates a compositor lock. Returns NULL if it is not possible to lock at
+  // this time (i.e. we're waiting to complete a previous unlock). If the
+  // timeout is null, then no timeout is used.
   std::unique_ptr<CompositorLock> GetCompositorLock(
       CompositorLockClient* client,
       base::TimeDelta timeout =
diff --git a/ui/compositor/compositor_unittest.cc b/ui/compositor/compositor_unittest.cc
index 4284504b..42491420 100644
--- a/ui/compositor/compositor_unittest.cc
+++ b/ui/compositor/compositor_unittest.cc
@@ -451,38 +451,6 @@
   compositor()->SetVisible(true);
 }
 
-// Verify that when allow_locks_to_extend_timeout_ is false and a lock with
-// no timeout has been created, a second lock that has a timeout will correctly
-// time out.
-TEST_F(CompositorTestWithMockedTime,
-       LockWithTimeoutOverridesLockWithNoTimeout) {
-  testing::StrictMock<MockCompositorLockClient> lock_client1;
-  std::unique_ptr<CompositorLock> lock1;
-  testing::StrictMock<MockCompositorLockClient> lock_client2;
-  std::unique_ptr<CompositorLock> lock2;
-
-  base::TimeDelta timeout1 = base::TimeDelta();
-  base::TimeDelta timeout2 = base::TimeDelta::FromMilliseconds(10);
-
-  // False is the default, but ensure this test is still valid if that ever
-  // changes.
-  compositor()->set_allow_locks_to_extend_timeout(false);
-
-  // Take a lock with no timeout.
-  lock1 = compositor()->GetCompositorLock(&lock_client1, timeout1);
-  EXPECT_TRUE(compositor()->IsLocked());
-
-  // Setting a lock with a timeout should cause boths locks to time out.
-  lock2 = compositor()->GetCompositorLock(&lock_client2, timeout2);
-  EXPECT_TRUE(compositor()->IsLocked());
-
-  EXPECT_CALL(lock_client1, CompositorLockTimedOut()).Times(1);
-  EXPECT_CALL(lock_client2, CompositorLockTimedOut()).Times(1);
-  task_runner()->FastForwardBy(timeout2);
-  task_runner()->RunUntilIdle();
-  EXPECT_FALSE(compositor()->IsLocked());
-}
-
 #if defined(OS_WIN)
 // TODO(crbug.com/608436): Flaky on windows trybots
 #define MAYBE_CreateAndReleaseOutputSurface \
diff --git a/ui/ozone/platform/drm/gpu/drm_device.cc b/ui/ozone/platform/drm/gpu/drm_device.cc
index c07e03e..315b1c0 100644
--- a/ui/ozone/platform/drm/gpu/drm_device.cc
+++ b/ui/ozone/platform/drm/gpu/drm_device.cc
@@ -25,7 +25,6 @@
 #include "ui/ozone/platform/drm/common/drm_util.h"
 #include "ui/ozone/platform/drm/gpu/hardware_display_plane_manager_atomic.h"
 #include "ui/ozone/platform/drm/gpu/hardware_display_plane_manager_legacy.h"
-#include "ui/ozone/public/ozone_switches.h"
 
 namespace ui {
 
@@ -414,18 +413,15 @@
     return false;
   }
 
-  if (use_atomic) {
-    if (!SetCapability(DRM_CLIENT_CAP_ATOMIC, 1)) {
-      LOG(ERROR) << "Drm atomic requested but capabilities don't allow it. To "
-                    "switch to legacy page flip remove the command line flag "
-                 << switches::kEnableDrmAtomic;
-      return false;
-    }
+  // Use atomic only if the build, kernel & flags all allow it.
+  if (use_atomic && SetCapability(DRM_CLIENT_CAP_ATOMIC, 1))
     plane_manager_.reset(new HardwareDisplayPlaneManagerAtomic());
-  } else {
-    plane_manager_.reset(new HardwareDisplayPlaneManagerLegacy());
-  }
 
+  LOG_IF(WARNING, use_atomic && !plane_manager_)
+      << "Drm atomic requested but capabilities don't allow it. Falling back "
+         "to legacy page flip.";
+  if (!plane_manager_)
+    plane_manager_.reset(new HardwareDisplayPlaneManagerLegacy());
   if (!plane_manager_->Initialize(this)) {
     LOG(ERROR) << "Failed to initialize the plane manager for "
                << device_path_.value();