diff --git a/DEPS b/DEPS
index 5e9b4322..fbef1933 100644
--- a/DEPS
+++ b/DEPS
@@ -40,11 +40,11 @@
   # Three lines of non-changing comments so that
   # the commit queue can handle CLs rolling Skia
   # and whatever else without interference from each other.
-  'skia_revision': '210e8551a30f6cb48894fd7c010d0a9be146ffa5',
+  'skia_revision': '878df6dd03d1e5b8c018f803eff5b4736af399f8',
   # 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': '7b1dc4fedaee89c90dd085e9f2819adcd6c1f170',
+  'v8_revision': 'b89390700f2ee1b26a5c4f38ff2b1c6fc46b23d4',
   # 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.
@@ -397,7 +397,7 @@
 
     # For Linux and Chromium OS.
     'src/third_party/cros_system_api':
-      Var('chromium_git') + '/chromiumos/platform/system_api.git' + '@' + '0995a6af616cd4ef0e94d278ecc92aa2537b16c3',
+      Var('chromium_git') + '/chromiumos/platform/system_api.git' + '@' + '05cee9da77790110786e6211f75c49d0c7e26ae8',
 
     # Note that this is different from Android's freetype repo.
     'src/third_party/freetype2/src':
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/UndoTabModelTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/UndoTabModelTest.java
index 9245627..d0a053f 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/UndoTabModelTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/UndoTabModelTest.java
@@ -1390,7 +1390,6 @@
      */
     @MediumTest
     @RetryOnFailure
-    @DisabledTest(message = "crbug.com/674654")
     public void testUndoNotSupported() throws InterruptedException {
         TabModel model = getActivity().getTabModelSelector().getModel(true);
         ChromeTabCreator tabCreator = getActivity().getTabCreator(true);
diff --git a/chrome/browser/android/compositor/tab_content_manager.cc b/chrome/browser/android/compositor/tab_content_manager.cc
index a516fdb..505c843 100644
--- a/chrome/browser/android/compositor/tab_content_manager.cc
+++ b/chrome/browser/android/compositor/tab_content_manager.cc
@@ -20,7 +20,9 @@
 #include "chrome/browser/android/compositor/layer/thumbnail_layer.h"
 #include "chrome/browser/android/tab_android.h"
 #include "chrome/browser/android/thumbnail/thumbnail.h"
+#include "content/public/browser/interstitial_page.h"
 #include "content/public/browser/readback_types.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_view_host.h"
 #include "content/public/browser/render_widget_host.h"
 #include "content/public/browser/render_widget_host_view.h"
@@ -45,22 +47,18 @@
 
 class TabContentManager::TabReadbackRequest {
  public:
-  TabReadbackRequest(content::WebContents* web_contents,
+  TabReadbackRequest(content::RenderWidgetHost* rwh,
                      float thumbnail_scale,
                      const TabReadbackCallback& end_callback)
       : thumbnail_scale_(thumbnail_scale),
         end_callback_(end_callback),
         drop_after_readback_(false),
         weak_factory_(this) {
-    DCHECK(web_contents);
+    DCHECK(rwh);
     content::ReadbackRequestCallback result_callback =
         base::Bind(&TabReadbackRequest::OnFinishGetTabThumbnailBitmap,
                    weak_factory_.GetWeakPtr());
 
-    content::RenderWidgetHost* rwh =
-        web_contents->GetRenderViewHost()->GetWidget();
-    DCHECK(rwh);
-
     SkColorType color_type = kN32_SkColorType;
     gfx::Rect src_rect = rwh->GetView()->GetViewBounds();
     gfx::Size dst_size(
@@ -217,11 +215,17 @@
   content::WebContents* web_contents = tab_android->web_contents();
   DCHECK(web_contents);
 
-  if (!web_contents->GetRenderViewHost() ||
-      !web_contents->GetRenderViewHost()->GetWidget() ||
-      !web_contents->GetRenderViewHost()
-           ->GetWidget()
-           ->CanCopyFromBackingStore() ||
+  content::RenderViewHost* rvh = web_contents->GetRenderViewHost();
+  if (web_contents->ShowingInterstitialPage()) {
+    if (!web_contents->GetInterstitialPage()->GetMainFrame())
+      return;
+
+    rvh = web_contents->GetInterstitialPage()->GetMainFrame()->
+        GetRenderViewHost();
+  }
+
+  if (!rvh || !rvh->GetWidget() ||
+      !rvh->GetWidget()->CanCopyFromBackingStore() ||
       pending_tab_readbacks_.find(tab_id) != pending_tab_readbacks_.end() ||
       pending_tab_readbacks_.size() >= kMaxReadbacks) {
     return;
@@ -232,7 +236,7 @@
         base::Bind(&TabContentManager::PutThumbnailIntoCache,
                    weak_factory_.GetWeakPtr(), tab_id);
     pending_tab_readbacks_[tab_id] = base::MakeUnique<TabReadbackRequest>(
-        web_contents, thumbnail_scale, readback_done_callback);
+        rvh->GetWidget(), thumbnail_scale, readback_done_callback);
   }
 }
 
diff --git a/chrome/browser/chromeos/policy/server_backed_state_keys_broker.h b/chrome/browser/chromeos/policy/server_backed_state_keys_broker.h
index 6fb80e9..de4baf7 100644
--- a/chrome/browser/chromeos/policy/server_backed_state_keys_broker.h
+++ b/chrome/browser/chromeos/policy/server_backed_state_keys_broker.h
@@ -46,10 +46,12 @@
   // requested yet, calling this will also trigger their initial fetch.
   Subscription RegisterUpdateCallback(const base::Closure& callback);
 
-  // Requests state keys asynchronously. Invokes the passed callback exactly
-  // once (unless |this| gets destroyed before the callback happens), with the
-  // current state keys passed as a parameter to the callback. If there's a
-  // problem determining the state keys, the passed vector will be empty.
+  // Requests state keys asynchronously. Invokes the passed callback at most
+  // once, with the current state keys passed as a parameter to the callback. If
+  // there's a problem determining the state keys, the passed vector will be
+  // empty. If |this| gets destroyed before the callback happens or if the time
+  // sync fails / the network is not established, then the |callback| is never
+  // invoked. See http://crbug.com/649422 for more context.
   void RequestStateKeys(const StateKeysCallback& callback);
 
   // Get the set of current state keys. Empty if state keys are unavailable
diff --git a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
index c1b2138..4e1cd2e8 100644
--- a/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
+++ b/chrome/browser/subresource_filter/subresource_filter_browsertest.cc
@@ -229,9 +229,13 @@
   ASSERT_TRUE(frame);
   EXPECT_FALSE(WasParsedScriptElementLoaded(frame));
 
+  // Support both pre-/post-PersistentHistograms worlds. The latter is enabled
+  // through field trials, so the former is still used on offical builders.
+  content::FetchHistogramsFromChildProcesses();
+  SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
+
   // The only frames where filtering was (even considered to be) activated
   // should be the main frame, and the child that was navigated to an HTTP URL.
-  SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
   histogram_tester.ExpectUniqueSample(
       "SubresourceFilter.DocumentLoad.ActivationState",
       static_cast<base::Histogram::Sample>(ActivationState::ENABLED), 2);
diff --git a/chromeos/dbus/OWNERS b/chromeos/dbus/OWNERS
index 4c9ab1f..d33c0344 100644
--- a/chromeos/dbus/OWNERS
+++ b/chromeos/dbus/OWNERS
@@ -1,7 +1,9 @@
 stevenjb@chromium.org
 hashimoto@chromium.org
 
-per-file *cryptohome*=dkrahn@chromium.org
 per-file *audio*=jennyz@chromium.org
 per-file *audio*=hychao@chromium.org
+per-file *auth_policy*=rsorokin@chromium.org
+per-file *auth_policy*=tnagel@chromium.org
+per-file *cryptohome*=dkrahn@chromium.org
 per-file *power*=derat@chromium.org
diff --git a/chromeos/dbus/session_manager_client.h b/chromeos/dbus/session_manager_client.h
index dc00918..527c92b 100644
--- a/chromeos/dbus/session_manager_client.h
+++ b/chromeos/dbus/session_manager_client.h
@@ -211,7 +211,8 @@
   // for the device to retrieve after a device factory reset.
   //
   // The state keys are returned asynchronously via |callback|. The callback
-  // will be invoked with an empty state key vector in case of errors.
+  // is invoked with an empty state key vector in case of errors. If the time
+  // sync fails or there's no network, the callback is never invoked.
   virtual void GetServerBackedStateKeys(const StateKeysCallback& callback) = 0;
 
   // Used for several ARC methods.  Takes a boolean indicating whether the
diff --git a/components/cronet/android/BUILD.gn b/components/cronet/android/BUILD.gn
index 24bd002..21eabaa 100644
--- a/components/cronet/android/BUILD.gn
+++ b/components/cronet/android/BUILD.gn
@@ -553,7 +553,10 @@
 
 android_resources("cronet_test_apk_resources") {
   testonly = true
-  resource_dirs = [ "test/res" ]
+  resource_dirs = [
+    "test/res",
+    "test/smoketests/res/native",
+  ]
   android_manifest = "test/AndroidManifest.xml"
 }
 
@@ -588,6 +591,35 @@
   run_findbugs_override = true
 }
 
+cronet_smoketests_platform_only_common_srcs = [
+  "test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java",
+  "test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestCase.java",
+  "test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java",
+  "test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java",
+  "test/smoketests/src/org/chromium/net/smoke/TestSupport.java",
+]
+
+cronet_smoketests_native_common_srcs = cronet_smoketests_platform_only_common_srcs + [
+                                         "test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java",
+                                         "test/smoketests/src/org/chromium/net/smoke/NativeCronetTestCase.java",
+                                       ]
+
+android_library("cronet_smoketests_native_java") {
+  testonly = true
+  java_files = [
+                 "test/smoketests/src/org/chromium/net/smoke/Http2Test.java",
+                 "test/smoketests/src/org/chromium/net/smoke/QuicTest.java",
+               ] + cronet_smoketests_native_common_srcs
+
+  deps = [
+    ":cronet_api_java",
+    ":cronet_test_apk_java",
+    "//base:base_java",
+    "//third_party/android_support_test_runner:runner_java",
+    "//third_party/netty4:netty_all_java",
+  ]
+}
+
 android_assets("cronet_test_apk_assets") {
   testonly = true
 
@@ -639,13 +671,21 @@
   loadable_modules = [ "$root_out_dir/libnetty-tcnative.so" ]
 
   deps = [
+    ":cronet_combine_proguard_flags",
     ":cronet_test_apk_assets",
-    ":cronet_test_apk_java",
     ":cronet_test_apk_resources",
     "//base:base_java",
     "//third_party/netty-tcnative:netty-tcnative-so",
   ]
 
+  proguard_enabled = true
+
+  proguard_configs = [
+    "$target_gen_dir/cronet_impl_native_proguard.cfg",
+    "cronet_impl_common_proguard.cfg",
+    "cronet_impl_platform_proguard.cfg",
+  ]
+
   run_findbugs_override = true
 }
 
@@ -710,6 +750,7 @@
     ":cronet_api_java",
     ":cronet_impl_all_java",
     ":cronet_javatests",
+    ":cronet_smoketests_native_java",
     ":cronet_test_apk_java",
     "//base:base_java",
     "//base:base_java_test_support",
@@ -723,6 +764,113 @@
     "//net:test_support",
   ]
 
+  proguard_enabled = true
+
+  proguard_configs = [ "test/proguard.cfg" ]
+
+  run_findbugs_override = true
+}
+
+android_resources("cronet_smoketests_platform_only_apk_resources") {
+  testonly = true
+  resource_dirs = [ "test/smoketests/res/platform_only" ]
+  android_manifest = "test/AndroidManifest.xml"
+}
+
+android_library("cronet_smoketests_platform_only_java") {
+  testonly = true
+  java_files = [ "test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java" ] + cronet_smoketests_platform_only_common_srcs
+  deps = [
+    ":cronet_api_java",
+    "//third_party/android_support_test_runner:runner_java",
+    "//third_party/netty4:netty_all_java",
+  ]
+}
+
+android_apk("cronet_smoketests_platform_only_apk") {
+  testonly = true
+  apk_name = "PlatformOnlyEngineSmokeTest"
+  android_manifest = "test/AndroidManifest.xml"
+  java_files = [ "test/src/org/chromium/net/CronetTestApplication.java" ]
+
+  proguard_enabled = true
+  proguard_configs = [
+    "cronet_impl_common_proguard.cfg",
+    "cronet_impl_platform_proguard.cfg",
+  ]
+
+  deps = [
+    ":cronet_api_java",
+    ":cronet_impl_common_java",
+    ":cronet_impl_platform_java",
+    ":cronet_smoketests_platform_only_apk_resources",
+  ]
+  run_findbugs_override = true
+}
+
+instrumentation_test_apk(
+    "cronet_smoketests_platform_only_instrumentation_apk") {
+  apk_name = "PlatformOnlyEngineSmokeTestInstrumentation"
+  apk_under_test = ":cronet_smoketests_platform_only_apk"
+  android_manifest = "test/javatests/AndroidManifest.xml"
+  deps = [
+    ":cronet_smoketests_platform_only_java",
+  ]
+
+  proguard_enabled = true
+
+  proguard_configs = [ "test/proguard.cfg" ]
+  run_findbugs_override = true
+}
+
+android_library("cronet_smoketests_missing_native_library_java") {
+  testonly = true
+  java_files = [ "test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java" ] + cronet_smoketests_native_common_srcs
+  deps = [
+    ":cronet_api_java",
+    ":cronet_test_apk_java",
+    "//base:base_java",
+    "//third_party/android_support_test_runner:runner_java",
+    "//third_party/netty4:netty_all_java",
+  ]
+}
+
+android_apk("cronet_smoketests_missing_native_library_apk") {
+  testonly = true
+  apk_name = "MissingNativeLibrarySmokeTest"
+  android_manifest = "test/AndroidManifest.xml"
+  deps = [
+    ":cronet_api_java",
+    ":cronet_combine_proguard_flags",
+    ":cronet_impl_common_java",
+    ":cronet_impl_platform_java",
+    ":cronet_test_apk_resources",
+  ]
+
+  proguard_enabled = true
+  proguard_configs = [
+    "$target_gen_dir/cronet_impl_native_proguard.cfg",
+    "cronet_impl_common_proguard.cfg",
+    "cronet_impl_platform_proguard.cfg",
+  ]
+
+  run_findbugs_override = true
+}
+
+instrumentation_test_apk(
+    "cronet_smoketests_missing_native_library_instrumentation_apk") {
+  apk_name = "MissingNativeLibrarySmokeTestInstrumentation"
+  apk_under_test = ":cronet_smoketests_missing_native_library_apk"
+  android_manifest = "test/javatests/AndroidManifest.xml"
+
+  deps = [
+    ":cronet_smoketests_missing_native_library_java",
+  ]
+
+  proguard_enabled = true
+
+  proguard_configs = [ "test/proguard.cfg" ]
+
   run_findbugs_override = true
 }
 
@@ -763,7 +911,7 @@
   proguard_configs = [
     "$target_gen_dir/cronet_impl_native_proguard.cfg",
     "cronet_impl_common_proguard.cfg",
-    "test/javaperftests/proguard.cfg",
+    "test/proguard.cfg",
     "//base/android/proguard/chromium_apk.flags",
   ]
 }
diff --git a/components/cronet/android/cronet_impl_common_proguard.cfg b/components/cronet/android/cronet_impl_common_proguard.cfg
index 531394a..a4ecdcd 100644
--- a/components/cronet/android/cronet_impl_common_proguard.cfg
+++ b/components/cronet/android/cronet_impl_common_proguard.cfg
@@ -3,4 +3,8 @@
 # This constructor is called using the reflection from Cronet API (cronet_api.jar).
 -keep class org.chromium.net.impl.CronetEngineBuilderImpl {
     public <init>(android.content.Context);
-}
\ No newline at end of file
+}
+
+# This class should be explicitly kept to avoid failure if
+# class/merging/horizontal proguard optimization is enabled.
+-keep class org.chromium.net.impl.ImplVersion
\ No newline at end of file
diff --git a/components/cronet/android/cronet_impl_native_proguard.cfg b/components/cronet/android/cronet_impl_native_proguard.cfg
index 9ee247f..c0493225 100644
--- a/components/cronet/android/cronet_impl_native_proguard.cfg
+++ b/components/cronet/android/cronet_impl_native_proguard.cfg
@@ -23,3 +23,7 @@
 # https://android.googlesource.com/platform/sdk/+/marshmallow-mr1-release/files/proguard-android.txt#54
 -dontwarn android.support.**
 
+# This class should be explicitly kept to avoid failure if
+# class/merging/horizontal proguard optimization is enabled.
+-keep class org.chromium.base.CollectionUtil
+
diff --git a/components/cronet/android/test/javaperftests/proguard.cfg b/components/cronet/android/test/javaperftests/proguard.cfg
deleted file mode 100644
index b89cdaf..0000000
--- a/components/cronet/android/test/javaperftests/proguard.cfg
+++ /dev/null
@@ -1,4 +0,0 @@
-# TODO(pauljensen): Remove when crbug.com/488192 is fixed.
--dontwarn org.apache.http.**
-# Javaperftest doesn't use Netty so ignore warnings.
--dontwarn io.netty.**
diff --git a/components/cronet/android/test/proguard.cfg b/components/cronet/android/test/proguard.cfg
new file mode 100644
index 0000000..22b0db0
--- /dev/null
+++ b/components/cronet/android/test/proguard.cfg
@@ -0,0 +1,19 @@
+# Proguard configuration that is common for all type of tests.
+
+-keepattributes Signature,InnerClasses,SourceFile,LineNumberTable
+-dontwarn io.netty.**
+-keep class io.netty.** { *; }
+-keep class org.chromium.net.smoke.ChromiumNativeTestSupport
+-keep class org.chromium.net.smoke.ChromiumPlatformOnlyTestSupport
+
+# TODO(jbudorick): Remove when crbug.com/488192 is fixed.
+-dontwarn org.apache.http.**
+
+-dontwarn android.support.test.runner.MonitoringInstrumentation
+
+# These classes should be explicitly kept to avoid failure if
+# class/merging/horizontal proguard optimization is enabled.
+# NOTE: make sure that only test classes are added to this list.
+-keep class org.chromium.base.test.util.**
+-keep class org.chromium.net.TestFilesInstaller
+-keep class org.chromium.net.MetricsTestUtil
\ No newline at end of file
diff --git a/components/cronet/android/test/smoketests/res/native/values/strings.xml b/components/cronet/android/test/smoketests/res/native/values/strings.xml
new file mode 100644
index 0000000..8538e13
--- /dev/null
+++ b/components/cronet/android/test/smoketests/res/native/values/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright 2016 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.
+ -->
+
+<resources>
+    <string name="TestSupportImplClass">org.chromium.net.smoke.ChromiumNativeTestSupport</string>
+</resources>
diff --git a/components/cronet/android/test/smoketests/res/native/xml/network_security_config.xml b/components/cronet/android/test/smoketests/res/native/xml/network_security_config.xml
new file mode 100644
index 0000000..8a0e91a9
--- /dev/null
+++ b/components/cronet/android/test/smoketests/res/native/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright 2016 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.
+-->
+
+<network-security-config/>
diff --git a/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml b/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml
new file mode 100644
index 0000000..bb27fea
--- /dev/null
+++ b/components/cronet/android/test/smoketests/res/platform_only/values/strings.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright 2016 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.
+ -->
+
+<resources>
+    <string name="TestSupportImplClass">org.chromium.net.smoke.ChromiumPlatformOnlyTestSupport</string>
+</resources>
diff --git a/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml b/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml
new file mode 100644
index 0000000..8a0e91a9
--- /dev/null
+++ b/components/cronet/android/test/smoketests/res/platform_only/xml/network_security_config.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright 2016 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.
+-->
+
+<network-security-config/>
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java
new file mode 100644
index 0000000..c80bc93
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumNativeTestSupport.java
@@ -0,0 +1,113 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+import org.chromium.base.Log;
+import org.chromium.net.CronetTestUtil;
+import org.chromium.net.ExperimentalCronetEngine;
+
+/**
+ * Provides support for tests that depend on QUIC and HTTP2 servers.
+ */
+class ChromiumNativeTestSupport extends ChromiumPlatformOnlyTestSupport {
+    private static final String TAG = "NativeTestSupport";
+
+    @Override
+    public TestServer createTestServer(Context context, Protocol protocol) {
+        switch (protocol) {
+            case QUIC:
+                return new QuicTestServer(context);
+            case HTTP2:
+                return new Http2TestServer(context);
+            case HTTP1:
+                return super.createTestServer(context, protocol);
+            default:
+                throw new RuntimeException("Unknown server protocol: " + protocol);
+        }
+    }
+
+    @Override
+    public void addHostResolverRules(JSONObject experimentalOptionsJson) {
+        try {
+            JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules();
+            experimentalOptionsJson.put("HostResolverRules", hostResolverParams);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder) {
+        CronetTestUtil.setMockCertVerifierForTesting(
+                builder, org.chromium.net.QuicTestServer.createMockCertVerifier());
+    }
+
+    @Override
+    public void loadTestNativeLibrary() {
+        System.loadLibrary("cronet_tests");
+    }
+
+    private static class QuicTestServer implements TestServer {
+        private final Context mContext;
+
+        QuicTestServer(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean start() {
+            org.chromium.net.QuicTestServer.startQuicTestServer(mContext);
+            return true;
+        }
+
+        @Override
+        public void shutdown() {
+            org.chromium.net.QuicTestServer.shutdownQuicTestServer();
+        }
+
+        @Override
+        public String getSuccessURL() {
+            return org.chromium.net.QuicTestServer.getServerURL() + "/simple.txt";
+        }
+    }
+
+    private static class Http2TestServer implements TestServer {
+        private final Context mContext;
+
+        Http2TestServer(Context context) {
+            mContext = context;
+        }
+
+        @Override
+        public boolean start() {
+            try {
+                return org.chromium.net.Http2TestServer.startHttp2TestServer(mContext,
+                        org.chromium.net.QuicTestServer.getServerCert(),
+                        org.chromium.net.QuicTestServer.getServerCertKey());
+            } catch (Exception e) {
+                Log.e(TAG, "Exception during Http2TestServer start", e);
+                return false;
+            }
+        }
+
+        @Override
+        public void shutdown() {
+            try {
+                org.chromium.net.Http2TestServer.shutdownHttp2TestServer();
+            } catch (Exception e) {
+                Log.e(TAG, "Exception during Http2TestServer shutdown", e);
+            }
+        }
+
+        @Override
+        public String getSuccessURL() {
+            return org.chromium.net.Http2TestServer.getEchoMethodUrl();
+        }
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java
new file mode 100644
index 0000000..0545f04
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/ChromiumPlatformOnlyTestSupport.java
@@ -0,0 +1,53 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+import org.chromium.net.ExperimentalCronetEngine;
+
+import java.io.File;
+
+/**
+ * Tests support for Java only Cronet engine tests. This class should not depend on
+ * Chromium 'base' or 'net'.
+ */
+public class ChromiumPlatformOnlyTestSupport implements TestSupport {
+    @Override
+    public TestServer createTestServer(Context context, Protocol protocol) {
+        switch (protocol) {
+            case QUIC:
+                throw new IllegalArgumentException("QUIC is not supported");
+            case HTTP2:
+                throw new IllegalArgumentException("HTTP2 is not supported");
+            case HTTP1:
+                return new HttpTestServer();
+            default:
+                throw new IllegalArgumentException("Unknown server protocol: " + protocol);
+        }
+    }
+
+    @Override
+    public void processNetLog(Context context, File file) {
+        // Do nothing
+    }
+
+    @Override
+    public void addHostResolverRules(JSONObject experimentalOptionsJson) {
+        throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport");
+    }
+
+    @Override
+    public void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder) {
+        throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport");
+    }
+
+    @Override
+    public void loadTestNativeLibrary() {
+        throw new UnsupportedOperationException("Unsupported by ChromiumPlatformOnlyTestSupport");
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestCase.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestCase.java
new file mode 100644
index 0000000..dd2f7ad
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/CronetSmokeTestCase.java
@@ -0,0 +1,90 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+import org.chromium.net.CronetEngine;
+import org.chromium.net.ExperimentalCronetEngine;
+import org.chromium.net.UrlResponseInfo;
+
+/**
+ * Base test class. This class should not import any classes from the org.chromium.base package.
+ */
+public class CronetSmokeTestCase extends AndroidTestCase {
+    /**
+     * The key in the string resource file that specifies {@link TestSupport} that should
+     * be instantiated.
+     */
+    private static final String SUPPORT_IMPL_RES_KEY = "TestSupportImplClass";
+
+    protected ExperimentalCronetEngine.Builder mCronetEngineBuilder;
+    protected CronetEngine mCronetEngine;
+    protected TestSupport mTestSupport;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mCronetEngineBuilder = new ExperimentalCronetEngine.Builder(getContext());
+        initTestSupport();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mCronetEngine != null) {
+            mCronetEngine.shutdown();
+        }
+        super.tearDown();
+    }
+
+    protected void initCronetEngine() {
+        mCronetEngine = mCronetEngineBuilder.build();
+    }
+
+    static void assertSuccessfulNonEmptyResponse(SmokeTestRequestCallback callback, String url) {
+        // Check the request state
+        if (callback.getFinalState() == SmokeTestRequestCallback.State.Failed) {
+            throw new RuntimeException(
+                    "The request failed with an error", callback.getFailureError());
+        }
+        assertEquals(SmokeTestRequestCallback.State.Succeeded, callback.getFinalState());
+
+        // Check the response info
+        UrlResponseInfo responseInfo = callback.getResponseInfo();
+        assertNotNull(responseInfo);
+        assertFalse(responseInfo.wasCached());
+        assertEquals(url, responseInfo.getUrl());
+        assertEquals(url, responseInfo.getUrlChain().get(responseInfo.getUrlChain().size() - 1));
+        assertEquals(200, responseInfo.getHttpStatusCode());
+        assertTrue(responseInfo.toString().length() > 0);
+    }
+
+    static void assertJavaEngine(CronetEngine engine) {
+        assertNotNull(engine);
+        assertEquals("org.chromium.net.impl.JavaCronetEngine", engine.getClass().getName());
+    }
+
+    static void assertNativeEngine(CronetEngine engine) {
+        assertNotNull(engine);
+        assertEquals("org.chromium.net.impl.CronetUrlRequestContext", engine.getClass().getName());
+    }
+
+    /**
+     * Instantiates a concrete implementation of {@link TestSupport} interface.
+     * The name of the implementation class is determined dynamically by reading
+     * the value of |TestSupportImplClass| from the Android string resource file.
+     *
+     * @throws Exception if the class cannot be instantiated.
+     */
+    private void initTestSupport() throws Exception {
+        Context ctx = getContext();
+        String packageName = ctx.getPackageName();
+        int resId = ctx.getResources().getIdentifier(SUPPORT_IMPL_RES_KEY, "string", packageName);
+        String className = ctx.getResources().getString(resId);
+        Class<? extends TestSupport> cl = Class.forName(className).asSubclass(TestSupport.class);
+        mTestSupport = cl.newInstance();
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java
new file mode 100644
index 0000000..c1d680d
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/Http2Test.java
@@ -0,0 +1,44 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.support.test.filters.SmallTest;
+
+import org.chromium.net.UrlRequest;
+
+/**
+ * HTTP2 Tests.
+ */
+public class Http2Test extends NativeCronetTestCase {
+    private TestSupport.TestServer mServer;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mServer = mTestSupport.createTestServer(getContext(), TestSupport.Protocol.HTTP2);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mServer.shutdown();
+        super.tearDown();
+    }
+
+    // Test that HTTP/2 is enabled by default but QUIC is not.
+    @SmallTest
+    public void testHttp2() throws Exception {
+        mTestSupport.installMockCertVerifierForTesting(mCronetEngineBuilder);
+        initCronetEngine();
+        assertTrue(mServer.start());
+        SmokeTestRequestCallback callback = new SmokeTestRequestCallback();
+        UrlRequest.Builder requestBuilder = mCronetEngine.newUrlRequestBuilder(
+                mServer.getSuccessURL(), callback, callback.getExecutor());
+        requestBuilder.build().start();
+        callback.blockForDone();
+
+        assertSuccessfulNonEmptyResponse(callback, mServer.getSuccessURL());
+        assertEquals("h2", callback.getResponseInfo().getNegotiatedProtocol());
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java
new file mode 100644
index 0000000..6b412316
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/HttpTestServer.java
@@ -0,0 +1,121 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.os.ConditionVariable;
+import android.util.Log;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpResponse;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpRequestDecoder;
+import io.netty.handler.codec.http.HttpResponseEncoder;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.util.CharsetUtil;
+
+/**
+ * A simple HTTP server for testing.
+ */
+public class HttpTestServer implements TestSupport.TestServer {
+    private static final String TAG = "HttpTestServer";
+    private static final String HOST = "127.0.0.1";
+    private static final int PORT = 8080;
+
+    private Channel mServerChannel;
+    private ConditionVariable mStartBlock = new ConditionVariable();
+    private ConditionVariable mShutdownBlock = new ConditionVariable();
+
+    @Override
+    public boolean start() {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    HttpTestServer.this.run();
+                } catch (Exception e) {
+                    Log.e(TAG, "Unable to start HttpTestServer", e);
+                }
+            }
+        }).start();
+        // Return false if the server cannot start within 5 seconds.
+        return mStartBlock.block(5000);
+    }
+
+    @Override
+    public void shutdown() {
+        if (mServerChannel != null) {
+            mServerChannel.close();
+            boolean success = mShutdownBlock.block(10000);
+            if (!success) {
+                Log.e(TAG, "Unable to shutdown the server. Is it already dead?");
+            }
+            mServerChannel = null;
+        }
+    }
+
+    @Override
+    public String getSuccessURL() {
+        return getServerUrl() + "/success";
+    }
+
+    private String getServerUrl() {
+        return "http://" + HOST + ":" + PORT;
+    }
+
+    private void run() throws Exception {
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        EventLoopGroup workerGroup = new NioEventLoopGroup(4);
+        try {
+            ServerBootstrap b = new ServerBootstrap();
+            b.group(bossGroup, workerGroup)
+                    .channel(NioServerSocketChannel.class)
+                    .handler(new LoggingHandler(LogLevel.INFO))
+                    .childHandler(new ChannelInitializer<SocketChannel>() {
+                        @Override
+                        public void initChannel(SocketChannel ch) throws Exception {
+                            ChannelPipeline p = ch.pipeline();
+                            p.addLast(new HttpRequestDecoder());
+                            p.addLast(new HttpResponseEncoder());
+                            p.addLast(new TestServerHandler());
+                        }
+                    });
+
+            // Start listening fo incoming connections.
+            mServerChannel = b.bind(PORT).sync().channel();
+            mStartBlock.open();
+            // Block until the channel is closed.
+            mServerChannel.closeFuture().sync();
+            mShutdownBlock.open();
+            Log.i(TAG, "HttpServer stopped");
+        } finally {
+            workerGroup.shutdownGracefully();
+            bossGroup.shutdownGracefully();
+        }
+    }
+
+    private static class TestServerHandler extends SimpleChannelInboundHandler {
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
+            FullHttpResponse response = new DefaultFullHttpResponse(
+                    HTTP_1_1, OK, Unpooled.copiedBuffer("Hello!", CharsetUtil.UTF_8));
+            ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
+        }
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java
new file mode 100644
index 0000000..23803b7
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/MissingNativeLibraryTest.java
@@ -0,0 +1,48 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.support.test.filters.SmallTest;
+
+import org.chromium.net.CronetEngine;
+import org.chromium.net.ExperimentalCronetEngine;
+
+/**
+ *  Tests scenarios when the native shared library file is missing in the APK or was built for a
+ *  wrong architecture.
+ */
+public class MissingNativeLibraryTest extends CronetSmokeTestCase {
+    /**
+     * If the ".so" file is missing, instantiating the Cronet engine should throw an exception.
+     */
+    @SmallTest
+    public void testExceptionWhenSoFileIsAbsent() throws Exception {
+        ExperimentalCronetEngine.Builder builder =
+                new ExperimentalCronetEngine.Builder(getContext());
+        try {
+            builder.build();
+            fail("Expected exception since the shared library '.so' file is absent");
+        } catch (Throwable t) {
+            // Find the root cause.
+            while (t.getCause() != null) {
+                t = t.getCause();
+            }
+            assertEquals(UnsatisfiedLinkError.class, t.getClass());
+        }
+    }
+
+    /**
+     * Tests the enableLegacyMode API that allows the embedder to force JavaCronetEngine
+     * instantiation even when the native Cronet engine implementation is available.
+     */
+    @SmallTest
+    public void testEnableLegacyMode() throws Exception {
+        ExperimentalCronetEngine.Builder builder =
+                new ExperimentalCronetEngine.Builder(getContext());
+        builder.enableLegacyMode(true);
+        CronetEngine engine = builder.build();
+        assertJavaEngine(engine);
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestCase.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestCase.java
new file mode 100644
index 0000000..6fcb18d
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/NativeCronetTestCase.java
@@ -0,0 +1,55 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.PathUtils;
+
+import java.io.File;
+
+/**
+ * Test base class for testing native Engine implementation. This class can import classes from the
+ * org.chromium.base package.
+ */
+public class NativeCronetTestCase extends CronetSmokeTestCase {
+    private static final String PRIVATE_DATA_DIRECTORY_SUFFIX = "cronet_test";
+    private static final String LOGFILE_NAME = "cronet-netlog.json";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        ContextUtils.initApplicationContext(getContext().getApplicationContext());
+        PathUtils.setPrivateDataDirectorySuffix(PRIVATE_DATA_DIRECTORY_SUFFIX);
+        mTestSupport.loadTestNativeLibrary();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        stopAndSaveNetLog();
+        super.tearDown();
+    }
+
+    @Override
+    protected void initCronetEngine() {
+        super.initCronetEngine();
+        assertNativeEngine(mCronetEngine);
+        startNetLog();
+    }
+
+    private void startNetLog() {
+        if (mCronetEngine != null) {
+            mCronetEngine.startNetLogToFile(
+                    PathUtils.getDataDirectory() + "/" + LOGFILE_NAME, false);
+        }
+    }
+
+    private void stopAndSaveNetLog() {
+        if (mCronetEngine == null) return;
+        mCronetEngine.stopNetLog();
+        File netLogFile = new File(PathUtils.getDataDirectory(), LOGFILE_NAME);
+        if (!netLogFile.exists()) return;
+        mTestSupport.processNetLog(getContext(), netLogFile);
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java
new file mode 100644
index 0000000..71cd784
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/PlatformOnlyEngineTest.java
@@ -0,0 +1,47 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.support.test.filters.SmallTest;
+
+import org.chromium.net.UrlRequest;
+
+/**
+ * Tests scenario when an app doesn't contain the native Cronet implementation.
+ */
+public class PlatformOnlyEngineTest extends CronetSmokeTestCase {
+    private String mURL;
+    private TestSupport.TestServer mServer;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Java-only implementation of the Cronet engine only supports Http/1 protocol.
+        mServer = mTestSupport.createTestServer(getContext(), TestSupport.Protocol.HTTP1);
+        assertTrue(mServer.start());
+        mURL = mServer.getSuccessURL();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mServer.shutdown();
+        super.tearDown();
+    }
+
+    /**
+     * Test a successful response when a request is sent by the Java Cronet Engine.
+     */
+    @SmallTest
+    public void testSuccessfulResponse() {
+        initCronetEngine();
+        assertJavaEngine(mCronetEngine);
+        SmokeTestRequestCallback callback = new SmokeTestRequestCallback();
+        UrlRequest.Builder requestBuilder =
+                mCronetEngine.newUrlRequestBuilder(mURL, callback, callback.getExecutor());
+        requestBuilder.build().start();
+        callback.blockForDone();
+        assertSuccessfulNonEmptyResponse(callback, mURL);
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java
new file mode 100644
index 0000000..5719a93
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/QuicTest.java
@@ -0,0 +1,72 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.support.test.filters.SmallTest;
+
+import org.json.JSONObject;
+
+import static org.chromium.net.smoke.TestSupport.Protocol.QUIC;
+
+import org.chromium.net.UrlRequest;
+
+import java.net.URL;
+
+/**
+ * QUIC Tests.
+ */
+public class QuicTest extends NativeCronetTestCase {
+    private TestSupport.TestServer mServer;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mServer = mTestSupport.createTestServer(getContext(), QUIC);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mServer.shutdown();
+        super.tearDown();
+    }
+
+    @SmallTest
+    public void testQuic() throws Exception {
+        assertTrue(mServer.start());
+        final String urlString = mServer.getSuccessURL();
+        final URL url = new URL(urlString);
+
+        mCronetEngineBuilder.enableQuic(true);
+        mCronetEngineBuilder.addQuicHint(url.getHost(), url.getPort(), url.getPort());
+        mTestSupport.installMockCertVerifierForTesting(mCronetEngineBuilder);
+
+        JSONObject quicParams = new JSONObject().put("delay_tcp_race", true);
+        JSONObject experimentalOptions = new JSONObject().put("QUIC", quicParams);
+        mTestSupport.addHostResolverRules(experimentalOptions);
+        experimentalOptions.put("host_whitelist", url.getHost());
+
+        mCronetEngineBuilder.setExperimentalOptions(experimentalOptions.toString());
+
+        initCronetEngine();
+
+        // QUIC is not guaranteed to win the race even with |delay_tcp_race| set, so try
+        // multiple times.
+        boolean quicNegotiated = false;
+
+        for (int i = 0; i < 5; i++) {
+            SmokeTestRequestCallback callback = new SmokeTestRequestCallback();
+            UrlRequest.Builder requestBuilder =
+                    mCronetEngine.newUrlRequestBuilder(urlString, callback, callback.getExecutor());
+            requestBuilder.build().start();
+            callback.blockForDone();
+            assertSuccessfulNonEmptyResponse(callback, urlString);
+            if (callback.getResponseInfo().getNegotiatedProtocol().startsWith("http/2+quic/")) {
+                quicNegotiated = true;
+                break;
+            }
+        }
+        assertTrue(quicNegotiated);
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java
new file mode 100644
index 0000000..fa07bfe
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/SmokeTestRequestCallback.java
@@ -0,0 +1,126 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import static junit.framework.Assert.assertTrue;
+
+import android.os.ConditionVariable;
+
+import org.chromium.net.CronetException;
+import org.chromium.net.UrlRequest;
+import org.chromium.net.UrlResponseInfo;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * A simple boilerplate implementation of {@link UrlRequest.Callback} that is used by smoke tests.
+ */
+class SmokeTestRequestCallback extends UrlRequest.Callback {
+    private static final int READ_BUFFER_SIZE = 10000;
+
+    // An executor that is used to execute {@link UrlRequest.Callback UrlRequest callbacks}.
+    private ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+
+    // Signals when the request is done either successfully or not.
+    private final ConditionVariable mDone = new ConditionVariable();
+
+    // The state of the request.
+    public enum State { NotSet, Succeeded, Failed, Canceled }
+
+    // The current state of the request.
+    private State mState = State.NotSet;
+
+    // Response info of the finished request.
+    private UrlResponseInfo mResponseInfo;
+
+    // Holds an error if the request failed.
+    private CronetException mError;
+
+    @Override
+    public void onRedirectReceived(UrlRequest request, UrlResponseInfo info, String newLocationUrl)
+            throws Exception {
+        request.followRedirect();
+    }
+
+    @Override
+    public void onResponseStarted(UrlRequest request, UrlResponseInfo info) throws Exception {
+        request.read(ByteBuffer.allocateDirect(READ_BUFFER_SIZE));
+    }
+
+    @Override
+    public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer)
+            throws Exception {
+        request.read(ByteBuffer.allocateDirect(READ_BUFFER_SIZE));
+    }
+
+    @Override
+    public void onSucceeded(UrlRequest request, UrlResponseInfo info) {
+        done(State.Succeeded, info);
+    }
+
+    @Override
+    public void onFailed(UrlRequest request, UrlResponseInfo info, CronetException error) {
+        mError = error;
+        done(State.Failed, info);
+    }
+
+    @Override
+    public void onCanceled(UrlRequest request, UrlResponseInfo info) {
+        done(State.Canceled, info);
+    }
+
+    /**
+     * Returns the request executor.
+     *
+     * @return the executor.
+     */
+    public Executor getExecutor() {
+        return mExecutor;
+    }
+
+    /**
+     * Blocks until the request is either succeeded, failed or canceled.
+     */
+    public void blockForDone() {
+        mDone.block();
+    }
+
+    /**
+     * Returns the final state of the request.
+     *
+     * @return the state.
+     */
+    public State getFinalState() {
+        return mState;
+    }
+
+    /**
+     * Returns an error that was passed to {@link #onFailed}  when the request failed.
+     *
+     * @return the error if the request failed; {@code null} otherwise.
+     */
+    public CronetException getFailureError() {
+        return mError;
+    }
+
+    /**
+     * Returns {@link UrlResponseInfo} of the finished response.
+     *
+     * @return the response info. {@code null} if the request hasn't completed yet.
+     */
+    public UrlResponseInfo getResponseInfo() {
+        return mResponseInfo;
+    }
+
+    private void done(State finalState, UrlResponseInfo responseInfo) {
+        assertTrue(mState == State.NotSet);
+        mResponseInfo = responseInfo;
+        mState = finalState;
+        mDone.open();
+    }
+}
diff --git a/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java
new file mode 100644
index 0000000..f205106
--- /dev/null
+++ b/components/cronet/android/test/smoketests/src/org/chromium/net/smoke/TestSupport.java
@@ -0,0 +1,93 @@
+// Copyright 2016 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.
+
+package org.chromium.net.smoke;
+
+import android.content.Context;
+
+import org.json.JSONObject;
+
+import org.chromium.net.ExperimentalCronetEngine;
+
+import java.io.File;
+
+/**
+ * Provides support for tests, so they can be run in different environments against different
+ * servers. It contains methods, which behavior can be different in different testing environments.
+ * The concrete implementation of this interface is determined dynamically at runtime by reading
+ * the value of |TestSupportImplClass| from the Android string resource file.
+ */
+public interface TestSupport {
+    enum Protocol {
+        HTTP1,
+        HTTP2,
+        QUIC,
+    }
+
+    /**
+     * Creates a new test server that supports a given {@code protocol}.
+     *
+     * @param context context.
+     * @param protocol protocol that should be supported by the server.
+     * @return an instance of the server.
+     *
+     * @throws UnsupportedOperationException if the implementation of this interface
+     *                                       does not support a given {@code protocol}.
+     */
+    TestServer createTestServer(Context context, Protocol protocol);
+
+    /**
+     * This method is called at the end of a test run if the netlog is available. An implementer
+     * of {@link TestSupport} can use it to process the result netlog; e.g., to copy the netlog
+     * to a directory where all test logs are collected. This method is optional and can be no-op.
+     *
+     * @param file the netlog file.
+     */
+    void processNetLog(Context context, File file);
+
+    /**
+     * Adds host resolver rules to a given experimental option JSON file.
+     * This method is optional.
+     *
+     * @param experimentalOptionsJson experimental options.
+     */
+    void addHostResolverRules(JSONObject experimentalOptionsJson);
+
+    /**
+     * Installs mock certificate verifier for a given {@code builder}.
+     * This method is optional.
+     *
+     * @param builder that should have the verifier installed.
+     */
+    void installMockCertVerifierForTesting(ExperimentalCronetEngine.Builder builder);
+
+    /**
+     * Loads a native library that is required for testing if any required.
+     */
+    void loadTestNativeLibrary();
+
+    /**
+     * A test server.
+     */
+    interface TestServer {
+        /**
+         * Starts the server.
+         *
+         * @return true if the server started successfully.
+         */
+        boolean start();
+
+        /**
+         * Shuts down the server.
+         */
+        void shutdown();
+
+        /**
+         * Return a URL that can be used by the test code to receive a successful response.
+         *
+         * @return the URL as a string.
+         */
+        String getSuccessURL();
+    }
+}
diff --git a/extensions/browser/event_listener_map.cc b/extensions/browser/event_listener_map.cc
index bd2524c..12dd5b4 100644
--- a/extensions/browser/event_listener_map.cc
+++ b/extensions/browser/event_listener_map.cc
@@ -14,6 +14,7 @@
 #include "extensions/browser/event_router.h"
 #include "ipc/ipc_message.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 
 using base::DictionaryValue;
 
@@ -37,7 +38,12 @@
     const GURL& listener_url,
     content::RenderProcessHost* process,
     std::unique_ptr<base::DictionaryValue> filter) {
-  return base::WrapUnique(new EventListener(event_name, "", listener_url,
+  // Use only the origin to identify the event listener, e.g. chrome://settings
+  // for chrome://settings/accounts, to avoid multiple events being triggered
+  // for the same process. See crbug.com/536858 for details. // TODO(devlin): If
+  // we dispatched events to processes more intelligently this could be avoided.
+  return base::WrapUnique(new EventListener(event_name, "",
+                                            url::Origin(listener_url).GetURL(),
                                             process, std::move(filter)));
 }
 
diff --git a/ios/chrome/browser/ui/browser_view_controller.mm b/ios/chrome/browser/ui/browser_view_controller.mm
index a612ccb..9263577d 100644
--- a/ios/chrome/browser/ui/browser_view_controller.mm
+++ b/ios/chrome/browser/ui/browser_view_controller.mm
@@ -3872,7 +3872,10 @@
 
   switch (command) {
     case IDC_BACK:
-      [[_model currentTab] goBack];
+      // TODO(crbug.com.677160): Remove |canGoBack| check.
+      if ([_model currentTab].canGoBack) {
+        [[_model currentTab] goBack];
+      }
       break;
     case IDC_BOOKMARK_PAGE:
       [self initializeBookmarkInteractionController];
@@ -3913,7 +3916,10 @@
       [self searchFindInPage];
       break;
     case IDC_FORWARD:
-      [[_model currentTab] goForward];
+      // TODO(crbug.com.677160): Remove |canGoForward| check.
+      if ([_model currentTab].canGoForward) {
+        [[_model currentTab] goForward];
+      }
       break;
     case IDC_FULLSCREEN:
       NOTIMPLEMENTED();
diff --git a/testing/buildbot/chromium.fyi.json b/testing/buildbot/chromium.fyi.json
index 5cb805d..9eb52c84 100644
--- a/testing/buildbot/chromium.fyi.json
+++ b/testing/buildbot/chromium.fyi.json
@@ -1694,7 +1694,6 @@
       {
         "args": [
           "--enable-browser-side-navigation",
-          "--test-launcher-filter-file=../../testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter",
           "--test-launcher-retry-limit=6"
         ],
         "swarming": {
@@ -1735,14 +1734,6 @@
     "scripts": [
       {
         "args": [
-          "browser_tests",
-          "../../testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter"
-        ],
-        "name": "count_filtered_tests_browser_tests",
-        "script": "count_filtered_tests.py"
-      },
-      {
-        "args": [
           "content_browsertests",
           "../../testing/buildbot/filters/browser-side-navigation.linux.content_browsertests.filter"
         ],
diff --git a/testing/buildbot/chromium.linux.json b/testing/buildbot/chromium.linux.json
index 5dd210d..fa85b29 100644
--- a/testing/buildbot/chromium.linux.json
+++ b/testing/buildbot/chromium.linux.json
@@ -2812,8 +2812,7 @@
       },
       {
         "args": [
-          "--enable-browser-side-navigation",
-          "--test-launcher-filter-file=../../testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter"
+          "--enable-browser-side-navigation"
         ],
         "name": "browser_side_navigation_browser_tests",
         "swarming": {
@@ -3482,8 +3481,7 @@
       },
       {
         "args": [
-          "--enable-browser-side-navigation",
-          "--test-launcher-filter-file=../../testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter"
+          "--enable-browser-side-navigation"
         ],
         "name": "browser_side_navigation_browser_tests",
         "swarming": {
diff --git a/testing/buildbot/filters/BUILD.gn b/testing/buildbot/filters/BUILD.gn
index 0fdb768..6804ad32 100644
--- a/testing/buildbot/filters/BUILD.gn
+++ b/testing/buildbot/filters/BUILD.gn
@@ -20,7 +20,6 @@
   testonly = true
 
   data = [
-    "//testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter",
     "//testing/buildbot/filters/isolate-extensions.browser_tests.filter",
     "//testing/buildbot/filters/mash.browser_tests.filter",
     "//testing/buildbot/filters/mojo.fyi.browser_tests.filter",
diff --git a/testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter b/testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter
deleted file mode 100644
index 6f3fc301..0000000
--- a/testing/buildbot/filters/browser-side-navigation.linux.browser_tests.filter
+++ /dev/null
@@ -1,6 +0,0 @@
--ChromeServiceWorkerTest.FallbackMainResourceRequestWhenJSDisabled
-
-# https://crbug.com/652767: NavigationHandle::GetResponseHeaders sometimes
-# returns null for browser-side navigations
--PageLoadMetricsBrowserTest.Ignore204Pages
--PageLoadMetricsBrowserTest.IgnoreDownloads
diff --git a/testing/buildbot/filters/browser-side-navigation.linux.content_browsertests.filter b/testing/buildbot/filters/browser-side-navigation.linux.content_browsertests.filter
index 15285ed..730c7a8 100644
--- a/testing/buildbot/filters/browser-side-navigation.linux.content_browsertests.filter
+++ b/testing/buildbot/filters/browser-side-navigation.linux.content_browsertests.filter
@@ -1,7 +1,2 @@
--NavigationControllerBrowserTest.EnsureSamePageNavigationUpdatesFrameNavigationEntry
-
 # Browser-initiated fragment navigations are handled improperly. https://crbug.com/663777
 -NavigationControllerBrowserTest.SamePageBrowserInitiated
-
-# Fail in Debug
--NavigationControllerBrowserTest.RaceCrossOriginNavigationAndSamePageHistoryNavigation
diff --git a/third_party/WebKit/LayoutTests/FlagExpectations/enable-slimming-paint-v2 b/third_party/WebKit/LayoutTests/FlagExpectations/enable-slimming-paint-v2
index b7f2a11e..c24abe4 100644
--- a/third_party/WebKit/LayoutTests/FlagExpectations/enable-slimming-paint-v2
+++ b/third_party/WebKit/LayoutTests/FlagExpectations/enable-slimming-paint-v2
@@ -538,6 +538,7 @@
 Bug(none) fast/canvas/canvas-composite-video.html [ Failure ]
 Bug(none) fast/canvas/canvas-css-clip-path.html [ Failure ]
 Bug(none) fast/canvas/webgl/pixelated.html [ Failure ]
+crbug.com/668342 fast/clip/010.html [ Failure ]
 Bug(none) fast/clip/nestedTransparencyClip.html [ Failure ]
 Bug(none) fast/clip/outline-overflowClip.html [ Failure ]
 Bug(none) fast/clip/overflow-border-radius-clip.html [ Failure ]
@@ -1247,6 +1248,7 @@
 Bug(none) svg/custom/grayscale-gradient-mask.svg [ Failure ]
 Bug(none) svg/custom/group-opacity.svg [ Failure ]
 Bug(none) svg/custom/image-with-preserveAspectRatio-none.html [ Failure ]
+Bug(none) svg/custom/image-with-transform-clip-filter.svg [ Failure ]
 Bug(none) svg/custom/inline-svg-in-xhtml.xml [ Failure ]
 Bug(none) svg/custom/invalid-css.svg [ Failure ]
 Bug(none) svg/custom/invalid-stroke-hex.svg [ Failure ]
@@ -1335,6 +1337,7 @@
 Bug(none) svg/custom/use-transform.svg [ Failure ]
 Bug(none) svg/custom/viewBox-hit.svg [ Failure ]
 Bug(none) svg/custom/viewbox-syntax.svg [ Failure ]
+Bug(none) svg/custom/viewport-clippath-invalidation.html [ Failure ]
 Bug(none) svg/custom/visibility-override-clip.svg [ Failure ]
 Bug(none) svg/custom/visibility-override-mask.svg [ Failure ]
 Bug(none) svg/custom/visited-link-color.svg [ Failure ]
@@ -2206,3 +2209,12 @@
 # SPv1 does not understand how to apply transforms properly in PaintLayerClipper (the issue is
 # also tracked in crbug.com/548184).
 # transforms/transform-overflow.html
+
+# Subpixel adjustments due to differences in compositing
+crbug.com/668342 fast/pagination/auto-height-with-break.html [ Failure ]
+crbug.com/668342 svg/custom/non-scaling-stroke-update.svg [ Failure ]
+crbug.com/668342 fast/css/transform-default-parameter.html [ Failure ]
+crbug.com/668342 svg/custom/use-css-events.svg [ Failure ]
+crbug.com/668342 svg/text/text-layout-crash.html [ Failure ]
+crbug.com/668342 fast/multicol/span/invalid-spanner-in-transform.html [ Failure ]
+crbug.com/668342 images/color-profile-iframe.html [ Failure ]
diff --git a/third_party/WebKit/LayoutTests/TestExpectations b/third_party/WebKit/LayoutTests/TestExpectations
index 9726cded..e08886a 100644
--- a/third_party/WebKit/LayoutTests/TestExpectations
+++ b/third_party/WebKit/LayoutTests/TestExpectations
@@ -43,7 +43,9 @@
 crbug.com/596780 virtual/spv2/compositing/framesets/composited-frame-alignment.html [ Pass ]
 crbug.com/596780 virtual/spv2/compositing/geometry/outline-change.html [ Pass ]
 crbug.com/600618 virtual/spv2/svg/custom/object-current-scale.html [ Pass ]
-crbug.com/600618 virtual/spv2/svg/custom/object-sizing-explicit-height.xhtml [ Pass ]
+
+# Re-add this once it rebaselines.
+# crbug.com/600618 virtual/spv2/svg/custom/object-sizing-explicit-height.xhtml [ Pass ]
 # SkiaBitLocker should avoid allocating huge offscreen buffer
 crbug.com/605812 [ Mac ] virtual/spv2/fast/overflow/overflow-height-float-not-removed-crash.html [ Skip ]
 crbug.com/605812 [ Mac ] virtual/spv2/fast/overflow/overflow-height-float-not-removed-crash2.html [ Skip ]
@@ -698,6 +700,8 @@
 crbug.com/520194 http/tests/xmlhttprequest/timeout/xmlhttprequest-timeout-worker-overridesexpires.html [ Failure Pass ]
 crbug.com/520194 virtual/mojo-loading/http/tests/xmlhttprequest/timeout/xmlhttprequest-timeout-worker-overridesexpires.html [ Failure Pass ]
 
+crbug.com/668342 virtual/spv2/svg/custom/object-sizing-explicit-height.xhtml [ NeedsRebaseline ]
+
 crbug.com/410974 fast/scroll-behavior/scroll-customization/scrollstate-basic.html [ Pass Failure ]
 crbug.com/410974 fast/scroll-behavior/scroll-customization/scrollstate-consume-deltas.html [ Pass Failure ]
 crbug.com/410974 fast/scroll-behavior/scroll-customization/scrollstate-consume-deltas-throw.html [ Pass Failure ]
@@ -2406,3 +2410,7 @@
 
 # Temporarily disable sharedarraybuffer test to land API change in v8
 crbug.com/676063 virtual/sharedarraybuffer/fast/workers/worker-sharedarraybuffer-transfer.html [ Pass Timeout ]
+
+# Flaky on Win7 (dbg)
+crbug.com/677145 [ Win7 Debug ] inspector/tracing/timeline-js/timeline-script-tag-1.html [ Pass Failure ]
+crbug.com/677145 [ Win7 Debug ] virtual/threaded/inspector/tracing/timeline-js/timeline-script-tag-1.html [ Pass Failure ]
diff --git a/third_party/WebKit/LayoutTests/paint/invalidation/overflow-hidden-yet-scrolled-with-custom-scrollbar-expected.txt b/third_party/WebKit/LayoutTests/paint/invalidation/overflow-hidden-yet-scrolled-with-custom-scrollbar-expected.txt
index 20097e11..47d9d336 100644
--- a/third_party/WebKit/LayoutTests/paint/invalidation/overflow-hidden-yet-scrolled-with-custom-scrollbar-expected.txt
+++ b/third_party/WebKit/LayoutTests/paint/invalidation/overflow-hidden-yet-scrolled-with-custom-scrollbar-expected.txt
@@ -30,6 +30,11 @@
         },
         {
           "object": "LayoutBlockFlow DIV id='target'",
+          "rect": [1, 601, 100, 100],
+          "reason": "subtree"
+        },
+        {
+          "object": "LayoutBlockFlow DIV id='target'",
           "rect": [1, 201, 100, 100],
           "reason": "subtree"
         },
diff --git a/third_party/WebKit/Source/core/frame/FrameView.cpp b/third_party/WebKit/Source/core/frame/FrameView.cpp
index be913bd..77f87433 100644
--- a/third_party/WebKit/Source/core/frame/FrameView.cpp
+++ b/third_party/WebKit/Source/core/frame/FrameView.cpp
@@ -1911,8 +1911,7 @@
     m_frame->document()->fetcher()->updateAllImageResourcePriorities();
 }
 
-void FrameView::updateLayersAndCompositingAfterScrollIfNeeded(
-    const ScrollOffset& scrollDelta) {
+void FrameView::updateLayersAndCompositingAfterScrollIfNeeded() {
   // Nothing to do after scrolling if there are no fixed position elements.
   if (!hasViewportConstrainedObjects())
     return;
@@ -1925,7 +1924,8 @@
       // TODO(skobes): Resolve circular dependency between scroll offset and
       // compositing state, and remove this disabler. https://crbug.com/420741
       DisableCompositingQueryAsserts disabler;
-      layer->updateLayerPositionsAfterOverflowScroll(scrollDelta);
+      layer->updateLayerPositionsAfterOverflowScroll();
+      layoutObject->setMayNeedPaintInvalidationSubtree();
     }
   }
 
@@ -3804,7 +3804,7 @@
 
   if (scrollTypeClearsFragmentAnchor(scrollType))
     clearFragmentAnchor();
-  updateLayersAndCompositingAfterScrollIfNeeded(scrollDelta);
+  updateLayersAndCompositingAfterScrollIfNeeded();
 
   Document* document = m_frame->document();
   document->enqueueScrollEventForNode(document);
diff --git a/third_party/WebKit/Source/core/frame/FrameView.h b/third_party/WebKit/Source/core/frame/FrameView.h
index 6f8b92d..47517031 100644
--- a/third_party/WebKit/Source/core/frame/FrameView.h
+++ b/third_party/WebKit/Source/core/frame/FrameView.h
@@ -933,8 +933,7 @@
   void scrollToFragmentAnchor();
   void didScrollTimerFired(TimerBase*);
 
-  void updateLayersAndCompositingAfterScrollIfNeeded(
-      const ScrollOffset& scrollDelta);
+  void updateLayersAndCompositingAfterScrollIfNeeded();
 
   static bool computeCompositedSelection(LocalFrame&, CompositedSelection&);
   void updateCompositedSelectionIfNeeded();
diff --git a/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp b/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp
index bd0101a2..16faf3f 100644
--- a/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp
+++ b/third_party/WebKit/Source/core/layout/LayoutBoxModelObject.cpp
@@ -306,17 +306,14 @@
   }
 
   if (RuntimeEnabledFeatures::slimmingPaintInvalidationEnabled()) {
-    // hasLayer status will affect whether to create localBorderBoxProperties.
-    // hasTransformRelatedProperty will affect whether to create transform node.
-    if (hadLayer != hasLayer() ||
-        hadTransformRelatedProperty != hasTransformRelatedProperty()) {
-      setNeedsPaintPropertyUpdate();
-    } else if (oldStyle && oldStyle->position() != styleRef().position() &&
-               (oldStyle->position() == FixedPosition ||
-                styleRef().position() == FixedPosition)) {
-      // Fixed-position status affects whether to create paintOffsetTranslation.
-      // TODO(chrishtr): Update the condition here when changing the condition
-      // in PaintPropertyTreeBuilder::updatePaintOffsetTranslation().
+    if ((oldStyle && oldStyle->position() != styleRef().position()) ||
+        hadLayer != hasLayer()) {
+      // This may affect paint properties of the current object, and descendants
+      // even if paint properties of the current object won't change. E.g. the
+      // stacking context and/or containing block of descendants may change.
+      setSubtreeNeedsPaintPropertyUpdate();
+    } else if (hadTransformRelatedProperty != hasTransformRelatedProperty()) {
+      // This affects whether to create transform node.
       setNeedsPaintPropertyUpdate();
     }
   }
diff --git a/third_party/WebKit/Source/core/layout/LayoutObject.cpp b/third_party/WebKit/Source/core/layout/LayoutObject.cpp
index 574b9fa..ec4269c 100644
--- a/third_party/WebKit/Source/core/layout/LayoutObject.cpp
+++ b/third_party/WebKit/Source/core/layout/LayoutObject.cpp
@@ -1259,13 +1259,6 @@
   return rect;
 }
 
-void LayoutObject::adjustPreviousPaintInvalidationForScrollIfNeeded(
-    const DoubleSize& scrollDelta) {
-  if (containerForPaintInvalidation().usesCompositedScrolling())
-    return;
-  m_previousVisualRect.move(LayoutSize(scrollDelta));
-}
-
 void LayoutObject::clearPreviousVisualRects() {
   setPreviousVisualRect(LayoutRect());
   ObjectPaintInvalidator(*this).setPreviousLocationInBacking(LayoutPoint());
diff --git a/third_party/WebKit/Source/core/layout/LayoutObject.h b/third_party/WebKit/Source/core/layout/LayoutObject.h
index 6115e81..f0138f4 100644
--- a/third_party/WebKit/Source/core/layout/LayoutObject.h
+++ b/third_party/WebKit/Source/core/layout/LayoutObject.h
@@ -1577,11 +1577,6 @@
     return m_previousPaintOffset;
   }
 
-  // Only adjusts if the paint invalidation container is not a composited
-  // scroller.
-  void adjustPreviousPaintInvalidationForScrollIfNeeded(
-      const DoubleSize& scrollDelta);
-
   PaintInvalidationReason fullPaintInvalidationReason() const {
     return m_bitfields.fullPaintInvalidationReason();
   }
diff --git a/third_party/WebKit/Source/core/paint/PaintLayer.cpp b/third_party/WebKit/Source/core/paint/PaintLayer.cpp
index 4f39b06..7a098e40 100644
--- a/third_party/WebKit/Source/core/paint/PaintLayer.cpp
+++ b/third_party/WebKit/Source/core/paint/PaintLayer.cpp
@@ -295,15 +295,6 @@
 void PaintLayer::updateLayerPositionRecursive() {
   updateLayerPosition();
 
-  // FIXME(400589): We would like to do this in
-  // PaintLayerScrollableArea::updateAfterLayout, but it depends on the size
-  // computed by updateLayerPosition.
-  if (m_scrollableArea) {
-    if (ScrollAnimatorBase* scrollAnimator =
-            m_scrollableArea->existingScrollAnimator())
-      scrollAnimator->updateAfterLayout();
-  }
-
   for (PaintLayer* child = firstChild(); child; child = child->nextSibling())
     child->updateLayerPositionRecursive();
 }
@@ -378,35 +369,9 @@
   return ancestorScrollingLayer() != other->ancestorScrollingLayer();
 }
 
-void PaintLayer::updateLayerPositionsAfterOverflowScroll(
-    const DoubleSize& scrollDelta) {
+void PaintLayer::updateLayerPositionsAfterOverflowScroll() {
   clipper().clearClipRectsIncludingDescendants();
-  updateLayerPositionsAfterScrollRecursive(scrollDelta,
-                                           isPaintInvalidationContainer());
-}
-
-void PaintLayer::updateLayerPositionsAfterScrollRecursive(
-    const DoubleSize& scrollDelta,
-    bool paintInvalidationContainerWasScrolled) {
-  updateLayerPosition();
-  if (paintInvalidationContainerWasScrolled &&
-      !isPaintInvalidationContainer()) {
-    // Paint invalidation rects are in the coordinate space of the paint
-    // invalidation container.  If it has scrolled, the rect must be adjusted.
-    // Note that it is not safe to reset it to the current bounds rect, as the
-    // LayoutObject may have moved since the
-    // last invalidation.
-    // FIXME(416535): Ideally, pending invalidations of scrolling content should
-    // be stored in the coordinate space of the scrolling content layer, so that
-    // they need no adjustment.
-    m_layoutObject->adjustPreviousPaintInvalidationForScrollIfNeeded(
-        scrollDelta);
-  }
-  for (PaintLayer* child = firstChild(); child; child = child->nextSibling()) {
-    child->updateLayerPositionsAfterScrollRecursive(
-        scrollDelta, paintInvalidationContainerWasScrolled &&
-                         !child->isPaintInvalidationContainer());
-  }
+  updateLayerPositionRecursive();
 }
 
 void PaintLayer::updateTransformationMatrix() {
diff --git a/third_party/WebKit/Source/core/paint/PaintLayer.h b/third_party/WebKit/Source/core/paint/PaintLayer.h
index 8f736a50..5ec98e3 100644
--- a/third_party/WebKit/Source/core/paint/PaintLayer.h
+++ b/third_party/WebKit/Source/core/paint/PaintLayer.h
@@ -292,7 +292,7 @@
   void updateLayerPosition();
 
   void updateLayerPositionsAfterLayout();
-  void updateLayerPositionsAfterOverflowScroll(const DoubleSize& scrollDelta);
+  void updateLayerPositionsAfterOverflowScroll();
 
   PaintLayer* enclosingPaginationLayer() const {
     return m_rareData ? m_rareData->enclosingPaginationLayer : nullptr;
@@ -990,9 +990,6 @@
   void dirtyAncestorChainHasSelfPaintingLayerDescendantStatus();
 
   void updateLayerPositionRecursive();
-  void updateLayerPositionsAfterScrollRecursive(
-      const DoubleSize& scrollDelta,
-      bool paintInvalidationContainerWasScrolled);
 
   void setNextSibling(PaintLayer* next) { m_next = next; }
   void setPreviousSibling(PaintLayer* prev) { m_previous = prev; }
diff --git a/third_party/WebKit/Source/core/paint/PaintLayerClipper.h b/third_party/WebKit/Source/core/paint/PaintLayerClipper.h
index 51232a6..df71e64 100644
--- a/third_party/WebKit/Source/core/paint/PaintLayerClipper.h
+++ b/third_party/WebKit/Source/core/paint/PaintLayerClipper.h
@@ -182,7 +182,7 @@
   LayoutRect localClipRect(const PaintLayer* ancestorLayer) const;
 
   // Computes the same thing as backgroundRect in calculateRects(), but skips
-  // apllying CSS clip and the visualOverflowRect() of |m_layer|.
+  // applying CSS clip and the visualOverflowRect() of |m_layer|.
   ClipRect backgroundClipRect(const ClipRectsContext&) const;
 
   // This method figures out our layerBounds in coordinates relative to
diff --git a/third_party/WebKit/Source/core/paint/PaintLayerScrollableArea.cpp b/third_party/WebKit/Source/core/paint/PaintLayerScrollableArea.cpp
index af28b1f..02acdf8 100644
--- a/third_party/WebKit/Source/core/paint/PaintLayerScrollableArea.cpp
+++ b/third_party/WebKit/Source/core/paint/PaintLayerScrollableArea.cpp
@@ -372,7 +372,6 @@
     return;
 
   showOverlayScrollbars();
-  ScrollOffset scrollDelta = getScrollOffset() - newOffset;
   m_scrollOffset = newOffset;
 
   LocalFrame* frame = box().frame();
@@ -393,7 +392,7 @@
   if (!frameView->isInPerformLayout()) {
     // If we're in the middle of layout, we'll just update layers once layout
     // has finished.
-    layer()->updateLayerPositionsAfterOverflowScroll(scrollDelta);
+    layer()->updateLayerPositionsAfterOverflowScroll();
     // Update regions, scrolling may change the clip of a particular region.
     frameView->updateDocumentAnnotatedRegions();
     frameView->setNeedsUpdateWidgetGeometries();
diff --git a/third_party/WebKit/Source/core/paint/PaintLayerTest.cpp b/third_party/WebKit/Source/core/paint/PaintLayerTest.cpp
index f137b1c..aa3d293a 100644
--- a/third_party/WebKit/Source/core/paint/PaintLayerTest.cpp
+++ b/third_party/WebKit/Source/core/paint/PaintLayerTest.cpp
@@ -345,4 +345,59 @@
                    ->m_needsDescendantDependentFlagsUpdate);
 }
 
+TEST_P(PaintLayerTest, PaintInvalidationOnNonCompositedScroll) {
+  if (RuntimeEnabledFeatures::slimmingPaintV2Enabled())
+    return;
+
+  setBodyInnerHTML(
+      "<style>* { margin: 0 } ::-webkit-scrollbar { display: none }</style>"
+      "<div id='scroller' style='overflow: scroll; width: 50px; height: 50px'>"
+      "  <div style='height: 400px'>"
+      "    <div id='content-layer' style='position: relative; height: 10px;"
+      "        top: 30px; background: blue'>"
+      "      <div id='content' style='height: 5px; background: yellow'></div>"
+      "    </div>"
+      "  </div>"
+      "</div>");
+
+  LayoutBox* scroller = toLayoutBox(getLayoutObjectByElementId("scroller"));
+  LayoutObject* contentLayer = getLayoutObjectByElementId("content-layer");
+  LayoutObject* content = getLayoutObjectByElementId("content");
+  EXPECT_EQ(LayoutRect(0, 30, 50, 10), contentLayer->visualRect());
+  EXPECT_EQ(LayoutRect(0, 30, 50, 5), content->visualRect());
+
+  scroller->getScrollableArea()->setScrollOffset(ScrollOffset(0, 20),
+                                                 ProgrammaticScroll);
+  document().view()->updateAllLifecyclePhases();
+  EXPECT_EQ(LayoutRect(0, 10, 50, 10), contentLayer->visualRect());
+  EXPECT_EQ(LayoutRect(0, 10, 50, 5), content->visualRect());
+}
+
+TEST_P(PaintLayerTest, PaintInvalidationOnCompositedScroll) {
+  enableCompositing();
+  setBodyInnerHTML(
+      "<style>* { margin: 0 } ::-webkit-scrollbar { display: none }</style>"
+      "<div id='scroller' style='overflow: scroll; width: 50px; height: 50px;"
+      "    will-change: transform'>"
+      "  <div style='height: 400px'>"
+      "    <div id='content-layer' style='position: relative; height: 10px;"
+      "        top: 30px; background: blue'>"
+      "      <div id='content' style='height: 5px; background: yellow'></div>"
+      "    </div>"
+      "  </div>"
+      "</div>");
+
+  LayoutBox* scroller = toLayoutBox(getLayoutObjectByElementId("scroller"));
+  LayoutObject* contentLayer = getLayoutObjectByElementId("content-layer");
+  LayoutObject* content = getLayoutObjectByElementId("content");
+  EXPECT_EQ(LayoutRect(0, 30, 50, 10), contentLayer->visualRect());
+  EXPECT_EQ(LayoutRect(0, 30, 50, 5), content->visualRect());
+
+  scroller->getScrollableArea()->setScrollOffset(ScrollOffset(0, 20),
+                                                 ProgrammaticScroll);
+  document().view()->updateAllLifecyclePhases();
+  EXPECT_EQ(LayoutRect(0, 30, 50, 10), contentLayer->visualRect());
+  EXPECT_EQ(LayoutRect(0, 30, 50, 5), content->visualRect());
+}
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilder.cpp b/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilder.cpp
index f329b31..6baebf0 100644
--- a/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilder.cpp
+++ b/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilder.cpp
@@ -319,6 +319,18 @@
       !object.styleRef().subtreeWillChangeContents())
     compositingReasons |= CompositingReasonWillChangeCompositingHint;
 
+  if (object.isBoxModelObject()) {
+    const LayoutBoxModelObject* box = toLayoutBoxModelObject(&object);
+    if (box->hasLayer()) {
+      // TODO(chrishtr): move this to the descendant-dependent flags recursion
+      // PaintLayer::updateDescendantDependentFlags.
+      box->layer()->update3DTransformedDescendantStatus();
+
+      if (box->layer()->has3DTransformedDescendant())
+        compositingReasons |= CompositingReason3DTransform;
+    }
+  }
+
   return compositingReasons;
 }
 
diff --git a/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilderTest.cpp b/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilderTest.cpp
index 969f3fa..10952ba 100644
--- a/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilderTest.cpp
+++ b/third_party/WebKit/Source/core/paint/PaintPropertyTreeBuilderTest.cpp
@@ -3156,4 +3156,31 @@
             willChange->paintProperties()->transform()->origin());
 }
 
+TEST_P(PaintPropertyTreeBuilderTest, ChangePositionUpdateDescendantProperties) {
+  setBodyInnerHTML(
+      "<style>"
+      "  * { margin: 0; }"
+      "  #ancestor { position: absolute; overflow: hidden }"
+      "  #descendant { position: absolute }"
+      "</style>"
+      "<div id='ancestor'>"
+      "  <div id='descendant'></div>"
+      "</div>");
+
+  LayoutObject* ancestor = getLayoutObjectByElementId("ancestor");
+  LayoutObject* descendant = getLayoutObjectByElementId("descendant");
+  EXPECT_EQ(ancestor->paintProperties()->overflowClip(),
+            descendant->paintProperties()
+                ->localBorderBoxProperties()
+                ->propertyTreeState.clip());
+
+  toElement(ancestor->node())
+      ->setAttribute(HTMLNames::styleAttr, "position: static");
+  document().view()->updateAllLifecyclePhases();
+  EXPECT_NE(ancestor->paintProperties()->overflowClip(),
+            descendant->paintProperties()
+                ->localBorderBoxProperties()
+                ->propertyTreeState.clip());
+}
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/core/paint/README.md b/third_party/WebKit/Source/core/paint/README.md
index 1fdc4b1..34d5d57 100644
--- a/third_party/WebKit/Source/core/paint/README.md
+++ b/third_party/WebKit/Source/core/paint/README.md
@@ -209,7 +209,8 @@
 
 *   Building paint property tree: creates paint property tree nodes for special
     things in the layout tree, including but not limit to: overflow clip, transform,
-    fixed-pos, animation, mask, filter, etc.
+    fixed-pos, animation, mask, filter, etc. Also sets direct compositing reasons to be
+    used later for compositing.
 
 *   Paint invalidation: Not implemented yet. TODO(wangxianzhu): add details after
     it's implemented.
diff --git a/third_party/WebKit/Source/platform/BUILD.gn b/third_party/WebKit/Source/platform/BUILD.gn
index 7f7fe873..5b3be6b 100644
--- a/third_party/WebKit/Source/platform/BUILD.gn
+++ b/third_party/WebKit/Source/platform/BUILD.gn
@@ -1024,6 +1024,7 @@
     "graphics/paint/PaintChunker.h",
     "graphics/paint/PaintController.cpp",
     "graphics/paint/PaintController.h",
+    "graphics/paint/PropertyTreeState.cpp",
     "graphics/paint/PropertyTreeState.h",
     "graphics/paint/RasterInvalidationTracking.cpp",
     "graphics/paint/RasterInvalidationTracking.h",
@@ -1755,6 +1756,7 @@
     "graphics/paint/PaintChunkTest.cpp",
     "graphics/paint/PaintChunkerTest.cpp",
     "graphics/paint/PaintControllerTest.cpp",
+    "graphics/paint/PropertyTreeStateTest.cpp",
     "image-decoders/FastSharedBufferReaderTest.cpp",
     "image-decoders/ImageDecoderTest.cpp",
     "image-decoders/ImageDecoderTestHelpers.cpp",
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.cpp b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.cpp
index a6ab798..00b42f49 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.cpp
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.cpp
@@ -7,9 +7,12 @@
 #include "cc/layers/content_layer_client.h"
 #include "cc/layers/layer.h"
 #include "cc/layers/picture_layer.h"
+#include "cc/playback/compositing_display_item.h"
 #include "cc/playback/display_item_list.h"
 #include "cc/playback/display_item_list_settings.h"
 #include "cc/playback/drawing_display_item.h"
+#include "cc/playback/filter_display_item.h"
+#include "cc/playback/float_clip_display_item.h"
 #include "cc/playback/transform_display_item.h"
 #include "cc/trees/clip_node.h"
 #include "cc/trees/effect_node.h"
@@ -22,7 +25,9 @@
 #include "platform/graphics/paint/DisplayItem.h"
 #include "platform/graphics/paint/DrawingDisplayItem.h"
 #include "platform/graphics/paint/ForeignLayerDisplayItem.h"
+#include "platform/graphics/paint/GeometryMapper.h"
 #include "platform/graphics/paint/PaintArtifact.h"
+#include "platform/graphics/paint/PropertyTreeState.h"
 #include "platform/graphics/paint/RasterInvalidationTracking.h"
 #include "platform/graphics/paint/ScrollPaintPropertyNode.h"
 #include "platform/graphics/paint/TransformPaintPropertyNode.h"
@@ -236,31 +241,6 @@
   }
 }
 
-static scoped_refptr<cc::DisplayItemList> recordPaintChunk(
-    const PaintArtifact& artifact,
-    const PaintChunk& chunk,
-    const gfx::Rect& combinedBounds) {
-  cc::DisplayItemListSettings settings;
-  scoped_refptr<cc::DisplayItemList> list =
-      cc::DisplayItemList::Create(settings);
-
-  gfx::Transform translation;
-  translation.Translate(-combinedBounds.x(), -combinedBounds.y());
-  // Passing combinedBounds as the visual rect for the begin/end transform item
-  // would normally be the sensible thing to do, but see comment above re:
-  // visual rects for drawing items and further rework in flight.
-  list->CreateAndAppendPairedBeginItem<cc::TransformDisplayItem>(translation);
-
-  const DisplayItemList& displayItems = artifact.getDisplayItemList();
-  for (const auto& displayItem : displayItems.itemsInPaintChunk(chunk))
-    appendDisplayItemToCcDisplayItemList(displayItem, list.get());
-
-  list->CreateAndAppendPairedEndItem<cc::EndTransformDisplayItem>();
-
-  list->Finalize();
-  return list;
-}
-
 scoped_refptr<cc::Layer> foreignLayerForPaintChunk(
     const PaintArtifact& paintArtifact,
     const PaintChunk& paintChunk,
@@ -290,8 +270,220 @@
 constexpr int kSecondaryRootNodeId = 1;
 constexpr int kPropertyTreeSequenceNumber = 1;
 
+enum EndDisplayItemType { EndTransform, EndClip, EndEffect };
+
+// Applies the clips between |localState| and |ancestorState| into a single
+// combined cc::FloatClipDisplayItem on |ccList|.
+static void applyClipsBetweenStates(const PropertyTreeState& localState,
+                                    const PropertyTreeState& ancestorState,
+                                    cc::DisplayItemList& ccList,
+                                    Vector<EndDisplayItemType>& endDisplayItems,
+                                    GeometryMapper& geometryMapper) {
+  DCHECK(localState.transform() == ancestorState.transform());
+#ifdef DCHECK_IS_ON
+  const TransformPaintPropertyNode* transformNode =
+      localState.clip()->localTransformSpace();
+  if (transformNode != ancestorState.transform()) {
+    bool success = false;
+    const TransformationMatrix& localToAncestorMatrix =
+        geometryMapper.localToAncestorMatrix(transformNode, ancestorState,
+                                             success);
+    DCHECK(success);
+    // Clips are only in descendant spaces that are transformed by one
+    // or more scrolls.
+    DCHECK(localToAncestorMatrix.isIdentityOrTranslation());
+  }
+#endif
+
+  FloatRect combinedClip;
+  bool success = false;
+  // TODO(chrishtr) :get rid of infiniteIntRect here.
+  combinedClip = geometryMapper.localToVisualRectInAncestorSpace(
+      FloatRect(LayoutRect::infiniteIntRect()), localState, ancestorState,
+      success);
+
+  DCHECK(success);
+
+  ccList.CreateAndAppendPairedBeginItem<cc::FloatClipDisplayItem>(
+      gfx::RectF(combinedClip));
+  endDisplayItems.push_back(EndClip);
+}
+
+static void recordPairedBeginDisplayItems(
+    const Vector<PropertyTreeState>& pairedStates,
+    const PropertyTreeState& pendingLayerState,
+    cc::DisplayItemList& ccList,
+    Vector<EndDisplayItemType>& endDisplayItems,
+    GeometryMapper& geometryMapper) {
+  PropertyTreeState mappedClipDestinationSpace = pendingLayerState;
+  PropertyTreeState clipSpace = pendingLayerState;
+  bool hasClip = false;
+
+  for (Vector<PropertyTreeState>::const_reverse_iterator pairedState =
+           pairedStates.rbegin();
+       pairedState != pairedStates.rend(); ++pairedState) {
+    switch (pairedState->innermostNode()) {
+      case PropertyTreeState::Transform: {
+        if (hasClip) {
+          applyClipsBetweenStates(clipSpace, mappedClipDestinationSpace, ccList,
+                                  endDisplayItems, geometryMapper);
+          hasClip = false;
+        }
+        mappedClipDestinationSpace = *pairedState;
+        clipSpace = *pairedState;
+
+        TransformationMatrix matrix = pairedState->transform()->matrix();
+        matrix.applyTransformOrigin(pairedState->transform()->origin());
+
+        gfx::Transform transform(gfx::Transform::kSkipInitialization);
+        transform.matrix() = TransformationMatrix::toSkMatrix44(matrix);
+
+        ccList.CreateAndAppendPairedBeginItem<cc::TransformDisplayItem>(
+            transform);
+        endDisplayItems.push_back(EndTransform);
+        break;
+      }
+      case PropertyTreeState::Clip: {
+        // Clips are handled in |applyClips| when ending the iterator, or
+        // transitioning between transform spaces. Here we store off the
+        // PropertyTreeState of the first found clip, under the transform of
+        // pairedState->transform(). All subsequent clips before applying the
+        // transform will be applied in applyClips.
+        clipSpace = *pairedState;
+        hasClip = true;
+#ifdef DCHECK_IS_ON
+        if (pairedState->clip()->localTransformSpace() !=
+            pairedState->transform()) {
+          const TransformationMatrix& localTransformMatrix =
+              pairedState->effect()->localTransformSpace()->matrix();
+          // Clips are only in descendant spaces that are transformed by scroll.
+          DCHECK(localTransformMatrix.isIdentityOrTranslation());
+        }
+#endif
+        break;
+      }
+      case PropertyTreeState::Effect: {
+        // TODO(chrishtr): skip effect and/or compositing display items if
+        // not necessary.
+
+        FloatRect clipRect =
+            pairedState->effect()->outputClip()->clipRect().rect();
+        // TODO(chrishtr): specify origin of the filter.
+        FloatPoint filterOrigin;
+        if (pairedState->effect()->localTransformSpace() !=
+            pairedState->transform()) {
+          bool success = false;
+          const TransformPaintPropertyNode* transformNode =
+              pairedState->effect()->localTransformSpace();
+          const TransformationMatrix& localToAncestorMatrix =
+              geometryMapper.localToAncestorMatrix(transformNode, *pairedState,
+                                                   success);
+          DCHECK(success);
+          // Effects are only in descendant spaces that are transformed by one
+          // or more scrolls.
+          DCHECK(localToAncestorMatrix.isIdentityOrTranslation());
+
+          clipRect = localToAncestorMatrix.mapRect(clipRect);
+          filterOrigin = localToAncestorMatrix.mapPoint(filterOrigin);
+        }
+
+        const bool kLcdTextRequiresOpaqueLayer = true;
+        ccList.CreateAndAppendPairedBeginItem<cc::CompositingDisplayItem>(
+            static_cast<uint8_t>(
+                gfx::ToFlooredInt(255 * pairedState->effect()->opacity())),
+            pairedState->effect()->blendMode(),
+            // TODO(chrishtr): compute bounds as necessary.
+            nullptr, nullptr, kLcdTextRequiresOpaqueLayer);
+
+        ccList.CreateAndAppendPairedBeginItem<cc::FilterDisplayItem>(
+            pairedState->effect()->filter().asCcFilterOperations(), clipRect,
+            gfx::PointF(filterOrigin.x(), filterOrigin.y()));
+
+        endDisplayItems.push_back(EndEffect);
+        break;
+      }
+      case PropertyTreeState::None:
+        break;
+    }
+  }
+
+  if (hasClip) {
+    applyClipsBetweenStates(clipSpace, mappedClipDestinationSpace, ccList,
+                            endDisplayItems, geometryMapper);
+  }
+}
+
+static void recordPairedEndDisplayItems(
+    const Vector<EndDisplayItemType>& endDisplayItemTypes,
+    cc::DisplayItemList* ccList) {
+  for (Vector<EndDisplayItemType>::const_reverse_iterator endType =
+           endDisplayItemTypes.rbegin();
+       endType != endDisplayItemTypes.rend(); ++endType) {
+    switch (*endType) {
+      case EndTransform:
+        ccList->CreateAndAppendPairedEndItem<cc::EndTransformDisplayItem>();
+        break;
+      case EndClip:
+        ccList->CreateAndAppendPairedEndItem<cc::EndFloatClipDisplayItem>();
+        break;
+      case EndEffect:
+        ccList->CreateAndAppendPairedEndItem<cc::EndFilterDisplayItem>();
+        ccList->CreateAndAppendPairedEndItem<cc::EndCompositingDisplayItem>();
+        break;
+    }
+  }
+}
+
 }  // namespace
 
+scoped_refptr<cc::DisplayItemList> PaintArtifactCompositor::recordPendingLayer(
+    const PaintArtifact& artifact,
+    const PendingLayer& pendingLayer,
+    const gfx::Rect& combinedBounds,
+    GeometryMapper& geometryMapper) {
+  cc::DisplayItemListSettings settings;
+  scoped_refptr<cc::DisplayItemList> ccList =
+      cc::DisplayItemList::Create(settings);
+
+  gfx::Transform translation;
+  translation.Translate(-combinedBounds.x(), -combinedBounds.y());
+  // Passing combinedBounds as the visual rect for the begin/end transform item
+  // would normally be the sensible thing to do, but see comment above re:
+  // visual rects for drawing items and further rework in flight.
+  ccList->CreateAndAppendPairedBeginItem<cc::TransformDisplayItem>(translation);
+
+  const DisplayItemList& displayItems = artifact.getDisplayItemList();
+  for (const auto& paintChunk : pendingLayer.paintChunks) {
+    const PropertyTreeState* state = &paintChunk->properties.propertyTreeState;
+    PropertyTreeStateIterator iterator(*state);
+    Vector<PropertyTreeState> pairedStates;
+    for (; state && *state != pendingLayer.propertyTreeState;
+         state = iterator.next()) {
+      if (state->innermostNode() != PropertyTreeState::None)
+        pairedStates.push_back(*state);
+    }
+
+    // TODO(chrishtr): we can avoid some extra paired display items if
+    // multiple PaintChunks share them. We can also collapse clips between
+    // transforms into single clips in the same way that PaintLayerClipper does.
+    Vector<EndDisplayItemType> endDisplayItems;
+
+    recordPairedBeginDisplayItems(pairedStates, pendingLayer.propertyTreeState,
+                                  *ccList.get(), endDisplayItems,
+                                  geometryMapper);
+
+    for (const auto& displayItem : displayItems.itemsInPaintChunk(*paintChunk))
+      appendDisplayItemToCcDisplayItemList(displayItem, ccList.get());
+
+    recordPairedEndDisplayItems(endDisplayItems, ccList.get());
+  }
+
+  ccList->CreateAndAppendPairedEndItem<cc::EndTransformDisplayItem>();
+
+  ccList->Finalize();
+  return ccList;
+}
+
 std::unique_ptr<PaintArtifactCompositor::ContentLayerClientImpl>
 PaintArtifactCompositor::clientForPaintChunk(
     const PaintChunk& paintChunk,
@@ -310,61 +502,83 @@
           : paintArtifact.getDisplayItemList()[paintChunk.beginIndex].getId()));
 }
 
-scoped_refptr<cc::Layer> PaintArtifactCompositor::layerForPaintChunk(
+scoped_refptr<cc::Layer>
+PaintArtifactCompositor::compositedLayerForPendingLayer(
     const PaintArtifact& paintArtifact,
-    const PaintChunk& paintChunk,
+    const PendingLayer& pendingLayer,
     gfx::Vector2dF& layerOffset,
     Vector<std::unique_ptr<ContentLayerClientImpl>>& newContentLayerClients,
-    RasterInvalidationTracking* tracking,
-    bool storeDebugInfo) {
-  DCHECK(paintChunk.size());
+    RasterInvalidationTrackingMap<const PaintChunk>* trackingMap,
+    bool storeDebugInfo,
+    GeometryMapper& geometryMapper) {
+  DCHECK(pendingLayer.paintChunks.size());
+  const PaintChunk& firstPaintChunk = *pendingLayer.paintChunks[0];
+  DCHECK(firstPaintChunk.size());
+#if DCHECK_IS_ON
+  for (const auto& paintChunk : pendingLayer.paintChunks) {
+    DCHECK(paintChunk.properties == firstPaintChunk.properties);
+  }
+#endif
 
   // If the paint chunk is a foreign layer, just return that layer.
-  if (scoped_refptr<cc::Layer> foreignLayer =
-          foreignLayerForPaintChunk(paintArtifact, paintChunk, layerOffset))
+  if (scoped_refptr<cc::Layer> foreignLayer = foreignLayerForPaintChunk(
+          paintArtifact, firstPaintChunk, layerOffset)) {
+    DCHECK_EQ(pendingLayer.paintChunks.size(), 1u);
     return foreignLayer;
+  }
 
   // The common case: create or reuse a PictureLayer for painted content.
   std::unique_ptr<ContentLayerClientImpl> contentLayerClient =
-      clientForPaintChunk(paintChunk, paintArtifact);
+      clientForPaintChunk(firstPaintChunk, paintArtifact);
 
-  gfx::Rect combinedBounds = enclosingIntRect(paintChunk.bounds);
-  scoped_refptr<cc::DisplayItemList> displayList =
-      recordPaintChunk(paintArtifact, paintChunk, combinedBounds);
+  gfx::Rect ccCombinedBounds(enclosingIntRect(pendingLayer.bounds));
+
+  scoped_refptr<cc::DisplayItemList> displayList = recordPendingLayer(
+      paintArtifact, pendingLayer, ccCombinedBounds, geometryMapper);
   contentLayerClient->SetDisplayList(std::move(displayList));
-  contentLayerClient->SetPaintableRegion(gfx::Rect(combinedBounds.size()));
+  contentLayerClient->SetPaintableRegion(gfx::Rect(ccCombinedBounds.size()));
 
-  layerOffset = combinedBounds.OffsetFromOrigin();
+  layerOffset = ccCombinedBounds.OffsetFromOrigin();
+
   scoped_refptr<cc::PictureLayer> ccPictureLayer =
       contentLayerClient->ccPictureLayer();
-  ccPictureLayer->SetBounds(combinedBounds.size());
+  ccPictureLayer->SetBounds(ccCombinedBounds.size());
   ccPictureLayer->SetIsDrawable(true);
-  if (paintChunk.knownToBeOpaque)
-    ccPictureLayer->SetContentsOpaque(true);
-  DCHECK(!tracking ||
-         tracking->trackedRasterInvalidations.size() ==
-             paintChunk.rasterInvalidationRects.size());
-
+  ccPictureLayer->SetContentsOpaque(pendingLayer.knownToBeOpaque);
   contentLayerClient->clearPaintChunkDebugData();
-  if (storeDebugInfo) {
-    contentLayerClient->addPaintChunkDebugData(
-        paintArtifact.getDisplayItemList().subsequenceAsJSON(
-            paintChunk.beginIndex, paintChunk.endIndex,
-            DisplayItemList::SkipNonDrawings |
-                DisplayItemList::ShownOnlyDisplayItemTypes));
-  }
 
-  for (unsigned index = 0; index < paintChunk.rasterInvalidationRects.size();
-       ++index) {
-    IntRect rect(enclosingIntRect(paintChunk.rasterInvalidationRects[index]));
-    gfx::Rect ccInvalidationRect(rect.x(), rect.y(), std::max(0, rect.width()),
-                                 std::max(0, rect.height()));
-    // Raster paintChunk.rasterInvalidationRects is in the space of the
-    // containing transform node, so need to subtract off the layer offset.
-    ccInvalidationRect.Offset(-combinedBounds.OffsetFromOrigin());
-    contentLayerClient->setNeedsDisplayRect(
-        ccInvalidationRect,
-        tracking ? &tracking->trackedRasterInvalidations[index] : nullptr);
+  for (const auto& paintChunk : pendingLayer.paintChunks) {
+    RasterInvalidationTracking* rasterTracking =
+        trackingMap ? trackingMap->find(paintChunk) : nullptr;
+    DCHECK(!rasterTracking ||
+           rasterTracking->trackedRasterInvalidations.size() ==
+               paintChunk->rasterInvalidationRects.size());
+
+    if (storeDebugInfo) {
+      contentLayerClient->addPaintChunkDebugData(
+          paintArtifact.getDisplayItemList().subsequenceAsJSON(
+              paintChunk->beginIndex, paintChunk->endIndex,
+              DisplayItemList::SkipNonDrawings |
+                  DisplayItemList::ShownOnlyDisplayItemTypes));
+    }
+
+    for (unsigned index = 0; index < paintChunk->rasterInvalidationRects.size();
+         ++index) {
+      IntRect rect(
+          enclosingIntRect(paintChunk->rasterInvalidationRects[index]));
+      gfx::Rect ccInvalidationRect(rect.x(), rect.y(),
+                                   std::max(0, rect.width()),
+                                   std::max(0, rect.height()));
+      if (ccInvalidationRect.IsEmpty())
+        continue;
+      // Raster paintChunk.rasterInvalidationRects is in the space of the
+      // containing transform node, so need to subtract off the layer offset.
+      ccInvalidationRect.Offset(-ccCombinedBounds.OffsetFromOrigin());
+      contentLayerClient->setNeedsDisplayRect(
+          ccInvalidationRect,
+          rasterTracking ? &rasterTracking->trackedRasterInvalidations[index]
+                         : nullptr);
+    }
   }
 
   newContentLayerClients.append(std::move(contentLayerClient));
@@ -677,6 +891,8 @@
   return result;
 }
 
+// TODO(chrishtr): templatize this to avoid duplication of
+// GeometryMapper::leastCommonAncestor.
 const EffectPaintPropertyNode* lowestCommonAncestor(
     const EffectPaintPropertyNode* nodeA,
     const EffectPaintPropertyNode* nodeB) {
@@ -791,10 +1007,109 @@
 
 }  // namespace
 
+bool PaintArtifactCompositor::canMergeInto(
+    const PaintArtifact& paintArtifact,
+    const PaintChunk& newChunk,
+    const PendingLayer& candidatePendingLayer) {
+  const PaintChunk& pendingLayerFirstChunk =
+      *candidatePendingLayer.paintChunks[0];
+  if (paintArtifact.getDisplayItemList()[newChunk.beginIndex].isForeignLayer())
+    return false;
+
+  if (paintArtifact.getDisplayItemList()[pendingLayerFirstChunk.beginIndex]
+          .isForeignLayer())
+    return false;
+
+  if (newChunk.properties.backfaceHidden !=
+      pendingLayerFirstChunk.properties.backfaceHidden)
+    return false;
+
+  DCHECK_GE(candidatePendingLayer.paintChunks.size(), 1u);
+  PropertyTreeStateIterator iterator(newChunk.properties.propertyTreeState);
+  for (const PropertyTreeState* currentState =
+           &newChunk.properties.propertyTreeState;
+       currentState; currentState = iterator.next()) {
+    if (currentState->hasDirectCompositingReasons())
+      return false;
+    if (*currentState == candidatePendingLayer.propertyTreeState)
+      return true;
+  }
+  return false;
+}
+
+bool PaintArtifactCompositor::mightOverlap(
+    const PaintChunk& paintChunk,
+    const PendingLayer& candidatePendingLayer,
+    GeometryMapper& geometryMapper) {
+  // TODO(chrishtr): implement
+  return true;
+}
+
+PaintArtifactCompositor::PendingLayer::PendingLayer(
+    const PaintChunk& firstPaintChunk)
+    : bounds(firstPaintChunk.bounds),
+      knownToBeOpaque(firstPaintChunk.knownToBeOpaque),
+      backfaceHidden(firstPaintChunk.properties.backfaceHidden),
+      propertyTreeState(firstPaintChunk.properties.propertyTreeState) {
+  paintChunks.append(&firstPaintChunk);
+}
+
+void PaintArtifactCompositor::PendingLayer::add(
+    const PaintChunk& paintChunk,
+    GeometryMapper* geometryMapper) {
+  DCHECK(paintChunk.properties.backfaceHidden == backfaceHidden);
+  paintChunks.append(&paintChunk);
+  FloatRect mappedBounds = paintChunk.bounds;
+  if (geometryMapper) {
+    bool success = false;
+    mappedBounds = geometryMapper->localToAncestorRect(
+        mappedBounds, paintChunk.properties.propertyTreeState,
+        propertyTreeState, success);
+    DCHECK(success);
+  }
+  bounds.unite(mappedBounds);
+  if (bounds.size() != paintChunks[0]->bounds.size()) {
+    if (bounds.size() != paintChunk.bounds.size())
+      knownToBeOpaque = false;
+    else
+      knownToBeOpaque = paintChunk.knownToBeOpaque;
+  }
+}
+
+void PaintArtifactCompositor::collectPendingLayers(
+    const PaintArtifact& paintArtifact,
+    Vector<PendingLayer>& pendingLayers,
+    GeometryMapper& geometryMapper) {
+  // n = # of paint chunks. Memoizing canMergeInto() can get it to O(n^2), and
+  // other heuristics can make worst-case behavior better.
+  for (const PaintChunk& paintChunk : paintArtifact.paintChunks()) {
+    bool createNew = true;
+    for (Vector<PendingLayer>::reverse_iterator candidatePendingLayer =
+             pendingLayers.rbegin();
+         candidatePendingLayer != pendingLayers.rend();
+         ++candidatePendingLayer) {
+      if (canMergeInto(paintArtifact, paintChunk, *candidatePendingLayer)) {
+        candidatePendingLayer->add(paintChunk, &geometryMapper);
+        createNew = false;
+        break;
+      }
+      if (mightOverlap(paintChunk, *candidatePendingLayer, geometryMapper)) {
+        break;
+      }
+    }
+    if (createNew)
+      pendingLayers.append(PendingLayer(paintChunk));
+  }
+}
+
 void PaintArtifactCompositor::update(
     const PaintArtifact& paintArtifact,
     RasterInvalidationTrackingMap<const PaintChunk>* rasterChunkInvalidations,
     bool storeDebugInfo) {
+#ifndef NDEBUG
+  storeDebugInfo = true;
+#endif
+
   DCHECK(m_rootLayer);
 
   cc::LayerTree* layerTree = m_rootLayer->GetLayerTree();
@@ -813,24 +1128,26 @@
   PropertyTreeManager propertyTreeManager(*layerTree->property_trees(),
                                           m_rootLayer.get());
 
+  Vector<PendingLayer, 0> pendingLayers;
+  GeometryMapper geometryMapper;
+  collectPendingLayers(paintArtifact, pendingLayers, geometryMapper);
+
   Vector<std::unique_ptr<ContentLayerClientImpl>> newContentLayerClients;
   newContentLayerClients.reserveCapacity(paintArtifact.paintChunks().size());
-  for (const PaintChunk& paintChunk : paintArtifact.paintChunks()) {
+  for (const PendingLayer& pendingLayer : pendingLayers) {
     gfx::Vector2dF layerOffset;
-    scoped_refptr<cc::Layer> layer = layerForPaintChunk(
-        paintArtifact, paintChunk, layerOffset, newContentLayerClients,
-        rasterChunkInvalidations ? rasterChunkInvalidations->find(&paintChunk)
-                                 : nullptr,
-        storeDebugInfo);
+    scoped_refptr<cc::Layer> layer = compositedLayerForPendingLayer(
+        paintArtifact, pendingLayer, layerOffset, newContentLayerClients,
+        rasterChunkInvalidations, storeDebugInfo, geometryMapper);
 
     int transformId = propertyTreeManager.compositorIdForTransformNode(
-        paintChunk.properties.propertyTreeState.transform());
+        pendingLayer.propertyTreeState.transform());
     int scrollId = propertyTreeManager.compositorIdForScrollNode(
-        paintChunk.properties.propertyTreeState.scroll());
+        pendingLayer.propertyTreeState.scroll());
     int clipId = propertyTreeManager.compositorIdForClipNode(
-        paintChunk.properties.propertyTreeState.clip());
+        pendingLayer.propertyTreeState.clip());
     int effectId = propertyTreeManager.switchToEffectNode(
-        *paintChunk.properties.propertyTreeState.effect());
+        *pendingLayer.propertyTreeState.effect());
 
     propertyTreeManager.updateScrollOffset(layer->id(), scrollId);
 
@@ -849,8 +1166,7 @@
                                      ->transform_tree.Node(transformId)
                                      ->sorting_context_id);
 
-    layer->SetShouldCheckBackfaceVisibility(
-        paintChunk.properties.backfaceHidden);
+    layer->SetShouldCheckBackfaceVisibility(pendingLayer.backfaceHidden);
 
     if (m_extraDataForTestingEnabled)
       m_extraDataForTesting->contentLayers.append(layer);
@@ -864,4 +1180,13 @@
   layerTree->property_trees()->ResetCachedData();
 }
 
+#ifndef NDEBUG
+void PaintArtifactCompositor::showDebugData() {
+  LOG(ERROR) << layersAsJSON(LayerTreeIncludesDebugInfo)
+                    ->toPrettyJSONString()
+                    .utf8()
+                    .data();
+}
+#endif
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.h b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.h
index bdb58952..38e955b 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.h
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositor.h
@@ -16,6 +16,7 @@
 #include <memory>
 
 namespace cc {
+class DisplayItemList;
 class Layer;
 }
 
@@ -25,6 +26,7 @@
 
 namespace blink {
 
+class GeometryMapper;
 class JSONObject;
 class PaintArtifact;
 class WebLayer;
@@ -81,24 +83,50 @@
 
   std::unique_ptr<JSONObject> layersAsJSON(LayerTreeFlags) const;
 
+#ifndef NDEBUG
+  void showDebugData();
+#endif
+
  private:
+  // A pending layer is a collection of paint chunks that will end up in
+  // the same cc::Layer.
+  struct PLATFORM_EXPORT PendingLayer {
+    PendingLayer(const PaintChunk& firstPaintChunk);
+    void add(const PaintChunk&, GeometryMapper*);
+    FloatRect bounds;
+    Vector<const PaintChunk*> paintChunks;
+    bool knownToBeOpaque;
+    bool backfaceHidden;
+    PropertyTreeState propertyTreeState;
+  };
+
   PaintArtifactCompositor();
 
   class ContentLayerClientImpl;
 
+  // Collects the PaintChunks into groups which will end up in the same
+  // cc layer. This includes testing PaintChunks for "merge" compatibility (e.g.
+  // directly composited property tree states are separately composited)
+  // and overlap testing (PaintChunks that overlap existing PaintLayers they
+  // are not compatible with must be separately composited).
+  void collectPendingLayers(const PaintArtifact&,
+                            Vector<PendingLayer>& pendingLayers,
+                            GeometryMapper&);
+
   // Builds a leaf layer that represents a single paint chunk.
   // Note: cc::Layer API assumes the layer bounds start at (0, 0), but the
   // bounding box of a paint chunk does not necessarily start at (0, 0) (and
   // could even be negative). Internally the generated layer translates the
   // paint chunk to align the bounding box to (0, 0) and return the actual
   // origin of the paint chunk in the |layerOffset| outparam.
-  scoped_refptr<cc::Layer> layerForPaintChunk(
+  scoped_refptr<cc::Layer> compositedLayerForPendingLayer(
       const PaintArtifact&,
-      const PaintChunk&,
+      const PendingLayer&,
       gfx::Vector2dF& layerOffset,
       Vector<std::unique_ptr<ContentLayerClientImpl>>& newContentLayerClients,
-      RasterInvalidationTracking*,
-      bool storeDebugInfo);
+      RasterInvalidationTrackingMap<const PaintChunk>*,
+      bool storeDebugInfo,
+      GeometryMapper&);
 
   // Finds a client among the current vector of clients that matches the paint
   // chunk's id, or otherwise allocates a new one.
@@ -106,6 +134,26 @@
       const PaintChunk&,
       const PaintArtifact&);
 
+  // This method is an implementation of Algorithm step 4 from goo.gl/6xP8Oe.
+  static scoped_refptr<cc::DisplayItemList> recordPendingLayer(
+      const PaintArtifact&,
+      const PendingLayer&,
+      const gfx::Rect& combinedBounds,
+      GeometryMapper&);
+
+  static bool canMergeInto(const PaintArtifact&,
+                           const PaintChunk& newChunk,
+                           const PendingLayer& candidatePendingLayer);
+
+  // Returns true if |newChunk| might overlap |candidatePendingLayer| in the
+  // root property tree space. If it does overlap, it will always return true.
+  // If it doesn't overlap, it might return true in cases were we can't
+  // efficiently determine a false value, or the truth depends on
+  // compositor animations.
+  static bool mightOverlap(const PaintChunk& newChunk,
+                           const PendingLayer& candidatePendingLayer,
+                           GeometryMapper&);
+
   scoped_refptr<cc::Layer> m_rootLayer;
   std::unique_ptr<WebLayer> m_webLayer;
   Vector<std::unique_ptr<ContentLayerClientImpl>> m_contentLayerClients;
@@ -115,6 +163,35 @@
   friend class StubChromeClientForSPv2;
 
   bool m_isTrackingRasterInvalidations;
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           ForeignLayerPassesThrough);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           MergeSimpleChunks);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           Merge2DTransform);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           MergeClip);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           MergeOpacity);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           MergeNested);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           ClipPushedUp);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           EffectPushedUp);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           EffectAndClipPushedUp);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           ClipAndEffectNoTransform);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           TwoClips);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           TwoEffects);
+
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           PendingLayer);
+  FRIEND_TEST_ALL_PREFIXES(PaintArtifactCompositorTestWithPropertyTrees,
+                           PendingLayerWithGeometry);
 };
 
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
index 3f9df35..fb445b3 100644
--- a/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
+++ b/third_party/WebKit/Source/platform/graphics/compositing/PaintArtifactCompositorTest.cpp
@@ -16,6 +16,7 @@
 #include "cc/trees/scroll_node.h"
 #include "cc/trees/transform_node.h"
 #include "platform/graphics/paint/EffectPaintPropertyNode.h"
+#include "platform/graphics/paint/GeometryMapper.h"
 #include "platform/graphics/paint/PaintArtifact.h"
 #include "platform/graphics/paint/ScrollPaintPropertyNode.h"
 #include "platform/testing/PaintPropertyTestHelpers.h"
@@ -28,7 +29,6 @@
 #include <memory>
 
 namespace blink {
-namespace {
 
 using ::blink::testing::createOpacityOnlyEffect;
 using ::testing::Pointee;
@@ -140,9 +140,9 @@
 TEST_F(PaintArtifactCompositorTestWithPropertyTrees, OneTransform) {
   // A 90 degree clockwise rotation about (100, 100).
   RefPtr<TransformPaintPropertyNode> transform =
-      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
-                                         TransformationMatrix().rotate(90),
-                                         FloatPoint3D(100, 100, 0));
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(), TransformationMatrix().rotate(90),
+          FloatPoint3D(100, 100, 0), false, 0, CompositingReason3DTransform);
 
   TestPaintArtifact artifact;
   artifact
@@ -190,9 +190,9 @@
 TEST_F(PaintArtifactCompositorTestWithPropertyTrees, TransformCombining) {
   // A translation by (5, 5) within a 2x scale about (10, 10).
   RefPtr<TransformPaintPropertyNode> transform1 =
-      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
-                                         TransformationMatrix().scale(2),
-                                         FloatPoint3D(10, 10, 0));
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(), TransformationMatrix().scale(2),
+          FloatPoint3D(10, 10, 0), false, 0, CompositingReason3DTransform);
   RefPtr<TransformPaintPropertyNode> transform2 =
       TransformPaintPropertyNode::create(
           transform1, TransformationMatrix().translate(5, 5), FloatPoint3D());
@@ -298,15 +298,18 @@
   // Establishes a 3D rendering context.
   RefPtr<TransformPaintPropertyNode> transform2 =
       TransformPaintPropertyNode::create(transform1, TransformationMatrix(),
-                                         FloatPoint3D(), false, 1);
+                                         FloatPoint3D(), false, 1,
+                                         CompositingReason3DTransform);
   // Extends the 3D rendering context of transform2.
   RefPtr<TransformPaintPropertyNode> transform3 =
       TransformPaintPropertyNode::create(transform2, TransformationMatrix(),
-                                         FloatPoint3D(), false, 1);
+                                         FloatPoint3D(), false, 1,
+                                         CompositingReason3DTransform);
   // Establishes a 3D rendering context distinct from transform2.
   RefPtr<TransformPaintPropertyNode> transform4 =
       TransformPaintPropertyNode::create(transform2, TransformationMatrix(),
-                                         FloatPoint3D(), false, 2);
+                                         FloatPoint3D(), false, 2,
+                                         CompositingReason3DTransform);
 
   TestPaintArtifact artifact;
   artifact
@@ -399,10 +402,12 @@
 TEST_F(PaintArtifactCompositorTestWithPropertyTrees, NestedClips) {
   RefPtr<ClipPaintPropertyNode> clip1 = ClipPaintPropertyNode::create(
       ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
-      FloatRoundedRect(100, 100, 700, 700));
+      FloatRoundedRect(100, 100, 700, 700),
+      CompositingReasonOverflowScrollingTouch);
   RefPtr<ClipPaintPropertyNode> clip2 =
       ClipPaintPropertyNode::create(clip1, TransformPaintPropertyNode::root(),
-                                    FloatRoundedRect(200, 200, 700, 100));
+                                    FloatRoundedRect(200, 200, 700, 100),
+                                    CompositingReasonOverflowScrollingTouch);
 
   TestPaintArtifact artifact;
   artifact
@@ -555,13 +560,35 @@
        ForeignLayerPassesThrough) {
   scoped_refptr<cc::Layer> layer = cc::Layer::Create();
 
-  TestPaintArtifact artifact;
-  artifact.chunk(defaultPaintChunkProperties())
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact.chunk(defaultPaintChunkProperties())
       .foreignLayer(FloatPoint(50, 100), IntSize(400, 300), layer);
-  update(artifact.build());
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::gray);
 
-  ASSERT_EQ(1u, contentLayerCount());
-  EXPECT_EQ(layer, contentLayerAt(0));
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer1(
+      artifact.paintChunks()[0]);
+  // Foreign layers can't merge.
+  EXPECT_FALSE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer1));
+  PaintArtifactCompositor::PendingLayer pendingLayer2(
+      artifact.paintChunks()[1]);
+  EXPECT_FALSE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer2));
+
+  update(artifact);
+
+  ASSERT_EQ(3u, contentLayerCount());
+  EXPECT_EQ(layer, contentLayerAt(1));
   EXPECT_EQ(gfx::Size(400, 300), layer->bounds());
   EXPECT_EQ(translation(50, 100), layer->screen_space_transform());
 }
@@ -718,5 +745,606 @@
                MainThreadScrollingReason::kHasBackgroundAttachmentFixedObjects);
 }
 
-}  // namespace
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, MergeSimpleChunks) {
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(2u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, MergeClip) {
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), clip.get(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // Clip is applied to this PaintChunk.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(10, 20, 50, 60), Color::black));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 300, 400), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, Merge2DTransform) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(50, 50), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(transform.get(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // Transform is applied to this PaintChunk.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(50, 50, 100, 100), Color::black));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, MergeOpacity) {
+  float opacity = 2.0 / 255.0;
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      ClipPaintPropertyNode::root(), CompositorFilterOperations(), opacity,
+      SkBlendMode::kSrcOver);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             effect.get())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // Transform is applied to this PaintChunk.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100),
+                      Color(Color::black).combineWithAlpha(opacity).rgb()));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, MergeNested) {
+  // Tests merging of an opacity effect, inside of a clip, inside of a
+  // transform.
+
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(50, 50), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform.get(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  float opacity = 2.0 / 255.0;
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform.get(), clip.get(),
+      CompositorFilterOperations(), opacity, SkBlendMode::kSrcOver);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact.chunk(transform.get(), clip.get(), effect.get())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // Transform is applied to this PaintChunk.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(60, 70, 50, 60),
+                      Color(Color::black).combineWithAlpha(opacity).rgb()));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, ClipPushedUp) {
+  // Tests merging of an element which has a clipapplied to it,
+  // but has an ancestor transform of them. This can happen for fixed-
+  // or absolute-position elements which escape scroll transforms.
+
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(20, 25), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  RefPtr<TransformPaintPropertyNode> transform2 =
+      TransformPaintPropertyNode::create(
+          transform.get(), TransformationMatrix().translate(20, 25),
+          FloatPoint3D(100, 100, 0), false, 0);
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform2.get(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), clip.get(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // The two transforms (combined translation of (40, 50)) are applied here,
+    // before clipping.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(50, 70, 50, 60), Color(Color::black)));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, EffectPushedUp) {
+  // Tests merging of an element which has an effect applied to it,
+  // but has an ancestor transform of them. This can happen for fixed-
+  // or absolute-position elements which escape scroll transforms.
+
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(20, 25), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  RefPtr<TransformPaintPropertyNode> transform2 =
+      TransformPaintPropertyNode::create(
+          transform.get(), TransformationMatrix().translate(20, 25),
+          FloatPoint3D(100, 100, 0), false, 0);
+
+  float opacity = 2.0 / 255.0;
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform2.get(),
+      ClipPaintPropertyNode::root(), CompositorFilterOperations(), opacity,
+      SkBlendMode::kSrcOver);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             effect.get())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 300, 400),
+                      Color(Color::black).combineWithAlpha(opacity).rgb()));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, EffectAndClipPushedUp) {
+  // Tests merging of an element which has an effect applied to it,
+  // but has an ancestor transform of them. This can happen for fixed-
+  // or absolute-position elements which escape scroll transforms.
+
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(20, 25), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  RefPtr<TransformPaintPropertyNode> transform2 =
+      TransformPaintPropertyNode::create(
+          transform.get(), TransformationMatrix().translate(20, 25),
+          FloatPoint3D(100, 100, 0), false, 0);
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform.get(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  float opacity = 2.0 / 255.0;
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform2.get(), clip.get(),
+      CompositorFilterOperations(), opacity, SkBlendMode::kSrcOver);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), clip.get(), effect.get())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // The clip is under |transform| but not |transform2|, so only an adjustment
+    // of (20, 25) occurs.
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(30, 45, 50, 60),
+                      Color(Color::black).combineWithAlpha(opacity).rgb()));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, ClipAndEffectNoTransform) {
+  // Tests merging of an element which has a clip and effect in the root
+  // transform space.
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  float opacity = 2.0 / 255.0;
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      clip.get(), CompositorFilterOperations(), opacity, SkBlendMode::kSrcOver);
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), clip.get(), effect.get())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(10, 20, 50, 60),
+                      Color(Color::black).combineWithAlpha(opacity).rgb()));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, TwoClips) {
+  // Tests merging of an element which has two clips in the root
+  // transform space.
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect(20, 30, 10, 20));
+
+  RefPtr<ClipPaintPropertyNode> clip2 = ClipPaintPropertyNode::create(
+      clip.get(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect(10, 20, 50, 60));
+
+  TestPaintArtifact testArtifact;
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 100, 100), Color::white);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), clip2.get(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 300, 400), Color::black);
+  testArtifact
+      .chunk(TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+             EffectPaintPropertyNode::root())
+      .rectDrawing(FloatRect(0, 0, 200, 300), Color::gray);
+
+  const PaintArtifact& artifact = testArtifact.build();
+
+  ASSERT_EQ(3u, artifact.paintChunks().size());
+  PaintArtifactCompositor::PendingLayer pendingLayer(artifact.paintChunks()[0]);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[1], pendingLayer));
+  pendingLayer.add(artifact.paintChunks()[1], nullptr);
+  EXPECT_TRUE(PaintArtifactCompositor::canMergeInto(
+      artifact, artifact.paintChunks()[2], pendingLayer));
+  update(artifact);
+
+  ASSERT_EQ(1u, contentLayerCount());
+  {
+    Vector<RectWithColor> rectsWithColor;
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 100, 100), Color::white));
+    // The interesction of the two clips is (20, 30, 10, 20).
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(20, 30, 10, 20), Color(Color::black)));
+    rectsWithColor.push_back(
+        RectWithColor(FloatRect(0, 0, 200, 300), Color::gray));
+
+    const cc::Layer* layer = contentLayerAt(0);
+    EXPECT_THAT(layer->GetPicture(), Pointee(drawsRectangles(rectsWithColor)));
+  }
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, PendingLayer) {
+  PaintChunk chunk1;
+  chunk1.properties.propertyTreeState = PropertyTreeState(
+      TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+      EffectPaintPropertyNode::root(), ScrollPaintPropertyNode::root());
+  chunk1.properties.backfaceHidden = true;
+  chunk1.knownToBeOpaque = true;
+  chunk1.bounds = FloatRect(0, 0, 30, 40);
+
+  PaintArtifactCompositor::PendingLayer pendingLayer(chunk1);
+
+  EXPECT_TRUE(pendingLayer.backfaceHidden);
+  EXPECT_TRUE(pendingLayer.knownToBeOpaque);
+  EXPECT_FLOAT_RECT_EQ(FloatRect(0, 0, 30, 40), pendingLayer.bounds);
+
+  PaintChunk chunk2;
+  chunk2.properties.propertyTreeState = chunk1.properties.propertyTreeState;
+  chunk2.properties.backfaceHidden = true;
+  chunk2.knownToBeOpaque = true;
+  chunk2.bounds = FloatRect(10, 20, 30, 40);
+  pendingLayer.add(chunk2, nullptr);
+
+  EXPECT_TRUE(pendingLayer.backfaceHidden);
+  // Bounds not equal to one PaintChunk.
+  EXPECT_FALSE(pendingLayer.knownToBeOpaque);
+  EXPECT_FLOAT_RECT_EQ(FloatRect(0, 0, 40, 60), pendingLayer.bounds);
+
+  PaintChunk chunk3;
+  chunk3.properties.propertyTreeState = chunk1.properties.propertyTreeState;
+  chunk3.properties.backfaceHidden = true;
+  chunk3.knownToBeOpaque = true;
+  chunk3.bounds = FloatRect(-5, -25, 20, 20);
+  pendingLayer.add(chunk3, nullptr);
+
+  EXPECT_TRUE(pendingLayer.backfaceHidden);
+  EXPECT_FALSE(pendingLayer.knownToBeOpaque);
+  EXPECT_FLOAT_RECT_EQ(FloatRect(-5, -25, 45, 85), pendingLayer.bounds);
+}
+
+TEST_F(PaintArtifactCompositorTestWithPropertyTrees, PendingLayerWithGeometry) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(
+          TransformPaintPropertyNode::root(),
+          TransformationMatrix().translate(20, 25), FloatPoint3D(100, 100, 0),
+          false, 0);
+
+  PaintChunk chunk1;
+  chunk1.properties.propertyTreeState = PropertyTreeState(
+      TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+      EffectPaintPropertyNode::root(), ScrollPaintPropertyNode::root());
+  chunk1.bounds = FloatRect(0, 0, 30, 40);
+
+  PaintArtifactCompositor::PendingLayer pendingLayer(chunk1);
+
+  EXPECT_FLOAT_RECT_EQ(FloatRect(0, 0, 30, 40), pendingLayer.bounds);
+
+  PaintChunk chunk2;
+  chunk2.properties.propertyTreeState = chunk1.properties.propertyTreeState;
+  chunk2.properties.propertyTreeState.setTransform(transform);
+  chunk2.bounds = FloatRect(0, 0, 50, 60);
+  GeometryMapper geometryMapper;
+  pendingLayer.add(chunk2, &geometryMapper);
+
+  EXPECT_FLOAT_RECT_EQ(FloatRect(0, 0, 70, 85), pendingLayer.bounds);
+}
+
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/graphics/compositing/README.md b/third_party/WebKit/Source/platform/graphics/compositing/README.md
new file mode 100644
index 0000000..29eb505
--- /dev/null
+++ b/third_party/WebKit/Source/platform/graphics/compositing/README.md
@@ -0,0 +1,59 @@
+# `Source/platform/graphics/compositing`
+
+This directory contains the implementation of the "Blink compositing algorithm".
+
+This code is owned by the [paint team][paint-team-site].
+[paint-team-site]: https://www.chromium.org/developers/paint-team
+
+This document explains the SPv2 world as it develops, not the SPv1 world it
+replaces.
+
+## Blink compositing algorithm
+
+Design document: goo.gl/6xP8Oe
+
+Inputs: `PaintArtifact`
+Outputs: List of `cc::Layer` objects and `cc::PropertyTree`'s.
+
+The algorithm walks through the list of `PaintChunks` in the `PaintArtifact`,
+allocating new `c::Layers` if the `PaintChunk` cannot merge into an existing
+`cc::Layer`. The reasons why it would not be able to do so are:
+
+1. The `PaintChunk` requires a foreign layer (see below)
+
+2. The `PaintChunk` cannot merge with any existing layer, due incompatible
+direct compositing reasons on its `PropertyTreeState`.
+
+3. The `PaintChunk` overlaps with an earlier `cc::Layer` that it can't merge with
+due to reason 2, and there is no later-drawn `cc::Layer` for which reasons 1 and
+2 do not apply.
+
+In the worst case, this algorithm has an O(n^2) running time, where n is the
+number of `PaintChunks`.
+
+All property tree nodes referred to by any `PaintChunk` are currently copied
+into their equivalent `cc::PropertyTree` node, regardless of whether they are
+required by the above.
+
+### Flattening property tree nodes
+
+When `PaintChunks` can merge into an existing `cc::Layer`, they may have
+different `PropertyTreeState`s than the `PropertyTreeState` of the `cc::Layer`.
+If so, we need to *flatten* down the nodes that are different between the
+`PropertyTreeState` of the `PaintChunk` and the `cc::Layer`. This is done
+by iterating over the "innermostNode" of the `PropertyTreeState`, finding
+the sequence of transform, clip and effect nodes that need to be flattened,
+and turning them into a sequence of paired display items representing the
+transforms, clips and effects. The algorithm for this is spelled out in
+the designdoc at http://goo.gl/6xP8Oe.
+
+### Foreign layers
+
+Some `PaintChunk` content requires a foreign layer, meaning a layer that is
+managed outside of the scope of this code. Examples are composited video, a
+and 2D/3D (WebGL) canvas.
+
+### Raster invalidations
+
+Any raster invalidates on a `PaintChunk` are also mapped to the space of the
+backing `cc::Layer`.
diff --git a/third_party/WebKit/Source/platform/graphics/paint/ClipPaintPropertyNode.h b/third_party/WebKit/Source/platform/graphics/paint/ClipPaintPropertyNode.h
index e86f72bc..80ec78512 100644
--- a/third_party/WebKit/Source/platform/graphics/paint/ClipPaintPropertyNode.h
+++ b/third_party/WebKit/Source/platform/graphics/paint/ClipPaintPropertyNode.h
@@ -31,9 +31,11 @@
   static PassRefPtr<ClipPaintPropertyNode> create(
       PassRefPtr<const ClipPaintPropertyNode> parent,
       PassRefPtr<const TransformPaintPropertyNode> localTransformSpace,
-      const FloatRoundedRect& clipRect) {
+      const FloatRoundedRect& clipRect,
+      CompositingReasons directCompositingReasons = CompositingReasonNone) {
     return adoptRef(new ClipPaintPropertyNode(
-        std::move(parent), std::move(localTransformSpace), clipRect));
+        std::move(parent), std::move(localTransformSpace), clipRect,
+        directCompositingReasons));
   }
 
   void update(PassRefPtr<const ClipPaintPropertyNode> parent,
@@ -59,8 +61,9 @@
   // The clone function is used by FindPropertiesNeedingUpdate.h for recording
   // a clip node before it has been updated, to later detect changes.
   PassRefPtr<ClipPaintPropertyNode> clone() const {
-    return adoptRef(
-        new ClipPaintPropertyNode(m_parent, m_localTransformSpace, m_clipRect));
+    return adoptRef(new ClipPaintPropertyNode(m_parent, m_localTransformSpace,
+                                              m_clipRect,
+                                              m_directCompositingReasons));
   }
 
   // The equality operator is used by FindPropertiesNeedingUpdate.h for checking
@@ -74,18 +77,25 @@
 
   String toString() const;
 
+  bool hasDirectCompositingReasons() const {
+    return m_directCompositingReasons != CompositingReasonNone;
+  }
+
  private:
   ClipPaintPropertyNode(
       PassRefPtr<const ClipPaintPropertyNode> parent,
       PassRefPtr<const TransformPaintPropertyNode> localTransformSpace,
-      const FloatRoundedRect& clipRect)
+      const FloatRoundedRect& clipRect,
+      CompositingReasons directCompositingReasons)
       : m_parent(parent),
         m_localTransformSpace(localTransformSpace),
-        m_clipRect(clipRect) {}
+        m_clipRect(clipRect),
+        m_directCompositingReasons(directCompositingReasons) {}
 
   RefPtr<const ClipPaintPropertyNode> m_parent;
   RefPtr<const TransformPaintPropertyNode> m_localTransformSpace;
   FloatRoundedRect m_clipRect;
+  CompositingReasons m_directCompositingReasons;
 };
 
 // Redeclared here to avoid ODR issues.
diff --git a/third_party/WebKit/Source/platform/graphics/paint/GeometryMapper.h b/third_party/WebKit/Source/platform/graphics/paint/GeometryMapper.h
index 7df60f2..b12dcffd 100644
--- a/third_party/WebKit/Source/platform/graphics/paint/GeometryMapper.h
+++ b/third_party/WebKit/Source/platform/graphics/paint/GeometryMapper.h
@@ -117,6 +117,14 @@
                                 const PropertyTreeState& ancestorState,
                                 bool& success);
 
+  // Returns the matrix used in |LocalToAncestorRect|. Sets |success| to false
+  // iff |localTransformNode| is not equal to or a descendant of
+  // |ancestorState.transform|.
+  const TransformationMatrix& localToAncestorMatrix(
+      const TransformPaintPropertyNode* localTransformNode,
+      const PropertyTreeState& ancestorState,
+      bool& success);
+
  private:
   // Used by mapToVisualRectInDestinationSpace() after fast mapping (assuming
   // destination is an ancestor of source) failed.
@@ -134,14 +142,6 @@
       const PropertyTreeState& destinationState,
       bool& success);
 
-  // Returns the matrix used in |LocalToAncestorRect|. Sets |success| to false
-  // iff |localTransformNode| is not equal to or a descendant of
-  // |ancestorState.transform|.
-  const TransformationMatrix& localToAncestorMatrix(
-      const TransformPaintPropertyNode* localTransformNode,
-      const PropertyTreeState& ancestorState,
-      bool& success);
-
   // Returns the "clip visual rect" between |localTransformState| and
   // |ancestorState|. See above for the definition of "clip visual rect".
   FloatRect localToAncestorClipRect(
diff --git a/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.cpp b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.cpp
new file mode 100644
index 0000000..8f2317a
--- /dev/null
+++ b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.cpp
@@ -0,0 +1,76 @@
+// Copyright 2016 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 "platform/graphics/paint/PropertyTreeState.h"
+
+#include "platform/graphics/paint/GeometryMapper.h"
+
+namespace blink {
+
+bool PropertyTreeState::hasDirectCompositingReasons() const {
+  switch (innermostNode()) {
+    case Transform:
+      return transform()->hasDirectCompositingReasons();
+    case Clip:
+      return clip()->hasDirectCompositingReasons();
+    case Effect:
+      return effect()->hasDirectCompositingReasons();
+    default:
+      return false;
+  }
+}
+
+template <typename PropertyNode>
+bool isAncestorOf(const PropertyNode* ancestor, const PropertyNode* child) {
+  while (child && child != ancestor) {
+    child = child->parent();
+  }
+  return child == ancestor;
+}
+
+PropertyTreeState::InnermostNode PropertyTreeState::innermostNode() const {
+  // TODO(chrishtr): this is very inefficient when innermostNode() is called
+  // repeatedly.
+  bool clipTransformStrictAncestorOfTransform =
+      m_clip->localTransformSpace() != m_transform.get() &&
+      isAncestorOf<TransformPaintPropertyNode>(m_clip->localTransformSpace(),
+                                               m_transform.get());
+  bool effectTransformStrictAncestorOfTransform =
+      m_effect->localTransformSpace() != m_transform.get() &&
+      isAncestorOf<TransformPaintPropertyNode>(m_effect->localTransformSpace(),
+                                               m_transform.get());
+
+  if (!m_transform->isRoot() && clipTransformStrictAncestorOfTransform &&
+      effectTransformStrictAncestorOfTransform)
+    return Transform;
+
+  bool clipAncestorOfEffect =
+      isAncestorOf<ClipPaintPropertyNode>(m_clip.get(), m_effect->outputClip());
+
+  if (!m_effect->isRoot() && clipAncestorOfEffect) {
+    return Effect;
+  }
+  if (!m_clip->isRoot())
+    return Clip;
+  return None;
+}
+
+const PropertyTreeState* PropertyTreeStateIterator::next() {
+  switch (m_properties.innermostNode()) {
+    case PropertyTreeState::Transform:
+      m_properties.setTransform(m_properties.transform()->parent());
+      return &m_properties;
+    case PropertyTreeState::Clip:
+      m_properties.setClip(m_properties.clip()->parent());
+      return &m_properties;
+    case PropertyTreeState::Effect:
+      m_properties.setEffect(m_properties.effect()->parent());
+      return &m_properties;
+    case PropertyTreeState::None:
+      return nullptr;
+  }
+  return nullptr;
+}
+
+}  // namespace blink
diff --git a/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.h b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.h
index c59fc27..1955559 100644
--- a/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.h
+++ b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeState.h
@@ -18,7 +18,7 @@
 // other objects.  RefPtrs are used to guard against use-after-free bugs and
 // DCHECKs ensure PropertyTreeState never retains the last reference to a
 // property tree node.
-class PropertyTreeState {
+class PLATFORM_EXPORT PropertyTreeState {
  public:
   PropertyTreeState(const TransformPaintPropertyNode* transform,
                     const ClipPaintPropertyNode* clip,
@@ -34,6 +34,8 @@
     DCHECK(!m_scroll || !m_scroll->hasOneRef());
   }
 
+  bool hasDirectCompositingReasons() const;
+
   const TransformPaintPropertyNode* transform() const {
     DCHECK(!m_transform || !m_transform->hasOneRef());
     return m_transform.get();
@@ -66,6 +68,50 @@
     m_scroll = std::move(node);
   }
 
+  enum InnermostNode {
+    None,  // None means that all nodes are their root values
+    Transform,
+    Clip,
+    Effect,
+  };
+
+  // There is always a well-defined order in which the transform, clip
+  // and effects of a PropertyTreeState apply. This method returns which
+  // of them applies first to content drawn with this PropertyTreeState.
+  // Note that it may be the case that multiple nodes from the same tree apply
+  // before any from another tree. This can happen, for example, if multiple
+  // effects or clips apply to a descendant transform state from the transform
+  // node.
+  //
+  // This method is meant to be used in concert with
+  // |PropertyTreeStateIterator|, which allows one to iterate over the nodes in
+  // the order in which they apply.
+  //
+  // Example:
+  //
+  //  Transform tree      Clip tree      Effect tree
+  //  ~~~~~~~~~~~~~~      ~~~~~~~~~      ~~~~~~~~~~~
+  //       Root              Root            Root
+  //        |                 |               |
+  //        T                 C               E
+  //
+  // Suppose that PropertyTreeState(T, C, E).innerMostNode() is E, and
+  // PropertytreeState(T, C, Root).innermostNode() is C. Then a PaintChunk
+  // that has propertyTreeState = PropertyTreeState(T, C, E) can be painted
+  // with the following display list structure:
+  //
+  // [BeginTransform] [BeginClip] [BeginEffect] PaintChunk drawings
+  //    [EndEffect] [EndClip] [EndTransform]
+  //
+  // The PropertyTreeStateIterator will behave like this:
+  //
+  // PropertyTreeStateIterator iterator(PropertyTreeState(T, C, E));
+  // DCHECK(iterator.innermostNode() == Effect);
+  // DCHECK(iterator.next()->innermostNode() == Clip);
+  // DCHECK(iterator.next()->innermostNode() == Transform);
+  // DCHECK(iterator.next()->innermostNode() == None);
+  InnermostNode innermostNode() const;
+
  private:
   RefPtr<const TransformPaintPropertyNode> m_transform;
   RefPtr<const ClipPaintPropertyNode> m_clip;
@@ -78,6 +124,21 @@
          a.effect() == b.effect() && a.scroll() == b.scroll();
 }
 
+// Iterates over the sequence transforms, clips and effects for a
+// PropertyTreeState between that state and the "root" state (all nodes equal
+// to *::Root()), in the order that they apply.
+//
+// See also PropertyTreeState::innermostNode for a more detailed example.
+class PLATFORM_EXPORT PropertyTreeStateIterator {
+ public:
+  PropertyTreeStateIterator(const PropertyTreeState& properties)
+      : m_properties(properties) {}
+  const PropertyTreeState* next();
+
+ private:
+  PropertyTreeState m_properties;
+};
+
 }  // namespace blink
 
 #endif  // PropertyTreeState_h
diff --git a/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeStateTest.cpp b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeStateTest.cpp
new file mode 100644
index 0000000..d211d86
--- /dev/null
+++ b/third_party/WebKit/Source/platform/graphics/paint/PropertyTreeStateTest.cpp
@@ -0,0 +1,156 @@
+// Copyright 2016 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 "platform/graphics/paint/PropertyTreeState.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace blink {
+
+class PropertyTreeStateTest : public ::testing::Test {};
+
+TEST_F(PropertyTreeStateTest, TrasformOnEffectOnClip) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect());
+
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      clip.get(), CompositorFilterOperations(), 1.0, SkBlendMode::kSrcOver);
+
+  PropertyTreeState state(transform.get(), clip.get(), effect.get(),
+                          ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::Transform, state.innermostNode());
+
+  PropertyTreeStateIterator iterator(state);
+  EXPECT_EQ(PropertyTreeState::Effect, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::Clip, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::None, iterator.next()->innermostNode());
+}
+
+TEST_F(PropertyTreeStateTest, RootState) {
+  PropertyTreeState state(
+      TransformPaintPropertyNode::root(), ClipPaintPropertyNode::root(),
+      EffectPaintPropertyNode::root(), ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::None, state.innermostNode());
+}
+
+TEST_F(PropertyTreeStateTest, EffectOnClipOnTransform) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform.get(), FloatRoundedRect());
+
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform.get(), clip.get(),
+      CompositorFilterOperations(), 1.0, SkBlendMode::kSrcOver);
+
+  PropertyTreeState state(transform.get(), clip.get(), effect.get(),
+                          ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::Effect, state.innermostNode());
+
+  PropertyTreeStateIterator iterator(state);
+  EXPECT_EQ(PropertyTreeState::Clip, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::Transform, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::None, iterator.next()->innermostNode());
+}
+
+TEST_F(PropertyTreeStateTest, ClipOnEffectOnTransform) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform.get(), FloatRoundedRect());
+
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform.get(),
+      ClipPaintPropertyNode::root(), CompositorFilterOperations(), 1.0,
+      SkBlendMode::kSrcOver);
+
+  PropertyTreeState state(transform.get(), clip.get(), effect.get(),
+                          ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::Clip, state.innermostNode());
+
+  PropertyTreeStateIterator iterator(state);
+  EXPECT_EQ(PropertyTreeState::Effect, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::Transform, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::None, iterator.next()->innermostNode());
+}
+
+TEST_F(PropertyTreeStateTest, ClipDescendantOfTransform) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<TransformPaintPropertyNode> transform2 =
+      TransformPaintPropertyNode::create(
+          transform.get(), TransformationMatrix(), FloatPoint3D());
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), transform2.get(), FloatRoundedRect());
+
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      ClipPaintPropertyNode::root(), CompositorFilterOperations(), 1.0,
+      SkBlendMode::kSrcOver);
+
+  // Here the clip is inside of its own transform, but the transform is an
+  // ancestor of the clip's transform. This models situations such as
+  // a clip inside a scroller that applies to an absolute-positioned element
+  // which escapes the scroll transform but not the clip.
+  PropertyTreeState state(transform.get(), clip.get(), effect.get(),
+                          ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::Clip, state.innermostNode());
+
+  PropertyTreeStateIterator iterator(state);
+  EXPECT_EQ(PropertyTreeState::Transform, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::Effect, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::None, iterator.next()->innermostNode());
+}
+
+TEST_F(PropertyTreeStateTest, EffectDescendantOfTransform) {
+  RefPtr<TransformPaintPropertyNode> transform =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<ClipPaintPropertyNode> clip = ClipPaintPropertyNode::create(
+      ClipPaintPropertyNode::root(), TransformPaintPropertyNode::root(),
+      FloatRoundedRect());
+
+  RefPtr<TransformPaintPropertyNode> transform2 =
+      TransformPaintPropertyNode::create(TransformPaintPropertyNode::root(),
+                                         TransformationMatrix(),
+                                         FloatPoint3D());
+
+  RefPtr<EffectPaintPropertyNode> effect = EffectPaintPropertyNode::create(
+      EffectPaintPropertyNode::root(), transform2.get(), clip.get(),
+      CompositorFilterOperations(), 1.0, SkBlendMode::kSrcOver);
+
+  // Here the clip is inside of its own transform, but the transform is an
+  // ancestor of the clip's transform. This models situations such as
+  // a clip inside a scroller that applies to an absolute-positioned element
+  // which escapes the scroll transform but not the clip.
+  PropertyTreeState state(transform.get(), clip.get(), effect.get(),
+                          ScrollPaintPropertyNode::root());
+  EXPECT_EQ(PropertyTreeState::Effect, state.innermostNode());
+
+  PropertyTreeStateIterator iterator(state);
+  EXPECT_EQ(PropertyTreeState::Transform, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::Clip, iterator.next()->innermostNode());
+  EXPECT_EQ(PropertyTreeState::None, iterator.next()->innermostNode());
+}
+
+}  // namespace blink
diff --git a/third_party/WebKit/Source/platform/scroll/ScrollAnimatorBase.h b/third_party/WebKit/Source/platform/scroll/ScrollAnimatorBase.h
index 6a81c42b..758431ce 100644
--- a/third_party/WebKit/Source/platform/scroll/ScrollAnimatorBase.h
+++ b/third_party/WebKit/Source/platform/scroll/ScrollAnimatorBase.h
@@ -90,7 +90,6 @@
   virtual void mouseMovedInContentArea() const {}
   virtual void mouseEnteredScrollbar(Scrollbar&) const {}
   virtual void mouseExitedScrollbar(Scrollbar&) const {}
-  virtual void updateAfterLayout() {}
   virtual void contentsResized() const {}
   virtual void contentAreaDidShow() const {}
   virtual void contentAreaDidHide() const {}
diff --git a/third_party/WebKit/Source/platform/testing/PictureMatchers.cpp b/third_party/WebKit/Source/platform/testing/PictureMatchers.cpp
index 1324f7b..5f04765a 100644
--- a/third_party/WebKit/Source/platform/testing/PictureMatchers.cpp
+++ b/third_party/WebKit/Source/platform/testing/PictureMatchers.cpp
@@ -16,81 +16,165 @@
 
 namespace {
 
-class DrawsRectangleCanvas : public SkCanvas {
- public:
-  DrawsRectangleCanvas() : SkCanvas(800, 600) {}
-  const Vector<std::pair<FloatQuad, Color>>& quads() const { return m_quads; }
-  void onDrawRect(const SkRect& rect, const SkPaint& paint) override {
-    SkPoint quad[4];
-    getTotalMatrix().mapRectToQuad(quad, rect);
-    FloatQuad floatQuad(quad);
-    m_quads.append(std::make_pair(floatQuad, Color(paint.getColor())));
-    SkCanvas::onDrawRect(rect, paint);
-  }
-
- private:
-  Vector<std::pair<FloatQuad, Color>> m_quads;
+struct QuadWithColor {
+  FloatQuad quad;
+  Color color;
 };
 
-class DrawsRectangleMatcher
+class DrawsRectangleCanvas : public SkCanvas {
+ public:
+  DrawsRectangleCanvas()
+      : SkCanvas(800, 600),
+        m_saveCount(0),
+        m_alpha(255),
+        m_alphaSaveLayerCount(-1) {}
+  const Vector<QuadWithColor>& quadsWithColor() const { return m_quads; }
+
+  void onDrawRect(const SkRect& rect, const SkPaint& paint) override {
+    SkRect clippedRect(rect);
+    for (Vector<ClipAndIndex>::const_reverse_iterator clip = m_clips.rbegin();
+         clip != m_clips.rend(); clip++) {
+      if (SkRect::Intersects(rect, clip->rect))
+        CHECK(clippedRect.intersect(clip->rect));
+    }
+    SkPoint quad[4];
+    getTotalMatrix().mapRectToQuad(quad, clippedRect);
+    QuadWithColor quadWithColor;
+    quadWithColor.quad = FloatQuad(quad);
+
+    unsigned paintAlpha = static_cast<unsigned>(paint.getAlpha());
+    SkPaint paintWithAlpha(paint);
+    paintWithAlpha.setAlpha(static_cast<U8CPU>(m_alpha * paintAlpha / 255));
+    quadWithColor.color = Color(paintWithAlpha.getColor());
+    m_quads.append(quadWithColor);
+    SkCanvas::onDrawRect(clippedRect, paint);
+  }
+
+  SkCanvas::SaveLayerStrategy getSaveLayerStrategy(
+      const SaveLayerRec& rec) override {
+    m_saveCount++;
+    unsigned layerAlpha = static_cast<unsigned>(rec.fPaint->getAlpha());
+    if (layerAlpha < 255) {
+      DCHECK_EQ(m_alphaSaveLayerCount, -1);
+      m_alphaSaveLayerCount = m_saveCount;
+      m_alpha = layerAlpha;
+    }
+    return SkCanvas::getSaveLayerStrategy(rec);
+  }
+
+  void willSave() override {
+    m_saveCount++;
+    SkCanvas::willSave();
+  }
+
+  void willRestore() override {
+    DCHECK_GT(m_saveCount, 0);
+    if (m_clips.size() && m_saveCount == m_clips.back().saveCount)
+      m_clips.pop_back();
+    if (m_alphaSaveLayerCount == m_saveCount) {
+      m_alpha = 255;
+      m_alphaSaveLayerCount = -1;
+    }
+    m_saveCount--;
+    SkCanvas::willRestore();
+  }
+
+  void onClipRect(const SkRect& rect,
+                  SkClipOp op,
+                  ClipEdgeStyle style) override {
+    ClipAndIndex clipStruct;
+    clipStruct.rect = rect;
+    clipStruct.saveCount = m_saveCount;
+    m_clips.push_back(clipStruct);
+    SkCanvas::onClipRect(rect, op, style);
+  }
+
+  struct ClipAndIndex {
+    SkRect rect;
+    int saveCount;
+  };
+
+ private:
+  Vector<QuadWithColor> m_quads;
+  Vector<ClipAndIndex> m_clips;
+  int m_saveCount;
+  unsigned m_alpha;
+  int m_alphaSaveLayerCount;
+};
+
+class DrawsRectanglesMatcher
     : public ::testing::MatcherInterface<const SkPicture&> {
  public:
-  DrawsRectangleMatcher(const FloatRect& rect, Color color)
-      : m_rect(rect), m_color(color) {}
+  DrawsRectanglesMatcher(const Vector<RectWithColor>& rectsWithColor)
+      : m_rectsWithColor(rectsWithColor) {}
 
   bool MatchAndExplain(
       const SkPicture& picture,
       ::testing::MatchResultListener* listener) const override {
     DrawsRectangleCanvas canvas;
     picture.playback(&canvas);
-    const auto& quads = canvas.quads();
-
-    if (quads.size() != 1) {
+    const auto& quads = canvas.quadsWithColor();
+    if (quads.size() != m_rectsWithColor.size()) {
       *listener << "which draws " << quads.size() << " quads";
       return false;
     }
 
-    const FloatQuad& quad = quads[0].first;
-    if (!quad.isRectilinear()) {
-      if (listener->IsInterested()) {
-        *listener << "which draws ";
-        PrintTo(quad, listener->stream());
-        *listener << " with color "
-                  << quads[0].second.serialized().ascii().data();
+    for (unsigned index = 0; index < quads.size(); index++) {
+      const auto& quadWithColor = quads[index];
+      const auto& rectWithColor = m_rectsWithColor[index];
+      if (!quadWithColor.quad.isRectilinear()) {
+        if (listener->IsInterested()) {
+          *listener << "at index " << index << " which draws ";
+          PrintTo(quadWithColor.quad, listener->stream());
+          *listener << " with color "
+                    << quadWithColor.color.serialized().ascii().data() << "\n";
+        }
+        return false;
       }
-      return false;
-    }
 
-    const FloatRect& rect = quad.boundingBox();
-    if (rect != m_rect || quads[0].second != m_color) {
-      if (listener->IsInterested()) {
-        *listener << "which draws ";
-        PrintTo(rect, listener->stream());
-        *listener << " with color "
-                  << quads[0].second.serialized().ascii().data();
+      const FloatRect& rect = quadWithColor.quad.boundingBox();
+      if (rect != rectWithColor.rect ||
+          quadWithColor.color != rectWithColor.color) {
+        if (listener->IsInterested()) {
+          *listener << "at index " << index << " which draws ";
+          PrintTo(rect, listener->stream());
+          *listener << " with color "
+                    << quadWithColor.color.serialized().ascii().data() << "\n";
+        }
+        return false;
       }
-      return false;
     }
 
     return true;
   }
 
   void DescribeTo(::std::ostream* os) const override {
-    *os << "draws ";
-    PrintTo(m_rect, os);
-    *os << " with color " << m_color.serialized().ascii().data();
+    *os << "\n";
+    for (unsigned index = 0; index < m_rectsWithColor.size(); index++) {
+      const auto& rectWithColor = m_rectsWithColor[index];
+      *os << "at index " << index << " rect draws ";
+      PrintTo(rectWithColor.rect, os);
+      *os << " with color " << rectWithColor.color.serialized().ascii().data()
+          << "\n";
+    }
   }
 
  private:
-  const FloatRect m_rect;
-  const Color m_color;
+  const Vector<RectWithColor> m_rectsWithColor;
 };
 
 }  // namespace
 
 ::testing::Matcher<const SkPicture&> drawsRectangle(const FloatRect& rect,
                                                     Color color) {
-  return ::testing::MakeMatcher(new DrawsRectangleMatcher(rect, color));
+  Vector<RectWithColor> rectsWithColor;
+  rectsWithColor.push_back(RectWithColor(rect, color));
+  return ::testing::MakeMatcher(new DrawsRectanglesMatcher(rectsWithColor));
+}
+
+::testing::Matcher<const SkPicture&> drawsRectangles(
+    const Vector<RectWithColor>& rectsWithColor) {
+  return ::testing::MakeMatcher(new DrawsRectanglesMatcher(rectsWithColor));
 }
 
 }  // namespace blink
diff --git a/third_party/WebKit/Source/platform/testing/PictureMatchers.h b/third_party/WebKit/Source/platform/testing/PictureMatchers.h
index 01368f2..3f400a9e 100644
--- a/third_party/WebKit/Source/platform/testing/PictureMatchers.h
+++ b/third_party/WebKit/Source/platform/testing/PictureMatchers.h
@@ -5,6 +5,7 @@
 #ifndef PictureMatchers_h
 #define PictureMatchers_h
 
+#include "platform/geometry/FloatRect.h"
 #include "platform/graphics/Color.h"
 #include "testing/gmock/include/gmock/gmock.h"
 
@@ -15,10 +16,25 @@
 class FloatRect;
 
 // Matches if the picture draws exactly one rectangle, which (after accounting
-// for the total transformation matrix) matches the rect provided, and whose
-// paint has the color requested.
+// for the total transformation matrix and applying any clips inside that
+// transform) matches the rect provided, and whose paint has the color
+// requested.
+// Note that clips which appear outside of a transform are not currently
+// supported.
 ::testing::Matcher<const SkPicture&> drawsRectangle(const FloatRect&, Color);
 
+struct RectWithColor {
+  RectWithColor(const FloatRect& rectArg, const Color& colorArg)
+      : rect(rectArg), color(colorArg) {}
+  FloatRect rect;
+  Color color;
+};
+
+// Same as above, but matches a number of rectangles equal to the size of the
+// given vector.
+::testing::Matcher<const SkPicture&> drawsRectangles(
+    const Vector<RectWithColor>&);
+
 }  // namespace blink
 
 #endif  // PictureMatchers_h
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py
index fece01a..fc70eec 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/controllers/test_result_writer.py
@@ -29,6 +29,7 @@
 
 import logging
 
+from webkitpy.common.html_diff import html_diff
 from webkitpy.layout_tests.controllers import repaint_overlay
 from webkitpy.layout_tests.models import test_failures
 
@@ -113,7 +114,7 @@
     FILENAME_SUFFIX_SAMPLE = "-sample"
     FILENAME_SUFFIX_LEAK_LOG = "-leak-log"
     FILENAME_SUFFIX_WDIFF = "-wdiff.html"
-    FILENAME_SUFFIX_PRETTY_PATCH = "-pretty-diff.html"
+    FILENAME_SUFFIX_HTML_DIFF = "-pretty-diff.html"
     FILENAME_SUFFIX_IMAGE_DIFF = "-diff.png"
     FILENAME_SUFFIX_IMAGE_DIFFS_HTML = "-diffs.html"
     FILENAME_SUFFIX_OVERLAY = "-overlay.html"
@@ -199,11 +200,10 @@
         if not actual_text or not expected_text:
             return
 
+        # Output a plain-text diff file.
         file_type = '.txt'
         actual_filename = self.output_filename(self.FILENAME_SUFFIX_ACTUAL + file_type)
         expected_filename = self.output_filename(self.FILENAME_SUFFIX_EXPECTED + file_type)
-        # We treat diff output as binary. Diff output may contain multiple files
-        # in conflicting encodings.
         diff = self._port.diff_text(expected_text, actual_text, expected_filename, actual_filename)
         diff_filename = self.output_filename(self.FILENAME_SUFFIX_DIFF + file_type)
         self._write_file(diff_filename, diff)
@@ -214,11 +214,10 @@
             wdiff_filename = self.output_filename(self.FILENAME_SUFFIX_WDIFF)
             self._write_file(wdiff_filename, wdiff)
 
-        # Use WebKit's PrettyPatch.rb to get an HTML diff.
-        if self._port.pretty_patch_available():
-            pretty_patch = self._port.pretty_patch_text(diff_filename)
-            pretty_patch_filename = self.output_filename(self.FILENAME_SUFFIX_PRETTY_PATCH)
-            self._write_file(pretty_patch_filename, pretty_patch)
+        # Output a HTML diff file.
+        html_diff_filename = self.output_filename(self.FILENAME_SUFFIX_HTML_DIFF)
+        html_diff_contents = html_diff(expected_text, actual_text)
+        self._write_file(html_diff_filename, html_diff_contents)
 
     def create_repaint_overlay_result(self, actual_text, expected_text):
         html = repaint_overlay.generate_repaint_overlay_html(self._test_name, actual_text, expected_text)
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/models/test_run_results.py b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/models/test_run_results.py
index 35706f7a..e58a5de 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/models/test_run_results.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/models/test_run_results.py
@@ -329,7 +329,7 @@
     results['interrupted'] = initial_results.interrupted
     results['layout_tests_dir'] = port_obj.layout_tests_dir()
     results['has_wdiff'] = port_obj.wdiff_available()
-    results['has_pretty_patch'] = port_obj.pretty_patch_available()
+    results['has_pretty_patch'] = True
     results['pixel_tests_enabled'] = port_obj.get_option('pixel_tests')
     results['seconds_since_epoch'] = int(time.time())
     results['build_number'] = port_obj.get_option('build_number')
@@ -337,6 +337,9 @@
     if port_obj.get_option('order') == 'random':
         results['random_order_seed'] = port_obj.get_option('seed')
     results['path_delimiter'] = '/'
+    # The pretty-diff.html files should always be available.
+    # TODO(qyearsley): Change this key since PrettyPatch.rb has been removed.
+    results['has_pretty_patch'] = True
 
     # Don't do this by default since it takes >100ms.
     # It's only used for rebaselining and uploading data to the flakiness dashboard.
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
index 589d20ea..2c60207f 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/run_webkit_tests_unittest.py
@@ -967,19 +967,19 @@
         self.assertNotIn('platform/test-win-win7/http/test.html', tests_run)
 
     def test_output_diffs(self):
-        # Test to ensure that we don't generate -wdiff.html or -pretty.html if wdiff and PrettyPatch
-        # aren't available.
+        # Test to ensure that we don't generate -wdiff.html if wdiff isn't available,
+        # but we always generate -diff.txt an -pretty-diff.html.
         host = MockHost()
         logging_run(['--pixel-tests', 'failures/unexpected/text-image-checksum.html'], tests_included=True, host=host)
         written_files = host.filesystem.written_files
-        self.assertTrue(any(path.endswith('-diff.txt') for path in written_files.keys()))
-        self.assertFalse(any(path.endswith('-wdiff.html') for path in written_files.keys()))
-        self.assertFalse(any(path.endswith('-pretty-diff.html') for path in written_files.keys()))
+        self.assertTrue(any(path.endswith('-diff.txt') for path in written_files))
+        self.assertTrue(any(path.endswith('-pretty-diff.html') for path in written_files))
+        self.assertFalse(any(path.endswith('-wdiff.html') for path in written_files))
 
         full_results_text = host.filesystem.read_text_file('/tmp/layout-test-results/full_results.json')
         full_results = json.loads(full_results_text.replace("ADD_RESULTS(", "").replace(");", ""))
         self.assertEqual(full_results['has_wdiff'], False)
-        self.assertEqual(full_results['has_pretty_patch'], False)
+        self.assertEqual(full_results['has_pretty_patch'], True)
 
     def test_unsupported_platform(self):
         stdout = StringIO.StringIO()
diff --git a/tools/win/pdb_compare_globals.py b/tools/win/pdb_compare_globals.py
new file mode 100644
index 0000000..0455a5d2
--- /dev/null
+++ b/tools/win/pdb_compare_globals.py
@@ -0,0 +1,92 @@
+# Copyright (c) 2016 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.
+
+"""
+This script uses ShowGlobals.exe to compare two PDBs to see what interesting
+globals are present in one but not the other. You can either pass in a .pdb file
+or you can pass in a .txt file that contains the results of calling ShowGlobals.
+This helps when investigating size regressions. Often code-size regressions are
+associated with global variable changes, and those global variables can be
+easier to track and investigate than the code.
+
+Typical output from ShowGlobals.exe is lines like these:
+
+  #Dups   DupSize   Size  Section Symbol name     PDB name
+
+  0       0       122784  2       kBrotliDictionary       chrome.dll.pdb
+  1       1824    0       0       LcidToLocaleNameTable   chrome.dll.pdb
+"""
+
+import os
+import subprocess
+import sys
+
+
+def LoadSymbols(pdb_name):
+  result = {}
+  extension = os.path.splitext(pdb_name)[1].lower()
+  if extension in ['.pdb']:
+    command = 'ShowGlobals.exe "%s"' % pdb_name
+    lines = subprocess.check_output(command).splitlines()
+  elif extension in ['.txt']:
+    lines = open(pdb_name).readlines()
+  else:
+    print 'Unrecognized extension in %s' % pdb_name
+    return result
+  for line in lines:
+    parts = line.split('\t')
+    if len(parts) >= 5 and not line.startswith('#'):
+      # Put the first four columns (the numerical data associated with a symbol)
+      # into a dictionary indexed by the fifth column, which is the symbol name.
+      symbol_name = parts[4]
+      result[symbol_name] = parts[:4]
+  return result
+
+
+def ShowExtras(symbols_A, symbols_B, name_A, name_B):
+  print 'Symbols that are in %s but not in %s' % (name_A, name_B)
+  for key in symbols_A:
+    if not key in symbols_B:
+      # Print all the numerical data, followed by the symbol name, separated by
+      # tabs.
+      print '\t'.join(symbols_A[key] + [key])
+  print
+
+
+def ShowDifferences(symbols_A, symbols_B, name_A, name_B):
+  print 'Symbols that are changed from %s to %s' % (name_A, name_B)
+  for key in symbols_A:
+    if key in symbols_B:
+      value_a = symbols_A[key]
+      value_b = symbols_B[key]
+      if value_a != value_b:
+        # Print the symbol name and then the two versions of the numerical data,
+        # indented.
+        print '%s changed from/to:' % key
+        print '\t' + '\t'.join(value_a)
+        print '\t' + '\t'.join(value_b)
+  print
+
+
+def main():
+  symbols_1 = LoadSymbols(sys.argv[1])
+  symbols_2 = LoadSymbols(sys.argv[2])
+
+  if len(symbols_1) == 0:
+    print 'No data found in %s - fastlink?' % sys.argv[1]
+    return
+  if len(symbols_2) == 0:
+    print 'No data found in %s - fastlink?' % sys.argv[2]
+    return
+
+  print ('%d interesting globals in %s, %d interesting globals in %s' %
+         (len(symbols_1), sys.argv[1], len(symbols_2), sys.argv[2]))
+
+  ShowExtras(symbols_1, symbols_2, sys.argv[1], sys.argv[2])
+  ShowExtras(symbols_2, symbols_1, sys.argv[2], sys.argv[1])
+  ShowDifferences(symbols_1, symbols_2, sys.argv[1], sys.argv[2])
+
+
+if __name__ == '__main__':
+  sys.exit(main())