diff --git a/DEPS b/DEPS
index ee83273..8bab1e8 100644
--- a/DEPS
+++ b/DEPS
@@ -235,7 +235,7 @@
     Var('chromium_git') + '/native_client/src/third_party/scons-2.0.1.git' + '@' + '1c1550e17fc26355d08627fbdec13d8291227067',
 
   'src/third_party/webrtc':
-    Var('chromium_git') + '/external/webrtc/trunk/webrtc.git' + '@' + '2048bc47c23b16f614fd6ab680b6faa0cf02a2f4', # commit position 19063
+    Var('chromium_git') + '/external/webrtc/trunk/webrtc.git' + '@' + 'a7461e3086866272cb69fbfa23c7fc8b0321a425', # commit position 19097
 
   'src/third_party/openmax_dl':
     Var('chromium_git') + '/external/webrtc/deps/third_party/openmax.git' + '@' +  Var('openmax_dl_revision'),
diff --git a/base/BUILD.gn b/base/BUILD.gn
index b50a8c05..bba01d22 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -948,14 +948,12 @@
     "trace_event/heap_profiler_allocation_register_win.cc",
     "trace_event/heap_profiler_event_filter.cc",
     "trace_event/heap_profiler_event_filter.h",
-    "trace_event/heap_profiler_event_writer.cc",
-    "trace_event/heap_profiler_event_writer.h",
+    "trace_event/heap_profiler_heap_dump_writer.cc",
+    "trace_event/heap_profiler_heap_dump_writer.h",
     "trace_event/heap_profiler_serialization_state.cc",
     "trace_event/heap_profiler_serialization_state.h",
     "trace_event/heap_profiler_stack_frame_deduplicator.cc",
     "trace_event/heap_profiler_stack_frame_deduplicator.h",
-    "trace_event/heap_profiler_string_deduplicator.cc",
-    "trace_event/heap_profiler_string_deduplicator.h",
     "trace_event/heap_profiler_type_name_deduplicator.cc",
     "trace_event/heap_profiler_type_name_deduplicator.h",
     "trace_event/java_heap_dump_provider_android.cc",
@@ -2232,9 +2230,8 @@
     "trace_event/event_name_filter_unittest.cc",
     "trace_event/heap_profiler_allocation_context_tracker_unittest.cc",
     "trace_event/heap_profiler_allocation_register_unittest.cc",
-    "trace_event/heap_profiler_event_writer_unittest.cc",
+    "trace_event/heap_profiler_heap_dump_writer_unittest.cc",
     "trace_event/heap_profiler_stack_frame_deduplicator_unittest.cc",
-    "trace_event/heap_profiler_string_deduplicator_unittest.cc",
     "trace_event/heap_profiler_type_name_deduplicator_unittest.cc",
     "trace_event/java_heap_dump_provider_android_unittest.cc",
     "trace_event/memory_allocator_dump_unittest.cc",
diff --git a/base/trace_event/heap_profiler_event_writer.cc b/base/trace_event/heap_profiler_event_writer.cc
deleted file mode 100644
index c09a1e2..0000000
--- a/base/trace_event/heap_profiler_event_writer.cc
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/trace_event/heap_profiler_event_writer.h"
-
-#include <stdint.h>
-
-#include <tuple>
-#include <unordered_map>
-
-#include "base/bind.h"
-#include "base/memory/ptr_util.h"
-#include "base/numerics/safe_conversions.h"
-#include "base/trace_event/heap_profiler_serialization_state.h"
-#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
-#include "base/trace_event/sharded_allocation_register.h"
-#include "base/trace_event/trace_event.h"
-#include "base/trace_event/trace_event_argument.h"
-
-namespace base {
-namespace trace_event {
-
-namespace {
-
-struct AggregationKey {
-  int backtrace_id;
-  int type_id;
-
-  struct Hasher {
-    size_t operator()(const AggregationKey& key) const {
-      return base::HashInts(key.backtrace_id, key.type_id);
-    }
-  };
-
-  bool operator==(const AggregationKey& other) const {
-    return backtrace_id == other.backtrace_id && type_id == other.type_id;
-  }
-};
-
-}  // namespace
-
-std::unique_ptr<TracedValue> SerializeHeapDump(
-    const ShardedAllocationRegister& allocation_register,
-    HeapProfilerSerializationState* serialization_state) {
-  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"), "SerializeHeapDump");
-  // Aggregate allocations by {backtrace_id, type_id} key.
-  using MetricsMap = std::unordered_map<AggregationKey, AllocationMetrics,
-                                        AggregationKey::Hasher>;
-  MetricsMap metrics_by_key;
-
-  auto visit_allocation =
-      [](HeapProfilerSerializationState* serialization_state,
-         MetricsMap* metrics_by_key,
-         const AllocationRegister::Allocation& allocation) {
-        int backtrace_id =
-            serialization_state->stack_frame_deduplicator()->Insert(
-                std::begin(allocation.context.backtrace.frames),
-                std::begin(allocation.context.backtrace.frames) +
-                    allocation.context.backtrace.frame_count);
-
-        int type_id = serialization_state->type_name_deduplicator()->Insert(
-            allocation.context.type_name);
-
-        AggregationKey key = {backtrace_id, type_id};
-        AllocationMetrics& metrics = (*metrics_by_key)[key];
-        metrics.size += allocation.size;
-        metrics.count += 1;
-      };
-  {
-    TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"),
-                 "SerializeHeapDump.VisitAllocations");
-    allocation_register.VisitAllocations(base::BindRepeating(
-        visit_allocation, base::Unretained(serialization_state),
-        base::Unretained(&metrics_by_key)));
-  }
-
-  auto traced_value = MakeUnique<TracedValue>();
-
-  traced_value->BeginArray("nodes");
-  for (const auto& key_and_metrics : metrics_by_key)
-    traced_value->AppendInteger(key_and_metrics.first.backtrace_id);
-  traced_value->EndArray();
-
-  traced_value->BeginArray("types");
-  for (const auto& key_and_metrics : metrics_by_key)
-    traced_value->AppendInteger(key_and_metrics.first.type_id);
-  traced_value->EndArray();
-
-  traced_value->BeginArray("counts");
-  for (const auto& key_and_metrics : metrics_by_key)
-    traced_value->AppendInteger(
-        saturated_cast<int>(key_and_metrics.second.count));
-  traced_value->EndArray();
-
-  traced_value->BeginArray("sizes");
-  for (const auto& key_and_metrics : metrics_by_key)
-    traced_value->AppendInteger(
-        saturated_cast<int>(key_and_metrics.second.size));
-  traced_value->EndArray();
-
-  return traced_value;
-}
-
-std::unique_ptr<TracedValue> SerializeHeapProfileEventData(
-    const SerializedHeapDumpsMap& heap_dumps,
-    HeapProfilerSerializationState* serialization_state) {
-  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"),
-               "SerializeHeapProfileEventData");
-  auto traced_value = MakeUnique<TracedValue>();
-
-  // See brief description of the format in the header file.
-  traced_value->SetInteger("version", 1);
-
-  traced_value->BeginDictionary("allocators");
-  for (const auto& name_and_dump : heap_dumps) {
-    traced_value->SetValueWithCopiedName(name_and_dump.first.c_str(),
-                                         *name_and_dump.second);
-  }
-  traced_value->EndDictionary();
-
-  traced_value->BeginDictionary("maps");
-
-  if (auto* deduplicator = serialization_state->stack_frame_deduplicator()) {
-    traced_value->BeginArray("nodes");
-    deduplicator->SerializeIncrementally(&*traced_value);
-    traced_value->EndArray();
-  }
-
-  if (auto* deduplicator = serialization_state->type_name_deduplicator()) {
-    traced_value->BeginArray("types");
-    deduplicator->SerializeIncrementally(&*traced_value);
-    traced_value->EndArray();
-  }
-
-  if (auto* deduplicator = serialization_state->string_deduplicator()) {
-    traced_value->BeginArray("strings");
-    deduplicator->SerializeIncrementally(&*traced_value);
-    traced_value->EndArray();
-  }
-
-  traced_value->EndDictionary();
-
-  return traced_value;
-}
-
-}  // namespace trace_event
-}  // namespace base
diff --git a/base/trace_event/heap_profiler_event_writer.h b/base/trace_event/heap_profiler_event_writer.h
deleted file mode 100644
index 78fa89d..0000000
--- a/base/trace_event/heap_profiler_event_writer.h
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef BASE_TRACE_EVENT_HEAP_PROFILER_EVENT_WRITER_H_
-#define BASE_TRACE_EVENT_HEAP_PROFILER_EVENT_WRITER_H_
-
-#include <stddef.h>
-
-#include <memory>
-#include <string>
-#include <unordered_map>
-
-#include "base/base_export.h"
-
-/*
-  Heap profile event data format.
-
-  Input is:
-    1. Per allocator AllocationRegister
-    2. Per process deduplicators (for stack frames, types, strings)
-
-  Formatting event data is done in two steps:
-    1. Call SerializeHeapDump() on allocation registers and accumulate
-       results in SerializedHeapDumpsMap. SerializeHeapDump() exports
-       allocation register as "allocators/<allocator>" dictionary outlined
-       below; serialization uses deduplicators from MemoryDumpSessionState.
-    2. Call SerializeHeapProfileEventData() with SerializedHeapDumpsMap and
-       MemoryDumpSessionState. This puts everything together:
-       a. Entries from SerializedHeapDumpsMap are formatted as
-          "allocators/<allocator>" nodes.
-       b. Deduplicators from MemoryDumpSessionState are formatted as
-          "maps/" nodes. Deduplicators are exported incrementally using
-          their ExportIncrementally() methods.
-
-  SerializeHeapDump() aggregates allocation register entries first by backtrace,
-  then by type (i.e. creates map {(backtrace, type) -> AllocationMetrics}).
-  During aggregation backtraces and types are deduplicated.
-
-  Resulting event data format:
-  {
-    "version": 1,
-
-    "allocators": {
-      ["malloc", "partition_alloc", "blinkgc"]: {
-        "nodes":  [<stack_frame_id1>, <stack_frame_id2>, ...],
-        "types":  [<type_id1>,        <type_id2>,        ...],
-        "counts": [<count1>,          <count2>,          ...],
-        "sizes":  [<size1>,           <size2>,           ...]
-      }
-    },
-
-    "maps": {
-      "nodes": [
-        {
-          "id": <stack_frame_id>,
-          "parent": <parent_id>,
-          "name_sid": <name_string_id>
-        },
-        ...
-      ],
-      "types": [
-        {
-          "id": <type_id>,
-          "name_sid": <name_string_id>
-        }
-      ],
-      "strings": [
-        {
-          "id": <string_id>,
-          "string": <string>
-        }
-      ]
-    }
-  }
-*/
-
-namespace base {
-namespace trace_event {
-
-class ShardedAllocationRegister;
-class HeapProfilerSerializationState;
-class TracedValue;
-
-// Exports heap allocations as "allocators/<allocator>" dictionary described
-// above. Return value is supposed to be added to SerializedHeapDumpsMap map
-// and later passed to SerializeHeapProfileEventData().
-BASE_EXPORT std::unique_ptr<TracedValue> SerializeHeapDump(
-    const ShardedAllocationRegister& allocation_register,
-    HeapProfilerSerializationState* serialization_state);
-
-// Maps allocator name to its heap dump.
-using SerializedHeapDumpsMap =
-    std::unordered_map<std::string, std::unique_ptr<TracedValue>>;
-
-// Exports event data according to the format described above.
-BASE_EXPORT std::unique_ptr<TracedValue> SerializeHeapProfileEventData(
-    const SerializedHeapDumpsMap& heap_dumps,
-    HeapProfilerSerializationState* serialization_state);
-
-}  // namespace trace_event
-}  // namespace base
-
-#endif  // BASE_TRACE_EVENT_HEAP_PROFILER_EVENT_WRITER_H_
diff --git a/base/trace_event/heap_profiler_event_writer_unittest.cc b/base/trace_event/heap_profiler_event_writer_unittest.cc
deleted file mode 100644
index ded36408..0000000
--- a/base/trace_event/heap_profiler_event_writer_unittest.cc
+++ /dev/null
@@ -1,288 +0,0 @@
-// Copyright 2015 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/trace_event/heap_profiler_event_writer.h"
-
-#include <stdint.h>
-
-#include <algorithm>
-
-#include "base/memory/ptr_util.h"
-#include "base/trace_event/heap_profiler_allocation_context.h"
-#include "base/trace_event/heap_profiler_serialization_state.h"
-#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
-#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
-#include "base/trace_event/sharded_allocation_register.h"
-#include "base/trace_event/trace_event_argument.h"
-#include "base/values.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace base {
-namespace trace_event {
-
-namespace {
-
-using base::trace_event::StackFrame;
-
-// Define all strings once, because the deduplicator requires pointer equality,
-// and string interning is unreliable.
-StackFrame kBrowserMain = StackFrame::FromTraceEventName("BrowserMain");
-StackFrame kRendererMain = StackFrame::FromTraceEventName("RendererMain");
-StackFrame kCreateWidget = StackFrame::FromTraceEventName("CreateWidget");
-StackFrame kInitialize = StackFrame::FromTraceEventName("Initialize");
-StackFrame kGetBitmap = StackFrame::FromTraceEventName("GetBitmap");
-
-const char kInt[] = "int";
-const char kBool[] = "bool";
-
-class AllocationRegisterHelper : public ShardedAllocationRegister {
- public:
-  AllocationRegisterHelper() : next_address_(0x100000) { SetEnabled(); }
-
-  void Allocate(size_t size,
-                const char* type_name,
-                std::initializer_list<StackFrame> backtrace) {
-    AllocationContext context;
-    context.backtrace.frame_count = backtrace.size();
-    std::copy(backtrace.begin(), backtrace.end(),
-              std::begin(context.backtrace.frames));
-    context.type_name = type_name;
-    Insert(reinterpret_cast<void*>(next_address_), size, context);
-    next_address_ += size;
-  }
-
- private:
-  uintptr_t next_address_;
-};
-
-struct HeapDumpEntry {
-  int backtrace_id;
-  int type_id;
-  int size;
-  int count;
-
-  bool operator==(const HeapDumpEntry& other) const {
-    return backtrace_id == other.backtrace_id && type_id == other.type_id &&
-           size == other.size && count == other.count;
-  }
-};
-
-::testing::AssertionResult AssertHeapDump(
-    const DictionaryValue& heap_dump,
-    std::initializer_list<HeapDumpEntry> expected_entries) {
-  auto get_list_value = [&](const char* list_name, size_t index,
-                            int* value) -> ::testing::AssertionResult {
-    const ListValue* list = nullptr;
-    if (!heap_dump.GetList(list_name, &list)) {
-      return ::testing::AssertionFailure()
-             << "'" << list_name << "' doesn't exist or is not a list";
-    }
-    if (list->GetSize() != expected_entries.size()) {
-      return ::testing::AssertionFailure()
-             << "size of '" << list_name << "' is " << list->GetSize()
-             << ", expected " << expected_entries.size();
-    }
-    if (!list->GetInteger(index, value)) {
-      return ::testing::AssertionFailure()
-             << "'" << list_name << "' value at index " << index
-             << " is not an integer";
-    }
-    return ::testing::AssertionSuccess();
-  };
-
-  constexpr size_t kValueCount = 4;  // nodes, types, counts, sizes
-  if (heap_dump.size() != kValueCount) {
-    return ::testing::AssertionFailure()
-           << "heap dump has " << heap_dump.size() << " values"
-           << ", expected " << kValueCount;
-  }
-
-  for (size_t i = 0; i != expected_entries.size(); ++i) {
-    HeapDumpEntry entry;
-
-    ::testing::AssertionResult assertion = ::testing::AssertionSuccess();
-    if (!(assertion = get_list_value("nodes", i, &entry.backtrace_id)) ||
-        !(assertion = get_list_value("types", i, &entry.type_id)) ||
-        !(assertion = get_list_value("sizes", i, &entry.size)) ||
-        !(assertion = get_list_value("counts", i, &entry.count))) {
-      return assertion;
-    }
-
-    auto* entry_iter =
-        std::find(expected_entries.begin(), expected_entries.end(), entry);
-    if (entry_iter == expected_entries.end()) {
-      return ::testing::AssertionFailure()
-             << "unexpected HeapDumpEntry{" << entry.backtrace_id << ", "
-             << entry.type_id << ", " << entry.size << ", " << entry.count
-             << "} at index " << i;
-    }
-  }
-
-  return ::testing::AssertionSuccess();
-}
-
-std::unique_ptr<DictionaryValue> ToDictionary(
-    const std::unique_ptr<TracedValue>& traced_value) {
-  if (!traced_value) {
-    return nullptr;
-  }
-  return DictionaryValue::From(traced_value->ToBaseValue());
-}
-
-}  // namespace
-
-TEST(EventWriterTest, HeapDumpNoBacktraceNoType) {
-  AllocationRegisterHelper allocation_register;
-  auto bt = {kBrowserMain};
-  std::initializer_list<StackFrame> empty_bt = {};
-  allocation_register.Allocate(10, nullptr, bt);
-  allocation_register.Allocate(100, kInt, empty_bt);
-  allocation_register.Allocate(1000, nullptr, empty_bt);
-
-  auto state = make_scoped_refptr(new HeapProfilerSerializationState);
-  state->CreateDeduplicators();
-  auto heap_dump =
-      ToDictionary(SerializeHeapDump(allocation_register, state.get()));
-  ASSERT_TRUE(heap_dump);
-
-  int bt_id =
-      state->stack_frame_deduplicator()->Insert(std::begin(bt), std::end(bt));
-  int int_id = state->type_name_deduplicator()->Insert(kInt);
-
-  // NULL type and empty backtrace IDs should be 0.
-  auto expected_entries = {
-      HeapDumpEntry{bt_id, 0, 10, 1},    // no type
-      HeapDumpEntry{0, int_id, 100, 1},  // no backtrace
-      HeapDumpEntry{0, 0, 1000, 1},      // no type, no backtrace
-  };
-  ASSERT_TRUE(AssertHeapDump(*heap_dump, expected_entries))
-      << "heap_dump = " << *heap_dump;
-}
-
-TEST(EventWriterTest, HeapDumpAggregation) {
-  //
-  // |- (no backtrace)   int*1, int*2, bool*3,
-  // |                   (no type)*4, (no type)*5
-  // |
-  // |- kBrowserMain     (no type)*6, (no type)*7, (no type)*8
-  // |-- kCreateWidget   int*10, bool*20
-  // |---- kGetBitmap    int*100, int*200, bool*300
-  //
-  // Aggregation is done by {backtrace_id, type_id}, so the following
-  // entries should be aggregated:
-  //  - int*1 + int*2
-  //  - (no type)*4 + (no type)*5
-  //  - (no type)*6 + (no type)*7 + (no type)*8
-  //  - int*100 + int*200
-
-  AllocationRegisterHelper allocation_register;
-
-  std::initializer_list<StackFrame> empty_bt = {};
-  allocation_register.Allocate(1, kInt, empty_bt);
-  allocation_register.Allocate(2, kInt, empty_bt);
-  allocation_register.Allocate(3, kBool, empty_bt);
-  allocation_register.Allocate(4, nullptr, empty_bt);
-  allocation_register.Allocate(5, nullptr, empty_bt);
-
-  auto bt1 = {kBrowserMain};
-  allocation_register.Allocate(6, nullptr, bt1);
-  allocation_register.Allocate(7, nullptr, bt1);
-  allocation_register.Allocate(8, nullptr, bt1);
-
-  auto bt2 = {kBrowserMain, kCreateWidget};
-  allocation_register.Allocate(10, kInt, bt2);
-  allocation_register.Allocate(20, kBool, bt2);
-
-  auto bt3 = {kBrowserMain, kCreateWidget, kGetBitmap};
-  allocation_register.Allocate(100, kInt, bt3);
-  allocation_register.Allocate(200, kInt, bt3);
-  allocation_register.Allocate(300, kBool, bt3);
-
-  auto state = make_scoped_refptr(new HeapProfilerSerializationState);
-  state->CreateDeduplicators();
-
-  auto heap_dump =
-      ToDictionary(SerializeHeapDump(allocation_register, state.get()));
-  ASSERT_TRUE(heap_dump);
-
-  int bt1_id =
-      state->stack_frame_deduplicator()->Insert(std::begin(bt1), std::end(bt1));
-  int bt2_id =
-      state->stack_frame_deduplicator()->Insert(std::begin(bt2), std::end(bt2));
-  int bt3_id =
-      state->stack_frame_deduplicator()->Insert(std::begin(bt3), std::end(bt3));
-
-  int int_id = state->type_name_deduplicator()->Insert(kInt);
-  int bool_id = state->type_name_deduplicator()->Insert(kBool);
-
-  auto expected_entries = {
-      HeapDumpEntry{0, int_id, 3, 2},
-      HeapDumpEntry{0, bool_id, 3, 1},
-      HeapDumpEntry{0, 0, 9, 2},
-      HeapDumpEntry{bt1_id, 0, 21, 3},
-      HeapDumpEntry{bt2_id, int_id, 10, 1},
-      HeapDumpEntry{bt2_id, bool_id, 20, 1},
-      HeapDumpEntry{bt3_id, int_id, 300, 2},
-      HeapDumpEntry{bt3_id, bool_id, 300, 1},
-  };
-  ASSERT_TRUE(AssertHeapDump(*heap_dump, expected_entries))
-      << "heap_dump = " << *heap_dump;
-}
-
-TEST(EventWriterTest, SerializeHeapProfileEventData) {
-  AllocationRegisterHelper foo_register;
-  foo_register.Allocate(10, "Widget", {kBrowserMain, kCreateWidget});
-  foo_register.Allocate(16, "int[]", {kBrowserMain, kCreateWidget});
-
-  AllocationRegisterHelper bar_register;
-  bar_register.Allocate(10, "Widget", {kRendererMain, kCreateWidget});
-  bar_register.Allocate(71, "char[]", {kRendererMain});
-
-  auto state = make_scoped_refptr(new HeapProfilerSerializationState);
-  state->CreateDeduplicators();
-
-  SerializedHeapDumpsMap heap_dumps;
-  heap_dumps["foo"] = SerializeHeapDump(foo_register, state.get());
-  heap_dumps["bar"] = SerializeHeapDump(bar_register, state.get());
-
-  auto event_data =
-      ToDictionary(SerializeHeapProfileEventData(heap_dumps, state.get()));
-  ASSERT_TRUE(event_data);
-
-  constexpr size_t kTopCount = 3;  // version, allocators, maps
-  ASSERT_EQ(kTopCount, event_data->size());
-
-  int version;
-  ASSERT_TRUE(event_data->GetInteger("version", &version));
-  ASSERT_EQ(1, version);
-
-  const DictionaryValue* allocators;
-  ASSERT_TRUE(event_data->GetDictionary("allocators", &allocators));
-  {
-    constexpr size_t kAllocatorCount = 2;  // foo, bar
-    ASSERT_EQ(kAllocatorCount, allocators->size());
-
-    const DictionaryValue* foo_dump;
-    ASSERT_TRUE(allocators->GetDictionary("foo", &foo_dump));
-    ASSERT_TRUE(ToDictionary(heap_dumps["foo"])->Equals(foo_dump));
-
-    const DictionaryValue* bar_dump;
-    ASSERT_TRUE(allocators->GetDictionary("bar", &bar_dump));
-    ASSERT_TRUE(ToDictionary(heap_dumps["bar"])->Equals(bar_dump));
-  }
-
-  const DictionaryValue* maps;
-  ASSERT_TRUE(event_data->GetDictionary("maps", &maps));
-  {
-    constexpr size_t kMapCount = 3;  // nodes, types, strings
-    ASSERT_EQ(kMapCount, maps->size());
-
-    ASSERT_TRUE(maps->HasKey("nodes"));
-    ASSERT_TRUE(maps->HasKey("types"));
-    ASSERT_TRUE(maps->HasKey("strings"));
-  }
-}
-
-}  // namespace trace_event
-}  // namespace base
diff --git a/base/trace_event/heap_profiler_heap_dump_writer.cc b/base/trace_event/heap_profiler_heap_dump_writer.cc
new file mode 100644
index 0000000..9f64f6ed
--- /dev/null
+++ b/base/trace_event/heap_profiler_heap_dump_writer.cc
@@ -0,0 +1,323 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/trace_event/heap_profiler_heap_dump_writer.h"
+
+#include <stdint.h>
+
+#include <algorithm>
+#include <iterator>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "base/format_macros.h"
+#include "base/logging.h"
+#include "base/macros.h"
+#include "base/strings/stringprintf.h"
+#include "base/trace_event/heap_profiler_serialization_state.h"
+#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
+#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
+#include "base/trace_event/trace_config.h"
+#include "base/trace_event/trace_event.h"
+#include "base/trace_event/trace_event_argument.h"
+#include "base/trace_event/trace_log.h"
+
+// Most of what the |HeapDumpWriter| does is aggregating detailed information
+// about the heap and deciding what to dump. The Input to this process is a list
+// of |AllocationContext|s and size pairs.
+//
+// The pairs are grouped into |Bucket|s. A bucket is a group of (context, size)
+// pairs where the properties of the contexts share a prefix. (Type name is
+// considered a list of length one here.) First all pairs are put into one
+// bucket that represents the entire heap. Then this bucket is recursively
+// broken down into smaller buckets. Each bucket keeps track of whether further
+// breakdown is possible.
+
+namespace base {
+namespace trace_event {
+namespace internal {
+namespace {
+
+// Denotes a property of |AllocationContext| to break down by.
+enum class BreakDownMode { kByBacktrace, kByTypeName };
+
+// A group of bytes for which the context shares a prefix.
+struct Bucket {
+  Bucket()
+      : size(0),
+        count(0),
+        backtrace_cursor(0),
+        is_broken_down_by_type_name(false) {}
+
+  std::vector<std::pair<const AllocationContext*, AllocationMetrics>>
+      metrics_by_context;
+
+  // The sum of the sizes of |metrics_by_context|.
+  size_t size;
+
+  // The sum of number of allocations of |metrics_by_context|.
+  size_t count;
+
+  // The index of the stack frame that has not yet been broken down by. For all
+  // elements in this bucket, the stack frames 0 up to (but not including) the
+  // cursor, must be equal.
+  size_t backtrace_cursor;
+
+  // When true, the type name for all elements in this bucket must be equal.
+  bool is_broken_down_by_type_name;
+};
+
+// Comparison operator to order buckets by their size.
+bool operator<(const Bucket& lhs, const Bucket& rhs) {
+  return lhs.size < rhs.size;
+}
+
+// Groups the allocations in the bucket by |break_by|. The buckets in the
+// returned list will have |backtrace_cursor| advanced or
+// |is_broken_down_by_type_name| set depending on the property to group by.
+std::vector<Bucket> GetSubbuckets(const Bucket& bucket,
+                                  BreakDownMode break_by) {
+  std::unordered_map<const void*, Bucket> breakdown;
+
+  if (break_by == BreakDownMode::kByBacktrace) {
+    for (const auto& context_and_metrics : bucket.metrics_by_context) {
+      const Backtrace& backtrace = context_and_metrics.first->backtrace;
+      const StackFrame* begin = std::begin(backtrace.frames);
+      const StackFrame* end = begin + backtrace.frame_count;
+      const StackFrame* cursor = begin + bucket.backtrace_cursor;
+
+      DCHECK_LE(cursor, end);
+
+      if (cursor != end) {
+        Bucket& subbucket = breakdown[cursor->value];
+        subbucket.size += context_and_metrics.second.size;
+        subbucket.count += context_and_metrics.second.count;
+        subbucket.metrics_by_context.push_back(context_and_metrics);
+        subbucket.backtrace_cursor = bucket.backtrace_cursor + 1;
+        subbucket.is_broken_down_by_type_name =
+            bucket.is_broken_down_by_type_name;
+        DCHECK_GT(subbucket.size, 0u);
+        DCHECK_GT(subbucket.count, 0u);
+      }
+    }
+  } else if (break_by == BreakDownMode::kByTypeName) {
+    if (!bucket.is_broken_down_by_type_name) {
+      for (const auto& context_and_metrics : bucket.metrics_by_context) {
+        const AllocationContext* context = context_and_metrics.first;
+        Bucket& subbucket = breakdown[context->type_name];
+        subbucket.size += context_and_metrics.second.size;
+        subbucket.count += context_and_metrics.second.count;
+        subbucket.metrics_by_context.push_back(context_and_metrics);
+        subbucket.backtrace_cursor = bucket.backtrace_cursor;
+        subbucket.is_broken_down_by_type_name = true;
+        DCHECK_GT(subbucket.size, 0u);
+        DCHECK_GT(subbucket.count, 0u);
+      }
+    }
+  }
+
+  std::vector<Bucket> buckets;
+  buckets.reserve(breakdown.size());
+  for (auto key_bucket : breakdown)
+    buckets.push_back(key_bucket.second);
+
+  return buckets;
+}
+
+// Breaks down the bucket by |break_by|. Returns only buckets that contribute
+// more than |min_size_bytes| to the total size. The long tail is omitted.
+std::vector<Bucket> BreakDownBy(const Bucket& bucket,
+                                BreakDownMode break_by,
+                                size_t min_size_bytes) {
+  std::vector<Bucket> buckets = GetSubbuckets(bucket, break_by);
+
+  // Ensure that |buckets| is a max-heap (the data structure, not memory heap),
+  // so its front contains the largest bucket. Buckets should be iterated
+  // ordered by size, but sorting the vector is overkill because the long tail
+  // of small buckets will be discarded. By using a max-heap, the optimal case
+  // where all but the first bucket are discarded is O(n). The worst case where
+  // no bucket is discarded is doing a heap sort, which is O(n log n).
+  std::make_heap(buckets.begin(), buckets.end());
+
+  // Keep including buckets until adding one would increase the number of
+  // bytes accounted for by |min_size_bytes|. The large buckets end up in
+  // [it, end()), [begin(), it) is the part that contains the max-heap
+  // of small buckets.
+  std::vector<Bucket>::iterator it;
+  for (it = buckets.end(); it != buckets.begin(); --it) {
+    if (buckets.front().size < min_size_bytes)
+      break;
+
+    // Put the largest bucket in [begin, it) at |it - 1| and max-heapify
+    // [begin, it - 1). This puts the next largest bucket at |buckets.front()|.
+    std::pop_heap(buckets.begin(), it);
+  }
+
+  // At this point, |buckets| looks like this (numbers are bucket sizes):
+  //
+  // <-- max-heap of small buckets --->
+  //                                  <-- large buckets by ascending size -->
+  // [ 19 | 11 | 13 | 7 | 2 | 5 | ... | 83 | 89 | 97 ]
+  //   ^                                ^              ^
+  //   |                                |              |
+  //   begin()                          it             end()
+
+  // Discard the long tail of buckets that contribute less than a percent.
+  buckets.erase(buckets.begin(), it);
+
+  return buckets;
+}
+
+}  // namespace
+
+bool operator<(Entry lhs, Entry rhs) {
+  // There is no need to compare |size|. If the backtrace and type name are
+  // equal then the sizes must be equal as well.
+  return std::tie(lhs.stack_frame_id, lhs.type_id) <
+         std::tie(rhs.stack_frame_id, rhs.type_id);
+}
+
+HeapDumpWriter::HeapDumpWriter(StackFrameDeduplicator* stack_frame_deduplicator,
+                               TypeNameDeduplicator* type_name_deduplicator,
+                               uint32_t breakdown_threshold_bytes)
+    : stack_frame_deduplicator_(stack_frame_deduplicator),
+      type_name_deduplicator_(type_name_deduplicator),
+      breakdown_threshold_bytes_(breakdown_threshold_bytes) {}
+
+HeapDumpWriter::~HeapDumpWriter() {}
+
+bool HeapDumpWriter::AddEntryForBucket(const Bucket& bucket) {
+  // The contexts in the bucket are all different, but the [begin, cursor) range
+  // is equal for all contexts in the bucket, and the type names are the same if
+  // |is_broken_down_by_type_name| is set.
+  DCHECK(!bucket.metrics_by_context.empty());
+
+  const AllocationContext* context = bucket.metrics_by_context.front().first;
+
+  const StackFrame* backtrace_begin = std::begin(context->backtrace.frames);
+  const StackFrame* backtrace_end = backtrace_begin + bucket.backtrace_cursor;
+  DCHECK_LE(bucket.backtrace_cursor, arraysize(context->backtrace.frames));
+
+  Entry entry;
+  entry.stack_frame_id =
+      stack_frame_deduplicator_->Insert(backtrace_begin, backtrace_end);
+
+  // Deduplicate the type name, or use ID -1 if type name is not set.
+  entry.type_id = bucket.is_broken_down_by_type_name
+                      ? type_name_deduplicator_->Insert(context->type_name)
+                      : -1;
+
+  entry.size = bucket.size;
+  entry.count = bucket.count;
+
+  auto position_and_inserted = entries_.insert(entry);
+  return position_and_inserted.second;
+}
+
+void HeapDumpWriter::BreakDown(const Bucket& bucket) {
+  auto by_backtrace = BreakDownBy(bucket, BreakDownMode::kByBacktrace,
+                                  breakdown_threshold_bytes_);
+  auto by_type_name = BreakDownBy(bucket, BreakDownMode::kByTypeName,
+                                  breakdown_threshold_bytes_);
+
+  // Insert entries for the buckets. If a bucket was not present before, it has
+  // not been broken down before, so recursively continue breaking down in that
+  // case. There might be multiple routes to the same entry (first break down
+  // by type name, then by backtrace, or first by backtrace and then by type),
+  // so a set is used to avoid dumping and breaking down entries more than once.
+
+  for (const Bucket& subbucket : by_backtrace)
+    if (AddEntryForBucket(subbucket))
+      BreakDown(subbucket);
+
+  for (const Bucket& subbucket : by_type_name)
+    if (AddEntryForBucket(subbucket))
+      BreakDown(subbucket);
+}
+
+const std::set<Entry>& HeapDumpWriter::Summarize(
+    const std::unordered_map<AllocationContext, AllocationMetrics>&
+        metrics_by_context) {
+  // Start with one bucket that represents the entire heap. Iterate by
+  // reference, because the allocation contexts are going to point to allocation
+  // contexts stored in |metrics_by_context|.
+  Bucket root_bucket;
+  for (const auto& context_and_metrics : metrics_by_context) {
+    DCHECK_GT(context_and_metrics.second.size, 0u);
+    DCHECK_GT(context_and_metrics.second.count, 0u);
+    const AllocationContext* context = &context_and_metrics.first;
+    root_bucket.metrics_by_context.push_back(
+        std::make_pair(context, context_and_metrics.second));
+    root_bucket.size += context_and_metrics.second.size;
+    root_bucket.count += context_and_metrics.second.count;
+  }
+
+  AddEntryForBucket(root_bucket);
+
+  // Recursively break down the heap and fill |entries_| with entries to dump.
+  BreakDown(root_bucket);
+
+  return entries_;
+}
+
+std::unique_ptr<TracedValue> Serialize(const std::set<Entry>& entries) {
+  std::string buffer;
+  std::unique_ptr<TracedValue> traced_value(new TracedValue);
+
+  traced_value->BeginArray("entries");
+
+  for (const Entry& entry : entries) {
+    traced_value->BeginDictionary();
+
+    // Format size as hexadecimal string into |buffer|.
+    SStringPrintf(&buffer, "%" PRIx64, static_cast<uint64_t>(entry.size));
+    traced_value->SetString("size", buffer);
+
+    SStringPrintf(&buffer, "%" PRIx64, static_cast<uint64_t>(entry.count));
+    traced_value->SetString("count", buffer);
+
+    if (entry.stack_frame_id == -1) {
+      // An empty backtrace (which will have ID -1) is represented by the empty
+      // string, because there is no leaf frame to reference in |stackFrames|.
+      traced_value->SetString("bt", "");
+    } else {
+      // Format index of the leaf frame as a string, because |stackFrames| is a
+      // dictionary, not an array.
+      SStringPrintf(&buffer, "%i", entry.stack_frame_id);
+      traced_value->SetString("bt", buffer);
+    }
+
+    // Type ID -1 (cumulative size for all types) is represented by the absence
+    // of the "type" key in the dictionary.
+    if (entry.type_id != -1) {
+      // Format the type ID as a string.
+      SStringPrintf(&buffer, "%i", entry.type_id);
+      traced_value->SetString("type", buffer);
+    }
+
+    traced_value->EndDictionary();
+  }
+
+  traced_value->EndArray();  // "entries"
+  return traced_value;
+}
+
+}  // namespace internal
+
+std::unique_ptr<TracedValue> ExportHeapDump(
+    const std::unordered_map<AllocationContext, AllocationMetrics>&
+        metrics_by_context,
+    const HeapProfilerSerializationState& heap_profiler_serialization_state) {
+  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"), "ExportHeapDump");
+  internal::HeapDumpWriter writer(
+      heap_profiler_serialization_state.stack_frame_deduplicator(),
+      heap_profiler_serialization_state.type_name_deduplicator(),
+      heap_profiler_serialization_state
+          .heap_profiler_breakdown_threshold_bytes());
+  return Serialize(writer.Summarize(metrics_by_context));
+}
+
+}  // namespace trace_event
+}  // namespace base
diff --git a/base/trace_event/heap_profiler_heap_dump_writer.h b/base/trace_event/heap_profiler_heap_dump_writer.h
new file mode 100644
index 0000000..3366c28
--- /dev/null
+++ b/base/trace_event/heap_profiler_heap_dump_writer.h
@@ -0,0 +1,115 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BASE_TRACE_EVENT_HEAP_PROFILER_HEAP_DUMP_WRITER_H_
+#define BASE_TRACE_EVENT_HEAP_PROFILER_HEAP_DUMP_WRITER_H_
+
+#include <stddef.h>
+
+#include <memory>
+#include <set>
+#include <unordered_map>
+
+#include "base/base_export.h"
+#include "base/macros.h"
+#include "base/trace_event/heap_profiler_allocation_context.h"
+
+namespace base {
+namespace trace_event {
+
+class HeapProfilerSerializationState;
+class StackFrameDeduplicator;
+class TracedValue;
+class TypeNameDeduplicator;
+
+// Aggregates |metrics_by_context|, recursively breaks down the heap, and
+// returns a traced value with an "entries" array that can be dumped in the
+// trace log, following the format described in https://goo.gl/KY7zVE. The
+// number of entries is kept reasonable because long tails are not included.
+BASE_EXPORT std::unique_ptr<TracedValue> ExportHeapDump(
+    const std::unordered_map<AllocationContext, AllocationMetrics>&
+        metrics_by_context,
+    const HeapProfilerSerializationState& heap_profiler_serialization_state);
+
+namespace internal {
+
+namespace {
+struct Bucket;
+}
+
+// An entry in the "entries" array as described in https://goo.gl/KY7zVE.
+struct BASE_EXPORT Entry {
+  size_t size;
+  size_t count;
+
+  // References a backtrace in the stack frame deduplicator. -1 means empty
+  // backtrace (the root of the tree).
+  int stack_frame_id;
+
+  // References a type name in the type name deduplicator. -1 indicates that
+  // the size is the cumulative size for all types (the root of the tree).
+  int type_id;
+};
+
+// Comparison operator to enable putting |Entry| in a |std::set|.
+BASE_EXPORT bool operator<(Entry lhs, Entry rhs);
+
+// Serializes entries to an "entries" array in a traced value.
+BASE_EXPORT std::unique_ptr<TracedValue> Serialize(const std::set<Entry>& dump);
+
+// Helper class to dump a snapshot of an |AllocationRegister| or other heap
+// bookkeeping structure into a |TracedValue|. This class is intended to be
+// used as a one-shot local instance on the stack.
+class BASE_EXPORT HeapDumpWriter {
+ public:
+  // The |stack_frame_deduplicator| and |type_name_deduplicator| are not owned.
+  // The heap dump writer assumes exclusive access to them during the lifetime
+  // of the dump writer. The heap dumps are broken down for allocations bigger
+  // than |breakdown_threshold_bytes|.
+  HeapDumpWriter(StackFrameDeduplicator* stack_frame_deduplicator,
+                 TypeNameDeduplicator* type_name_deduplicator,
+                 uint32_t breakdown_threshold_bytes);
+
+  ~HeapDumpWriter();
+
+  // Aggregates allocations to compute the total size of the heap, then breaks
+  // down the heap recursively. This produces the values that should be dumped
+  // in the "entries" array. The number of entries is kept reasonable because
+  // long tails are not included. Use |Serialize| to convert to a traced value.
+  const std::set<Entry>& Summarize(
+      const std::unordered_map<AllocationContext, AllocationMetrics>&
+          metrics_by_context);
+
+ private:
+  // Inserts an |Entry| for |Bucket| into |entries_|. Returns false if the
+  // entry was present before, true if it was not.
+  bool AddEntryForBucket(const Bucket& bucket);
+
+  // Recursively breaks down a bucket into smaller buckets and adds entries for
+  // the buckets worth dumping to |entries_|.
+  void BreakDown(const Bucket& bucket);
+
+  // The collection of entries that is filled by |Summarize|.
+  std::set<Entry> entries_;
+
+  // Helper for generating the |stackFrames| dictionary. Not owned, must outlive
+  // this heap dump writer instance.
+  StackFrameDeduplicator* const stack_frame_deduplicator_;
+
+  // Helper for converting type names to IDs. Not owned, must outlive this heap
+  // dump writer instance.
+  TypeNameDeduplicator* const type_name_deduplicator_;
+
+  // Minimum size of an allocation for which an allocation bucket will be
+  // broken down with children.
+  uint32_t breakdown_threshold_bytes_;
+
+  DISALLOW_COPY_AND_ASSIGN(HeapDumpWriter);
+};
+
+}  // namespace internal
+}  // namespace trace_event
+}  // namespace base
+
+#endif  // BASE_TRACE_EVENT_HEAP_PROFILER_HEAP_DUMP_WRITER_H_
diff --git a/base/trace_event/heap_profiler_heap_dump_writer_unittest.cc b/base/trace_event/heap_profiler_heap_dump_writer_unittest.cc
new file mode 100644
index 0000000..93e8feea
--- /dev/null
+++ b/base/trace_event/heap_profiler_heap_dump_writer_unittest.cc
@@ -0,0 +1,330 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/trace_event/heap_profiler_heap_dump_writer.h"
+
+#include <stddef.h>
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "base/json/json_reader.h"
+#include "base/macros.h"
+#include "base/memory/ptr_util.h"
+#include "base/trace_event/heap_profiler_allocation_context.h"
+#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
+#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
+#include "base/trace_event/trace_event_argument.h"
+#include "base/values.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+using base::trace_event::StackFrame;
+
+// Define all strings once, because the deduplicator requires pointer equality,
+// and string interning is unreliable.
+StackFrame kBrowserMain = StackFrame::FromTraceEventName("BrowserMain");
+StackFrame kRendererMain = StackFrame::FromTraceEventName("RendererMain");
+StackFrame kCreateWidget = StackFrame::FromTraceEventName("CreateWidget");
+StackFrame kInitialize = StackFrame::FromTraceEventName("Initialize");
+StackFrame kGetBitmap = StackFrame::FromTraceEventName("GetBitmap");
+
+const char kInt[] = "int";
+const char kBool[] = "bool";
+const char kString[] = "string";
+
+}  // namespace
+
+namespace base {
+namespace trace_event {
+namespace internal {
+
+std::unique_ptr<const Value> WriteAndReadBack(const std::set<Entry>& entries) {
+  std::unique_ptr<TracedValue> traced_value = Serialize(entries);
+  std::string json;
+  traced_value->AppendAsTraceFormat(&json);
+  return JSONReader::Read(json);
+}
+
+std::unique_ptr<const DictionaryValue> WriteAndReadBackEntry(Entry entry) {
+  std::set<Entry> input_entries;
+  input_entries.insert(entry);
+
+  std::unique_ptr<const Value> json_dict = WriteAndReadBack(input_entries);
+
+  // Note: Ideally these should use |ASSERT_TRUE| instead of |EXPECT_TRUE|, but
+  // |ASSERT_TRUE| can only be used in void functions.
+  const DictionaryValue* dictionary;
+  EXPECT_TRUE(json_dict->GetAsDictionary(&dictionary));
+
+  const ListValue* json_entries;
+  EXPECT_TRUE(dictionary->GetList("entries", &json_entries));
+
+  const DictionaryValue* json_entry;
+  EXPECT_TRUE(json_entries->GetDictionary(0, &json_entry));
+
+  return json_entry->CreateDeepCopy();
+}
+
+// Given a desired stack frame ID and type ID, looks up the entry in the set and
+// asserts that it is present and has the expected size and count.
+void AssertSizeAndCountEq(const std::set<Entry>& entries,
+                          int stack_frame_id,
+                          int type_id,
+                          const AllocationMetrics& expected) {
+  // The comparison operator for |Entry| does not take size into account, so by
+  // setting only stack frame ID and type ID, the real entry can be found.
+  Entry entry;
+  entry.stack_frame_id = stack_frame_id;
+  entry.type_id = type_id;
+  auto it = entries.find(entry);
+
+  ASSERT_NE(entries.end(), it) << "No entry found for sf = " << stack_frame_id
+                               << ", type = " << type_id << ".";
+  ASSERT_EQ(expected.size, it->size) << "Wrong size for sf = " << stack_frame_id
+                                     << ", type = " << type_id << ".";
+  ASSERT_EQ(expected.count, it->count)
+      << "Wrong count for sf = " << stack_frame_id << ", type = " << type_id
+      << ".";
+}
+
+// Given a desired stack frame ID and type ID, asserts that no entry was dumped
+// for that that particular combination of stack frame and type.
+void AssertNotDumped(const std::set<Entry>& entries,
+                     int stack_frame_id,
+                     int type_id) {
+  // The comparison operator for |Entry| does not take size into account, so by
+  // setting only stack frame ID and type ID, the real entry can be found.
+  Entry entry;
+  entry.stack_frame_id = stack_frame_id;
+  entry.type_id = type_id;
+  auto it = entries.find(entry);
+  ASSERT_EQ(entries.end(), it)
+      << "Entry should not be present for sf = " << stack_frame_id
+      << ", type = " << type_id << ".";
+}
+
+TEST(HeapDumpWriterTest, BacktraceIndex) {
+  Entry entry;
+  entry.stack_frame_id = -1;  // -1 means empty backtrace.
+  entry.type_id = 0;
+  entry.size = 1;
+  entry.count = 1;
+
+  std::unique_ptr<const DictionaryValue> json_entry =
+      WriteAndReadBackEntry(entry);
+
+  // For an empty backtrace, the "bt" key cannot reference a stack frame.
+  // Instead it should be set to the empty string.
+  std::string backtrace_index;
+  ASSERT_TRUE(json_entry->GetString("bt", &backtrace_index));
+  ASSERT_EQ("", backtrace_index);
+
+  // Also verify that a non-negative backtrace index is dumped properly.
+  entry.stack_frame_id = 2;
+  json_entry = WriteAndReadBackEntry(entry);
+  ASSERT_TRUE(json_entry->GetString("bt", &backtrace_index));
+  ASSERT_EQ("2", backtrace_index);
+}
+
+TEST(HeapDumpWriterTest, TypeId) {
+  Entry entry;
+  entry.type_id = -1;  // -1 means sum over all types.
+  entry.stack_frame_id = 0;
+  entry.size = 1;
+  entry.count = 1;
+
+  std::unique_ptr<const DictionaryValue> json_entry =
+      WriteAndReadBackEntry(entry);
+
+  // Entries for the cumulative size of all types should not have the "type"
+  // key set.
+  ASSERT_FALSE(json_entry->HasKey("type"));
+
+  // Also verify that a non-negative type ID is dumped properly.
+  entry.type_id = 2;
+  json_entry = WriteAndReadBackEntry(entry);
+  std::string type_id;
+  ASSERT_TRUE(json_entry->GetString("type", &type_id));
+  ASSERT_EQ("2", type_id);
+}
+
+TEST(HeapDumpWriterTest, SizeAndCountAreHexadecimal) {
+  // Take a number between 2^63 and 2^64 (or between 2^31 and 2^32 if |size_t|
+  // is not 64 bits).
+  const size_t large_value =
+      sizeof(size_t) == 8 ? 0xffffffffffffffc5 : 0xffffff9d;
+  const char* large_value_str =
+      sizeof(size_t) == 8 ? "ffffffffffffffc5" : "ffffff9d";
+  Entry entry;
+  entry.type_id = 0;
+  entry.stack_frame_id = 0;
+  entry.size = large_value;
+  entry.count = large_value;
+
+  std::unique_ptr<const DictionaryValue> json_entry =
+      WriteAndReadBackEntry(entry);
+
+  std::string size;
+  ASSERT_TRUE(json_entry->GetString("size", &size));
+  ASSERT_EQ(large_value_str, size);
+
+  std::string count;
+  ASSERT_TRUE(json_entry->GetString("count", &count));
+  ASSERT_EQ(large_value_str, count);
+}
+
+TEST(HeapDumpWriterTest, BacktraceTypeNameTable) {
+  std::unordered_map<AllocationContext, AllocationMetrics> metrics_by_context;
+
+  AllocationContext ctx;
+  ctx.backtrace.frames[0] = kBrowserMain;
+  ctx.backtrace.frames[1] = kCreateWidget;
+  ctx.backtrace.frame_count = 2;
+  ctx.type_name = kInt;
+
+  // 10 bytes with context { type: int, bt: [BrowserMain, CreateWidget] }.
+  metrics_by_context[ctx] = {10, 5};
+
+  ctx.type_name = kBool;
+
+  // 18 bytes with context { type: bool, bt: [BrowserMain, CreateWidget] }.
+  metrics_by_context[ctx] = {18, 18};
+
+  ctx.backtrace.frames[0] = kRendererMain;
+  ctx.backtrace.frames[1] = kInitialize;
+  ctx.backtrace.frame_count = 2;
+
+  // 30 bytes with context { type: bool, bt: [RendererMain, Initialize] }.
+  metrics_by_context[ctx] = {30, 30};
+
+  ctx.type_name = kString;
+
+  // 19 bytes with context { type: string, bt: [RendererMain, Initialize] }.
+  metrics_by_context[ctx] = {19, 4};
+
+  // At this point the heap looks like this:
+  //
+  // |        | CrWidget <- BrMain | Init <- RenMain |     Sum     |
+  // +--------+--------------------+-----------------+-------------+
+  // |        |       size   count |   size    count | size  count |
+  // | int    |         10       5 |      0        0 |   10      5 |
+  // | bool   |         18      18 |     30       30 |   48     48 |
+  // | string |          0       0 |     19        4 |   19      4 |
+  // +--------+--------------------+-----------------+-------------+
+  // | Sum    |         28      23 |     49      34  |   77     57 |
+
+  auto stack_frame_deduplicator = WrapUnique(new StackFrameDeduplicator);
+  auto type_name_deduplicator = WrapUnique(new TypeNameDeduplicator);
+  HeapDumpWriter writer(stack_frame_deduplicator.get(),
+                        type_name_deduplicator.get(), 10u);
+  const std::set<Entry>& dump = writer.Summarize(metrics_by_context);
+
+  // Get the indices of the backtraces and types by adding them again to the
+  // deduplicator. Because they were added before, the same number is returned.
+  StackFrame bt0[] = {kRendererMain, kInitialize};
+  StackFrame bt1[] = {kBrowserMain, kCreateWidget};
+  int bt_renderer_main = stack_frame_deduplicator->Insert(bt0, bt0 + 1);
+  int bt_browser_main = stack_frame_deduplicator->Insert(bt1, bt1 + 1);
+  int bt_renderer_main_initialize =
+      stack_frame_deduplicator->Insert(bt0, bt0 + 2);
+  int bt_browser_main_create_widget =
+      stack_frame_deduplicator->Insert(bt1, bt1 + 2);
+  int type_id_int = type_name_deduplicator->Insert(kInt);
+  int type_id_bool = type_name_deduplicator->Insert(kBool);
+  int type_id_string = type_name_deduplicator->Insert(kString);
+
+  // Full heap should have size 77.
+  AssertSizeAndCountEq(dump, -1, -1, {77, 57});
+
+  // 49 bytes in 34 chunks were allocated in RendererMain and children. Also
+  // check the type breakdown.
+  AssertSizeAndCountEq(dump, bt_renderer_main, -1, {49, 34});
+  AssertSizeAndCountEq(dump, bt_renderer_main, type_id_bool, {30, 30});
+  AssertSizeAndCountEq(dump, bt_renderer_main, type_id_string, {19, 4});
+
+  // 28 bytes in 23 chunks were allocated in BrowserMain and children. Also
+  // check the type breakdown.
+  AssertSizeAndCountEq(dump, bt_browser_main, -1, {28, 23});
+  AssertSizeAndCountEq(dump, bt_browser_main, type_id_int, {10, 5});
+  AssertSizeAndCountEq(dump, bt_browser_main, type_id_bool, {18, 18});
+
+  // In this test all bytes are allocated in leaf nodes, so check again one
+  // level deeper.
+  AssertSizeAndCountEq(dump, bt_renderer_main_initialize, -1, {49, 34});
+  AssertSizeAndCountEq(dump, bt_renderer_main_initialize, type_id_bool,
+                       {30, 30});
+  AssertSizeAndCountEq(dump, bt_renderer_main_initialize, type_id_string,
+                       {19, 4});
+  AssertSizeAndCountEq(dump, bt_browser_main_create_widget, -1, {28, 23});
+  AssertSizeAndCountEq(dump, bt_browser_main_create_widget, type_id_int,
+                       {10, 5});
+  AssertSizeAndCountEq(dump, bt_browser_main_create_widget, type_id_bool,
+                       {18, 18});
+
+  // The type breakdown of the entrie heap should have been dumped as well.
+  AssertSizeAndCountEq(dump, -1, type_id_int, {10, 5});
+  AssertSizeAndCountEq(dump, -1, type_id_bool, {48, 48});
+  AssertSizeAndCountEq(dump, -1, type_id_string, {19, 4});
+}
+
+TEST(HeapDumpWriterTest, InsignificantValuesNotDumped) {
+  std::unordered_map<AllocationContext, AllocationMetrics> metrics_by_context;
+
+  AllocationContext ctx;
+  ctx.backtrace.frames[0] = kBrowserMain;
+  ctx.backtrace.frames[1] = kCreateWidget;
+  ctx.backtrace.frame_count = 2;
+
+  // 0.5 KiB and 1 chunk in BrowserMain -> CreateWidget itself.
+  metrics_by_context[ctx] = {512, 1};
+
+  // 1 MiB and 1 chunk in BrowserMain -> CreateWidget -> GetBitmap.
+  ctx.backtrace.frames[2] = kGetBitmap;
+  ctx.backtrace.frame_count = 3;
+  metrics_by_context[ctx] = {1024 * 1024, 1};
+
+  // 400B and 1 chunk in BrowserMain -> CreateWidget -> Initialize.
+  ctx.backtrace.frames[2] = kInitialize;
+  ctx.backtrace.frame_count = 3;
+  metrics_by_context[ctx] = {400, 1};
+
+  auto stack_frame_deduplicator = WrapUnique(new StackFrameDeduplicator);
+  auto type_name_deduplicator = WrapUnique(new TypeNameDeduplicator);
+  HeapDumpWriter writer(stack_frame_deduplicator.get(),
+                        type_name_deduplicator.get(), 512u);
+  const std::set<Entry>& dump = writer.Summarize(metrics_by_context);
+
+  // Get the indices of the backtraces and types by adding them again to the
+  // deduplicator. Because they were added before, the same number is returned.
+  StackFrame bt0[] = {kBrowserMain, kCreateWidget, kGetBitmap};
+  StackFrame bt1[] = {kBrowserMain, kCreateWidget, kInitialize};
+  int bt_browser_main = stack_frame_deduplicator->Insert(bt0, bt0 + 1);
+  int bt_create_widget = stack_frame_deduplicator->Insert(bt0, bt0 + 2);
+  int bt_get_bitmap = stack_frame_deduplicator->Insert(bt0, bt0 + 3);
+  int bt_initialize = stack_frame_deduplicator->Insert(bt1, bt1 + 3);
+
+  // Full heap should have size of 1 MiB + .9 KiB and 3 chunks.
+  AssertSizeAndCountEq(dump, -1, -1 /* No type specified */,
+                       {1024 * 1024 + 512 + 400, 3});
+
+  // |GetBitmap| allocated 1 MiB and 1 chunk.
+  AssertSizeAndCountEq(dump, bt_get_bitmap, -1, {1024 * 1024, 1});
+
+  // Because |GetBitmap| was dumped, all of its parent nodes should have been
+  // dumped too. |CreateWidget| has 1 MiB in |GetBitmap|, 400 bytes in
+  // |Initialize|, and 512 bytes of its own and each in 1 chunk.
+  AssertSizeAndCountEq(dump, bt_create_widget, -1,
+                       {1024 * 1024 + 400 + 512, 3});
+  AssertSizeAndCountEq(dump, bt_browser_main, -1, {1024 * 1024 + 400 + 512, 3});
+
+  // Initialize was not significant, it should not have been dumped.
+  AssertNotDumped(dump, bt_initialize, -1);
+}
+
+}  // namespace internal
+}  // namespace trace_event
+}  // namespace base
diff --git a/base/trace_event/heap_profiler_serialization_state.cc b/base/trace_event/heap_profiler_serialization_state.cc
index c7784eeb..d332d43c 100644
--- a/base/trace_event/heap_profiler_serialization_state.cc
+++ b/base/trace_event/heap_profiler_serialization_state.cc
@@ -4,11 +4,6 @@
 
 #include "base/trace_event/heap_profiler_serialization_state.h"
 
-#include "base/memory/ptr_util.h"
-#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
-
 namespace base {
 namespace trace_event {
 
@@ -16,12 +11,16 @@
     : heap_profiler_breakdown_threshold_bytes_(0) {}
 HeapProfilerSerializationState::~HeapProfilerSerializationState() {}
 
-void HeapProfilerSerializationState::CreateDeduplicators() {
-  string_deduplicator_ = base::MakeUnique<StringDeduplicator>();
-  stack_frame_deduplicator_ =
-      base::MakeUnique<StackFrameDeduplicator>(string_deduplicator_.get());
-  type_name_deduplicator_ =
-      base::MakeUnique<TypeNameDeduplicator>(string_deduplicator_.get());
+void HeapProfilerSerializationState::SetStackFrameDeduplicator(
+    std::unique_ptr<StackFrameDeduplicator> stack_frame_deduplicator) {
+  DCHECK(!stack_frame_deduplicator_);
+  stack_frame_deduplicator_ = std::move(stack_frame_deduplicator);
+}
+
+void HeapProfilerSerializationState::SetTypeNameDeduplicator(
+    std::unique_ptr<TypeNameDeduplicator> type_name_deduplicator) {
+  DCHECK(!type_name_deduplicator_);
+  type_name_deduplicator_ = std::move(type_name_deduplicator);
 }
 
 }  // namespace trace_event
diff --git a/base/trace_event/heap_profiler_serialization_state.h b/base/trace_event/heap_profiler_serialization_state.h
index 4c0ffd7..3d388b20 100644
--- a/base/trace_event/heap_profiler_serialization_state.h
+++ b/base/trace_event/heap_profiler_serialization_state.h
@@ -29,18 +29,17 @@
     return stack_frame_deduplicator_.get();
   }
 
+  void SetStackFrameDeduplicator(
+      std::unique_ptr<StackFrameDeduplicator> stack_frame_deduplicator);
+
   // Returns the type name deduplicator that should be used by memory dump
   // providers when doing a heap dump.
   TypeNameDeduplicator* type_name_deduplicator() const {
     return type_name_deduplicator_.get();
   }
 
-  // Returns generic string deduplicator used by other deduplicators.
-  StringDeduplicator* string_deduplicator() const {
-    return string_deduplicator_.get();
-  }
-
-  void CreateDeduplicators();
+  void SetTypeNameDeduplicator(
+      std::unique_ptr<TypeNameDeduplicator> type_name_deduplicator);
 
   void SetAllowedDumpModes(
       std::set<MemoryDumpLevelOfDetail> allowed_dump_modes);
@@ -67,10 +66,6 @@
   // trace is finalized.
   std::unique_ptr<TypeNameDeduplicator> type_name_deduplicator_;
 
-  // Generic string deduplicator, used by other deduplicators; must be defined
-  // after ones that depend on it.
-  std::unique_ptr<StringDeduplicator> string_deduplicator_;
-
   uint32_t heap_profiler_breakdown_threshold_bytes_;
 };
 
diff --git a/base/trace_event/heap_profiler_stack_frame_deduplicator.cc b/base/trace_event/heap_profiler_stack_frame_deduplicator.cc
index 12110b1..351117f1 100644
--- a/base/trace_event/heap_profiler_stack_frame_deduplicator.cc
+++ b/base/trace_event/heap_profiler_stack_frame_deduplicator.cc
@@ -13,7 +13,6 @@
 
 #include "base/hash.h"
 #include "base/strings/stringprintf.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
 #include "base/trace_event/memory_usage_estimator.h"
 #include "base/trace_event/trace_event.h"
 #include "base/trace_event/trace_event_argument.h"
@@ -46,13 +45,7 @@
   return base::trace_event::EstimateMemoryUsage(children);
 }
 
-StackFrameDeduplicator::StackFrameDeduplicator(
-    StringDeduplicator* string_deduplicator)
-    : string_deduplicator_(string_deduplicator), last_exported_index_(0) {
-  // Add implicit entry for id 0 (empty backtraces).
-  frames_.push_back(FrameNode(StackFrame::FromTraceEventName(nullptr),
-                              FrameNode::kInvalidFrameIndex));
-}
+StackFrameDeduplicator::StackFrameDeduplicator() {}
 StackFrameDeduplicator::~StackFrameDeduplicator() {}
 
 bool StackFrameDeduplicator::Match(int frame_index,
@@ -84,8 +77,7 @@
 int StackFrameDeduplicator::Insert(const StackFrame* begin_frame,
                                    const StackFrame* end_frame) {
   if (begin_frame == end_frame) {
-    // Empty backtraces are mapped to id 0.
-    return 0;
+    return FrameNode::kInvalidFrameIndex;
   }
 
   size_t backtrace_hash = HashBacktrace(begin_frame, end_frame);
@@ -136,45 +128,58 @@
   return frame_index;
 }
 
-void StackFrameDeduplicator::SerializeIncrementally(TracedValue* traced_value) {
+void StackFrameDeduplicator::AppendAsTraceFormat(std::string* out) const {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"),
-               "StackFrameDeduplicator::SerializeIncrementally");
+               "StackFrameDeduplicator::AppendAsTraceFormat");
+  out->append("{");  // Begin the |stackFrames| dictionary.
+
+  int i = 0;
+  auto frame_node = begin();
+  auto it_end = end();
   std::string stringify_buffer;
 
-  for (; last_exported_index_ < frames_.size(); ++last_exported_index_) {
-    const auto& frame_node = frames_[last_exported_index_];
-    traced_value->BeginDictionary();
+  while (frame_node != it_end) {
+    // The |stackFrames| format is a dictionary, not an array, so the
+    // keys are stringified indices. Write the index manually, then use
+    // |TracedValue| to format the object. This is to avoid building the
+    // entire dictionary as a |TracedValue| in memory.
+    SStringPrintf(&stringify_buffer, "\"%d\":", i);
+    out->append(stringify_buffer);
 
-    traced_value->SetInteger("id", last_exported_index_);
-
-    int name_string_id = 0;
-    const StackFrame& frame = frame_node.frame;
+    std::unique_ptr<TracedValue> frame_node_value(new TracedValue);
+    const StackFrame& frame = frame_node->frame;
     switch (frame.type) {
       case StackFrame::Type::TRACE_EVENT_NAME:
-        name_string_id =
-            string_deduplicator_->Insert(static_cast<const char*>(frame.value));
+        frame_node_value->SetString("name",
+                                    static_cast<const char*>(frame.value));
         break;
       case StackFrame::Type::THREAD_NAME:
         SStringPrintf(&stringify_buffer,
                       "[Thread: %s]",
                       static_cast<const char*>(frame.value));
-        name_string_id = string_deduplicator_->Insert(stringify_buffer);
+        frame_node_value->SetString("name", stringify_buffer);
         break;
       case StackFrame::Type::PROGRAM_COUNTER:
         SStringPrintf(&stringify_buffer,
                       "pc:%" PRIxPTR,
                       reinterpret_cast<uintptr_t>(frame.value));
-        name_string_id = string_deduplicator_->Insert(stringify_buffer);
+        frame_node_value->SetString("name", stringify_buffer);
         break;
     }
-    traced_value->SetInteger("name_sid", name_string_id);
-
-    if (frame_node.parent_frame_index != FrameNode::kInvalidFrameIndex) {
-      traced_value->SetInteger("parent", frame_node.parent_frame_index);
+    if (frame_node->parent_frame_index != FrameNode::kInvalidFrameIndex) {
+      SStringPrintf(&stringify_buffer, "%d", frame_node->parent_frame_index);
+      frame_node_value->SetString("parent", stringify_buffer);
     }
+    frame_node_value->AppendAsTraceFormat(out);
 
-    traced_value->EndDictionary();
+    i++;
+    frame_node++;
+
+    if (frame_node != it_end)
+      out->append(",");
   }
+
+  out->append("}");  // End the |stackFrames| dictionary.
 }
 
 void StackFrameDeduplicator::EstimateTraceMemoryOverhead(
diff --git a/base/trace_event/heap_profiler_stack_frame_deduplicator.h b/base/trace_event/heap_profiler_stack_frame_deduplicator.h
index 7b6c662..ce86a16 100644
--- a/base/trace_event/heap_profiler_stack_frame_deduplicator.h
+++ b/base/trace_event/heap_profiler_stack_frame_deduplicator.h
@@ -13,13 +13,12 @@
 #include "base/containers/flat_map.h"
 #include "base/macros.h"
 #include "base/trace_event/heap_profiler_allocation_context.h"
+#include "base/trace_event/trace_event_impl.h"
 
 namespace base {
 namespace trace_event {
 
-class StringDeduplicator;
 class TraceEventMemoryOverhead;
-class TracedValue;
 
 // A data structure that allows grouping a set of backtraces in a space-
 // efficient manner by creating a call tree and writing it as a set of (node,
@@ -28,7 +27,7 @@
 // of |StackFrame|s to index into |frames_|. So there is a trie for bottum-up
 // lookup of a backtrace for deduplication, and a tree for compact storage in
 // the trace log.
-class BASE_EXPORT StackFrameDeduplicator {
+class BASE_EXPORT StackFrameDeduplicator : public ConvertableToTraceFormat {
  public:
   // A node in the call tree.
   struct FrameNode {
@@ -51,10 +50,8 @@
 
   using ConstIterator = std::deque<FrameNode>::const_iterator;
 
-  // |string_deduplication| is used during serialization, and is expected
-  // to outlive instances of this class.
-  explicit StackFrameDeduplicator(StringDeduplicator* string_deduplicator);
-  ~StackFrameDeduplicator();
+  StackFrameDeduplicator();
+  ~StackFrameDeduplicator() override;
 
   // Inserts a backtrace where |beginFrame| is a pointer to the bottom frame
   // (e.g. main) and |endFrame| is a pointer past the top frame (most recently
@@ -66,12 +63,12 @@
   ConstIterator begin() const { return frames_.begin(); }
   ConstIterator end() const { return frames_.end(); }
 
-  // Appends new |stackFrames| dictionary items that were added after the
-  // last call to this function.
-  void SerializeIncrementally(TracedValue* traced_value);
+  // Writes the |stackFrames| dictionary as defined in https://goo.gl/GerkV8 to
+  // the trace log.
+  void AppendAsTraceFormat(std::string* out) const override;
 
   // Estimates memory overhead including |sizeof(StackFrameDeduplicator)|.
-  void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead);
+  void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead) override;
 
  private:
   // Checks that existing backtrace identified by |frame_index| equals
@@ -80,11 +77,8 @@
              const StackFrame* begin_frame,
              const StackFrame* end_frame) const;
 
-  StringDeduplicator* string_deduplicator_;
-
   base::flat_map<StackFrame, int> roots_;
   std::deque<FrameNode> frames_;
-  size_t last_exported_index_;
 
   // {backtrace_hash -> frame_index} map for finding backtraces that are
   // already added. Backtraces themselves are not stored in the map, instead
diff --git a/base/trace_event/heap_profiler_stack_frame_deduplicator_unittest.cc b/base/trace_event/heap_profiler_stack_frame_deduplicator_unittest.cc
index c5b94ad9..194c7aad2 100644
--- a/base/trace_event/heap_profiler_stack_frame_deduplicator_unittest.cc
+++ b/base/trace_event/heap_profiler_stack_frame_deduplicator_unittest.cc
@@ -8,84 +8,12 @@
 #include <memory>
 
 #include "base/macros.h"
-#include "base/memory/ptr_util.h"
 #include "base/trace_event/heap_profiler_allocation_context.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-#include "base/trace_event/trace_event_argument.h"
-#include "base/values.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace base {
 namespace trace_event {
 
-namespace {
-
-constexpr static int kInvalidStackFrameIndex =
-    StackFrameDeduplicator::FrameNode::kInvalidFrameIndex;
-
-// Calls StackFrameDeduplicator::SerializeIncrementally() and returns
-// ListValue with serialized entries.
-std::unique_ptr<ListValue> SerializeEntriesIncrementally(
-    StackFrameDeduplicator* dedup) {
-  TracedValue traced_value;
-  traced_value.BeginArray("");
-  dedup->SerializeIncrementally(&traced_value);
-  traced_value.EndArray();
-
-  auto base_value = traced_value.ToBaseValue();
-  DictionaryValue* dictionary;
-  std::unique_ptr<Value> entries;
-  if (!base_value->GetAsDictionary(&dictionary) ||
-      !dictionary->Remove("", &entries)) {
-    return nullptr;
-  }
-  return ListValue::From(std::move(entries));
-}
-
-struct StackFrameMapping {
-  StackFrameMapping(int id,
-                    StackFrame frame,
-                    int parent_id = kInvalidStackFrameIndex)
-      : id(id), parent_id(parent_id) {
-    EXPECT_EQ(StackFrame::Type::TRACE_EVENT_NAME, frame.type);
-    name = static_cast<const char*>(frame.value);
-  }
-
-  int id;
-  const char* name;
-  int parent_id;
-};
-
-std::unique_ptr<ListValue> SerializeMappingsAsEntries(
-    StringDeduplicator* string_dedup,
-    std::initializer_list<StackFrameMapping> mappings) {
-  auto entries = MakeUnique<ListValue>();
-  for (const auto& mapping : mappings) {
-    auto entry = MakeUnique<DictionaryValue>();
-    entry->SetInteger("id", mapping.id);
-    entry->SetInteger("name_sid", string_dedup->Insert(mapping.name));
-    if (mapping.parent_id != kInvalidStackFrameIndex) {
-      entry->SetInteger("parent", mapping.parent_id);
-    }
-    entries->Append(std::move(entry));
-  }
-  return entries;
-}
-
-void ExpectIncrementalEntries(
-    StackFrameDeduplicator* dedup,
-    StringDeduplicator* string_dedup,
-    std::initializer_list<StackFrameMapping> mappings) {
-  auto entries = SerializeEntriesIncrementally(dedup);
-  ASSERT_TRUE(entries);
-
-  auto expected_entries = SerializeMappingsAsEntries(string_dedup, mappings);
-  ASSERT_TRUE(expected_entries->Equals(entries.get()))
-      << "expected_entries = " << *expected_entries << "entries = " << *entries;
-}
-
-}  // namespace
-
 // Define all strings once, because the deduplicator requires pointer equality,
 // and string interning is unreliable.
 StackFrame kBrowserMain = StackFrame::FromTraceEventName("BrowserMain");
@@ -93,52 +21,30 @@
 StackFrame kCreateWidget = StackFrame::FromTraceEventName("CreateWidget");
 StackFrame kInitialize = StackFrame::FromTraceEventName("Initialize");
 StackFrame kMalloc = StackFrame::FromTraceEventName("malloc");
-StackFrame kNull = StackFrame::FromTraceEventName(nullptr);
-
-TEST(StackFrameDeduplicatorTest, ImplicitId0) {
-  StackFrame null_bt[] = {kNull};
-
-  // Empty backtraces (begin == end) are mapped to an implicitly added
-  // node #0. However, backtrace with a single null frame is not empty,
-  // and should be mapped to some other id.
-
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-
-  // Node #0 is added implicitly and corresponds to an empty backtrace.
-  ASSERT_TRUE(dedup.begin() + 1 == dedup.end());
-  ASSERT_EQ(0, dedup.Insert(std::begin(null_bt), std::begin(null_bt)));
-
-  // Placeholder entry for ID 0 is a frame with NULL name and no parent.
-  // However inserting such a frame should yield a different ID.
-  ExpectIncrementalEntries(&dedup, &string_dedup, {{0, kNull}});
-  ASSERT_EQ(1, dedup.Insert(std::begin(null_bt), std::end(null_bt)));
-}
 
 TEST(StackFrameDeduplicatorTest, SingleBacktrace) {
   StackFrame bt[] = {kBrowserMain, kCreateWidget, kMalloc};
 
   // The call tree should look like this (index in brackets).
   //
-  // BrowserMain [1]
-  //   CreateWidget [2]
-  //     malloc [3]
+  // BrowserMain [0]
+  //   CreateWidget [1]
+  //     malloc [2]
 
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-  ASSERT_EQ(3, dedup.Insert(std::begin(bt), std::end(bt)));
+  std::unique_ptr<StackFrameDeduplicator> dedup(new StackFrameDeduplicator);
+  ASSERT_EQ(2, dedup->Insert(std::begin(bt), std::end(bt)));
 
-  auto iter = dedup.begin() + 1;  // skip implicit node #0
+  auto iter = dedup->begin();
   ASSERT_EQ(kBrowserMain, (iter + 0)->frame);
-  ASSERT_EQ(kInvalidStackFrameIndex, (iter + 0)->parent_frame_index);
+  ASSERT_EQ(-1, (iter + 0)->parent_frame_index);
 
   ASSERT_EQ(kCreateWidget, (iter + 1)->frame);
-  ASSERT_EQ(1, (iter + 1)->parent_frame_index);
+  ASSERT_EQ(0, (iter + 1)->parent_frame_index);
 
   ASSERT_EQ(kMalloc, (iter + 2)->frame);
-  ASSERT_EQ(2, (iter + 2)->parent_frame_index);
+  ASSERT_EQ(1, (iter + 2)->parent_frame_index);
 
-  ASSERT_TRUE(iter + 3 == dedup.end());
+  ASSERT_TRUE(iter + 3 == dedup->end());
 }
 
 TEST(StackFrameDeduplicatorTest, SingleBacktraceWithNull) {
@@ -150,25 +56,24 @@
   //
   // So the call tree should look like this (index in brackets).
   //
-  // BrowserMain [1]
-  //   (null) [2]
-  //     malloc [3]
+  // BrowserMain [0]
+  //   (null) [1]
+  //     malloc [2]
 
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-  ASSERT_EQ(3, dedup.Insert(std::begin(bt), std::end(bt)));
+  std::unique_ptr<StackFrameDeduplicator> dedup(new StackFrameDeduplicator);
+  ASSERT_EQ(2, dedup->Insert(std::begin(bt), std::end(bt)));
 
-  auto iter = dedup.begin() + 1;  // skip implicit node #0
+  auto iter = dedup->begin();
   ASSERT_EQ(kBrowserMain, (iter + 0)->frame);
-  ASSERT_EQ(kInvalidStackFrameIndex, (iter + 0)->parent_frame_index);
+  ASSERT_EQ(-1, (iter + 0)->parent_frame_index);
 
   ASSERT_EQ(null_frame, (iter + 1)->frame);
-  ASSERT_EQ(1, (iter + 1)->parent_frame_index);
+  ASSERT_EQ(0, (iter + 1)->parent_frame_index);
 
   ASSERT_EQ(kMalloc, (iter + 2)->frame);
-  ASSERT_EQ(2, (iter + 2)->parent_frame_index);
+  ASSERT_EQ(1, (iter + 2)->parent_frame_index);
 
-  ASSERT_TRUE(iter + 3 == dedup.end());
+  ASSERT_TRUE(iter + 3 == dedup->end());
 }
 
 // Test that there can be different call trees (there can be multiple bottom
@@ -180,33 +85,32 @@
 
   // The call tree should look like this (index in brackets).
   //
-  // BrowserMain [1]
-  //   CreateWidget [2]
-  // RendererMain [3]
-  //   CreateWidget [4]
+  // BrowserMain [0]
+  //   CreateWidget [1]
+  // RendererMain [2]
+  //   CreateWidget [3]
   //
   // Note that there will be two instances of CreateWidget,
   // with different parents.
 
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-  ASSERT_EQ(2, dedup.Insert(std::begin(bt0), std::end(bt0)));
-  ASSERT_EQ(4, dedup.Insert(std::begin(bt1), std::end(bt1)));
+  std::unique_ptr<StackFrameDeduplicator> dedup(new StackFrameDeduplicator);
+  ASSERT_EQ(1, dedup->Insert(std::begin(bt0), std::end(bt0)));
+  ASSERT_EQ(3, dedup->Insert(std::begin(bt1), std::end(bt1)));
 
-  auto iter = dedup.begin() + 1;  // skip implicit node #0
+  auto iter = dedup->begin();
   ASSERT_EQ(kBrowserMain, (iter + 0)->frame);
-  ASSERT_EQ(kInvalidStackFrameIndex, (iter + 0)->parent_frame_index);
+  ASSERT_EQ(-1, (iter + 0)->parent_frame_index);
 
   ASSERT_EQ(kCreateWidget, (iter + 1)->frame);
-  ASSERT_EQ(1, (iter + 1)->parent_frame_index);
+  ASSERT_EQ(0, (iter + 1)->parent_frame_index);
 
   ASSERT_EQ(kRendererMain, (iter + 2)->frame);
-  ASSERT_EQ(kInvalidStackFrameIndex, (iter + 2)->parent_frame_index);
+  ASSERT_EQ(-1, (iter + 2)->parent_frame_index);
 
   ASSERT_EQ(kCreateWidget, (iter + 3)->frame);
-  ASSERT_EQ(3, (iter + 3)->parent_frame_index);
+  ASSERT_EQ(2, (iter + 3)->parent_frame_index);
 
-  ASSERT_TRUE(iter + 4 == dedup.end());
+  ASSERT_TRUE(iter + 4 == dedup->end());
 }
 
 TEST(StackFrameDeduplicatorTest, Deduplication) {
@@ -215,53 +119,33 @@
 
   // The call tree should look like this (index in brackets).
   //
-  // BrowserMain [1]
-  //   CreateWidget [2]
-  //   Initialize [3]
+  // BrowserMain [0]
+  //   CreateWidget [1]
+  //   Initialize [2]
   //
   // Note that BrowserMain will be re-used.
 
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-  ASSERT_EQ(2, dedup.Insert(std::begin(bt0), std::end(bt0)));
-  ASSERT_EQ(3, dedup.Insert(std::begin(bt1), std::end(bt1)));
+  std::unique_ptr<StackFrameDeduplicator> dedup(new StackFrameDeduplicator);
+  ASSERT_EQ(1, dedup->Insert(std::begin(bt0), std::end(bt0)));
+  ASSERT_EQ(2, dedup->Insert(std::begin(bt1), std::end(bt1)));
 
-  auto iter = dedup.begin() + 1;  // skip implicit node #0
+  auto iter = dedup->begin();
   ASSERT_EQ(kBrowserMain, (iter + 0)->frame);
-  ASSERT_EQ(kInvalidStackFrameIndex, (iter + 0)->parent_frame_index);
+  ASSERT_EQ(-1, (iter + 0)->parent_frame_index);
 
   ASSERT_EQ(kCreateWidget, (iter + 1)->frame);
-  ASSERT_EQ(1, (iter + 1)->parent_frame_index);
+  ASSERT_EQ(0, (iter + 1)->parent_frame_index);
 
   ASSERT_EQ(kInitialize, (iter + 2)->frame);
-  ASSERT_EQ(1, (iter + 2)->parent_frame_index);
+  ASSERT_EQ(0, (iter + 2)->parent_frame_index);
 
-  ASSERT_TRUE(iter + 3 == dedup.end());
+  ASSERT_TRUE(iter + 3 == dedup->end());
 
   // Inserting the same backtrace again should return the index of the existing
   // node.
-  ASSERT_EQ(2, dedup.Insert(std::begin(bt0), std::end(bt0)));
-  ASSERT_EQ(3, dedup.Insert(std::begin(bt1), std::end(bt1)));
-  ASSERT_EQ(4 /* 1 implicit + 3 added */, dedup.end() - dedup.begin());
-}
-
-TEST(StackFrameDeduplicatorTest, SerializeIncrementally) {
-  StringDeduplicator string_dedup;
-  StackFrameDeduplicator dedup(&string_dedup);
-
-  StackFrame bt0[] = {kBrowserMain, kCreateWidget};
-  ASSERT_EQ(2, dedup.Insert(std::begin(bt0), std::end(bt0)));
-
-  ExpectIncrementalEntries(
-      &dedup, &string_dedup,
-      {{0, kNull}, {1, kBrowserMain}, {2, kCreateWidget, 1}});
-
-  StackFrame bt1[] = {kBrowserMain, kInitialize};
-  ASSERT_EQ(3, dedup.Insert(std::begin(bt1), std::end(bt1)));
-
-  ExpectIncrementalEntries(&dedup, &string_dedup, {{3, kInitialize, 1}});
-
-  ExpectIncrementalEntries(&dedup, &string_dedup, {});
+  ASSERT_EQ(1, dedup->Insert(std::begin(bt0), std::end(bt0)));
+  ASSERT_EQ(2, dedup->Insert(std::begin(bt1), std::end(bt1)));
+  ASSERT_TRUE(dedup->begin() + 3 == dedup->end());
 }
 
 }  // namespace trace_event
diff --git a/base/trace_event/heap_profiler_string_deduplicator.cc b/base/trace_event/heap_profiler_string_deduplicator.cc
deleted file mode 100644
index 5938e306..0000000
--- a/base/trace_event/heap_profiler_string_deduplicator.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-
-#include "base/trace_event/memory_usage_estimator.h"
-#include "base/trace_event/trace_event.h"
-#include "base/trace_event/trace_event_argument.h"
-#include "base/trace_event/trace_event_memory_overhead.h"
-
-namespace base {
-namespace trace_event {
-
-StringDeduplicator::StringDeduplicator() : last_serialized_index_(0) {
-  // Add implicit entry for id 0 (NULL strings).
-  strings_.push_back("[null]");
-}
-
-StringDeduplicator::~StringDeduplicator() {}
-
-int StringDeduplicator::Insert(StringPiece string) {
-  if (!string.data()) {
-    // NULL strings are mapped to id 0.
-    return 0;
-  }
-  auto it = string_ids_.find(string);
-  if (it != string_ids_.end())
-    return it->second;
-
-  // Insert new mapping. Note that |string_ids_| keys reference values
-  // from |strings_|.
-  int string_id = static_cast<int>(strings_.size());
-  strings_.push_back(string.as_string());
-  auto iter_and_flag = string_ids_.insert({strings_.back(), string_id});
-  DCHECK(iter_and_flag.second);  // insert() must succeed
-  return string_id;
-}
-
-void StringDeduplicator::SerializeIncrementally(TracedValue* traced_value) {
-  TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"),
-               "StringDeduplicator::SerializeIncrementally");
-  for (; last_serialized_index_ != strings_.size(); ++last_serialized_index_) {
-    traced_value->BeginDictionary();
-    traced_value->SetInteger("id", last_serialized_index_);
-    traced_value->SetString("string", strings_[last_serialized_index_]);
-    traced_value->EndDictionary();
-  }
-}
-
-void StringDeduplicator::EstimateTraceMemoryOverhead(
-    TraceEventMemoryOverhead* overhead) {
-  size_t memory_usage =
-      EstimateMemoryUsage(string_ids_) + EstimateMemoryUsage(strings_);
-  overhead->Add(TraceEventMemoryOverhead::kHeapProfilerStringDeduplicator,
-                sizeof(StringDeduplicator) + memory_usage);
-}
-
-}  // namespace trace_event
-}  // namespace base
diff --git a/base/trace_event/heap_profiler_string_deduplicator.h b/base/trace_event/heap_profiler_string_deduplicator.h
deleted file mode 100644
index e43d0a5f..0000000
--- a/base/trace_event/heap_profiler_string_deduplicator.h
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef BASE_TRACE_EVENT_HEAP_PROFILER_STRING_DEDUPLICATOR_H_
-#define BASE_TRACE_EVENT_HEAP_PROFILER_STRING_DEDUPLICATOR_H_
-
-#include <deque>
-#include <string>
-#include <unordered_map>
-
-#include "base/base_export.h"
-#include "base/macros.h"
-#include "base/strings/string_piece.h"
-
-namespace base {
-namespace trace_event {
-
-class TraceEventMemoryOverhead;
-class TracedValue;
-
-// Data structure that assigns a unique numeric ID to |const char*|s.
-class BASE_EXPORT StringDeduplicator {
- public:
-  StringDeduplicator();
-  ~StringDeduplicator();
-
-  // Inserts a string and returns its ID.
-  int Insert(StringPiece string);
-
-  // Append {ID -> string} mappings that were added after the last call
-  // to this function.
-  void SerializeIncrementally(TracedValue* traced_value);
-
-  // Estimates memory overhead including |sizeof(StringDeduplicator)|.
-  void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead);
-
- private:
-  // StringPieces in the map reference values from |string_|.
-  std::unordered_map<StringPiece, int, StringPieceHash> string_ids_;
-  std::deque<std::string> strings_;
-  size_t last_serialized_index_;
-
-  DISALLOW_COPY_AND_ASSIGN(StringDeduplicator);
-};
-
-}  // namespace trace_event
-}  // namespace base
-
-#endif  // BASE_TRACE_EVENT_HEAP_PROFILER_STRING_DEDUPLICATOR_H_
diff --git a/base/trace_event/heap_profiler_string_deduplicator_unittest.cc b/base/trace_event/heap_profiler_string_deduplicator_unittest.cc
deleted file mode 100644
index eca3743..0000000
--- a/base/trace_event/heap_profiler_string_deduplicator_unittest.cc
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-
-#include <memory>
-#include <string>
-
-#include "base/memory/ptr_util.h"
-#include "base/trace_event/trace_event_argument.h"
-#include "base/values.h"
-#include "testing/gtest/include/gtest/gtest.h"
-
-namespace base {
-namespace trace_event {
-
-namespace {
-
-// Calls StringDeduplicator::SerializeIncrementally() and returns ListValue
-// with serialized entries.
-std::unique_ptr<ListValue> SerializeEntriesIncrementally(
-    StringDeduplicator* dedup) {
-  TracedValue traced_value;
-  traced_value.BeginArray("");
-  dedup->SerializeIncrementally(&traced_value);
-  traced_value.EndArray();
-
-  auto base_value = traced_value.ToBaseValue();
-  DictionaryValue* dictionary;
-  std::unique_ptr<Value> entries;
-  if (!base_value->GetAsDictionary(&dictionary) ||
-      !dictionary->Remove("", &entries)) {
-    return nullptr;
-  }
-  return ListValue::From(std::move(entries));
-}
-
-struct StringMapping {
-  const int id;
-  const char* const string;
-};
-
-std::unique_ptr<ListValue> SerializeMappingsAsEntries(
-    std::initializer_list<StringMapping> mappings) {
-  auto entries = MakeUnique<ListValue>();
-  for (const auto& mapping : mappings) {
-    auto entry = MakeUnique<DictionaryValue>();
-    entry->SetInteger("id", mapping.id);
-    entry->SetString("string", mapping.string);
-    entries->Append(std::move(entry));
-  }
-  return entries;
-}
-
-void ExpectIncrementalEntries(StringDeduplicator* dedup,
-                              std::initializer_list<StringMapping> mappings) {
-  auto entries = SerializeEntriesIncrementally(dedup);
-  ASSERT_TRUE(entries);
-
-  auto expected_entries = SerializeMappingsAsEntries(mappings);
-  ASSERT_TRUE(expected_entries->Equals(entries.get()))
-      << "expected_entries = " << *expected_entries << "entries = " << *entries;
-}
-
-}  // namespace
-
-TEST(StringDeduplicatorTest, ImplicitId0) {
-  StringDeduplicator dedup;
-
-  // NULL string is mapped to an implicitly added ID #0.
-  ExpectIncrementalEntries(&dedup, {{0, "[null]"}});
-  ASSERT_EQ(0, dedup.Insert(nullptr));
-
-  // Even though ID #0 is serialized as "[null]", it's distinct from
-  // explicitly added "[null]" string.
-  ASSERT_EQ(1, dedup.Insert("[null]"));
-  ExpectIncrementalEntries(&dedup, {{1, "[null]"}});
-}
-
-TEST(StringDeduplicatorTest, Deduplicate) {
-  StringDeduplicator dedup;
-
-  ASSERT_EQ(1, dedup.Insert("foo"));
-  ASSERT_EQ(2, dedup.Insert("bar"));
-  ASSERT_EQ(3, dedup.Insert("baz"));
-
-  // Inserting again should return the same IDs.
-  ASSERT_EQ(2, dedup.Insert("bar"));
-  ASSERT_EQ(1, dedup.Insert("foo"));
-  ASSERT_EQ(3, dedup.Insert("baz"));
-}
-
-TEST(StringDeduplicatorTest, InsertCopies) {
-  StringDeduplicator dedup;
-
-  std::string string = "foo";
-  ASSERT_EQ(1, dedup.Insert(string));
-
-  // StringDeduplicatorTest::Insert() takes StringPiece, which implicitly
-  // constructs from both const char* and std::string. Check that Insert()
-  // actually copies string data, and doesn't simply copy StringPieces.
-  string = "???";
-  ASSERT_EQ(1, dedup.Insert("foo"));
-}
-
-TEST(StringDeduplicatorTest, SerializeIncrementally) {
-  StringDeduplicator dedup;
-
-  ASSERT_EQ(1, dedup.Insert("foo"));
-  ASSERT_EQ(2, dedup.Insert("bar"));
-
-  ExpectIncrementalEntries(&dedup, {{0, "[null]"}, {1, "foo"}, {2, "bar"}});
-
-  ASSERT_EQ(2, dedup.Insert("bar"));
-  ASSERT_EQ(3, dedup.Insert("baz"));
-
-  ExpectIncrementalEntries(&dedup, {{3, "baz"}});
-
-  ExpectIncrementalEntries(&dedup, {});
-}
-
-}  // namespace trace_event
-}  // namespace base
diff --git a/base/trace_event/heap_profiler_type_name_deduplicator.cc b/base/trace_event/heap_profiler_type_name_deduplicator.cc
index 8224b5a9..26e8aee 100644
--- a/base/trace_event/heap_profiler_type_name_deduplicator.cc
+++ b/base/trace_event/heap_profiler_type_name_deduplicator.cc
@@ -9,11 +9,11 @@
 #include <string>
 #include <utility>
 
+#include "base/json/string_escape.h"
 #include "base/strings/string_split.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
+#include "base/strings/stringprintf.h"
 #include "base/trace_event/memory_usage_estimator.h"
 #include "base/trace_event/trace_event.h"
-#include "base/trace_event/trace_event_argument.h"
 #include "base/trace_event/trace_event_memory_overhead.h"
 
 namespace base {
@@ -62,11 +62,9 @@
 
 }  // namespace
 
-TypeNameDeduplicator::TypeNameDeduplicator(
-    StringDeduplicator* string_deduplicator)
-    : string_deduplicator_(string_deduplicator) {
-  // Add implicit entry for id 0 (NULL type names).
-  Insert(nullptr);
+TypeNameDeduplicator::TypeNameDeduplicator() {
+  // A null pointer has type ID 0 ("unknown type");
+  type_ids_.insert(std::make_pair(nullptr, 0));
 }
 
 TypeNameDeduplicator::~TypeNameDeduplicator() {}
@@ -80,37 +78,45 @@
     // The type IDs are assigned sequentially and they are zero-based, so
     // |size() - 1| is the ID of the new element.
     elem->second = static_cast<int>(type_ids_.size() - 1);
-    new_type_ids_.push_back(&*result.first);
   }
 
   return elem->second;
 }
 
-void TypeNameDeduplicator::SerializeIncrementally(TracedValue* traced_value) {
+void TypeNameDeduplicator::AppendAsTraceFormat(std::string* out) const {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("memory-infra"),
-               "TypeNameDeduplicator::SerializeIncrementally");
-  for (const auto* name_and_id : new_type_ids_) {
-    traced_value->BeginDictionary();
+               "TypeNameDeduplicator::AppendAsTraceFormat");
+  out->append("{");  // Begin the type names dictionary.
 
-    traced_value->SetInteger("id", name_and_id->second);
+  auto it = type_ids_.begin();
+  std::string buffer;
+
+  // Write the first entry manually; the null pointer must not be dereferenced.
+  // (The first entry is the null pointer because a |std::map| is ordered.)
+  it++;
+  out->append("\"0\":\"[unknown]\"");
+
+  for (; it != type_ids_.end(); it++) {
+    // Type IDs in the trace are strings, write them as stringified keys of
+    // a dictionary.
+    SStringPrintf(&buffer, ",\"%d\":", it->second);
 
     // TODO(ssid): crbug.com/594803 the type name is misused for file name in
     // some cases.
-    StringPiece name = name_and_id->first
-                           ? ExtractCategoryFromTypeName(name_and_id->first)
-                           : "[unknown]";
-    int name_string_id = string_deduplicator_->Insert(name);
-    traced_value->SetInteger("name_sid", name_string_id);
+    StringPiece type_info = ExtractCategoryFromTypeName(it->first);
 
-    traced_value->EndDictionary();
+    // |EscapeJSONString| appends, it does not overwrite |buffer|.
+    bool put_in_quotes = true;
+    EscapeJSONString(type_info, put_in_quotes, &buffer);
+    out->append(buffer);
   }
-  new_type_ids_.clear();
+
+  out->append("}");  // End the type names dictionary.
 }
 
 void TypeNameDeduplicator::EstimateTraceMemoryOverhead(
     TraceEventMemoryOverhead* overhead) {
-  size_t memory_usage =
-      EstimateMemoryUsage(type_ids_) + EstimateMemoryUsage(new_type_ids_);
+  size_t memory_usage = EstimateMemoryUsage(type_ids_);
   overhead->Add(TraceEventMemoryOverhead::kHeapProfilerTypeNameDeduplicator,
                 sizeof(TypeNameDeduplicator) + memory_usage);
 }
diff --git a/base/trace_event/heap_profiler_type_name_deduplicator.h b/base/trace_event/heap_profiler_type_name_deduplicator.h
index 17651f3c..2d26c73 100644
--- a/base/trace_event/heap_profiler_type_name_deduplicator.h
+++ b/base/trace_event/heap_profiler_type_name_deduplicator.h
@@ -7,46 +7,34 @@
 
 #include <map>
 #include <string>
-#include <vector>
 
 #include "base/base_export.h"
 #include "base/macros.h"
+#include "base/trace_event/trace_event_impl.h"
 
 namespace base {
 namespace trace_event {
 
-class StringDeduplicator;
 class TraceEventMemoryOverhead;
-class TracedValue;
 
-// Data structure that assigns a unique numeric ID to type names.
-class BASE_EXPORT TypeNameDeduplicator {
+// Data structure that assigns a unique numeric ID to |const char*|s.
+class BASE_EXPORT TypeNameDeduplicator : public ConvertableToTraceFormat {
  public:
-  // |string_deduplication| is used during serialization, and is expected
-  // to outlive instances of this class.
-  explicit TypeNameDeduplicator(StringDeduplicator* string_deduplicator);
-  ~TypeNameDeduplicator();
+  TypeNameDeduplicator();
+  ~TypeNameDeduplicator() override;
 
   // Inserts a type name and returns its ID.
   int Insert(const char* type_name);
 
-  // Appends {ID -> type name} mappings that were added after the last call
-  // to this function. |traced_value| must be in 'array' mode.
-  void SerializeIncrementally(TracedValue* traced_value);
+  // Writes the type ID -> type name mapping to the trace log.
+  void AppendAsTraceFormat(std::string* out) const override;
 
   // Estimates memory overhead including |sizeof(TypeNameDeduplicator)|.
-  void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead);
+  void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead) override;
 
  private:
-  StringDeduplicator* string_deduplicator_;
-
-  // Map from type name to type ID. The reason this class has its own map
-  // and does not use string_deduplicator_ in Insert() is that type names
-  // are sometimes file names, and we need post-process them to extract
-  // categories.
-  using TypeMap = std::map<const char*, int>;
-  TypeMap type_ids_;
-  std::vector<const TypeMap::value_type*> new_type_ids_;
+  // Map from type name to type ID.
+  std::map<const char*, int> type_ids_;
 
   DISALLOW_COPY_AND_ASSIGN(TypeNameDeduplicator);
 };
diff --git a/base/trace_event/heap_profiler_type_name_deduplicator_unittest.cc b/base/trace_event/heap_profiler_type_name_deduplicator_unittest.cc
index cfc2aad..b2e681ab 100644
--- a/base/trace_event/heap_profiler_type_name_deduplicator_unittest.cc
+++ b/base/trace_event/heap_profiler_type_name_deduplicator_unittest.cc
@@ -2,14 +2,11 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
-
 #include <memory>
 #include <string>
 
-#include "base/memory/ptr_util.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
-#include "base/trace_event/trace_event_argument.h"
+#include "base/json/json_reader.h"
+#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
 #include "base/values.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -18,6 +15,13 @@
 
 namespace {
 
+// Define all strings once, because the deduplicator requires pointer equality,
+// and string interning is unreliable.
+const char kInt[] = "int";
+const char kBool[] = "bool";
+const char kString[] = "string";
+const char kNeedsEscape[] = "\"quotes\"";
+
 #if defined(OS_POSIX)
 const char kTaskFileName[] = "../../base/trace_event/trace_log.cc";
 const char kTaskPath[] = "base/trace_event";
@@ -26,115 +30,66 @@
 const char kTaskPath[] = "base\\memory";
 #endif
 
-// Calls TypeNameDeduplicator::SerializeIncrementally() and returns ListValue
-// with serialized entries.
-std::unique_ptr<ListValue> SerializeEntriesIncrementally(
-    TypeNameDeduplicator* dedup) {
-  TracedValue traced_value;
-  traced_value.BeginArray("");
-  dedup->SerializeIncrementally(&traced_value);
-  traced_value.EndArray();
-
-  auto base_value = traced_value.ToBaseValue();
-  DictionaryValue* dictionary;
-  std::unique_ptr<Value> entries;
-  if (!base_value->GetAsDictionary(&dictionary) ||
-      !dictionary->Remove("", &entries)) {
-    return nullptr;
-  }
-  return ListValue::From(std::move(entries));
+std::unique_ptr<Value> DumpAndReadBack(
+    const TypeNameDeduplicator& deduplicator) {
+  std::string json;
+  deduplicator.AppendAsTraceFormat(&json);
+  return JSONReader::Read(json);
 }
 
-struct TypeNameMapping {
-  const int id;
-  const char* const name;
-};
+// Inserts a single type name into a new TypeNameDeduplicator instance and
+// checks if the value gets inserted and the exported value for |type_name| is
+// the same as |expected_value|.
+void TestInsertTypeAndReadback(const char* type_name,
+                               const char* expected_value) {
+  std::unique_ptr<TypeNameDeduplicator> dedup(new TypeNameDeduplicator);
+  ASSERT_EQ(1, dedup->Insert(type_name));
 
-std::unique_ptr<ListValue> SerializeMappingsAsEntries(
-    StringDeduplicator* string_dedup,
-    std::initializer_list<TypeNameMapping> mappings) {
-  auto entries = MakeUnique<ListValue>();
-  for (const auto& mapping : mappings) {
-    auto entry = MakeUnique<DictionaryValue>();
-    entry->SetInteger("id", mapping.id);
-    entry->SetInteger("name_sid", string_dedup->Insert(mapping.name));
-    entries->Append(std::move(entry));
-  }
-  return entries;
-}
+  std::unique_ptr<Value> type_names = DumpAndReadBack(*dedup);
+  ASSERT_NE(nullptr, type_names);
 
-void ExpectIncrementalEntries(TypeNameDeduplicator* dedup,
-                              StringDeduplicator* string_dedup,
-                              std::initializer_list<TypeNameMapping> mappings) {
-  auto entries = SerializeEntriesIncrementally(dedup);
-  ASSERT_TRUE(entries);
+  const DictionaryValue* dictionary;
+  ASSERT_TRUE(type_names->GetAsDictionary(&dictionary));
 
-  auto expected_entries = SerializeMappingsAsEntries(string_dedup, mappings);
-  ASSERT_TRUE(expected_entries->Equals(entries.get()))
-      << "expected_entries = " << *expected_entries << "entries = " << *entries;
+  // When the type name was inserted, it got ID 1. The exported key "1"
+  // should be equal to |expected_value|.
+  std::string value;
+  ASSERT_TRUE(dictionary->GetString("1", &value));
+  ASSERT_EQ(expected_value, value);
 }
 
 }  // namespace
 
-TEST(TypeNameDeduplicatorTest, ImplicitId0) {
-  StringDeduplicator string_dedup;
-  TypeNameDeduplicator dedup(&string_dedup);
-
-  // NULL type name is mapped to an implicitly added ID #0.
-  ExpectIncrementalEntries(&dedup, &string_dedup, {{0, "[unknown]"}});
-  ASSERT_EQ(0, dedup.Insert(nullptr));
-
-  // Even though ID #0 is serialized as "[unknown]" string, it's distinct
-  // from "[unknown]" type name.
-  ASSERT_EQ(1, dedup.Insert("[unknown]"));
-  ExpectIncrementalEntries(&dedup, &string_dedup, {{1, "[unknown]"}});
-}
-
-TEST(TypeNameDeduplicatorTest, Deduplicate) {
+TEST(TypeNameDeduplicatorTest, Deduplication) {
   // The type IDs should be like this:
   // 0: [unknown]
   // 1: int
   // 2: bool
   // 3: string
 
-  StringDeduplicator string_dedup;
-  TypeNameDeduplicator dedup(&string_dedup);
-  ASSERT_EQ(1, dedup.Insert("int"));
-  ASSERT_EQ(2, dedup.Insert("bool"));
-  ASSERT_EQ(3, dedup.Insert("string"));
+  std::unique_ptr<TypeNameDeduplicator> dedup(new TypeNameDeduplicator);
+  ASSERT_EQ(1, dedup->Insert(kInt));
+  ASSERT_EQ(2, dedup->Insert(kBool));
+  ASSERT_EQ(3, dedup->Insert(kString));
 
   // Inserting again should return the same IDs.
-  ASSERT_EQ(2, dedup.Insert("bool"));
-  ASSERT_EQ(1, dedup.Insert("int"));
-  ASSERT_EQ(3, dedup.Insert("string"));
+  ASSERT_EQ(2, dedup->Insert(kBool));
+  ASSERT_EQ(1, dedup->Insert(kInt));
+  ASSERT_EQ(3, dedup->Insert(kString));
+
+  // A null pointer should yield type ID 0.
+  ASSERT_EQ(0, dedup->Insert(nullptr));
+}
+
+TEST(TypeNameDeduplicatorTest, EscapeTypeName) {
+  // Reading json should not fail, because the type name should have been
+  // escaped properly and exported value should contain quotes.
+  TestInsertTypeAndReadback(kNeedsEscape, kNeedsEscape);
 }
 
 TEST(TypeNameDeduplicatorTest, TestExtractFileName) {
-  StringDeduplicator string_dedup;
-  TypeNameDeduplicator dedup(&string_dedup);
-
-  ASSERT_EQ(1, dedup.Insert(kTaskFileName));
-
-  ExpectIncrementalEntries(&dedup, &string_dedup,
-                           {{0, "[unknown]"}, {1, kTaskPath}});
-}
-
-TEST(TypeNameDeduplicatorTest, SerializeIncrementally) {
-  StringDeduplicator string_dedup;
-  TypeNameDeduplicator dedup(&string_dedup);
-
-  ASSERT_EQ(1, dedup.Insert("int"));
-  ASSERT_EQ(2, dedup.Insert("bool"));
-
-  ExpectIncrementalEntries(&dedup, &string_dedup,
-                           {{0, "[unknown]"}, {1, "int"}, {2, "bool"}});
-
-  ASSERT_EQ(2, dedup.Insert("bool"));
-  ASSERT_EQ(3, dedup.Insert("string"));
-
-  ExpectIncrementalEntries(&dedup, &string_dedup, {{3, "string"}});
-
-  ExpectIncrementalEntries(&dedup, &string_dedup, {});
+  // The exported value for passed file name should be the folders in the path.
+  TestInsertTypeAndReadback(kTaskFileName, kTaskPath);
 }
 
 }  // namespace trace_event
diff --git a/base/trace_event/malloc_dump_provider.cc b/base/trace_event/malloc_dump_provider.cc
index 43c21e5..14ba0a2 100644
--- a/base/trace_event/malloc_dump_provider.cc
+++ b/base/trace_event/malloc_dump_provider.cc
@@ -11,12 +11,10 @@
 #include "base/allocator/allocator_extension.h"
 #include "base/allocator/allocator_shim.h"
 #include "base/allocator/features.h"
-#include "base/bind.h"
 #include "base/debug/profiler.h"
 #include "base/trace_event/heap_profiler_allocation_context.h"
 #include "base/trace_event/heap_profiler_allocation_context_tracker.h"
-#include "base/trace_event/heap_profiler_allocation_register.h"
-#include "base/trace_event/heap_profiler_event_writer.h"
+#include "base/trace_event/heap_profiler_heap_dump_writer.h"
 #include "base/trace_event/process_memory_dump.h"
 #include "base/trace_event/trace_event_argument.h"
 #include "build/build_config.h"
@@ -300,19 +298,11 @@
   // Enclosing all the temporary data structures in a scope, so that the heap
   // profiler does not see unbalanced malloc/free calls from these containers.
   {
+    TraceEventMemoryOverhead overhead;
+    std::unordered_map<AllocationContext, AllocationMetrics> metrics_by_context;
     if (args.level_of_detail == MemoryDumpLevelOfDetail::DETAILED) {
-      struct ShimMetrics {
-        size_t size;
-        size_t count;
-      };
-      ShimMetrics shim_metrics = {0};
-      auto visit_allocation = [](ShimMetrics* metrics,
-                                 const AllocationRegister::Allocation& alloc) {
-        metrics->size += alloc.size;
-        metrics->count += 1;
-      };
-      allocation_register_.VisitAllocations(base::BindRepeating(
-          visit_allocation, base::Unretained(&shim_metrics)));
+      ShardedAllocationRegister::OutputMetrics shim_metrics =
+          allocation_register_.UpdateAndReturnsMetrics(metrics_by_context);
 
       // Aggregate data for objects allocated through the shim.
       inner_dump->AddScalar("shim_allocated_objects_size",
@@ -322,8 +312,9 @@
                             MemoryAllocatorDump::kUnitsObjects,
                             shim_metrics.count);
     }
+    allocation_register_.EstimateTraceMemoryOverhead(&overhead);
 
-    pmd->DumpHeapUsage(allocation_register_, "malloc");
+    pmd->DumpHeapUsage(metrics_by_context, overhead, "malloc");
   }
   tid_dumping_heap_ = kInvalidThreadId;
 
diff --git a/base/trace_event/memory_dump_manager.cc b/base/trace_event/memory_dump_manager.cc
index 8b5e301..87c6ead 100644
--- a/base/trace_event/memory_dump_manager.cc
+++ b/base/trace_event/memory_dump_manager.cc
@@ -23,7 +23,9 @@
 #include "base/trace_event/heap_profiler.h"
 #include "base/trace_event/heap_profiler_allocation_context_tracker.h"
 #include "base/trace_event/heap_profiler_event_filter.h"
-#include "base/trace_event/heap_profiler_event_writer.h"
+#include "base/trace_event/heap_profiler_serialization_state.h"
+#include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
+#include "base/trace_event/heap_profiler_type_name_deduplicator.h"
 #include "base/trace_event/malloc_dump_provider.h"
 #include "base/trace_event/memory_dump_provider.h"
 #include "base/trace_event/memory_dump_scheduler.h"
@@ -59,6 +61,37 @@
   global_dump_fn.Run(args);
 }
 
+// Proxy class which wraps a ConvertableToTraceFormat owned by the
+// |heap_profiler_serialization_state| into a proxy object that can be added to
+// the trace event log. This is to solve the problem that the
+// HeapProfilerSerializationState is refcounted but the tracing subsystem wants
+// a std::unique_ptr<ConvertableToTraceFormat>.
+template <typename T>
+struct SessionStateConvertableProxy : public ConvertableToTraceFormat {
+  using GetterFunctPtr = T* (HeapProfilerSerializationState::*)() const;
+
+  SessionStateConvertableProxy(scoped_refptr<HeapProfilerSerializationState>
+                                   heap_profiler_serialization_state,
+                               GetterFunctPtr getter_function)
+      : heap_profiler_serialization_state(heap_profiler_serialization_state),
+        getter_function(getter_function) {}
+
+  void AppendAsTraceFormat(std::string* out) const override {
+    return (heap_profiler_serialization_state.get()->*getter_function)()
+        ->AppendAsTraceFormat(out);
+  }
+
+  void EstimateTraceMemoryOverhead(
+      TraceEventMemoryOverhead* overhead) override {
+    return (heap_profiler_serialization_state.get()->*getter_function)()
+        ->EstimateTraceMemoryOverhead(overhead);
+  }
+
+  scoped_refptr<HeapProfilerSerializationState>
+      heap_profiler_serialization_state;
+  GetterFunctPtr const getter_function;
+};
+
 }  // namespace
 
 // static
@@ -649,14 +682,28 @@
       ->set_heap_profiler_breakdown_threshold_bytes(
           memory_dump_config.heap_profiler_options.breakdown_threshold_bytes);
   if (heap_profiling_enabled_) {
-    heap_profiler_serialization_state->CreateDeduplicators();
-    // TODO(dskiba): support continuous mode (crbug.com/701052)
-    DLOG_IF(
-        ERROR,
-        TraceLog::GetInstance()->GetCurrentTraceConfig().GetTraceRecordMode() ==
-            RECORD_CONTINUOUSLY)
-        << "Heap profile format is incremental and doesn't yet fully support "
-        << "continuous mode.";
+    // If heap profiling is enabled, the stack frame deduplicator and type name
+    // deduplicator will be in use. Add a metadata events to write the frames
+    // and type IDs.
+    heap_profiler_serialization_state->SetStackFrameDeduplicator(
+        WrapUnique(new StackFrameDeduplicator));
+
+    heap_profiler_serialization_state->SetTypeNameDeduplicator(
+        WrapUnique(new TypeNameDeduplicator));
+
+    TRACE_EVENT_API_ADD_METADATA_EVENT(
+        TraceLog::GetCategoryGroupEnabled("__metadata"), "stackFrames",
+        "stackFrames",
+        MakeUnique<SessionStateConvertableProxy<StackFrameDeduplicator>>(
+            heap_profiler_serialization_state,
+            &HeapProfilerSerializationState::stack_frame_deduplicator));
+
+    TRACE_EVENT_API_ADD_METADATA_EVENT(
+        TraceLog::GetCategoryGroupEnabled("__metadata"), "typeNames",
+        "typeNames",
+        MakeUnique<SessionStateConvertableProxy<TypeNameDeduplicator>>(
+            heap_profiler_serialization_state,
+            &HeapProfilerSerializationState::type_name_deduplicator));
   }
 
   AutoLock lock(lock_);
diff --git a/base/trace_event/process_memory_dump.cc b/base/trace_event/process_memory_dump.cc
index 7d9b656..47cb55f3 100644
--- a/base/trace_event/process_memory_dump.cc
+++ b/base/trace_event/process_memory_dump.cc
@@ -8,15 +8,14 @@
 
 #include <vector>
 
-#include "base/bind.h"
 #include "base/memory/ptr_util.h"
 #include "base/memory/shared_memory_tracker.h"
 #include "base/process/process_metrics.h"
 #include "base/strings/stringprintf.h"
-#include "base/trace_event/heap_profiler_event_writer.h"
+#include "base/trace_event/heap_profiler_heap_dump_writer.h"
+#include "base/trace_event/heap_profiler_serialization_state.h"
 #include "base/trace_event/memory_infra_background_whitelist.h"
 #include "base/trace_event/process_memory_totals.h"
-#include "base/trace_event/sharded_allocation_register.h"
 #include "base/trace_event/trace_event_argument.h"
 #include "base/unguessable_token.h"
 #include "build/build_config.h"
@@ -254,9 +253,12 @@
 }
 
 void ProcessMemoryDump::DumpHeapUsage(
-    const ShardedAllocationRegister& allocation_register,
+    const std::unordered_map<base::trace_event::AllocationContext,
+                             base::trace_event::AllocationMetrics>&
+        metrics_by_context,
+    base::trace_event::TraceEventMemoryOverhead& overhead,
     const char* allocator_name) {
-  if (dump_args_.level_of_detail == MemoryDumpLevelOfDetail::DETAILED) {
+  if (!metrics_by_context.empty()) {
     // We shouldn't end up here unless we're doing a detailed dump with
     // heap profiling enabled and if that is the case tracing should be
     // enabled which sets up the heap profiler serialization state.
@@ -265,13 +267,11 @@
       return;
     }
     DCHECK_EQ(0ul, heap_dumps_.count(allocator_name));
-    std::unique_ptr<TracedValue> heap_dump = SerializeHeapDump(
-        allocation_register, heap_profiler_serialization_state_.get());
+    std::unique_ptr<TracedValue> heap_dump = ExportHeapDump(
+        metrics_by_context, *heap_profiler_serialization_state());
     heap_dumps_[allocator_name] = std::move(heap_dump);
   }
 
-  TraceEventMemoryOverhead overhead;
-  allocation_register.EstimateTraceMemoryOverhead(&overhead);
   std::string base_name = base::StringPrintf("tracing/heap_profiler_%s",
                                              allocator_name);
   overhead.DumpInto(base_name.c_str(), this);
@@ -335,9 +335,10 @@
   }
 
   if (heap_dumps_.size() > 0) {
-    auto profile_data = SerializeHeapProfileEventData(
-        heap_dumps_, heap_profiler_serialization_state_.get());
-    value->SetValue("heaps_v2", *profile_data);
+    value->BeginDictionary("heaps");
+    for (const auto& name_and_dump : heap_dumps_)
+      value->SetValueWithCopiedName(name_and_dump.first, *name_and_dump.second);
+    value->EndDictionary();  // "heaps"
   }
 
   value->BeginArray("allocators_graph");
diff --git a/base/trace_event/process_memory_dump.h b/base/trace_event/process_memory_dump.h
index a6b7d6d9..c13c0ac 100644
--- a/base/trace_event/process_memory_dump.h
+++ b/base/trace_event/process_memory_dump.h
@@ -12,7 +12,6 @@
 #include <vector>
 
 #include "base/base_export.h"
-#include "base/gtest_prod_util.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "base/trace_event/heap_profiler_serialization_state.h"
@@ -35,7 +34,6 @@
 
 namespace trace_event {
 
-class ShardedAllocationRegister;
 class HeapProfilerSerializationState;
 class TracedValue;
 
@@ -129,8 +127,12 @@
   const AllocatorDumpsMap& allocator_dumps() const { return allocator_dumps_; }
 
   // Dumps heap usage with |allocator_name|.
-  void DumpHeapUsage(const ShardedAllocationRegister& allocation_register,
-                     const char* allocator_name);
+  void DumpHeapUsage(
+      const std::unordered_map<base::trace_event::AllocationContext,
+                               base::trace_event::AllocationMetrics>&
+          metrics_by_context,
+      base::trace_event::TraceEventMemoryOverhead& overhead,
+      const char* allocator_name);
 
   // Adds an ownership relationship between two MemoryAllocatorDump(s) with the
   // semantics: |source| owns |target|, and has the effect of attributing
diff --git a/base/trace_event/process_memory_dump_unittest.cc b/base/trace_event/process_memory_dump_unittest.cc
index d1b81506..44c41eb0 100644
--- a/base/trace_event/process_memory_dump_unittest.cc
+++ b/base/trace_event/process_memory_dump_unittest.cc
@@ -12,7 +12,6 @@
 #include "base/process/process_metrics.h"
 #include "base/trace_event/memory_allocator_dump_guid.h"
 #include "base/trace_event/memory_infra_background_whitelist.h"
-#include "base/trace_event/sharded_allocation_register.h"
 #include "base/trace_event/trace_event_argument.h"
 #include "base/unguessable_token.h"
 #include "build/build_config.h"
@@ -122,29 +121,31 @@
 
 TEST(ProcessMemoryDumpTest, TakeAllDumpsFrom) {
   std::unique_ptr<TracedValue> traced_value(new TracedValue);
-  ShardedAllocationRegister allocation_register;
-  allocation_register.SetEnabled();
-  allocation_register.Insert("", 100, AllocationContext());
+  std::unordered_map<AllocationContext, AllocationMetrics> metrics_by_context;
+  metrics_by_context[AllocationContext()] = {1, 1};
+  TraceEventMemoryOverhead overhead;
 
   scoped_refptr<HeapProfilerSerializationState>
       heap_profiler_serialization_state = new HeapProfilerSerializationState;
-  heap_profiler_serialization_state->CreateDeduplicators();
+  heap_profiler_serialization_state->SetStackFrameDeduplicator(
+      WrapUnique(new StackFrameDeduplicator));
+  heap_profiler_serialization_state->SetTypeNameDeduplicator(
+      WrapUnique(new TypeNameDeduplicator));
   std::unique_ptr<ProcessMemoryDump> pmd1(new ProcessMemoryDump(
       heap_profiler_serialization_state.get(), kDetailedDumpArgs));
-
   auto* mad1_1 = pmd1->CreateAllocatorDump("pmd1/mad1");
   auto* mad1_2 = pmd1->CreateAllocatorDump("pmd1/mad2");
   pmd1->AddOwnershipEdge(mad1_1->guid(), mad1_2->guid());
-  pmd1->DumpHeapUsage(allocation_register, "pmd1/heap_dump1");
-  pmd1->DumpHeapUsage(allocation_register, "pmd1/heap_dump2");
+  pmd1->DumpHeapUsage(metrics_by_context, overhead, "pmd1/heap_dump1");
+  pmd1->DumpHeapUsage(metrics_by_context, overhead, "pmd1/heap_dump2");
 
   std::unique_ptr<ProcessMemoryDump> pmd2(new ProcessMemoryDump(
       heap_profiler_serialization_state.get(), kDetailedDumpArgs));
   auto* mad2_1 = pmd2->CreateAllocatorDump("pmd2/mad1");
   auto* mad2_2 = pmd2->CreateAllocatorDump("pmd2/mad2");
   pmd2->AddOwnershipEdge(mad2_1->guid(), mad2_2->guid());
-  pmd2->DumpHeapUsage(allocation_register, "pmd2/heap_dump1");
-  pmd2->DumpHeapUsage(allocation_register, "pmd2/heap_dump2");
+  pmd2->DumpHeapUsage(metrics_by_context, overhead, "pmd2/heap_dump1");
+  pmd2->DumpHeapUsage(metrics_by_context, overhead, "pmd2/heap_dump2");
 
   MemoryAllocatorDumpGuid shared_mad_guid1(1);
   MemoryAllocatorDumpGuid shared_mad_guid2(2);
@@ -172,9 +173,7 @@
   pmd2.reset();
 
   // Now check that |pmd1| has been effectively merged.
-  // Note that DumpHeapUsage() adds an implicit dump for AllocationRegister's
-  // memory overhead.
-  ASSERT_EQ(10u, pmd1->allocator_dumps().size());
+  ASSERT_EQ(6u, pmd1->allocator_dumps().size());
   ASSERT_EQ(1u, pmd1->allocator_dumps().count("pmd1/mad1"));
   ASSERT_EQ(1u, pmd1->allocator_dumps().count("pmd1/mad2"));
   ASSERT_EQ(1u, pmd1->allocator_dumps().count("pmd2/mad1"));
diff --git a/base/trace_event/sharded_allocation_register.cc b/base/trace_event/sharded_allocation_register.cc
index 112f48f..f1e2d3c 100644
--- a/base/trace_event/sharded_allocation_register.cc
+++ b/base/trace_event/sharded_allocation_register.cc
@@ -69,15 +69,24 @@
                 allocated, resident);
 }
 
-void ShardedAllocationRegister::VisitAllocations(
-    const AllocationVisitor& visitor) const {
+ShardedAllocationRegister::OutputMetrics
+ShardedAllocationRegister::UpdateAndReturnsMetrics(MetricsMap& map) const {
+  OutputMetrics output_metrics;
+  output_metrics.size = 0;
+  output_metrics.count = 0;
   for (size_t i = 0; i < ShardCount; ++i) {
     RegisterAndLock& ral = allocation_registers_[i];
     AutoLock lock(ral.lock);
-    for (const auto& alloc : ral.allocation_register) {
-      visitor.Run(alloc);
+    for (const auto& alloc_size : ral.allocation_register) {
+      AllocationMetrics& metrics = map[alloc_size.context];
+      metrics.size += alloc_size.size;
+      metrics.count++;
+
+      output_metrics.size += alloc_size.size;
+      output_metrics.count++;
     }
   }
+  return output_metrics;
 }
 
 ShardedAllocationRegister::RegisterAndLock::RegisterAndLock() = default;
diff --git a/base/trace_event/sharded_allocation_register.h b/base/trace_event/sharded_allocation_register.h
index 9c3337b..bba1eea 100644
--- a/base/trace_event/sharded_allocation_register.h
+++ b/base/trace_event/sharded_allocation_register.h
@@ -11,7 +11,6 @@
 
 #include "base/atomicops.h"
 #include "base/base_export.h"
-#include "base/callback.h"
 #include "base/macros.h"
 #include "base/synchronization/lock.h"
 #include "base/trace_event/heap_profiler_allocation_register.h"
@@ -26,6 +25,15 @@
 // This container is thread-safe.
 class BASE_EXPORT ShardedAllocationRegister {
  public:
+  using MetricsMap = std::unordered_map<AllocationContext, AllocationMetrics>;
+
+  struct OutputMetrics {
+    // Total size of allocated objects.
+    size_t size;
+    // Total count of allocated objects.
+    size_t count;
+  };
+
   ShardedAllocationRegister();
 
   // This class must be enabled before calling Insert() or Remove(). Once the
@@ -53,10 +61,9 @@
   // Estimates memory overhead including |sizeof(AllocationRegister)|.
   void EstimateTraceMemoryOverhead(TraceEventMemoryOverhead* overhead) const;
 
-  using AllocationVisitor =
-      base::RepeatingCallback<void(const AllocationRegister::Allocation&)>;
-
-  void VisitAllocations(const AllocationVisitor& visitor) const;
+  // Updates |map| with all allocated objects and their statistics.
+  // Returns aggregate statistics.
+  OutputMetrics UpdateAndReturnsMetrics(MetricsMap& map) const;
 
  private:
   struct RegisterAndLock {
diff --git a/base/trace_event/trace_event_memory_overhead.cc b/base/trace_event/trace_event_memory_overhead.cc
index 1db93a7..02a98bd 100644
--- a/base/trace_event/trace_event_memory_overhead.cc
+++ b/base/trace_event/trace_event_memory_overhead.cc
@@ -41,8 +41,6 @@
       return "TypeNameDeduplicator";
     case TraceEventMemoryOverhead::kHeapProfilerStackFrameDeduplicator:
       return "StackFrameDeduplicator";
-    case TraceEventMemoryOverhead::kHeapProfilerStringDeduplicator:
-      return "StringDeduplicator";
     case TraceEventMemoryOverhead::kStdString:
       return "std::string";
     case TraceEventMemoryOverhead::kBaseValue:
diff --git a/base/trace_event/trace_event_memory_overhead.h b/base/trace_event/trace_event_memory_overhead.h
index c9bea02..1587a30 100644
--- a/base/trace_event/trace_event_memory_overhead.h
+++ b/base/trace_event/trace_event_memory_overhead.h
@@ -36,7 +36,6 @@
     kHeapProfilerAllocationRegister,
     kHeapProfilerTypeNameDeduplicator,
     kHeapProfilerStackFrameDeduplicator,
-    kHeapProfilerStringDeduplicator,
     kStdString,
     kBaseValue,
     kTraceEventMemoryOverhead,
diff --git a/build/android/resource_sizes.py b/build/android/resource_sizes.py
index 76f03fb..099f71d 100755
--- a/build/android/resource_sizes.py
+++ b/build/android/resource_sizes.py
@@ -562,12 +562,17 @@
   # Calculate aggregate stats about resources across pak files.
   resource_count_map = collections.defaultdict(int)
   resource_size_map = collections.defaultdict(int)
+  seen_data_ids = set()
+  alias_overhead_bytes = 4
   resource_overhead_bytes = 6
   for pak in paks:
-    for r in pak.resources:
-      resource_count_map[r] += 1
-      resource_size_map[r] += len(pak.resources[r]) + resource_overhead_bytes
-
+    for k, v in pak.resources.iteritems():
+      resource_count_map[k] += 1
+      if id(v) not in seen_data_ids:
+        seen_data_ids.add(id(v))
+        resource_size_map[k] += resource_overhead_bytes + len(v)
+      else:
+        resource_size_map[k] += alias_overhead_bytes
   # Output the overall resource summary.
   total_resource_size = sum(resource_size_map.values())
   total_resource_count = len(resource_count_map)
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
index 540a1a4..56989bb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeActivity.java
@@ -254,8 +254,8 @@
     private ChromeFullscreenManager mFullscreenManager;
     private boolean mCreatedFullscreenManager;
 
-    private final PictureInPictureController mPictureInPictureController =
-            new PictureInPictureController();
+    // The PictureInPictureController is initialized lazily https://crbug.com/729738.
+    private PictureInPictureController mPictureInPictureController;
 
     private CompositorViewHolder mCompositorViewHolder;
     private InsetObserverView mInsetObserverView;
@@ -876,7 +876,9 @@
         FeatureUtilities.setIsInMultiWindowMode(
                 MultiWindowUtils.getInstance().isInMultiWindowMode(this));
 
-        mPictureInPictureController.cleanup(this);
+        if (mPictureInPictureController != null) {
+            mPictureInPictureController.cleanup(this);
+        }
         VrShellDelegate.maybeRegisterVrEntryHook(this);
     }
 
@@ -884,6 +886,9 @@
     protected void onUserLeaveHint() {
         super.onUserLeaveHint();
 
+        if (mPictureInPictureController == null) {
+            mPictureInPictureController = new PictureInPictureController();
+        }
         mPictureInPictureController.attemptPictureInPicture(this);
     }
 
@@ -926,7 +931,9 @@
 
     @Override
     public void onNewIntentWithNative(Intent intent) {
-        mPictureInPictureController.cleanup(this);
+        if (mPictureInPictureController != null) {
+            mPictureInPictureController.cleanup(this);
+        }
 
         super.onNewIntentWithNative(intent);
         if (mIntentHandler.shouldIgnoreIntent(intent)) return;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
index a760186..4ef154ee 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
@@ -1094,9 +1094,6 @@
                     Profile profile = Profile.getLastUsedProfile();
                     new LoadingPredictor(profile).cancelPageLoadHint(mSpeculation.url);
                     break;
-                case SpeculationParams.HIDDEN_TAB:
-                    mSpeculation.tab.destroy();
-                    break;
                 default:
                     return;
             }
@@ -1117,10 +1114,6 @@
                 && !ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_BACKGROUND_TAB)) {
             speculationMode = SpeculationParams.PRERENDER;
         }
-
-        // At most one on-going speculation, clears the previous one.
-        cancelSpeculation(null);
-
         switch (speculationMode) {
             case SpeculationParams.PREFETCH:
                 boolean didPrefetch = new LoadingPredictor(profile).prepareForPageLoad(url);
@@ -1197,25 +1190,30 @@
      * Creates a hidden tab and initiates a navigation.
      */
     private void launchUrlInHiddenTab(
-            final CustomTabsSessionToken session, String url, Bundle extras) {
-        ThreadUtils.assertOnUiThread();
-        Intent extrasIntent = new Intent();
-        if (extras != null) extrasIntent.putExtras(extras);
-        if (IntentHandler.getExtraHeadersFromIntent(extrasIntent) != null) return;
+            final CustomTabsSessionToken session, final String url, final Bundle extras) {
+        ThreadUtils.postOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                Intent extrasIntent = new Intent();
+                if (extras != null) extrasIntent.putExtras(extras);
+                if (IntentHandler.getExtraHeadersFromIntent(extrasIntent) != null) return;
 
-        Tab tab = Tab.createDetached(new CustomTabDelegateFactory(false, false, null));
+                Tab tab = Tab.createDetached(new CustomTabDelegateFactory(false, false, null));
 
-        // Updating post message as soon as we have a valid WebContents.
-        mClientManager.resetPostMessageHandlerForSession(
-                session, tab.getContentViewCore().getWebContents());
+                // Updating post message as soon as we have a valid WebContents.
+                mClientManager.resetPostMessageHandlerForSession(
+                        session, tab.getContentViewCore().getWebContents());
 
-        LoadUrlParams loadParams = new LoadUrlParams(url);
-        String referrer = getReferrer(session, extrasIntent);
-        if (referrer != null && !referrer.isEmpty()) {
-            loadParams.setReferrer(new Referrer(referrer, Referrer.REFERRER_POLICY_DEFAULT));
-        }
-        mSpeculation = SpeculationParams.forHiddenTab(session, url, tab, referrer, extras);
-        mSpeculation.tab.loadUrl(loadParams);
+                LoadUrlParams loadParams = new LoadUrlParams(url);
+                String referrer = getReferrer(session, extrasIntent);
+                if (referrer != null && !referrer.isEmpty()) {
+                    loadParams.setReferrer(
+                            new Referrer(referrer, Referrer.REFERRER_POLICY_DEFAULT));
+                }
+                mSpeculation = SpeculationParams.forHiddenTab(session, url, tab, referrer, extras);
+                mSpeculation.tab.loadUrl(loadParams);
+            }
+        });
     }
 
     @VisibleForTesting
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java
index 681dc1f..68b57bc3 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridge.java
@@ -393,8 +393,8 @@
      *   2. NotificationConstants.EXTRA_NOTIFICATION_TAG - set by us on browser UI that should
      *     launch specific site settings, e.g. the web notifications Site Settings button.
      *
-     * See {@link SiteChannelsManager#toChannelId} and {@link SiteChannelsManager#toSiteOrigin} for
-     * how we convert origins to and from channel ids.
+     * See {@link SiteChannelsManager#createChannelId} and {@link SiteChannelsManager#toSiteOrigin}
+     * for how we convert origins to and from channel ids.
      *
      * See {@link #makePlatformTag} for details about the format of the EXTRA_NOTIFICATION tag.
      *
@@ -623,10 +623,12 @@
         // Don't set a channelId for web apk notifications because the channel won't be
         // initialized for the web apk and it will crash on notify - see crbug.com/727178.
         // (It's okay to not set a channel on them because web apks don't target O yet.)
+        // TODO(crbug.com/700377): Channel ID should be retrieved from cache in native and passed
+        // through to here with other notification parameters.
         String channelId = forWebApk
                 ? null
                 : ChromeFeatureList.isEnabled(ChromeFeatureList.SITE_NOTIFICATION_CHANNELS)
-                        ? SiteChannelsManager.toChannelId(origin)
+                        ? SiteChannelsManager.getInstance().getChannelIdForOrigin(origin)
                         : ChannelDefinitions.CHANNEL_ID_SITES;
         if (useCustomLayouts(hasImage)) {
             return new CustomNotificationBuilder(context, channelId);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationSettingsBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationSettingsBridge.java
index 13b679c..ee3ed56d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationSettingsBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationSettingsBridge.java
@@ -4,8 +4,12 @@
 
 package org.chromium.chrome.browser.notifications;
 
+import android.app.NotificationManager;
+
 import org.chromium.base.BuildInfo;
 import org.chromium.base.annotations.CalledByNative;
+import org.chromium.chrome.browser.notifications.channels.Channel;
+import org.chromium.chrome.browser.notifications.channels.ChannelDefinitions;
 import org.chromium.chrome.browser.notifications.channels.SiteChannelsManager;
 
 /**
@@ -20,19 +24,23 @@
     }
 
     /**
-     * Creates a notification channel for the given origin.
+     * Creates a notification channel for the given origin, unless a channel for this origin
+     * already exists.
+     *
      * @param origin The site origin to be used as the channel name.
+     * @param creationTime A string representing the time of channel creation.
      * @param enabled True if the channel should be initially enabled, false if
      *                it should start off as blocked.
+     * @return The channel created for this origin.
      */
     @CalledByNative
-    static void createChannel(String origin, boolean enabled) {
-        SiteChannelsManager.getInstance().createSiteChannel(origin, enabled);
+    static SiteChannel createChannel(String origin, long creationTime, boolean enabled) {
+        return SiteChannelsManager.getInstance().createSiteChannel(origin, creationTime, enabled);
     }
 
     @CalledByNative
-    static @NotificationChannelStatus int getChannelStatus(String origin) {
-        return SiteChannelsManager.getInstance().getChannelStatus(origin);
+    static @NotificationChannelStatus int getChannelStatus(String channelId) {
+        return SiteChannelsManager.getInstance().getChannelStatus(channelId);
     }
 
     @CalledByNative
@@ -41,23 +49,33 @@
     }
 
     @CalledByNative
-    static void deleteChannel(String origin) {
-        SiteChannelsManager.getInstance().deleteSiteChannel(origin);
+    static void deleteChannel(String channelId) {
+        SiteChannelsManager.getInstance().deleteSiteChannel(channelId);
     }
 
     /**
      * Helper type for passing site channel objects across the JNI.
      */
     public static class SiteChannel {
+        private final String mId;
         private final String mOrigin;
+        private final long mTimestamp;
         private final @NotificationChannelStatus int mStatus;
 
-        public SiteChannel(String origin, @NotificationChannelStatus int status) {
+        public SiteChannel(String channelId, String origin, long creationTimestamp,
+                @NotificationChannelStatus int status) {
+            mId = channelId;
             mOrigin = origin;
+            mTimestamp = creationTimestamp;
             mStatus = status;
         }
 
         @CalledByNative("SiteChannel")
+        public long getTimestamp() {
+            return mTimestamp;
+        }
+
+        @CalledByNative("SiteChannel")
         public String getOrigin() {
             return mOrigin;
         }
@@ -66,5 +84,18 @@
         public @NotificationChannelStatus int getStatus() {
             return mStatus;
         }
+
+        @CalledByNative("SiteChannel")
+        public String getId() {
+            return mId;
+        }
+
+        public Channel toChannel() {
+            return new Channel(mId, mOrigin,
+                    mStatus == NotificationChannelStatus.BLOCKED
+                            ? NotificationManager.IMPORTANCE_NONE
+                            : NotificationManager.IMPORTANCE_DEFAULT,
+                    ChannelDefinitions.CHANNEL_GROUP_ID_SITES);
+        }
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelDefinitions.java b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelDefinitions.java
index d5d905e..5acd442c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelDefinitions.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelDefinitions.java
@@ -33,8 +33,8 @@
     // TODO(crbug.com/700377): Deprecate the 'sites' channel.
     public static final String CHANNEL_ID_SITES = "sites";
     public static final String CHANNEL_ID_PREFIX_SITES = "web:";
+    public static final String CHANNEL_GROUP_ID_SITES = "sites";
     static final String CHANNEL_GROUP_ID_GENERAL = "general";
-    static final String CHANNEL_GROUP_ID_SITES = "sites";
     /**
      * Version number identifying the current set of channels. This must be incremented whenever
      * the set of channels returned by {@link #getStartupChannelIds()} or
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializer.java b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializer.java
index 1bba79d..8fe691e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializer.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializer.java
@@ -52,10 +52,10 @@
      */
     public void ensureInitialized(String channelId) {
         if (channelId.startsWith(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES)) {
-            // TODO(crbug.com/700377): Initialize site channels via native, not directly as below,
-            // in order to keep track of when they were created.
-            SiteChannelsManager.getInstance().createSiteChannel(
-                    SiteChannelsManager.toSiteOrigin(channelId), /*enabled=*/true);
+            // If we have a valid site channel ID at this point, it is safe to assume a channel
+            // has already been created, since the only way to obtain a site channel ID is by
+            // creating a channel.
+            assert SiteChannelsManager.isValidSiteChannelId(channelId);
             return;
         }
         ChannelDefinitions.PredefinedChannel predefinedChannel =
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManager.java b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManager.java
index 51f41a19..872c2f1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManager.java
@@ -6,6 +6,7 @@
 
 import android.app.NotificationManager;
 import android.content.Context;
+import android.support.annotation.Nullable;
 
 import org.chromium.base.ContextUtils;
 import org.chromium.base.VisibleForTesting;
@@ -22,6 +23,8 @@
  * Creates/deletes and queries our notification channels for websites.
  */
 public class SiteChannelsManager {
+    private static final String CHANNEL_ID_SEPARATOR = ";";
+
     private final NotificationManagerProxy mNotificationManager;
 
     public static SiteChannelsManager getInstance() {
@@ -41,34 +44,52 @@
     }
 
     /**
-     * Creates a channel for the given origin. The channel will appear within the Sites channel
+     * Creates a channel for the given origin, unless a channel for this origin
+     * already exists. The channel will appear within the Sites channel
      * group, with default importance, or no importance if created as blocked.
      * @param origin The site origin, to be used as the channel's user-visible name.
+     * @param creationTime A string representing the time of channel creation.
      * @param enabled Determines whether the channel will be created as enabled or blocked.
+     * @return The channel created for the given origin.
      */
-    public void createSiteChannel(String origin, boolean enabled) {
+    public SiteChannel createSiteChannel(String origin, long creationTime, boolean enabled) {
+        SiteChannel preexistingChannel = getSiteChannelForOrigin(origin);
+        if (preexistingChannel != null) {
+            return preexistingChannel;
+        }
         // Channel group must be created before the channel.
         mNotificationManager.createNotificationChannelGroup(
                 ChannelDefinitions.getChannelGroup(ChannelDefinitions.CHANNEL_GROUP_ID_SITES));
-        mNotificationManager.createNotificationChannel(new Channel(toChannelId(origin), origin,
-                enabled ? NotificationManager.IMPORTANCE_DEFAULT
-                        : NotificationManager.IMPORTANCE_NONE,
-                ChannelDefinitions.CHANNEL_GROUP_ID_SITES));
+        SiteChannel siteChannel = new SiteChannel(createChannelId(origin, creationTime), origin,
+                creationTime,
+                enabled ? NotificationChannelStatus.ENABLED : NotificationChannelStatus.BLOCKED);
+        mNotificationManager.createNotificationChannel(siteChannel.toChannel());
+        return siteChannel;
+    }
+
+    private @Nullable SiteChannel getSiteChannelForOrigin(String origin) {
+        String normalizedOrigin = WebsiteAddress.create(origin).getOrigin();
+        for (SiteChannel channel : getSiteChannels()) {
+            if (channel.getOrigin().equals(normalizedOrigin)) {
+                return channel;
+            }
+        }
+        return null;
     }
 
     /**
-     * Deletes the channel associated with this origin.
+     * Deletes the channel associated with this channel ID.
      */
-    public void deleteSiteChannel(String origin) {
-        mNotificationManager.deleteNotificationChannel(toChannelId(origin));
+    public void deleteSiteChannel(String channelId) {
+        mNotificationManager.deleteNotificationChannel(channelId);
     }
 
     /**
-     * Gets the status of the channel associated with this origin.
+     * Gets the status of the channel associated with this channelId.
      * @return ALLOW, BLOCKED, or UNAVAILABLE (if the channel was never created or was deleted).
      */
-    public @NotificationChannelStatus int getChannelStatus(String origin) {
-        Channel channel = mNotificationManager.getNotificationChannel(toChannelId(origin));
+    public @NotificationChannelStatus int getChannelStatus(String channelId) {
+        Channel channel = mNotificationManager.getNotificationChannel(channelId);
         if (channel == null) return NotificationChannelStatus.UNAVAILABLE;
         return toChannelStatus(channel.getImportance());
     }
@@ -81,21 +102,34 @@
         List<Channel> channels = mNotificationManager.getNotificationChannels();
         List<SiteChannel> siteChannels = new ArrayList<>();
         for (Channel channel : channels) {
-            if (channel.getGroupId() != null
-                    && channel.getGroupId().equals(ChannelDefinitions.CHANNEL_GROUP_ID_SITES)) {
-                siteChannels.add(new SiteChannel(
-                        toSiteOrigin(channel.getId()), toChannelStatus(channel.getImportance())));
+            if (isValidSiteChannelId(channel.getId())) {
+                siteChannels.add(toSiteChannel(channel));
             }
         }
         return siteChannels.toArray(new SiteChannel[siteChannels.size()]);
     }
 
+    private static SiteChannel toSiteChannel(Channel channel) {
+        String originAndTimestamp =
+                channel.getId().substring(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES.length());
+        String[] parts = originAndTimestamp.split(CHANNEL_ID_SEPARATOR);
+        assert parts.length == 2;
+        return new SiteChannel(channel.getId(), parts[0], Long.parseLong(parts[1]),
+                toChannelStatus(channel.getImportance()));
+    }
+
+    static boolean isValidSiteChannelId(String channelId) {
+        return channelId.startsWith(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES)
+                && channelId.substring(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES.length())
+                           .contains(CHANNEL_ID_SEPARATOR);
+    }
+
     /**
-     * Converts a site's origin to a canonical channel id for that origin.
+     * Converts a site's origin and creation timestamp to a canonical channel id.
      */
-    public static String toChannelId(String origin) {
+    public static String createChannelId(String origin, long creationTime) {
         return ChannelDefinitions.CHANNEL_ID_PREFIX_SITES
-                + WebsiteAddress.create(origin).getOrigin();
+                + WebsiteAddress.create(origin).getOrigin() + CHANNEL_ID_SEPARATOR + creationTime;
     }
 
     /**
@@ -105,7 +139,8 @@
      */
     public static String toSiteOrigin(String channelId) {
         assert channelId.startsWith(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES);
-        return channelId.substring(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES.length());
+        return channelId.substring(ChannelDefinitions.CHANNEL_ID_PREFIX_SITES.length())
+                .split(CHANNEL_ID_SEPARATOR)[0];
     }
 
     /**
@@ -119,4 +154,10 @@
                 return NotificationChannelStatus.ENABLED;
         }
     }
+
+    public String getChannelIdForOrigin(String origin) {
+        SiteChannel channel = getSiteChannelForOrigin(origin);
+        // Fall back to generic Sites channel if a channel for this origin doesn't exist.
+        return channel == null ? ChannelDefinitions.CHANNEL_ID_SITES : channel.getId();
+    }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
index cc762a0..319d23d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageLayout.java
@@ -4,6 +4,7 @@
 
 package org.chromium.chrome.browser.ntp;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.Resources;
 import android.util.AttributeSet;
@@ -87,9 +88,9 @@
         mBottomSpacer = findViewById(R.id.ntp_bottom_spacer);
         mLogoSpacer = findViewById(R.id.search_provider_logo_spacer);
         mSearchBoxSpacer = findViewById(R.id.search_box_spacer);
-        mSearchProviderLogoView = (LogoView) findViewById(R.id.search_provider_logo);
+        mSearchProviderLogoView = findViewById(R.id.search_provider_logo);
         mSearchBoxView = findViewById(R.id.search_box);
-        mTileGridLayout = (TileGridLayout) findViewById(R.id.tile_grid_layout);
+        mTileGridLayout = findViewById(R.id.tile_grid_layout);
     }
 
     /**
@@ -117,6 +118,8 @@
      * - If our contents can fit on the screen, increase the spacing to fill the space (minus space
      *   for the CardsUI Peeking card).
      */
+    @SuppressLint("WrongCall") // We explicitly call super.onMeasure() as we have multiple measuring
+                               // passes and adjust the UI depending on the result of the previous.
     private void calculateVerticalSpacing(int widthMeasureSpec, int heightMeasureSpec) {
         mLogoSpacer.setVisibility(View.GONE);
         mSearchBoxSpacer.setVisibility(View.GONE);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageUma.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageUma.java
index 619a686..d40bba88 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageUma.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageUma.java
@@ -87,7 +87,8 @@
      * Possible results when sizing the NewTabPageLayout.
      * Do not remove or change existing values other than NUM_NTP_LAYOUT_RESULTS.
      */
-    @IntDef({NTP_LAYOUT_DOES_NOT_FIT, NTP_LAYOUT_FITS_WITHOUT_FIELD_TRIAL,
+    @IntDef({NTP_LAYOUT_DOES_NOT_FIT, NTP_LAYOUT_DOES_NOT_FIT_PUSH_MOST_LIKELY,
+            NTP_LAYOUT_FITS_NO_FIELD_TRIAL, NTP_LAYOUT_FITS_WITHOUT_FIELD_TRIAL,
             NTP_LAYOUT_FITS_WITH_FIELD_TRIAL, NTP_LAYOUT_CONDENSED, NUM_NTP_LAYOUT_RESULTS})
     @Retention(RetentionPolicy.SOURCE)
     public @interface NTPLayoutResult {}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
index f0ec4e32..16cd21d 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
@@ -289,7 +289,7 @@
         setSearchProviderHasLogo(searchProviderHasLogo);
         mSearchProviderLogoView.showSearchProviderInitialView();
 
-        mTileGroup.startObserving(getMaxTileRows(searchProviderHasLogo) * getMaxTileColumns());
+        mTileGroup.startObserving(getMaxTileRows(searchProviderHasLogo), getMaxTileColumns());
 
         mRecyclerView.init(mUiConfig, mContextMenuManager);
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java
index ce709a3..c2f25be 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/OfflinePageBridge.java
@@ -85,14 +85,14 @@
         /**
          * Called when the native side of offline pages is changed due to adding, removing or
          * update an offline page.
-         * @param addedPage The in-memory representation of the page added to the offline database.
          */
         public void offlinePageAdded(OfflinePageItem addedPage) {}
 
         /**
          * Called when an offline page is deleted. This can be called as a result of
          * #checkOfflinePageMetadata().
-         * @param deletedPage Information regarding the deleted offline page.
+         * @param offlineId The offline ID of the deleted offline page.
+         * @param clientId The client supplied ID of the deleted offline page.
          */
         public void offlinePageDeleted(DeletedPageInfo deletedPage) {}
     }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/SingleWebsitePreferences.java b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/SingleWebsitePreferences.java
index aebc10d..253cbe5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/SingleWebsitePreferences.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/preferences/website/SingleWebsitePreferences.java
@@ -385,14 +385,13 @@
                 public boolean onPreferenceClick(Preference preference) {
                     // There is no guarantee that a channel has been initialized yet for sites
                     // that were granted permission before the channel-initialization-on-grant
-                    // code was in place. So initialize it here before launching the settings.
-                    // TODO(awdf): Once upgrade code is in place, quickly check this ran instead.
-                    // (Right now this is laggy!)
-                    SiteChannelsManager.getInstance().createSiteChannel(
-                            mSite.getAddress().getOrigin(), value == ContentSetting.ALLOW);
-
-                    launchOsChannelSettings(preference.getContext(),
-                            SiteChannelsManager.toChannelId(mSite.getAddress().getOrigin()));
+                    // code was in place. However, getChannelIdForOrigin will fall back to the
+                    // generic Sites channel if no specific channel has been created for the given
+                    // origin, so it is safe to open the channel settings for whatever channel ID
+                    // it returns.
+                    String channelId = SiteChannelsManager.getInstance().getChannelIdForOrigin(
+                            mSite.getAddress().getOrigin());
+                    launchOsChannelSettings(preference.getContext(), channelId);
                     return true;
                 }
             });
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetContent.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetContent.java
index d47a6b8..85ee184 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetContent.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetContent.java
@@ -93,25 +93,22 @@
 
         mBottomSheetObserver = new SuggestionsSheetVisibilityChangeObserver(this, activity) {
             @Override
-            public void onSheetOpened() {
-                mRecyclerView.setAdapter(adapter);
+            public void onContentShown(boolean isFirstShown) {
+                if (isFirstShown) {
+                    mRecyclerView.setAdapter(adapter);
 
-                mRecyclerView.scrollToPosition(0);
-                adapter.refreshSuggestions();
-                mSuggestionsUiDelegate.getEventReporter().onSurfaceOpened();
-                mRecyclerView.getScrollEventReporter().reset();
+                    mRecyclerView.scrollToPosition(0);
+                    adapter.refreshSuggestions();
+                    mSuggestionsUiDelegate.getEventReporter().onSurfaceOpened();
+                    mRecyclerView.getScrollEventReporter().reset();
 
-                if (ChromeFeatureList.isEnabled(
-                            ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_CAROUSEL)
-                        && sheet.getActiveTab() != null) {
-                    updateContextualSuggestions(sheet.getActiveTab().getUrl());
+                    if (ChromeFeatureList.isEnabled(
+                                ChromeFeatureList.CONTEXTUAL_SUGGESTIONS_CAROUSEL)
+                            && sheet.getActiveTab() != null) {
+                        updateContextualSuggestions(sheet.getActiveTab().getUrl());
+                    }
                 }
 
-                super.onSheetOpened();
-            }
-
-            @Override
-            public void onContentShown() {
                 SuggestionsMetrics.recordSurfaceVisible();
             }
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsSheetVisibilityChangeObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsSheetVisibilityChangeObserver.java
index d8b23a0..6ab800a 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsSheetVisibilityChangeObserver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsSheetVisibilityChangeObserver.java
@@ -23,6 +23,7 @@
     @BottomSheet.SheetState
     private int mCurrentContentState;
     private boolean mCurrentVisibility;
+    private boolean mWasShownSinceLastOpen;
 
     private final ChromeActivity mActivity;
     private final BottomSheet.BottomSheetContent mContentObserved;
@@ -55,8 +56,11 @@
         mBottomSheet.removeObserver(this);
     }
 
-    /** Called when the observed sheet content becomes visible. */
-    public abstract void onContentShown();
+    /** Called when the observed sheet content becomes visible.
+     * @param isFirstShown Only {@code true} the first time suggestions are shown each time the
+     *                     sheet is opened.
+     */
+    public abstract void onContentShown(boolean isFirstShown);
 
     /** Called when the observed sheet content becomes invisible. */
     public abstract void onContentHidden();
@@ -72,6 +76,7 @@
     @Override
     @CallSuper
     public void onSheetOpened() {
+        mWasShownSinceLastOpen = false;
         onStateChange();
     }
 
@@ -109,7 +114,7 @@
     /**
      * Compares the current state of the bottom sheet and activity with the ones recorded at the
      * previous call and generates events based on the difference.
-     * @see #onContentShown()
+     * @see #onContentShown(boolean)
      * @see #onContentHidden()
      * @see #onContentStateChanged(int)
      */
@@ -132,7 +137,8 @@
 
         if (newVisibility != mCurrentVisibility) {
             if (newVisibility) {
-                onContentShown();
+                onContentShown(!mWasShownSinceLastOpen);
+                mWasShownSinceLastOpen = true;
             } else {
                 onContentHidden();
             }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGrid.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGrid.java
index 146072c6..67c447ac 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGrid.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGrid.java
@@ -44,7 +44,7 @@
         mTileGroup = new TileGroup(ContextUtils.getApplicationContext(), uiDelegate,
                 contextMenuManager, tileGroupDelegate,
                 /* observer = */ this, offlinePageBridge, getTileTitleLines());
-        mTileGroup.startObserving(getMaxTileRows() * MAX_TILE_COLUMNS);
+        mTileGroup.startObserving(getMaxTileRows(), MAX_TILE_COLUMNS);
     }
 
     @Override
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGridLayout.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGridLayout.java
index 99ee8ff..cab7f0e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGridLayout.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGridLayout.java
@@ -8,10 +8,13 @@
 import android.content.res.Resources;
 import android.text.TextUtils;
 import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
 import android.view.View;
 import android.widget.FrameLayout;
 
 import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.ContextUtils;
 import org.chromium.base.VisibleForTesting;
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.util.MathUtils;
@@ -20,6 +23,12 @@
  * A layout that arranges tiles in a grid.
  */
 public class TileGridLayout extends FrameLayout {
+    public static final int PADDING_START_PX = 0;
+    public static final int PADDING_END_PX = 0;
+
+    public static final int MARGIN_START_DP = 12;
+    public static final int MARGIN_END_DP = 12;
+
     private final int mVerticalSpacing;
     private final int mMinHorizontalSpacing;
     private final int mMaxHorizontalSpacing;
@@ -30,6 +39,36 @@
     private int mExtraVerticalSpacing;
 
     /**
+     * Calculate the number of tile columns that will actually be rendered. Uses constants, does not
+     * rely on anything already being rendered. Used for calculating the position of the home page
+     * tile.
+     *
+     * @see TileGroup#renderTileViews(ViewGroup, boolean)
+     * @return The number of rendered columns (at least 1). Still needs to be capped from above
+     * by the maximum number of columns.
+     */
+    public static int calculateNumColumns() {
+        Resources res = ContextUtils.getApplicationContext().getResources();
+        int minHorizontalSpacingPx =
+                res.getDimensionPixelOffset(R.dimen.tile_grid_layout_min_horizontal_spacing);
+        int maxWidthPx = res.getDimensionPixelOffset(R.dimen.tile_grid_layout_max_width);
+
+        DisplayMetrics metrics = res.getDisplayMetrics();
+        int totalWidthPx =
+                Math.min(maxWidthPx, Math.min(metrics.widthPixels, metrics.heightPixels));
+
+        // Determine the number of columns that will fit.
+        int marginsPx = Math.round(TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, MARGIN_START_DP + MARGIN_END_DP, metrics));
+        int gridWidthPx = totalWidthPx - PADDING_START_PX - PADDING_END_PX - marginsPx;
+        int childWidthPx = res.getDimensionPixelOffset(R.dimen.tile_view_width);
+
+        return Math.max(
+                (gridWidthPx + minHorizontalSpacingPx) / (childWidthPx + minHorizontalSpacingPx),
+                1);
+    }
+
+    /**
      * Constructor for inflating from XML.
      *
      * @param context The view context in which this item will be shown.
@@ -108,8 +147,7 @@
         }
 
         // Determine the number of columns that will fit.
-        int gridWidth = totalWidth - ApiCompatibilityUtils.getPaddingStart(this)
-                - ApiCompatibilityUtils.getPaddingEnd(this);
+        int gridWidth = totalWidth - PADDING_START_PX - PADDING_END_PX;
         int childHeight = getChildAt(0).getMeasuredHeight();
         int childWidth = getChildAt(0).getMeasuredWidth();
         int numColumns = MathUtils.clamp(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGroup.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGroup.java
index 4a8d7bb..f5a7c72 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGroup.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/TileGroup.java
@@ -201,6 +201,14 @@
     private boolean mHasReceivedData;
 
     /**
+     * The number of columns tiles get rendered in. Precalculated upon calling
+     * {@link #startObserving(int, int)} and constant from then on. Used for pinning the home page
+     * tile to the first tile row.
+     * @see #renderTileViews(ViewGroup, boolean)
+     */
+    private int mNumColumns;
+
+    /**
      * @param context Used for initialisation and resolving resources.
      * @param uiDelegate Delegate used to interact with the rest of the system.
      * @param contextMenuManager Used to handle context menu invocations on the tiles.
@@ -260,7 +268,22 @@
             // send non dupes URLs. Remove once https://crbug.com/703628 is fixed.
             if (addedUrls.contains(urls[i])) continue;
 
-            mPendingTiles.add(new Tile(titles[i], urls[i], whitelistIconPaths[i], i, sources[i]));
+            // The home page tile is pinned to the first row of tiles. It will appear on
+            // the position corresponding to its ranking among all tiles (obtained from the
+            // ntp_tiles C++ component). If its position is larger than the number of tiles
+            // in the first row, it will appear on the last position of the first row.
+            // Do note, that the number of tiles in a row (column number) is determined upon
+            // initialization and not changed afterwards.
+            if (sources[i] == TileSource.HOMEPAGE) {
+                int homeTilePosition = Math.min(mPendingTiles.size(), mNumColumns - 1);
+                mPendingTiles.add(homeTilePosition,
+                        new Tile(titles[i], urls[i], whitelistIconPaths[i], homeTilePosition,
+                                sources[i]));
+            } else {
+                mPendingTiles.add(new Tile(titles[i], urls[i], whitelistIconPaths[i],
+                        mPendingTiles.size(), sources[i]));
+            }
+
             addedUrls.add(urls[i]);
 
             if (urls[i].equals(mPendingRemovalUrl)) removalCompleted = false;
@@ -293,11 +316,13 @@
     /**
      * Instructs this instance to start listening for data. The {@link TileGroup.Observer} may be
      * called immediately if new data is received synchronously.
-     * @param maxResults The maximum number of sites to retrieve.
+     * @param maxRows The maximum number of rows to fetch.
+     * @param maxColumns The maximum number of columns to fetch.
      */
-    public void startObserving(int maxResults) {
+    public void startObserving(int maxRows, int maxColumns) {
         addTask(TileTask.FETCH_DATA);
-        mTileGroupDelegate.setMostVisitedSitesObserver(this, maxResults);
+        mNumColumns = Math.min(maxColumns, TileGridLayout.calculateNumColumns());
+        mTileGroupDelegate.setMostVisitedSitesObserver(this, maxRows * maxColumns);
     }
 
     /**
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index 028a43f..15cc87b 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -1629,6 +1629,7 @@
   "javatests/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetTest.java",
   "javatests/src/org/chromium/chrome/browser/suggestions/SuggestionsBottomSheetUiCaptureTest.java",
   "javatests/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java",
+  "javatests/src/org/chromium/chrome/browser/suggestions/TileGridLayoutTest.java",
   "javatests/src/org/chromium/chrome/browser/superviseduser/SupervisedUserContentProviderTest.java",
   "javatests/src/org/chromium/chrome/browser/sync/FakeProfileSyncService.java",
   "javatests/src/org/chromium/chrome/browser/sync/ui/PassphraseActivityTest.java",
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionTest.java
index 36f60b7..dab6580 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionTest.java
@@ -25,15 +25,10 @@
 import org.chromium.base.ThreadUtils;
 import org.chromium.base.library_loader.LibraryLoader;
 import org.chromium.base.library_loader.LibraryProcessType;
-import org.chromium.base.test.util.CommandLineFlags;
 import org.chromium.base.test.util.Restriction;
 import org.chromium.base.test.util.RetryOnFailure;
-import org.chromium.chrome.browser.ChromeFeatureList;
-import org.chromium.chrome.browser.ChromeSwitches;
 import org.chromium.chrome.browser.WarmupManager;
 import org.chromium.chrome.browser.preferences.PrefServiceBridge;
-import org.chromium.chrome.browser.tab.EmptyTabObserver;
-import org.chromium.chrome.browser.tab.Tab;
 import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
 import org.chromium.content_public.browser.WebContents;
 
@@ -41,8 +36,6 @@
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.FutureTask;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
 
 /** Tests for CustomTabsConnection. */
 @RunWith(ChromeJUnit4ClassRunner.class)
@@ -225,56 +218,6 @@
         assertWarmupAndMayLaunchUrl(null, "", true);
     }
 
-    /** Tests that a new mayLaunchUrl() call destroys the previous hidden tab. */
-    @Test
-    @SmallTest
-    @CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
-            "enable-features=" + ChromeFeatureList.CCT_BACKGROUND_TAB})
-    public void testOnlyOneHiddenTab() throws Exception {
-        Assert.assertTrue(mCustomTabsConnection.warmup(0));
-        CustomTabsSessionToken token = CustomTabsSessionToken.createDummySessionTokenForTesting();
-        Assert.assertTrue(mCustomTabsConnection.newSession(token));
-        mCustomTabsConnection.setSpeculationModeForSession(
-                token, CustomTabsConnection.SpeculationParams.HIDDEN_TAB);
-
-        // First hidden tab, add an observer to check that it's destroyed.
-        Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL), null, null));
-        final Semaphore destroyed = new Semaphore(0);
-        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                Assert.assertNotNull(mCustomTabsConnection.mSpeculation);
-                Tab tab = mCustomTabsConnection.mSpeculation.tab;
-                Assert.assertNotNull(tab);
-                tab.addObserver(new EmptyTabObserver() {
-                    @Override
-                    public void onDestroyed(Tab destroyedTab) {
-                        destroyed.release();
-                    }
-                });
-            }
-        });
-
-        // New hidden tab.
-        mCustomTabsConnection.resetThrottling(Process.myPid());
-        Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, Uri.parse(URL2), null, null));
-        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                Assert.assertNotNull(mCustomTabsConnection.mSpeculation);
-                Assert.assertNotNull(mCustomTabsConnection.mSpeculation.tab);
-                Assert.assertEquals(URL2, mCustomTabsConnection.mSpeculation.url);
-            }
-        });
-
-        Assert.assertTrue(
-                "Only one hidden tab should exist.", destroyed.tryAcquire(10, TimeUnit.SECONDS));
-
-        // Clears the second hidden tab.
-        mCustomTabsConnection.resetThrottling(Process.myPid());
-        Assert.assertTrue(mCustomTabsConnection.mayLaunchUrl(token, null, null, null));
-    }
-
     @Test
     @SmallTest
     @Restriction(RESTRICTION_TYPE_NON_LOW_END_DEVICE)
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializerTest.java
index f7afa95..8fa2d0b 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/ChannelsInitializerTest.java
@@ -230,12 +230,14 @@
     public void testEnsureInitialized_singleOriginSiteChannel() throws Exception {
         if (!BuildInfo.isAtLeastO()) return;
         String origin = "https://example.com";
-        mChannelsInitializer.ensureInitialized(SiteChannelsManager.toChannelId(origin));
+        long creationTime = 621046800000L;
+        mChannelsInitializer.ensureInitialized(
+                SiteChannelsManager.createChannelId(origin, creationTime));
 
         assertThat(getChannelsIgnoringMiscellaneous(), hasSize(1));
 
         Channel channel = getChannelsIgnoringMiscellaneous().get(0);
-        assertThat(channel.getId(), is(SiteChannelsManager.toChannelId(origin)));
+        assertThat(channel.getId(), is(SiteChannelsManager.createChannelId(origin, creationTime)));
         assertThat(channel.getName().toString(), is("https://example.com"));
         assertThat(channel.getImportance(), is(NotificationManager.IMPORTANCE_DEFAULT));
         assertThat(channel.getGroupId(), is(ChannelDefinitions.CHANNEL_GROUP_ID_SITES));
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManagerTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManagerTest.java
index ddcf943..bab8a1a1 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManagerTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/notifications/channels/SiteChannelsManagerTest.java
@@ -67,18 +67,19 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testCreateSiteChannel_enabled() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://chromium.org", true);
+        mSiteChannelsManager.createSiteChannel("https://chromium.org", 62102180000L, true);
         assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
         NotificationSettingsBridge.SiteChannel channel = mSiteChannelsManager.getSiteChannels()[0];
         assertThat(channel.getOrigin(), is("https://chromium.org"));
         assertThat(channel.getStatus(), matchesChannelStatus(NotificationChannelStatus.ENABLED));
+        assertThat(channel.getTimestamp(), is(62102180000L));
     }
 
     @Test
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testCreateSiteChannel_disabled() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://example.com", false);
+        mSiteChannelsManager.createSiteChannel("https://example.com", 0L, false);
         assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
         NotificationSettingsBridge.SiteChannel channel = mSiteChannelsManager.getSiteChannels()[0];
         assertThat(channel.getOrigin(), is("https://example.com"));
@@ -89,8 +90,9 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testDeleteSiteChannel_channelExists() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://chromium.org", true);
-        mSiteChannelsManager.deleteSiteChannel("https://chromium.org");
+        NotificationSettingsBridge.SiteChannel channel =
+                mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
+        mSiteChannelsManager.deleteSiteChannel(channel.getId());
         assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(0));
     }
 
@@ -98,7 +100,7 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testDeleteSiteChannel_channelDoesNotExist() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://chromium.org", true);
+        mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
         mSiteChannelsManager.deleteSiteChannel("https://some-other-origin.org");
         assertThat(Arrays.asList(mSiteChannelsManager.getSiteChannels()), hasSize(1));
     }
@@ -107,8 +109,9 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testGetChannelStatus_channelCreatedAsEnabled() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://chromium.org", true);
-        assertThat(mSiteChannelsManager.getChannelStatus("https://chromium.org"),
+        NotificationSettingsBridge.SiteChannel channel =
+                mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
+        assertThat(mSiteChannelsManager.getChannelStatus(channel.getId()),
                 matchesChannelStatus(NotificationChannelStatus.ENABLED));
     }
 
@@ -118,11 +121,9 @@
     public void testGetChannelStatus_channelCreatedAsBlocked() throws Exception {
         assertThat(mSiteChannelsManager.getChannelStatus("https://foobar.com"),
                 matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
-        mSiteChannelsManager.createSiteChannel("https://foobar.com", false);
-        assertThat(mNotificationManagerProxy.getNotificationChannel("web:https://foobar.com")
-                           .getImportance(),
-                is(NotificationManager.IMPORTANCE_NONE));
-        assertThat(mSiteChannelsManager.getChannelStatus("https://foobar.com"),
+        NotificationSettingsBridge.SiteChannel channel =
+                mSiteChannelsManager.createSiteChannel("https://foobar.com", 0L, false);
+        assertThat(mSiteChannelsManager.getChannelStatus(channel.getId()),
                 matchesChannelStatus(NotificationChannelStatus.BLOCKED));
     }
 
@@ -130,7 +131,7 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testGetChannelStatus_channelNotCreated() throws Exception {
-        assertThat(mSiteChannelsManager.getChannelStatus("https://chromium.org"),
+        assertThat(mSiteChannelsManager.getChannelStatus("invalid-channel-id"),
                 matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
     }
 
@@ -138,9 +139,10 @@
     @MinAndroidSdkLevel(26)
     @SmallTest
     public void testGetChannelStatus_channelCreatedThenDeleted() throws Exception {
-        mSiteChannelsManager.createSiteChannel("https://chromium.org", true);
-        mSiteChannelsManager.deleteSiteChannel("https://chromium.org");
-        assertThat(mSiteChannelsManager.getChannelStatus("https://chromium.org"),
+        NotificationSettingsBridge.SiteChannel channel =
+                mSiteChannelsManager.createSiteChannel("https://chromium.org", 0L, true);
+        mSiteChannelsManager.deleteSiteChannel(channel.getId());
+        assertThat(mSiteChannelsManager.getChannelStatus(channel.getId()),
                 matchesChannelStatus(NotificationChannelStatus.UNAVAILABLE));
     }
 
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/offlinepages/RecentTabsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/offlinepages/RecentTabsTest.java
index 6ab4db77..1808c63 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/offlinepages/RecentTabsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/offlinepages/RecentTabsTest.java
@@ -35,7 +35,6 @@
 import java.util.concurrent.Callable;
 import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
 
 /** Integration tests for the Last 1 feature of Offline Pages. */
 @RunWith(ChromeJUnit4ClassRunner.class)
@@ -46,13 +45,11 @@
     public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
 
     private static final String TEST_PAGE = "/chrome/test/data/android/about.html";
-    private static final String TEST_PAGE_2 = "/chrome/test/data/android/simple.html";
     private static final int TIMEOUT_MS = 5000;
 
     private OfflinePageBridge mOfflinePageBridge;
     private EmbeddedTestServer mTestServer;
     private String mTestPage;
-    private String mTestPage2;
 
     private void initializeBridgeForProfile(final boolean incognitoProfile)
             throws InterruptedException {
@@ -79,7 +76,7 @@
                 });
             }
         });
-        assertAcquire(semaphore);
+        Assert.assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
     @Before
@@ -88,10 +85,11 @@
         ThreadUtils.runOnUiThreadBlocking(new Runnable() {
             @Override
             public void run() {
+                // Ensure we start in an offline state.
+                NetworkChangeNotifier.forceConnectivityState(false);
                 if (!NetworkChangeNotifier.isInitialized()) {
                     NetworkChangeNotifier.init();
                 }
-                NetworkChangeNotifier.forceConnectivityState(true);
             }
         });
 
@@ -100,7 +98,6 @@
         mTestServer = EmbeddedTestServer.createAndStartServer(
                 InstrumentationRegistry.getInstrumentation().getContext());
         mTestPage = mTestServer.getURL(TEST_PAGE);
-        mTestPage2 = mTestServer.getURL(TEST_PAGE_2);
     }
 
     @After
@@ -121,9 +118,9 @@
         // The tab should be foreground and so no snapshot should exist.
         Assert.assertNull(getPageByClientId(firstTabClientId));
 
-        // Note: switching to a new tab must occur after the SnapshotController believes the page
-        // quality is good enough.  With the debug flag, the delay after DomContentLoaded is 0 so we
-        // can definitely snapshot after onload (which is what |loadUrlInNewTab| waits for).
+        // Note, that switching to a new tab must occur after the SnapshotController believes the
+        // page quality is good enough.  With the debug flag, the delay after DomContentLoaded is 0
+        // so we can definitely snapshot after onload (which is what |loadUrlInNewTab| waits for).
 
         // Switch to a new tab to cause the WebContents hidden event.
         mActivityTestRule.loadUrlInNewTab("about:blank");
@@ -132,10 +129,10 @@
     }
 
     /**
-     * Note: this test relies on a sleeping period because some of the monitored actions are
-     * difficult to track deterministically. A sleep time of 100 ms was chosen based on local
-     * testing and is expected to be "safe". Nevertheless if flakiness is detected it might have to
-     * be further increased.
+     * Note: this test relies on a sleeping period because some of the taking actions are
+     * complicated to track otherwise, so there is the possibility of flakiness. I chose 100ms from
+     * local testing and I expect it to be "safe" but it flakiness is detected it might have to be
+     * further increased.
      */
     @Test
     @CommandLineFlags.Add("short-offline-page-snapshot-delay-for-test")
@@ -191,94 +188,10 @@
         waitForPageWithClientId(firstTabClientId);
     }
 
-    /**
-     * Verifies that a snapshot created by last_n is properly deleted when the tab is navigated to
-     * another page. The deletion of snapshots for pages that should not be available anymore is a
-     * privacy requirement for last_n.
-     */
-    @Test
-    @CommandLineFlags.Add("short-offline-page-snapshot-delay-for-test")
-    @MediumTest
-    public void testLastNPageIsDeletedUponNavigation() throws Exception {
-        // The tab of interest.
-        final Tab tab = mActivityTestRule.loadUrlInNewTab(mTestPage);
-        final TabModelSelector tabModelSelector = tab.getTabModelSelector();
-
-        final ClientId firstTabClientId =
-                new ClientId(OfflinePageBridge.LAST_N_NAMESPACE, Integer.toString(tab.getId()));
-
-        // Switch to a new tab and wait for the snapshot to be created.
-        mActivityTestRule.loadUrlInNewTab("about:blank");
-        Assert.assertFalse(tab.equals(tabModelSelector.getCurrentTab()));
-        OfflinePageItem offlinePage = waitForPageWithClientId(firstTabClientId);
-
-        // Switch back to the initial tab.
-        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                TabModel tabModel = tabModelSelector.getModelForTabId(tab.getId());
-                int tabIndex = TabModelUtils.getTabIndexById(tabModel, tab.getId());
-                TabModelUtils.setIndex(tabModel, tabIndex);
-            }
-        });
-        Assert.assertEquals(tabModelSelector.getCurrentTab(), tab);
-
-        // Navigate to a new page and confirm that the previously created snapshot has been deleted.
-        Semaphore deletionSemaphore = installPageDeletionSemaphore(offlinePage.getOfflineId());
-        mActivityTestRule.loadUrl(mTestPage2);
-        assertAcquire(deletionSemaphore);
-    }
-
-    /**
-     * Verifies that a snapshot created by last_n is properly deleted when the tab is closed. The
-     * deletion of snapshots for pages that should not be available anymore is a privacy requirement
-     * for last_n.
-     */
-    @Test
-    @CommandLineFlags.Add("short-offline-page-snapshot-delay-for-test")
-    @MediumTest
-    public void testLastNPageIsDeletedUponClosure() throws Exception {
-        // The tab of interest.
-        final Tab tab = mActivityTestRule.loadUrlInNewTab(mTestPage);
-        final TabModelSelector tabModelSelector = tab.getTabModelSelector();
-
-        final ClientId firstTabClientId =
-                new ClientId(OfflinePageBridge.LAST_N_NAMESPACE, Integer.toString(tab.getId()));
-
-        // Switch to a new tab and wait for the snapshot to be created.
-        mActivityTestRule.loadUrlInNewTab("about:blank");
-        OfflinePageItem offlinePage = waitForPageWithClientId(firstTabClientId);
-
-        // Requests closing the tab allowing for undo -- so to exercise the subscribed notification
-        // -- but immediately requests final closure.
-        final int tabId = tab.getId();
-        Semaphore deletionSemaphore = installPageDeletionSemaphore(offlinePage.getOfflineId());
-        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                TabModel tabModel = tabModelSelector.getModelForTabId(tabId);
-                tabModel.closeTab(tab, false, false, true);
-                tabModel.commitTabClosure(tabId);
-            }
-        });
-
-        // Checks that the tab is no more and that the snapshot was deleted.
-        Assert.assertNull(tabModelSelector.getTabById(tabId));
-        assertAcquire(deletionSemaphore);
-    }
-
-    private void assertAcquire(Semaphore semaphore) throws InterruptedException {
-        Assert.assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
-    }
-
-    private OfflinePageItem waitForPageWithClientId(final ClientId clientId)
-            throws InterruptedException {
-        OfflinePageItem item = getPageByClientId(clientId);
-        if (item != null) return item;
+    private void waitForPageWithClientId(final ClientId clientId) throws InterruptedException {
+        if (getPageByClientId(clientId) != null) return;
 
         final Semaphore semaphore = new Semaphore(0);
-        final AtomicReference<OfflinePageItem> itemReference =
-                new AtomicReference<OfflinePageItem>();
         ThreadUtils.runOnUiThread(new Runnable() {
             @Override
             public void run() {
@@ -286,7 +199,6 @@
                     @Override
                     public void offlinePageAdded(OfflinePageItem newPage) {
                         if (newPage.getClientId().equals(clientId)) {
-                            itemReference.set(newPage);
                             mOfflinePageBridge.removeObserver(this);
                             semaphore.release();
                         }
@@ -294,27 +206,7 @@
                 });
             }
         });
-        assertAcquire(semaphore);
-        return itemReference.get();
-    }
-
-    private Semaphore installPageDeletionSemaphore(final long offlineId) {
-        final Semaphore semaphore = new Semaphore(0);
-        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
-            @Override
-            public void run() {
-                mOfflinePageBridge.addObserver(new OfflinePageModelObserver() {
-                    @Override
-                    public void offlinePageDeleted(DeletedPageInfo deletedPage) {
-                        if (deletedPage.getOfflineId() == offlineId) {
-                            mOfflinePageBridge.removeObserver(this);
-                            semaphore.release();
-                        }
-                    }
-                });
-            }
-        });
-        return semaphore;
+        Assert.assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
     }
 
     private OfflinePageItem getPageByClientId(ClientId clientId) throws InterruptedException {
@@ -338,27 +230,7 @@
                         });
             }
         });
-        assertAcquire(semaphore);
-        return result[0];
-    }
-
-    private OfflinePageItem getPageByOfflineId(final long offlineId) throws InterruptedException {
-        final OfflinePageItem[] result = {null};
-        final Semaphore semaphore = new Semaphore(0);
-
-        ThreadUtils.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mOfflinePageBridge.getPageByOfflineId(offlineId, new Callback<OfflinePageItem>() {
-                    @Override
-                    public void onResult(OfflinePageItem item) {
-                        result[0] = item;
-                        semaphore.release();
-                    }
-                });
-            }
-        });
-        assertAcquire(semaphore);
+        Assert.assertTrue(semaphore.tryAcquire(TIMEOUT_MS, TimeUnit.MILLISECONDS));
         return result[0];
     }
 }
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/TileGridLayoutTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/TileGridLayoutTest.java
new file mode 100644
index 0000000..37b992a
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/suggestions/TileGridLayoutTest.java
@@ -0,0 +1,227 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.suggestions;
+
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.filters.SmallTest;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.CommandLineFlags;
+import org.chromium.base.test.util.Feature;
+import org.chromium.base.test.util.RetryOnFailure;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.ChromeSwitches;
+import org.chromium.chrome.browser.UrlConstants;
+import org.chromium.chrome.browser.ntp.NewTabPage;
+import org.chromium.chrome.browser.ntp.cards.NewTabPageRecyclerView;
+import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.test.ChromeActivityTestRule;
+import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
+import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
+import org.chromium.chrome.test.util.NewTabPageTestUtils;
+import org.chromium.chrome.test.util.browser.RecyclerViewTestUtils;
+import org.chromium.chrome.test.util.browser.suggestions.FakeSuggestionsSource;
+import org.chromium.chrome.test.util.browser.suggestions.SuggestionsDependenciesRule;
+import org.chromium.content.browser.test.util.Criteria;
+import org.chromium.content.browser.test.util.CriteriaHelper;
+import org.chromium.net.test.EmbeddedTestServerRule;
+
+import java.util.Arrays;
+
+/**
+ * Instrumentation tests for the {@link TileGridLayout} on the New Tab Page.
+ */
+@RunWith(ChromeJUnit4ClassRunner.class)
+@CommandLineFlags.Add({
+        ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
+        ChromeActivityTestRule.DISABLE_NETWORK_PREDICTION_FLAG,
+})
+@RetryOnFailure
+public class TileGridLayoutTest {
+    @Rule
+    public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
+
+    @Rule
+    public SuggestionsDependenciesRule mSuggestionsDeps = new SuggestionsDependenciesRule();
+
+    @Rule
+    public EmbeddedTestServerRule mTestServerRule = new EmbeddedTestServerRule();
+
+    private static final String HOME_PAGE_URL = "http://ho.me/";
+
+    private static final String[] FAKE_MOST_VISITED_URLS = new String[] {
+            "/chrome/test/data/android/navigate/one.html",
+            "/chrome/test/data/android/navigate/two.html",
+            "/chrome/test/data/android/navigate/three.html",
+            "/chrome/test/data/android/navigate/four.html",
+            "/chrome/test/data/android/navigate/five.html",
+            "/chrome/test/data/android/navigate/six.html",
+            "/chrome/test/data/android/navigate/seven.html",
+            "/chrome/test/data/android/navigate/eight.html",
+            "/chrome/test/data/android/navigate/nine.html",
+    };
+
+    private NewTabPage mNtp;
+    private String[] mSiteSuggestionUrls;
+    private FakeMostVisitedSites mMostVisitedSites;
+
+    @Before
+    public void setUp() throws Exception {
+        mSiteSuggestionUrls = mTestServerRule.getServer().getURLs(FAKE_MOST_VISITED_URLS);
+        String[] tmpSiteSuggestionUrls = new String[mSiteSuggestionUrls.length + 1];
+        System.arraycopy(
+                mSiteSuggestionUrls, 0, tmpSiteSuggestionUrls, 0, mSiteSuggestionUrls.length);
+        tmpSiteSuggestionUrls[tmpSiteSuggestionUrls.length - 1] = HOME_PAGE_URL;
+        mSiteSuggestionUrls = tmpSiteSuggestionUrls;
+
+        mMostVisitedSites = new FakeMostVisitedSites();
+        mSuggestionsDeps.getFactory().mostVisitedSites = mMostVisitedSites;
+
+        String[] whitelistIconPaths = new String[mSiteSuggestionUrls.length];
+        Arrays.fill(whitelistIconPaths, "");
+
+        int[] sources = new int[mSiteSuggestionUrls.length];
+        Arrays.fill(sources, TileSource.TOP_SITES);
+        sources[mSiteSuggestionUrls.length - 1] = TileSource.HOMEPAGE;
+
+        mMostVisitedSites.setTileSuggestions(
+                mSiteSuggestionUrls, mSiteSuggestionUrls.clone(), whitelistIconPaths, sources);
+
+        mSuggestionsDeps.getFactory().suggestionsSource = new FakeSuggestionsSource();
+
+        mActivityTestRule.startMainActivityWithURL(UrlConstants.NTP_URL);
+        Tab mTab = mActivityTestRule.getActivity().getActivityTab();
+        NewTabPageTestUtils.waitForNtpLoaded(mTab);
+
+        Assert.assertTrue(mTab.getNativePage() instanceof NewTabPage);
+        mNtp = (NewTabPage) mTab.getNativePage();
+
+        RecyclerViewTestUtils.waitForStableRecyclerView(getRecyclerView());
+    }
+
+    /**
+     * Verifies that the assumed constants in {@link TileGridLayout#calculateNumColumns()}
+     * are correct.
+     */
+    @Test
+    @SmallTest
+    @Feature({"NewTabPage"})
+    public void testNumColumnsPaddingAndMarginSizes() {
+        TileGridLayout tileGridLayout = getTileGridLayout();
+        Assert.assertEquals(TileGridLayout.PADDING_START_PX,
+                ApiCompatibilityUtils.getPaddingStart(tileGridLayout));
+        Assert.assertEquals(
+                TileGridLayout.PADDING_END_PX, ApiCompatibilityUtils.getPaddingEnd(tileGridLayout));
+
+        DisplayMetrics metrics = tileGridLayout.getResources().getDisplayMetrics();
+        LinearLayout.LayoutParams layoutParams =
+                (LinearLayout.LayoutParams) tileGridLayout.getLayoutParams();
+        Assert.assertEquals(convertToPx(TileGridLayout.MARGIN_START_DP, metrics),
+                ApiCompatibilityUtils.getMarginStart(layoutParams));
+        Assert.assertEquals(convertToPx(TileGridLayout.MARGIN_END_DP, metrics),
+                ApiCompatibilityUtils.getMarginEnd(layoutParams));
+    }
+
+    @Test
+    @MediumTest
+    @Feature({"NewTabPage"})
+    public void testNumColumnsCalculatedEqualToMeasurement() {
+        final int maxColumns = 6;
+        final TileGridLayout layout = getTileGridLayout();
+        layout.setMaxRows(1);
+        layout.setMaxColumns(maxColumns);
+
+        reMeasureLayout(layout);
+        int calculatedNumColumns = Math.min(maxColumns, TileGridLayout.calculateNumColumns());
+
+        final Activity activity = mActivityTestRule.getActivity();
+        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        int portraitVisibleTiles = countVisibleTiles(layout, calculatedNumColumns);
+        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+        int landscapeVisibleTiles = countVisibleTiles(layout, calculatedNumColumns);
+        Assert.assertEquals("The calculated number of columns should be equal to the minimum number"
+                        + " of columns across screen orientations.",
+                Math.min(portraitVisibleTiles, landscapeVisibleTiles), calculatedNumColumns);
+    }
+
+    private int convertToPx(int dp, DisplayMetrics metrics) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics));
+    }
+
+    private void reMeasureLayout(final ViewGroup group) {
+        ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+            @Override
+            public void run() {
+                group.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+            }
+        });
+
+        // Wait until the view is updated. TODO(oskopek): Is there a better criterion to check for?
+        CriteriaHelper.pollUiThread(new Criteria() {
+            @Override
+            public boolean isSatisfied() {
+                boolean hasHiddenTile = false;
+                for (int i = 0; i < group.getChildCount(); i++) {
+                    if (!hasHiddenTile) {
+                        hasHiddenTile = group.getChildAt(i).getVisibility() == View.GONE;
+                    }
+                }
+
+                return hasHiddenTile;
+            }
+        });
+    }
+
+    private int countVisibleTiles(ViewGroup group, int calculatedNumColumns) {
+        reMeasureLayout(group);
+        int visibleTileViews = 0;
+        boolean hasHomePage = false;
+        for (int i = 0; i < group.getChildCount(); i++) {
+            TileView tileView = (TileView) group.getChildAt(i);
+            if (tileView.getVisibility() == View.VISIBLE) {
+                visibleTileViews++;
+                if (HOME_PAGE_URL.equals(tileView.getUrl())) {
+                    hasHomePage = true;
+                }
+            }
+        }
+
+        Assert.assertTrue("The calculated number of columns should be less than or equal to"
+                        + "the measured one.",
+                calculatedNumColumns <= visibleTileViews);
+        Assert.assertTrue("Visible tiles should contain the home page tile.", hasHomePage);
+
+        return visibleTileViews;
+    }
+
+    private NewTabPageRecyclerView getRecyclerView() {
+        return mNtp.getNewTabPageView().getRecyclerView();
+    }
+
+    private TileGridLayout getTileGridLayout() {
+        TileGridLayout tileGridLayout =
+                (TileGridLayout) mNtp.getNewTabPageView().findViewById(R.id.tile_grid_layout);
+        Assert.assertNotNull("Unable to retrieve the TileGridLayout.", tileGridLayout);
+        return tileGridLayout;
+    }
+}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeUnitTest.java
index a830dc2..1b046e0 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/notifications/NotificationPlatformBridgeUnitTest.java
@@ -120,7 +120,7 @@
         // Returns the expected origin for a channel id associated with a particular origin.
         assertEquals("https://example.com",
                 NotificationPlatformBridge.getOriginFromChannelId(
-                        SiteChannelsManager.toChannelId("https://example.com")));
+                        SiteChannelsManager.createChannelId("https://example.com", 62104680000L)));
 
         // Returns null for a channel id that is not associated with a particular origin.
         assertNull(NotificationPlatformBridge.getOriginFromChannelId(
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java
index 4d38e0c..7e925933 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/suggestions/TileGroupTest.java
@@ -60,7 +60,8 @@
 @Features({@Features.Register(ChromeFeatureList.NTP_OFFLINE_PAGES_FEATURE_NAME),
         @Features.Register(ChromeFeatureList.SUGGESTIONS_HOME_MODERN_LAYOUT)})
 public class TileGroupTest {
-    private static final int MAX_TILES_TO_FETCH = 4;
+    private static final int MAX_COLUMNS_TO_FETCH = 4;
+    private static final int MAX_ROWS_TO_FETCH = 1;
     private static final int TILE_TITLE_LINES = 1;
     private static final String[] URLS = {"https://www.google.com", "https://tellmedadjokes.com"};
 
@@ -93,7 +94,7 @@
                 new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class),
                         mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                         mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
 
         notifyTileUrlsAvailable(URLS);
 
@@ -115,7 +116,7 @@
                 new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class),
                         mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                         mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
 
         notifyTileUrlsAvailable(/* nothing! */);
 
@@ -205,7 +206,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
 
         notifyTileUrlsAvailable(URLS);
 
@@ -220,7 +221,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
 
         notifyTileUrlsAvailable(URLS);
         reset(mTileGroupObserver);
@@ -260,7 +261,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
         ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
 
         // Initialise the internal list of tiles
@@ -281,7 +282,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
         ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
 
         // Initialise the internal list of tiles
@@ -301,7 +302,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
         notifyTileUrlsAvailable(URLS);
 
         // Initialise the layout with views whose URLs don't match the ones of the new tiles.
@@ -327,7 +328,7 @@
                 new TileGroup(RuntimeEnvironment.application, mock(SuggestionsUiDelegate.class),
                         mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                         mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
         notifyTileUrlsAvailable(URLS);
 
         // Initialise the layout with views whose URLs match the ones of the new tiles.
@@ -438,7 +439,7 @@
 
     /**
      * Notifies the tile group of new tiles created from the provided URLs. Requires
-     * {@link TileGroup#startObserving(int)} to have been called on the tile group under test.
+     * {@link TileGroup#startObserving(int, int)} to have been called on the tile group under test.
      * @see TileGroup#onMostVisitedURLsAvailable(String[], String[], String[], int[])
      */
     private void notifyTileUrlsAvailable(String... urls) {
@@ -471,7 +472,7 @@
         TileGroup tileGroup = new TileGroup(RuntimeEnvironment.application, uiDelegate,
                 mock(ContextMenuManager.class), mTileGroupDelegate, mTileGroupObserver,
                 mock(OfflinePageBridge.class), TILE_TITLE_LINES);
-        tileGroup.startObserving(MAX_TILES_TO_FETCH);
+        tileGroup.startObserving(MAX_ROWS_TO_FETCH, MAX_COLUMNS_TO_FETCH);
         notifyTileUrlsAvailable(urls);
 
         ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index d09e6d8..1625571b 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -487,7 +487,10 @@
   <message name="IDS_FILE_BROWSER_CUT_BUTTON_LABEL" desc="Context menu item that cuts the currently-selected file(s) to the clipboard.">
     Cut
   </message>
-  <message name="IDS_FILE_BROWSER_MORE_ACTIONS_BUTTON_LABEL" desc="Menu item label, showing dialog to choose extension to open selected files or directories.">
+  <message name="IDS_FILE_BROWSER_OPEN_WITH_BUTTON_LABEL" desc="Menu item label, showing dialog to choose extension to open selected files or directories.">
+    Open with...
+  </message>
+  <message name="IDS_FILE_BROWSER_MORE_ACTIONS_BUTTON_LABEL" desc="Menu item label, showing dialog to choose extension to open/share/pack/etc... selected files or directories.">
     More actions...
   </message>
   <message name="IDS_FILE_BROWSER_OPEN_WITH_VERB_BUTTON_LABEL" desc="Verb that describe the action of opening one or more files or directories with the given extension.">
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 14cdaba2..10c54f3 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -1409,6 +1409,14 @@
                  desc="Size and units downloaded, and the origin domain.">
           <ph name="DOWNLOAD_RECEIVED">$1<ex>154 MB</ex></ph> from <ph name="DOWNLOAD_DOMAIN">$2<ex>example.com</ex></ph>
         </message>
+        <message name="IDS_DOWNLOAD_NOTIFICATION_STATUS_SHORT"
+                 desc="Size and units downloaded, and the origin domain. ">
+          <ph name="DOWNLOAD_RECEIVED">$1<ex>154 MB</ex></ph> from <ph name="DOWNLOAD_DOMAIN">$2<ex>example.com</ex></ph>
+        </message>
+        <message name="IDS_DOWNLOAD_NOTIFICATION_DISPLAY_SOURCE"
+                 desc="The name of the notification source, which is download manager here. Shown in the notification header.">
+          Download manager
+        </message>
       </if>
       <message name="IDS_DOWNLOAD_STATUS_IN_PROGRESS"
                desc="Size and units downloaded, time remaining.">
@@ -7412,7 +7420,7 @@
           Use Smart Lock to sign in to your account
         </message>
          <message name="IDS_SETTINGS_EASY_UNLOCK_PROXIMITY_THRESHOLD_LABEL" desc="The text on the Easy unlock settings page indicating how close the phone should be for unlock to work.">
-          Distance from your phone to unlock your <ph name="DEVICE_TYPE">$1<ex>Chromebook</ex></ph>
+          Distance needed for phone to unlock this <ph name="DEVICE_TYPE">$1<ex>Chromebook</ex></ph>
         </message>
         <message name="IDS_SETTINGS_EASY_UNLOCK_PROXIMITY_THRESHOLD_VERY_CLOSE" desc="The text on the Easy unlock settings page indicating the phone should be very close for the unlock to work.">
           Very close
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index 43a86ea..314c750f 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -1181,6 +1181,8 @@
     "policy/bluetooth_policy_handler.h",
     "policy/browser_policy_connector_chromeos.cc",
     "policy/browser_policy_connector_chromeos.h",
+    "policy/cached_policy_key_loader_chromeos.cc",
+    "policy/cached_policy_key_loader_chromeos.h",
     "policy/cloud_external_data_manager_base.cc",
     "policy/cloud_external_data_manager_base.h",
     "policy/cloud_external_data_policy_observer.cc",
@@ -1240,6 +1242,8 @@
     "policy/policy_cert_verifier.h",
     "policy/policy_oauth2_token_fetcher.cc",
     "policy/policy_oauth2_token_fetcher.h",
+    "policy/pre_signin_policy_fetcher.cc",
+    "policy/pre_signin_policy_fetcher.h",
     "policy/recommendation_restorer.cc",
     "policy/recommendation_restorer.h",
     "policy/recommendation_restorer_factory.cc",
@@ -1818,6 +1822,7 @@
     "policy/android_management_client_unittest.cc",
     "policy/auto_enrollment_client_unittest.cc",
     "policy/bluetooth_policy_handler_unittest.cc",
+    "policy/cached_policy_key_loader_chromeos_unittest.cc",
     "policy/cloud_external_data_manager_base_unittest.cc",
     "policy/cloud_external_data_policy_observer_unittest.cc",
     "policy/cloud_external_data_store_unittest.cc",
@@ -1832,6 +1837,7 @@
     "policy/fake_affiliated_invalidation_service_provider.h",
     "policy/heartbeat_scheduler_unittest.cc",
     "policy/network_configuration_updater_unittest.cc",
+    "policy/pre_signin_policy_fetcher_unittest.cc",
     "policy/recommendation_restorer_unittest.cc",
     "policy/remote_commands/device_command_screenshot_job_unittest.cc",
     "policy/remote_commands/device_command_set_volume_job_unittest.cc",
diff --git a/chrome/browser/chromeos/login/existing_user_controller.cc b/chrome/browser/chromeos/login/existing_user_controller.cc
index 77bd407c..fa685a5d 100644
--- a/chrome/browser/chromeos/login/existing_user_controller.cc
+++ b/chrome/browser/chromeos/login/existing_user_controller.cc
@@ -5,6 +5,7 @@
 #include "chrome/browser/chromeos/login/existing_user_controller.h"
 
 #include <memory>
+#include <utility>
 #include <vector>
 
 #include "base/bind.h"
@@ -12,6 +13,7 @@
 #include "base/callback.h"
 #include "base/command_line.h"
 #include "base/logging.h"
+#include "base/memory/ptr_util.h"
 #include "base/message_loop/message_loop.h"
 #include "base/metrics/histogram_macros.h"
 #include "base/strings/string_util.h"
@@ -47,6 +49,7 @@
 #include "chrome/browser/chromeos/profiles/profile_helper.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/system/device_disabling_manager.h"
+#include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/signin/easy_unlock_service.h"
 #include "chrome/browser/ui/aura/accessibility/automation_manager_aura.h"
 #include "chrome/browser/ui/webui/chromeos/login/l10n_util.h"
@@ -55,19 +58,25 @@
 #include "chrome/common/url_constants.h"
 #include "chrome/grit/generated_resources.h"
 #include "chromeos/chromeos_switches.h"
+#include "chromeos/cryptohome/async_method_caller.h"
+#include "chromeos/cryptohome/cryptohome_parameters.h"
 #include "chromeos/dbus/dbus_thread_manager.h"
 #include "chromeos/dbus/power_manager_client.h"
 #include "chromeos/dbus/session_manager_client.h"
 #include "chromeos/login/auth/authpolicy_login_helper.h"
+#include "chromeos/login/auth/key.h"
 #include "chromeos/settings/cros_settings_names.h"
 #include "components/arc/arc_util.h"
 #include "components/google/core/browser/google_util.h"
+#include "components/policy/core/common/cloud/cloud_policy_client.h"
 #include "components/policy/core/common/cloud/cloud_policy_core.h"
 #include "components/policy/core/common/cloud/cloud_policy_store.h"
+#include "components/policy/core/common/cloud/device_management_service.h"
 #include "components/policy/core/common/policy_map.h"
 #include "components/policy/core/common/policy_service.h"
 #include "components/policy/core/common/policy_types.h"
 #include "components/policy/policy_constants.h"
+#include "components/policy/proto/cloud_policy.pb.h"
 #include "components/prefs/pref_service.h"
 #include "components/session_manager/core/session_manager.h"
 #include "components/signin/core/account_id/account_id.h"
@@ -92,6 +101,8 @@
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/views/widget/widget.h"
 
+using PolicyFetchResult = policy::PreSigninPolicyFetcher::PolicyFetchResult;
+
 namespace chromeos {
 
 namespace {
@@ -109,6 +120,22 @@
   LOGIN_PASSWORD_CHANGE_FLOW_COUNT,  // Must be the last entry.
 };
 
+// The action that should be taken when an ecryptfs user home which needs
+// migration is detected. This must match the order/values of the
+// EcryptfsMigrationStrategy policy.
+enum class EcryptfsMigrationAction {
+  // Don't migrate.
+  DISALLOW_ARC_NO_MIGRATION,
+  // Migrate.
+  MIGRATE,
+  // Wipe the user home and start again.
+  WIPE,
+  // Ask the user if migration should be performed.
+  ASK_USER,
+  // Last value for validity checks.
+  COUNT
+};
+
 // Delay for transferring the auth cache to the system profile.
 const long int kAuthCacheTransferDelayMs = 2000;
 
@@ -210,6 +237,30 @@
   return true;
 }
 
+// Decodes the EcryptfsMigrationStrategy user policy into the
+// EcryptfsMigrationAction enum. If the policy is present, returns true and sets
+// *|out_value|. Otherwise, returns false.
+bool DecodeMigrationActionFromPolicy(
+    const enterprise_management::CloudPolicySettings* policy,
+    EcryptfsMigrationAction* out_value) {
+  if (!policy->has_ecryptfsmigrationstrategy())
+    return false;
+
+  const enterprise_management::IntegerPolicyProto& policy_proto =
+      policy->ecryptfsmigrationstrategy();
+  if (!policy_proto.has_value())
+    return false;
+
+  if (policy_proto.value() < 0 ||
+      policy_proto.value() >=
+          static_cast<int64_t>(EcryptfsMigrationAction::COUNT)) {
+    return false;
+  }
+
+  *out_value = static_cast<EcryptfsMigrationAction>(policy_proto.value());
+  return true;
+}
+
 }  // namespace
 
 // static
@@ -534,6 +585,14 @@
   login_performer_->PerformLogin(user_context, auth_mode);
 }
 
+void ExistingUserController::ContinuePerformLoginWithoutMigration(
+    LoginPerformer::AuthorizationMode auth_mode,
+    const UserContext& user_context) {
+  UserContext user_context_ecryptfs = user_context;
+  user_context_ecryptfs.SetIsForcingDircrypto(false);
+  ContinuePerformLogin(auth_mode, user_context_ecryptfs);
+}
+
 void ExistingUserController::MigrateUserData(const std::string& old_password) {
   // LoginPerformer instance has state of the user so it should exist.
   if (login_performer_.get()) {
@@ -923,7 +982,129 @@
 void ExistingUserController::OnOldEncryptionDetected(
     const UserContext& user_context,
     bool has_incomplete_migration) {
-  ShowEncryptionMigrationScreen(user_context, has_incomplete_migration);
+  if (has_incomplete_migration) {
+    // If migration was incomplete, continue migration without checking user
+    // policy.
+    ShowEncryptionMigrationScreen(user_context, has_incomplete_migration);
+    return;
+  }
+
+  if (user_context.GetUserType() == user_manager::USER_TYPE_ARC_KIOSK_APP) {
+    // For ARC kiosk, don't check user policy.
+    // Note that migration will start immediately without asking the user - this
+    // is currently checked in EncryptionMigrationScreenHandler.
+    ShowEncryptionMigrationScreen(user_context,
+                                  false /* has_incomplete_migration */);
+    return;
+  }
+
+  // Fetch user policy.
+  policy::DeviceManagementService* const device_management_service =
+      g_browser_process->platform_part()
+          ->browser_policy_connector_chromeos()
+          ->device_management_service();
+  // Use signin profile request context
+  net::URLRequestContextGetter* const signin_profile_context =
+      ProfileHelper::GetSigninProfile()->GetRequestContext();
+  auto cloud_policy_client = base::MakeUnique<policy::CloudPolicyClient>(
+      std::string() /* machine_id */, std::string() /* machine_model */,
+      device_management_service, signin_profile_context,
+      nullptr /* signing_service */);
+  pre_signin_policy_fetcher_ = base::MakeUnique<policy::PreSigninPolicyFetcher>(
+      DBusThreadManager::Get()->GetCryptohomeClient(),
+      DBusThreadManager::Get()->GetSessionManagerClient(),
+      std::move(cloud_policy_client), user_context.GetAccountId(),
+      cryptohome::KeyDefinition(user_context.GetKey()->GetSecret(),
+                                std::string(), cryptohome::PRIV_DEFAULT));
+  pre_signin_policy_fetcher_->FetchPolicy(
+      base::BindOnce(&ExistingUserController::OnPolicyFetchResult,
+                     weak_factory_.GetWeakPtr(), user_context));
+}
+
+void ExistingUserController::OnPolicyFetchResult(
+    const UserContext& user_context,
+    PolicyFetchResult result,
+    std::unique_ptr<enterprise_management::CloudPolicySettings>
+        policy_payload) {
+  EcryptfsMigrationAction action =
+      EcryptfsMigrationAction::DISALLOW_ARC_NO_MIGRATION;
+  if (result == PolicyFetchResult::NO_POLICY) {
+    // There was no policy, the user is unmanaged. They get to choose themselves
+    // if they'd like to migrate.
+    VLOG(1) << "Policy pre-fetch result: No user policy present";
+    action = EcryptfsMigrationAction::ASK_USER;
+  } else if (result == PolicyFetchResult::SUCCESS) {
+    // User policy was retreived, adhere to it.
+    VLOG(1) << "Policy pre-fetch result: User policy fetched";
+    if (!DecodeMigrationActionFromPolicy(policy_payload.get(), &action)) {
+      // User policy was present, but the EcryptfsMigrationStrategy policy value
+      // was not there. Stay on the safe side and don't start migration.
+      action = EcryptfsMigrationAction::DISALLOW_ARC_NO_MIGRATION;
+    }
+  } else {
+    // We don't know if the user has policy or not. Stay on the safe side and
+    // don't start migration.
+    VLOG(1) << "Policy pre-fetch: User policy could not be fetched. Result: "
+            << static_cast<int>(result);
+    action = EcryptfsMigrationAction::DISALLOW_ARC_NO_MIGRATION;
+  }
+  VLOG(1) << "Migration action: " << static_cast<int>(action);
+
+  switch (action) {
+    case EcryptfsMigrationAction::MIGRATE:
+      // TODO(pmarko): Reusing resume may be wrong from UI perspective, in case
+      // we show a UI mentioning "resume"/"interrupted". If that's the case,
+      // introduce an enum instead of the bool parameter to choose between
+      // ask_user/continue_interrupted_migration/start_migration.
+      // Otherwise, at least rename the bool parameter to indicate that this may
+      // not only mean resuming.
+      ShowEncryptionMigrationScreen(user_context, true);
+      break;
+
+    case EcryptfsMigrationAction::ASK_USER:
+      ShowEncryptionMigrationScreen(user_context, false);
+      break;
+
+    case EcryptfsMigrationAction::WIPE:
+      cryptohome::AsyncMethodCaller::GetInstance()->AsyncRemove(
+          cryptohome::Identification(user_context.GetAccountId()),
+          base::Bind(&ExistingUserController::WipePerformed,
+                     weak_factory_.GetWeakPtr(), user_context));
+      break;
+
+    case EcryptfsMigrationAction::DISALLOW_ARC_NO_MIGRATION:
+    // Fall-through intended.
+    default:
+      ContinuePerformLoginWithoutMigration(login_performer_->auth_mode(),
+                                           user_context);
+      break;
+  }
+}
+
+void ExistingUserController::WipePerformed(const UserContext& user_context,
+                                           bool success,
+                                           cryptohome::MountError return_code) {
+  if (!success) {
+    LOG(ERROR) << "Removal of cryptohome for "
+               << user_context.GetAccountId().Serialize()
+               << " failed, return code: " << return_code;
+  }
+
+  // Let the user authenticate online because we lose the OAuth token by
+  // removing the user's cryptohome.  Without this, the user can sign-in offline
+  // but after sign-in would immediately see the "sign-in details are out of
+  // date" error message and be prompted to sign out.
+
+  // Save the necessity to sign-in online into UserManager in case the user
+  // aborts the online flow.
+  user_manager::UserManager::Get()->SaveForceOnlineSignin(
+      user_context.GetAccountId(), true);
+  host_->OnPreferencesChanged();
+
+  // Start online sign-in UI for the user.
+  is_login_in_progress_ = false;
+  login_performer_.reset();
+  login_display_->ShowSigninUI(user_context.GetAccountId().GetUserEmail());
 }
 
 void ExistingUserController::WhiteListCheckFailed(const std::string& email) {
diff --git a/chrome/browser/chromeos/login/existing_user_controller.h b/chrome/browser/chromeos/login/existing_user_controller.h
index ebf9e1e72..48d0a0f 100644
--- a/chrome/browser/chromeos/login/existing_user_controller.h
+++ b/chrome/browser/chromeos/login/existing_user_controller.h
@@ -23,6 +23,7 @@
 #include "chrome/browser/chromeos/login/session/user_session_manager.h"
 #include "chrome/browser/chromeos/login/signin/token_handle_util.h"
 #include "chrome/browser/chromeos/login/ui/login_display.h"
+#include "chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h"
 #include "chrome/browser/chromeos/settings/cros_settings.h"
 #include "chrome/browser/chromeos/settings/device_settings_service.h"
 #include "chromeos/login/auth/login_performer.h"
@@ -31,6 +32,7 @@
 #include "components/user_manager/user.h"
 #include "content/public/browser/notification_observer.h"
 #include "content/public/browser/notification_registrar.h"
+#include "third_party/cros_system_api/dbus/cryptohome/dbus-constants.h"
 #include "ui/gfx/geometry/rect.h"
 #include "url/gurl.h"
 
@@ -38,6 +40,10 @@
 class ListValue;
 }
 
+namespace enterprise_management {
+class CloudPolicySettings;
+}
+
 namespace chromeos {
 
 class BootstrapUserContextInitializer;
@@ -226,6 +232,12 @@
   void ContinuePerformLogin(LoginPerformer::AuthorizationMode auth_mode,
                             const UserContext& user_context);
 
+  // Removes the constraint that user home mount requires ext4 encryption from
+  // |user_context|, then calls login() on previously-used |login_performer|.
+  void ContinuePerformLoginWithoutMigration(
+      LoginPerformer::AuthorizationMode auth_mode,
+      const UserContext& user_context);
+
   // Updates the |login_display_| attached to this controller.
   void UpdateLoginDisplay(const user_manager::UserList& users);
 
@@ -280,6 +292,19 @@
       const AccountId&,
       TokenHandleUtil::TokenHandleStatus token_handle_status);
 
+  // Called on completition of a pre-signin policy fetch, which is performed to
+  // check if there is a user policy governing migration action.
+  void OnPolicyFetchResult(
+      const UserContext& user_context,
+      policy::PreSigninPolicyFetcher::PolicyFetchResult result,
+      std::unique_ptr<enterprise_management::CloudPolicySettings>
+          policy_payload);
+
+  // Called when cryptohome wipe has finished.
+  void WipePerformed(const UserContext& user_context,
+                     bool success,
+                     cryptohome::MountError return_code);
+
   // Clear the recorded displayed email, displayed name, given name so it won't
   // affect any future attempts.
   void ClearRecordedNames();
@@ -395,6 +420,8 @@
 
   std::unique_ptr<TokenHandleUtil> token_handle_util_;
 
+  std::unique_ptr<policy::PreSigninPolicyFetcher> pre_signin_policy_fetcher_;
+
   // Factory of callbacks.
   base::WeakPtrFactory<ExistingUserController> weak_factory_;
 
diff --git a/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.cc b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.cc
new file mode 100644
index 0000000..638c487
--- /dev/null
+++ b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.cc
@@ -0,0 +1,217 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h"
+
+#include <stddef.h>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_util.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/stringprintf.h"
+#include "base/task_runner_util.h"
+#include "chromeos/cryptohome/cryptohome_parameters.h"
+#include "chromeos/dbus/cryptohome_client.h"
+
+namespace policy {
+
+namespace {
+
+// Path within |user_policy_key_dir_| that contains the policy key.
+// "%s" must be substituted with the sanitized username.
+const base::FilePath::CharType kPolicyKeyFile[] =
+    FILE_PATH_LITERAL("%s/policy.pub");
+
+// Maximum key size that will be loaded, in bytes.
+const size_t kKeySizeLimit = 16 * 1024;
+
+// Failures that can happen when loading the policy key,
+// This enum is used to define the buckets for an enumerated UMA histogram.
+// Hence,
+//   (a) existing enumerated constants should never be deleted or reordered, and
+//   (b) new constants should only be appended at the end of the enumeration.
+enum class ValidationFailure {
+  DBUS,
+  LOAD_KEY,
+
+  // Number of histogram buckets. Has to be the last element.
+  MAX_VALUE,
+};
+
+void SampleValidationFailure(ValidationFailure sample) {
+  UMA_HISTOGRAM_ENUMERATION("Enterprise.UserPolicyValidationFailure", sample,
+                            ValidationFailure::MAX_VALUE);
+}
+
+}  // namespace
+
+CachedPolicyKeyLoaderChromeOS::CachedPolicyKeyLoaderChromeOS(
+    chromeos::CryptohomeClient* cryptohome_client,
+    scoped_refptr<base::SequencedTaskRunner> task_runner,
+    const AccountId& account_id,
+    const base::FilePath& user_policy_key_dir)
+    : task_runner_(task_runner),
+      cryptohome_client_(cryptohome_client),
+      account_id_(account_id),
+      user_policy_key_dir_(user_policy_key_dir),
+      weak_factory_(this) {}
+
+CachedPolicyKeyLoaderChromeOS::~CachedPolicyKeyLoaderChromeOS() {}
+
+void CachedPolicyKeyLoaderChromeOS::EnsurePolicyKeyLoaded(
+    base::OnceClosure callback) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (key_loaded_) {
+    std::move(callback).Run();
+    return;
+  }
+
+  key_loaded_callbacks_.push_back(std::move(callback));
+
+  // If a key load is in progress, the callback will be called once it finishes.
+  // No need to trigger another one.
+  if (key_load_in_progress_)
+    return;
+
+  key_load_in_progress_ = true;
+
+  // Get the hashed username that's part of the key's path, to determine
+  // |cached_policy_key_path_|.
+  cryptohome_client_->GetSanitizedUsername(
+      cryptohome::Identification(account_id_),
+      base::Bind(&CachedPolicyKeyLoaderChromeOS::OnGetSanitizedUsername,
+                 weak_factory_.GetWeakPtr()));
+}
+
+bool CachedPolicyKeyLoaderChromeOS::LoadPolicyKeyImmediately() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  const std::string sanitized_username =
+      cryptohome_client_->BlockingGetSanitizedUsername(
+          cryptohome::Identification(account_id_));
+  if (sanitized_username.empty())
+    return false;
+
+  cached_policy_key_path_ = user_policy_key_dir_.Append(
+      base::StringPrintf(kPolicyKeyFile, sanitized_username.c_str()));
+  cached_policy_key_ = LoadPolicyKey(cached_policy_key_path_);
+  key_loaded_ = true;
+  return true;
+}
+
+void CachedPolicyKeyLoaderChromeOS::ReloadPolicyKey(
+    base::OnceClosure callback) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  key_loaded_callbacks_.push_back(std::move(callback));
+
+  if (key_load_in_progress_) {
+    // When a load is in progress, cancel the current load by invalidating weak
+    // pointers and before starting a new load.
+    weak_factory_.InvalidateWeakPtrs();
+  }
+
+  key_load_in_progress_ = true;
+
+  if (cached_policy_key_path_.empty()) {
+    // Get the hashed username that's part of the key's path, to determine
+    // |cached_policy_key_path_|.
+    cryptohome_client_->GetSanitizedUsername(
+        cryptohome::Identification(account_id_),
+        base::Bind(&CachedPolicyKeyLoaderChromeOS::OnGetSanitizedUsername,
+                   weak_factory_.GetWeakPtr()));
+  } else {
+    TriggerLoadPolicyKey();
+  }
+}
+
+// static
+std::string CachedPolicyKeyLoaderChromeOS::LoadPolicyKey(
+    const base::FilePath& path) {
+  std::string key;
+
+  if (!base::PathExists(path)) {
+    // There is no policy key the first time that a user fetches policy. If
+    // |path| does not exist then that is the most likely scenario, so there's
+    // no need to sample a failure.
+    VLOG(1) << "No key at " << path.value();
+    return key;
+  }
+
+  const bool read_success =
+      base::ReadFileToStringWithMaxSize(path, &key, kKeySizeLimit);
+  // If the read was successful and the file size is 0 or if the read fails
+  // due to file size exceeding |kKeySizeLimit|, log error.
+  if ((read_success && key.length() == 0) ||
+      (!read_success && key.length() == kKeySizeLimit)) {
+    LOG(ERROR) << "Key at " << path.value()
+               << (read_success ? " is empty." : " exceeds size limit");
+    key.clear();
+  } else if (!read_success) {
+    LOG(ERROR) << "Failed to read key at " << path.value();
+  }
+
+  if (key.empty())
+    SampleValidationFailure(ValidationFailure::LOAD_KEY);
+
+  return key;
+}
+
+void CachedPolicyKeyLoaderChromeOS::TriggerLoadPolicyKey() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  base::PostTaskAndReplyWithResult(
+      task_runner_.get(), FROM_HERE,
+      base::BindOnce(&CachedPolicyKeyLoaderChromeOS::LoadPolicyKey,
+                     cached_policy_key_path_),
+      base::BindOnce(&CachedPolicyKeyLoaderChromeOS::OnPolicyKeyLoaded,
+                     weak_factory_.GetWeakPtr()));
+}
+
+void CachedPolicyKeyLoaderChromeOS::OnPolicyKeyLoaded(const std::string& key) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  cached_policy_key_ = key;
+  key_loaded_ = true;
+  key_load_in_progress_ = false;
+
+  NotifyAndClearCallbacks();
+}
+
+void CachedPolicyKeyLoaderChromeOS::OnGetSanitizedUsername(
+    chromeos::DBusMethodCallStatus call_status,
+    const std::string& sanitized_username) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  if (call_status != chromeos::DBUS_METHOD_CALL_SUCCESS ||
+      sanitized_username.empty()) {
+    SampleValidationFailure(ValidationFailure::DBUS);
+
+    // Don't bother trying to load a key if we don't know where it is - just
+    // signal that the load attempt has finished.
+    key_load_in_progress_ = false;
+    NotifyAndClearCallbacks();
+
+    return;
+  }
+
+  cached_policy_key_path_ = user_policy_key_dir_.Append(
+      base::StringPrintf(kPolicyKeyFile, sanitized_username.c_str()));
+  TriggerLoadPolicyKey();
+}
+
+void CachedPolicyKeyLoaderChromeOS::NotifyAndClearCallbacks() {
+  std::vector<base::OnceClosure> callbacks = std::move(key_loaded_callbacks_);
+  key_loaded_callbacks_.clear();
+
+  for (auto& callback : callbacks)
+    std::move(callback).Run();
+}
+
+}  // namespace policy
diff --git a/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h
new file mode 100644
index 0000000..a3eee4f
--- /dev/null
+++ b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h
@@ -0,0 +1,106 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_POLICY_CACHED_POLICY_KEY_LOADER_CHROMEOS_H_
+#define CHROME_BROWSER_CHROMEOS_POLICY_CACHED_POLICY_KEY_LOADER_CHROMEOS_H_
+
+#include <string>
+#include <vector>
+
+#include "base/callback_forward.h"
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "chromeos/dbus/dbus_method_call_status.h"
+#include "components/signin/core/account_id/account_id.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace chromeos {
+class CryptohomeClient;
+}
+
+namespace policy {
+
+// Loads policy key cached by session_manager.
+class CachedPolicyKeyLoaderChromeOS {
+ public:
+  CachedPolicyKeyLoaderChromeOS(
+      chromeos::CryptohomeClient* cryptohome_client,
+      scoped_refptr<base::SequencedTaskRunner> task_runner,
+      const AccountId& account_id,
+      const base::FilePath& user_policy_key_dir);
+  ~CachedPolicyKeyLoaderChromeOS();
+
+  // Invokes |callback| after loading |policy_key_|, if it hasn't been loaded
+  // yet; otherwise invokes |callback| immediately.
+  // This method may not be called while a load is currently in progres.
+  void EnsurePolicyKeyLoaded(base::OnceClosure callback);
+
+  // Invokes |callback| after reloading |policy_key_|.
+  void ReloadPolicyKey(base::OnceClosure callback);
+
+  // Loads the policy key synchronously on the current thread.
+  bool LoadPolicyKeyImmediately();
+
+  const std::string& cached_policy_key() const { return cached_policy_key_; }
+
+ private:
+  // Reads and returns the contents of |path|. If the path does not exist or the
+  // key is empty/not readable, returns an empty string. Also samples the
+  // validation failure UMA stat.
+  static std::string LoadPolicyKey(const base::FilePath& path);
+
+  // Posts a task to load the policy key on |task_runner_|. OnPolicyKeyLoaded()
+  // will be called on completition.
+  void TriggerLoadPolicyKey();
+
+  // Callback for the key reloading.
+  void OnPolicyKeyLoaded(const std::string& key);
+
+  // Callback for getting the sanitized username from |cryptohome_client_|.
+  void OnGetSanitizedUsername(chromeos::DBusMethodCallStatus call_status,
+                              const std::string& sanitized_username);
+
+  void NotifyAndClearCallbacks();
+
+  // Task runner for background file operations.
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
+
+  chromeos::CryptohomeClient* const cryptohome_client_;
+  const AccountId account_id_;
+  const base::FilePath user_policy_key_dir_;
+  base::FilePath cached_policy_key_path_;
+
+  // The current key used to verify signatures of policy. This value is loaded
+  // from the key cache file (which is owned and kept up to date by the Chrome
+  // OS session manager).
+  std::string cached_policy_key_;
+
+  // This will be true when a previous key load succeeded. It signals that
+  // EnsurePolicyKeyLoaded can call the passed callback immediately.
+  bool key_loaded_ = false;
+
+  // This will be true when an asynchronous key load (started by
+  // EnsurePolicyKeyLoaded or ReloadPolicyKey) is in progress.
+  bool key_load_in_progress_ = false;
+
+  // All callbacks that should be called when the async key load finishes.
+  std::vector<base::OnceClosure> key_loaded_callbacks_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  // Must be the last memeber.
+  base::WeakPtrFactory<CachedPolicyKeyLoaderChromeOS> weak_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(CachedPolicyKeyLoaderChromeOS);
+};
+
+}  // namespace policy
+
+#endif  // CHROME_BROWSER_CHROMEOS_POLICY_CACHED_POLICY_KEY_LOADER_CHROMEOS_H_
diff --git a/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos_unittest.cc b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos_unittest.cc
new file mode 100644
index 0000000..320b993
--- /dev/null
+++ b/chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos_unittest.cc
@@ -0,0 +1,207 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h"
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/ptr_util.h"
+#include "base/single_thread_task_runner.h"
+#include "base/test/scoped_task_environment.h"
+#include "chromeos/dbus/fake_cryptohome_client.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace policy {
+
+namespace {
+
+const char kDummyKey1[] = "dummy-key-1";
+const char kDummyKey2[] = "dummy-key-2";
+const char kTestUserName[] = "test-user@example.com";
+
+class CachedPolicyKeyLoaderTest : public testing::Test {
+ protected:
+  CachedPolicyKeyLoaderTest() = default;
+
+  void SetUp() override {
+    ASSERT_TRUE(tmp_dir_.CreateUniqueTempDir());
+
+    cached_policy_key_loader_ = base::MakeUnique<CachedPolicyKeyLoaderChromeOS>(
+        &cryptohome_client_, scoped_task_environment_.GetMainThreadTaskRunner(),
+        account_id_, user_policy_keys_dir());
+  }
+
+  void StoreUserPolicyKey(const std::string& public_key) {
+    ASSERT_TRUE(base::CreateDirectory(user_policy_key_file().DirName()));
+    ASSERT_EQ(static_cast<int>(public_key.size()),
+              base::WriteFile(user_policy_key_file(), public_key.data(),
+                              public_key.size()));
+  }
+
+  base::FilePath user_policy_keys_dir() const {
+    return tmp_dir_.GetPath().AppendASCII("var_run_user_policy");
+  }
+
+  base::FilePath user_policy_key_file() const {
+    const std::string sanitized_username =
+        chromeos::CryptohomeClient::GetStubSanitizedUsername(cryptohome_id_);
+    return user_policy_keys_dir()
+        .AppendASCII(sanitized_username)
+        .AppendASCII("policy.pub");
+  }
+
+  void OnPolicyKeyLoaded() { ++policy_key_loaded_callback_invocations_; }
+
+  void CallEnsurePolicyKeyLoaded() {
+    cached_policy_key_loader_->EnsurePolicyKeyLoaded(base::Bind(
+        &CachedPolicyKeyLoaderTest::OnPolicyKeyLoaded, base::Unretained(this)));
+  }
+
+  void CallReloadPolicyKey() {
+    cached_policy_key_loader_->ReloadPolicyKey(base::Bind(
+        &CachedPolicyKeyLoaderTest::OnPolicyKeyLoaded, base::Unretained(this)));
+  }
+
+  base::test::ScopedTaskEnvironment scoped_task_environment_ = {
+      base::test::ScopedTaskEnvironment::MainThreadType::UI};
+  chromeos::FakeCryptohomeClient cryptohome_client_;
+  const AccountId account_id_ = AccountId::FromUserEmail(kTestUserName);
+  const cryptohome::Identification cryptohome_id_ =
+      cryptohome::Identification(account_id_);
+
+  std::unique_ptr<CachedPolicyKeyLoaderChromeOS> cached_policy_key_loader_;
+
+  // Counts how many times OnPolicyKeyLoaded() has been invoked.
+  int policy_key_loaded_callback_invocations_ = 0;
+
+ private:
+  base::ScopedTempDir tmp_dir_;
+
+  DISALLOW_COPY_AND_ASSIGN(CachedPolicyKeyLoaderTest);
+};
+
+// Loads an existing key file using EnsurePolicyKeyLoaded.
+TEST_F(CachedPolicyKeyLoaderTest, Basic) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  CallEnsurePolicyKeyLoaded();
+
+  scoped_task_environment_.RunUntilIdle();
+
+  EXPECT_EQ(1, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+}
+
+// Tries to load key using EnsurePolicyKeyLoaded, but the key is missing.
+TEST_F(CachedPolicyKeyLoaderTest, KeyFileMissing) {
+  CallEnsurePolicyKeyLoaded();
+
+  scoped_task_environment_.RunUntilIdle();
+
+  EXPECT_EQ(1, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(std::string(), cached_policy_key_loader_->cached_policy_key());
+}
+
+// Loads an existing key file using EnsurePolicyKeyLoaded. While the load is in
+// progress, EnsurePolicyKeyLoaded is called again.
+TEST_F(CachedPolicyKeyLoaderTest, EnsureCalledTwice) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  CallEnsurePolicyKeyLoaded();
+  CallEnsurePolicyKeyLoaded();
+
+  EXPECT_EQ(0, policy_key_loaded_callback_invocations_);
+
+  scoped_task_environment_.RunUntilIdle();
+
+  // We expect that the callback was called for each EnsurePolicyKeyLoaded
+  // invocation.
+  EXPECT_EQ(2, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+}
+
+// After a successful load, changes the policy key file and calls
+// EnsurePolicyKeyLoaded.
+TEST_F(CachedPolicyKeyLoaderTest, EnsureAfterSuccessfulLoad) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  CallEnsurePolicyKeyLoaded();
+  EXPECT_EQ(0, policy_key_loaded_callback_invocations_);
+
+  scoped_task_environment_.RunUntilIdle();
+
+  EXPECT_EQ(1, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+
+  // Change the policy key file.
+  StoreUserPolicyKey(kDummyKey2);
+
+  CallEnsurePolicyKeyLoaded();
+
+  scoped_task_environment_.RunUntilIdle();
+
+  // We expect that the callback was invoked, but that the cached policy key is
+  // still the old one. EnsurePolicyKeyLoaded is not supposed to reload the key.
+  EXPECT_EQ(2, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+}
+
+// After a successful load, changes the policy key file and calls
+// ReloadPolicyKey.
+TEST_F(CachedPolicyKeyLoaderTest, ReloadAfterEnsure) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  CallEnsurePolicyKeyLoaded();
+  EXPECT_EQ(0, policy_key_loaded_callback_invocations_);
+
+  scoped_task_environment_.RunUntilIdle();
+
+  EXPECT_EQ(1, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+
+  // Change the policy key file.
+  StoreUserPolicyKey(kDummyKey2);
+
+  CallReloadPolicyKey();
+
+  scoped_task_environment_.RunUntilIdle();
+
+  // We expect that the callback was invoked, and that the policy key file has
+  // been reloded, so the cached policy key is now the new policy key.
+  EXPECT_EQ(2, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey2, cached_policy_key_loader_->cached_policy_key());
+}
+
+// During a load, ReloadPolicyKeyFile is invoked.
+TEST_F(CachedPolicyKeyLoaderTest, ReloadWhileLoading) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  CallEnsurePolicyKeyLoaded();
+  CallReloadPolicyKey();
+  EXPECT_EQ(0, policy_key_loaded_callback_invocations_);
+
+  scoped_task_environment_.RunUntilIdle();
+
+  // We expect that the callback was called for both the EnsurePolicyKeyLoaded
+  // and ReloadPolicyKey invocation.
+  EXPECT_EQ(2, policy_key_loaded_callback_invocations_);
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+}
+
+// Synchronous load on the caller's thread.
+TEST_F(CachedPolicyKeyLoaderTest, LoadImmediately) {
+  StoreUserPolicyKey(kDummyKey1);
+
+  cached_policy_key_loader_->LoadPolicyKeyImmediately();
+
+  EXPECT_EQ(kDummyKey1, cached_policy_key_loader_->cached_policy_key());
+}
+
+}  // namespace
+
+}  // namespace policy
diff --git a/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.cc b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.cc
new file mode 100644
index 0000000..f74351bd
--- /dev/null
+++ b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.cc
@@ -0,0 +1,322 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/location.h"
+#include "base/logging.h"
+#include "base/memory/ptr_util.h"
+#include "base/path_service.h"
+#include "base/sequenced_task_runner.h"
+#include "base/task_scheduler/post_task.h"
+#include "base/task_scheduler/task_traits.h"
+#include "base/time/time.h"
+#include "chromeos/chromeos_paths.h"
+#include "chromeos/cryptohome/homedir_methods.h"
+#include "chromeos/dbus/cryptohome_client.h"
+#include "components/policy/core/common/cloud/cloud_policy_constants.h"
+#include "components/policy/proto/device_management_backend.pb.h"
+#include "google_apis/gaia/gaia_auth_util.h"
+
+namespace em = enterprise_management;
+
+namespace policy {
+
+namespace {
+
+// We will abort fresh policy fetch after this time and use cached policy.
+const int kPolicyFetchTimeoutSecs = 10;
+
+// Traits for the tasks posted in pre-signin policy fetch. As this blocks
+// signin, the tasks have user-visible priority.
+constexpr base::TaskTraits kTaskTraits = {base::MayBlock(),
+                                          base::TaskPriority::USER_VISIBLE};
+}  // namespace
+
+PreSigninPolicyFetcher::PreSigninPolicyFetcher(
+    chromeos::CryptohomeClient* cryptohome_client,
+    chromeos::SessionManagerClient* session_manager_client,
+    std::unique_ptr<CloudPolicyClient> cloud_policy_client,
+    const AccountId& account_id,
+    const cryptohome::KeyDefinition& auth_key)
+    : cryptohome_client_(cryptohome_client),
+      session_manager_client_(session_manager_client),
+      cloud_policy_client_(std::move(cloud_policy_client)),
+      account_id_(account_id),
+      auth_key_(auth_key),
+      task_runner_(base::CreateSequencedTaskRunnerWithTraits(kTaskTraits)),
+      weak_ptr_factory_(this) {}
+
+PreSigninPolicyFetcher ::~PreSigninPolicyFetcher() {}
+
+void PreSigninPolicyFetcher::FetchPolicy(PolicyFetchResultCallback callback) {
+  DCHECK(callback_.is_null());
+  callback_ = std::move(callback);
+
+  cryptohome::MountRequest mount;
+  mount.set_hidden_mount(true);
+  cryptohome::HomedirMethods::GetInstance()->MountEx(
+      cryptohome::Identification(account_id_),
+      cryptohome::Authorization(auth_key_), mount,
+      base::Bind(&PreSigninPolicyFetcher::OnMountTemporaryUserHome,
+                 weak_ptr_factory_.GetWeakPtr()));
+}
+
+bool PreSigninPolicyFetcher::ForceTimeoutForTesting() {
+  if (!policy_fetch_timeout_.IsRunning())
+    return false;
+
+  policy_fetch_timeout_.Stop();
+  OnPolicyFetchTimeout();
+
+  return true;
+}
+
+void PreSigninPolicyFetcher::OnMountTemporaryUserHome(
+    bool success,
+    cryptohome::MountError return_code,
+    const std::string& mount_hash) {
+  if (!success || return_code != cryptohome::MOUNT_ERROR_NONE) {
+    LOG(ERROR) << "Temporary user home mount failed.";
+    NotifyCallback(PolicyFetchResult::ERROR, nullptr);
+    return;
+  }
+
+  session_manager_client_->RetrievePolicyForUserWithoutSession(
+      cryptohome::Identification(account_id_),
+      base::Bind(&PreSigninPolicyFetcher::OnCachedPolicyRetrieved,
+                 weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PreSigninPolicyFetcher::OnCachedPolicyRetrieved(
+    const std::string& policy_blob,
+    RetrievePolicyResponseType retrieve_policy_response) {
+  // We only need the cached policy key if there was policy.
+  if (!policy_blob.empty()) {
+    base::FilePath policy_key_dir;
+    CHECK(PathService::Get(chromeos::DIR_USER_POLICY_KEYS, &policy_key_dir));
+    cached_policy_key_loader_ = base::MakeUnique<CachedPolicyKeyLoaderChromeOS>(
+        cryptohome_client_, task_runner_, account_id_, policy_key_dir);
+    cached_policy_key_loader_->EnsurePolicyKeyLoaded(base::Bind(
+        &PreSigninPolicyFetcher::OnPolicyKeyLoaded,
+        weak_ptr_factory_.GetWeakPtr(), policy_blob, retrieve_policy_response));
+  } else {
+    // Skip and pretend we've loaded policy key. We won't need it anyway,
+    // because there is no policy to validate.
+    OnPolicyKeyLoaded(policy_blob, retrieve_policy_response);
+  }
+}
+
+void PreSigninPolicyFetcher::OnPolicyKeyLoaded(
+    const std::string& policy_blob,
+    RetrievePolicyResponseType retrieve_policy_response) {
+  cryptohome_client_->Unmount(base::Bind(
+      &PreSigninPolicyFetcher::OnUnmountTemporaryUserHome,
+      weak_ptr_factory_.GetWeakPtr(), policy_blob, retrieve_policy_response));
+}
+
+void PreSigninPolicyFetcher::OnUnmountTemporaryUserHome(
+    const std::string& policy_blob,
+    RetrievePolicyResponseType retrieve_policy_response,
+    chromeos::DBusMethodCallStatus unmount_call_status,
+    bool unmount_success) {
+  if (unmount_call_status != chromeos::DBUS_METHOD_CALL_SUCCESS ||
+      !unmount_success) {
+    // The temporary userhome mount could not be unmounted. Log an error and
+    // continue, and hope that the unmount will be successful on the next mount
+    // (temporary user homes are automatically unmounted by cryptohomed on every
+    // mount request).
+    LOG(ERROR) << "Couldn't unmount temporary mount point";
+  }
+
+  if (retrieve_policy_response != RetrievePolicyResponseType::SUCCESS) {
+    NotifyCallback(PolicyFetchResult::ERROR, nullptr);
+    return;
+  }
+
+  if (policy_blob.empty()) {
+    VLOG(1) << "No cached policy.";
+    NotifyCallback(PolicyFetchResult::NO_POLICY, nullptr);
+    return;
+  }
+
+  // Parse policy.
+  auto policy = base::MakeUnique<em::PolicyFetchResponse>();
+  if (!policy->ParseFromString(policy_blob)) {
+    NotifyCallback(PolicyFetchResult::ERROR, nullptr);
+    return;
+  }
+
+  // Before validating, check that we have a cached policy key.
+  if (cached_policy_key_loader_->cached_policy_key().empty()) {
+    LOG(ERROR) << "No cached policy key loaded.";
+    NotifyCallback(PolicyFetchResult::ERROR, nullptr);
+    return;
+  }
+
+  // Validate policy from session_manager.
+  UserCloudPolicyValidator::StartValidation(
+      CreateValidatorForCachedPolicy(std::move(policy)),
+      base::Bind(&PreSigninPolicyFetcher::OnCachedPolicyValidated,
+                 weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PreSigninPolicyFetcher::OnCachedPolicyValidated(
+    UserCloudPolicyValidator* validator) {
+  if (!validator->success()) {
+    NotifyCallback(PolicyFetchResult::ERROR, nullptr);
+    return;
+  }
+
+  policy_data_ = std::move(validator->policy_data());
+  policy_payload_ = std::move(validator->payload());
+
+  if (account_id_.GetAccountType() == AccountType::ACTIVE_DIRECTORY) {
+    // For AD, we don't support fresh policy fetch at the moment. Simply exit
+    // with cached policy.
+    NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+    return;
+  }
+
+  // Try to retrieve fresh policy.
+  cloud_policy_client_->SetupRegistration(policy_data_->request_token(),
+                                          policy_data_->device_id());
+  cloud_policy_client_->AddPolicyTypeToFetch(
+      dm_protocol::kChromeUserPolicyType,
+      std::string() /* settings_entity_id */);
+  if (policy_data_->has_public_key_version()) {
+    cloud_policy_client_->set_public_key_version(
+        policy_data_->public_key_version());
+  }
+  cloud_policy_client_->AddObserver(this);
+
+  // Start a timer that will limit how long we wait for fresh policy.
+  policy_fetch_timeout_.Start(
+      FROM_HERE, base::TimeDelta::FromSeconds(kPolicyFetchTimeoutSecs),
+      base::Bind(&PreSigninPolicyFetcher::OnPolicyFetchTimeout,
+                 weak_ptr_factory_.GetWeakPtr()));
+
+  cloud_policy_client_->FetchPolicy();
+}
+
+void PreSigninPolicyFetcher::OnPolicyFetched(CloudPolicyClient* client) {
+  policy_fetch_timeout_.Stop();
+
+  const em::PolicyFetchResponse* fetched_policy =
+      cloud_policy_client_->GetPolicyFor(
+          dm_protocol::kChromeUserPolicyType,
+          std::string() /* settings_entity_id */);
+  if (!fetched_policy || !fetched_policy->has_policy_data()) {
+    // policy_payload_ still holds cached policy loaded from session_manager.
+    NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+    return;
+  }
+
+  // Make a copy because there's currently no way to transfer ownership out of
+  // CloudPolicyClient.
+  auto fetched_policy_copy =
+      base::MakeUnique<em::PolicyFetchResponse>(*fetched_policy);
+
+  // Validate fresh policy.
+  UserCloudPolicyValidator::StartValidation(
+      CreateValidatorForFetchedPolicy(std::move(fetched_policy_copy)),
+      base::Bind(&PreSigninPolicyFetcher::OnFetchedPolicyValidated,
+                 weak_ptr_factory_.GetWeakPtr()));
+}
+
+void PreSigninPolicyFetcher::OnRegistrationStateChanged(
+    CloudPolicyClient* client) {
+  // Ignored.
+}
+
+void PreSigninPolicyFetcher::OnClientError(CloudPolicyClient* client) {
+  policy_fetch_timeout_.Stop();
+  VLOG(1) << "Fresh policy fetch failed.";
+  // policy_payload_ still holds cached policy loaded from session_manager.
+  NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+}
+
+void PreSigninPolicyFetcher::OnPolicyFetchTimeout() {
+  VLOG(1) << "Fresh policy fetch timed out.";
+  // Invalidate all weak pointrs so OnPolicyFetched is not called back anymore.
+  weak_ptr_factory_.InvalidateWeakPtrs();
+  NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+}
+
+void PreSigninPolicyFetcher::OnFetchedPolicyValidated(
+    UserCloudPolicyValidator* validator) {
+  if (!validator->success()) {
+    VLOG(1) << "Validation of fetched policy failed.";
+    // Return the cached policy.
+    NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+    return;
+  }
+
+  policy_data_ = std::move(validator->policy_data());
+  policy_payload_ = std::move(validator->payload());
+  NotifyCallback(PolicyFetchResult::SUCCESS, std::move(policy_payload_));
+}
+
+void PreSigninPolicyFetcher::NotifyCallback(
+    PolicyFetchResult result,
+    std::unique_ptr<em::CloudPolicySettings> policy_payload) {
+  // Clean up instances created during the policy fetch procedure.
+  cached_policy_key_loader_.reset();
+  policy_data_.reset();
+  if (cloud_policy_client_) {
+    cloud_policy_client_->RemoveObserver(this);
+  }
+
+  DCHECK(callback_);
+  std::move(callback_).Run(result, std::move(policy_payload));
+}
+
+std::unique_ptr<UserCloudPolicyValidator>
+PreSigninPolicyFetcher::CreateValidatorForCachedPolicy(
+    std::unique_ptr<em::PolicyFetchResponse> policy) {
+  std::unique_ptr<UserCloudPolicyValidator> validator =
+      UserCloudPolicyValidator::Create(std::move(policy), task_runner_);
+
+  validator->ValidatePolicyType(dm_protocol::kChromeUserPolicyType);
+  validator->ValidatePayload();
+
+  if (account_id_.GetAccountType() != AccountType::ACTIVE_DIRECTORY) {
+    // Also validate the user e-mail and the signature (except for authpolicy).
+    validator->ValidateUsername(account_id_.GetUserEmail(), true);
+    validator->ValidateSignature(
+        cached_policy_key_loader_->cached_policy_key());
+  }
+  return validator;
+}
+
+std::unique_ptr<UserCloudPolicyValidator>
+PreSigninPolicyFetcher::CreateValidatorForFetchedPolicy(
+    std::unique_ptr<em::PolicyFetchResponse> policy) {
+  // Configure the validator to validate based on cached policy.
+  std::unique_ptr<UserCloudPolicyValidator> validator =
+      UserCloudPolicyValidator::Create(std::move(policy), task_runner_);
+
+  validator->ValidatePolicyType(dm_protocol::kChromeUserPolicyType);
+  validator->ValidateAgainstCurrentPolicy(
+      policy_data_.get(), CloudPolicyValidatorBase::TIMESTAMP_VALIDATED,
+      CloudPolicyValidatorBase::DM_TOKEN_REQUIRED,
+      CloudPolicyValidatorBase::DEVICE_ID_REQUIRED);
+  validator->ValidatePayload();
+
+  if (account_id_.GetAccountType() != AccountType::ACTIVE_DIRECTORY) {
+    // Also validate the signature.
+    const std::string domain = gaia::ExtractDomainName(
+        gaia::CanonicalizeEmail(account_id_.GetUserEmail()));
+    validator->ValidateSignatureAllowingRotation(
+        cached_policy_key_loader_->cached_policy_key(), domain);
+  }
+  return validator;
+}
+
+}  // namespace policy
diff --git a/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h
new file mode 100644
index 0000000..60c5164
--- /dev/null
+++ b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h
@@ -0,0 +1,164 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_POLICY_PRE_SIGNIN_POLICY_FETCHER_H_
+#define CHROME_BROWSER_CHROMEOS_POLICY_PRE_SIGNIN_POLICY_FETCHER_H_
+
+#include <memory>
+#include <string>
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/timer/timer.h"
+#include "chrome/browser/chromeos/policy/cached_policy_key_loader_chromeos.h"
+#include "chromeos/cryptohome/cryptohome_parameters.h"
+#include "chromeos/dbus/dbus_method_call_status.h"
+#include "chromeos/dbus/session_manager_client.h"
+#include "components/policy/core/common/cloud/cloud_policy_client.h"
+#include "components/policy/core/common/cloud/cloud_policy_validator.h"
+#include "components/signin/core/account_id/account_id.h"
+#include "third_party/cros_system_api/dbus/cryptohome/dbus-constants.h"
+
+namespace base {
+class SequencedTaskRunner;
+}
+
+namespace chromeos {
+class CryptohomeClient;
+}
+
+namespace enterprise_management {
+class CloudPolicySettings;
+class PolicyData;
+class PolicyFetchResponse;
+}  // namespace enterprise_management
+
+namespace policy {
+
+// Performs a policy fetch before actually mounting cryptohome. This is done in
+// the following steps:
+// (1) Mount the user's cryptohome to a temporary location.
+// (2) Retrieve policy from there, along with the policy verification key.
+// (3) Unmount the temporary cryptohome.
+//
+// And if cached policy was found:
+// (4) Validate the cached policy.
+// (5) Try to fetch fresh policy from DMServer.
+// (6) Validate fresh policy.
+//
+// If steps (1)-(4) are successful, the cached policy will be used even if no
+// fresh policy is available.
+class PreSigninPolicyFetcher : public CloudPolicyClient::Observer {
+ public:
+  // The result of the pre-signin policy fetch.
+  enum class PolicyFetchResult {
+    // The policy could not be fetched, e.g. error in cryptohome/session_manager
+    // calls or it could not be validated.
+    ERROR,
+    // The user does not have policy.
+    NO_POLICY,
+    // The policy was successfully fetched. This could be a cached policy or a
+    // fresh policy.
+    SUCCESS,
+  };
+
+  using PolicyFetchResultCallback = base::OnceCallback<void(
+      PolicyFetchResult result,
+      std::unique_ptr<enterprise_management::CloudPolicySettings> policy_data)>;
+
+  PreSigninPolicyFetcher(chromeos::CryptohomeClient* cryptohome_client,
+                         chromeos::SessionManagerClient* session_manager_client,
+                         std::unique_ptr<CloudPolicyClient> cloud_policy_client,
+                         const AccountId& account_id,
+                         const cryptohome::KeyDefinition& auth_key);
+  ~PreSigninPolicyFetcher() override;
+
+  // Start the policy fetch procedure. |callback| will be invoked with the
+  // result.
+  void FetchPolicy(PolicyFetchResultCallback callback);
+
+  // Forces a policy fetch timeout.
+  // Returns true if the timer was running and a timeout was forced. If the
+  // timer was not running, returns false.
+  bool ForceTimeoutForTesting();
+
+ private:
+  using RetrievePolicyResponseType =
+      chromeos::SessionManagerClient::RetrievePolicyResponseType;
+
+  void OnMountTemporaryUserHome(bool success,
+                                cryptohome::MountError return_code,
+                                const std::string& mount_hash);
+
+  void OnCachedPolicyRetrieved(
+      const std::string& policy_blob,
+      RetrievePolicyResponseType retrieve_policy_response);
+
+  void OnPolicyKeyLoaded(const std::string& policy_blob,
+                         RetrievePolicyResponseType retrieve_policy_response);
+
+  void OnUnmountTemporaryUserHome(
+      const std::string& policy_blob,
+      RetrievePolicyResponseType retrieve_policy_response,
+      chromeos::DBusMethodCallStatus unmount_call_status,
+      bool unmount_success);
+
+  void OnCachedPolicyValidated(UserCloudPolicyValidator* validator);
+
+  // CloudPolicyClient::Observer:
+  void OnPolicyFetched(CloudPolicyClient* client) override;
+  void OnRegistrationStateChanged(CloudPolicyClient* client) override;
+  void OnClientError(CloudPolicyClient* client) override;
+
+  // Called by |policy_fetch_timeout_|.
+  void OnPolicyFetchTimeout();
+
+  void OnFetchedPolicyValidated(UserCloudPolicyValidator* validator);
+
+  // Invokes |callback_| with the passed result after cleaning up.
+  void NotifyCallback(
+      PolicyFetchResult result,
+      std::unique_ptr<enterprise_management::CloudPolicySettings>
+          policy_payload);
+
+  // Creates a validator which can be used to validate the cached policy.
+  std::unique_ptr<UserCloudPolicyValidator> CreateValidatorForCachedPolicy(
+      std::unique_ptr<enterprise_management::PolicyFetchResponse> policy);
+  // Creates a validator which can be used to validate the freshly fetched
+  // policy, based on the cached policy.
+  std::unique_ptr<UserCloudPolicyValidator> CreateValidatorForFetchedPolicy(
+      std::unique_ptr<enterprise_management::PolicyFetchResponse> policy);
+
+  chromeos::CryptohomeClient* const cryptohome_client_;
+  chromeos::SessionManagerClient* const session_manager_client_;
+  const std::unique_ptr<CloudPolicyClient> cloud_policy_client_;
+  const AccountId account_id_;
+  const cryptohome::KeyDefinition auth_key_;
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
+
+  // Callback passed to FetchPolicy method.
+  PolicyFetchResultCallback callback_;
+
+  // |policy_data_| and |policy_payload_| will first hold the cached policy on
+  // successful load and validation, and will then be reset to hold fresh policy
+  // if download and validation of the fresh policy is successful.
+  std::unique_ptr<enterprise_management::PolicyData> policy_data_;
+  std::unique_ptr<enterprise_management::CloudPolicySettings> policy_payload_;
+
+  // A timer that puts a hard limit on the maximum time to wait for the fresh
+  // policy fetch.
+  base::Timer policy_fetch_timeout_{false, false};
+
+  // Used to load the policy key provided by session manager as a file.
+  std::unique_ptr<CachedPolicyKeyLoaderChromeOS> cached_policy_key_loader_;
+
+  base::WeakPtrFactory<PreSigninPolicyFetcher> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(PreSigninPolicyFetcher);
+};
+
+}  // namespace policy
+
+#endif  // CHROME_BROWSER_CHROMEOS_POLICY_PRE_SIGNIN_POLICY_FETCHER_H_
diff --git a/chrome/browser/chromeos/policy/pre_signin_policy_fetcher_unittest.cc b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher_unittest.cc
new file mode 100644
index 0000000..c091307
--- /dev/null
+++ b/chrome/browser/chromeos/policy/pre_signin_policy_fetcher_unittest.cc
@@ -0,0 +1,427 @@
+// Copyright (c) 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/policy/pre_signin_policy_fetcher.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/bind_helpers.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/memory/ptr_util.h"
+#include "base/path_service.h"
+#include "base/run_loop.h"
+#include "base/test/scoped_task_environment.h"
+#include "chromeos/chromeos_paths.h"
+#include "chromeos/cryptohome/cryptohome_parameters.h"
+#include "chromeos/cryptohome/homedir_methods.h"
+#include "chromeos/cryptohome/mock_homedir_methods.h"
+#include "chromeos/dbus/cryptohome_client.h"
+#include "chromeos/dbus/fake_cryptohome_client.h"
+#include "chromeos/dbus/mock_session_manager_client.h"
+#include "components/policy/core/common/cloud/cloud_policy_constants.h"
+#include "components/policy/core/common/cloud/mock_cloud_policy_client.h"
+#include "components/policy/core/common/cloud/policy_builder.h"
+#include "components/policy/core/common/policy_types.h"
+#include "components/policy/proto/cloud_policy.pb.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::Invoke;
+using ::testing::Mock;
+using ::testing::WithArg;
+using ::testing::WithArgs;
+using ::testing::_;
+
+namespace em = enterprise_management;
+
+using RetrievePolicyResponseType =
+    chromeos::SessionManagerClient::RetrievePolicyResponseType;
+
+namespace policy {
+
+namespace {
+
+// Dummy URLs to distinguish between cached and fresh policy.
+const char kCachedHomepage[] = "http://cached.test";
+const char kFreshHomepage[] = "http://fresh.test";
+
+class PreSigninPolicyFetcherTest : public testing::Test {
+ protected:
+  PreSigninPolicyFetcherTest() = default;
+
+  void SetUp() override {
+    // Setup mock HomedirMethods - this is used by PreSigninPolicyFetcher to
+    // perform the temporary cryptohome mount.
+    // Ownership of mock_homedir_methods_ is passsed to
+    // HomedirMethods::InitializeForTesting.
+    mock_homedir_methods_ = new cryptohome::MockHomedirMethods;
+    cryptohome::HomedirMethods::InitializeForTesting(mock_homedir_methods_);
+
+    // Unmount calls will succeed (currently, PreSigninPolicyFetcher only logs
+    // if they fail, so there is no point in testing that).
+    cryptohome_client_.set_unmount_result(true);
+
+    // Create a temporary directory where the user policy keys will live (these
+    // are shared between session_manager and chrome through files) and set it
+    // into PathService, so PreSigninPolicyFetcher will use it.
+    ASSERT_TRUE(tmp_dir_.CreateUniqueTempDir());
+    PathService::Override(chromeos::DIR_USER_POLICY_KEYS,
+                          user_policy_keys_dir());
+
+    auto cloud_policy_client = base::MakeUnique<MockCloudPolicyClient>();
+    cloud_policy_client_ = cloud_policy_client.get();
+    pre_signin_policy_fetcher_ = base::MakeUnique<PreSigninPolicyFetcher>(
+        &cryptohome_client_, &session_manager_client_,
+        std::move(cloud_policy_client), account_id_, cryptohome_key_);
+    cached_policy_.payload().mutable_homepagelocation()->set_value(
+        kCachedHomepage);
+    cached_policy_.Build();
+
+    fresh_policy_.payload().mutable_homepagelocation()->set_value(
+        kFreshHomepage);
+    fresh_policy_.Build();
+  }
+
+  void TearDown() override {
+    base::RunLoop().RunUntilIdle();
+    cryptohome::HomedirMethods::Shutdown();
+    mock_homedir_methods_ = nullptr;
+  }
+
+  void StoreUserPolicyKey(const std::string& public_key) {
+    ASSERT_TRUE(base::CreateDirectory(user_policy_key_file().DirName()));
+    ASSERT_EQ(static_cast<int>(public_key.size()),
+              base::WriteFile(user_policy_key_file(), public_key.data(),
+                              public_key.size()));
+  }
+
+  base::FilePath user_policy_keys_dir() const {
+    return tmp_dir_.GetPath().AppendASCII("var_run_user_policy");
+  }
+
+  base::FilePath user_policy_key_file() const {
+    const std::string sanitized_username =
+        chromeos::CryptohomeClient::GetStubSanitizedUsername(cryptohome_id_);
+    return user_policy_keys_dir()
+        .AppendASCII(sanitized_username)
+        .AppendASCII("policy.pub");
+  }
+
+  // Expect that the hidden cryptohome mount will be attempted, and return
+  // the passed |mount_error|.
+  void ExpectTemporaryCryptohomeMount(cryptohome::MountError mount_error) {
+    EXPECT_CALL(*mock_homedir_methods_,
+                MountEx(cryptohome::Identification(account_id_),
+                        cryptohome::Authorization(cryptohome_key_), _, _))
+        .WillOnce(WithArgs<2, 3>(Invoke(
+            [mount_error](const cryptohome::MountRequest& mount_request,
+                          cryptohome::HomedirMethods::MountCallback callback) {
+              EXPECT_TRUE(mount_request.hidden_mount());
+              // Expect regular user home (not public mount). Note that ARC
+              // Kiosk apps will not run through PreSigninPolicyFetcher.
+              EXPECT_FALSE(mount_request.public_mount());
+              callback.Run(true /* success */, mount_error,
+                           std::string() /* mount_hash */);
+            })));
+  }
+
+  // Expect that the temporary cryptohome mount will be attempted, and return
+  // success.
+  void ExpectTemporaryCryptohomeMount() {
+    ExpectTemporaryCryptohomeMount(cryptohome::MOUNT_ERROR_NONE);
+  }
+
+  void ExpectRetrievePolicyForUserWithoutSession(
+      const std::string& policy_blob) {
+    EXPECT_CALL(session_manager_client_,
+                RetrievePolicyForUserWithoutSession(cryptohome_id_, _))
+        .WillOnce(WithArg<1>(Invoke(
+            [policy_blob](chromeos::SessionManagerClient::RetrievePolicyCallback
+                              callback) {
+              callback.Run(policy_blob, RetrievePolicyResponseType::SUCCESS);
+            })));
+  }
+
+  // Sets up expectations on |cloud_policy_client_|, expecting a fresh policy
+  // fetch call sequence.
+  void ExpectFreshPolicyFetchOnClient(const std::string& dm_token,
+                                      const std::string& client_id) {
+    EXPECT_CALL(*cloud_policy_client_, SetupRegistration(dm_token, client_id));
+    EXPECT_CALL(*cloud_policy_client_, FetchPolicy());
+
+    expecting_fresh_policy_fetch_ = true;
+  }
+
+  // Sets up expectations on |cloud_policy_client_|, expecting that no fresh
+  // policy fetch will be invoked.
+  void ExpectNoFreshPolicyFetchOnClient() {
+    EXPECT_CALL(*cloud_policy_client_, FetchPolicy()).Times(0);
+
+    expecting_fresh_policy_fetch_ = false;
+  }
+
+  void VerifyExpectationsOnClient() {
+    // Verify that the expected method calls happened.
+    Mock::VerifyAndClearExpectations(cloud_policy_client_);
+
+    if (expecting_fresh_policy_fetch_) {
+      // Verify that the public key version from the cached policy has been
+      // passed to CloudPolicyClient for the fresh request.
+      EXPECT_TRUE(cloud_policy_client_->public_key_version_valid_);
+      EXPECT_EQ(cached_policy_.policy_data().public_key_version(),
+                cloud_policy_client_->public_key_version_);
+    }
+  }
+
+  void OnPolicyRetrieved(
+      PreSigninPolicyFetcher::PolicyFetchResult result,
+      std::unique_ptr<em::CloudPolicySettings> policy_payload) {
+    ASSERT_FALSE(policy_retrieved_called_);
+    policy_retrieved_called_ = true;
+    obtained_policy_fetch_result_ = result;
+    obtained_policy_payload_ = std::move(policy_payload);
+  }
+
+  void ExecuteFetchPolicy() {
+    pre_signin_policy_fetcher_->FetchPolicy(
+        base::Bind(&PreSigninPolicyFetcherTest::OnPolicyRetrieved,
+                   base::Unretained(this)));
+    scoped_task_environment_.RunUntilIdle();
+  }
+
+  base::test::ScopedTaskEnvironment scoped_task_environment_ = {
+      base::test::ScopedTaskEnvironment::MainThreadType::UI};
+  cryptohome::MockHomedirMethods* mock_homedir_methods_ = nullptr;
+  chromeos::FakeCryptohomeClient cryptohome_client_;
+  chromeos::MockSessionManagerClient session_manager_client_;
+  UserPolicyBuilder cached_policy_;
+  UserPolicyBuilder fresh_policy_;
+  const AccountId account_id_ =
+      AccountId::FromUserEmail(PolicyBuilder::kFakeUsername);
+  const cryptohome::Identification cryptohome_id_ =
+      cryptohome::Identification(account_id_);
+  const cryptohome::KeyDefinition cryptohome_key_ =
+      cryptohome::KeyDefinition("secret",
+                                std::string() /* label */,
+                                cryptohome::PRIV_DEFAULT);
+
+  MockCloudPolicyClient* cloud_policy_client_ = nullptr;
+  std::unique_ptr<PreSigninPolicyFetcher> pre_signin_policy_fetcher_;
+
+  bool policy_retrieved_called_ = false;
+  PreSigninPolicyFetcher::PolicyFetchResult obtained_policy_fetch_result_;
+  std::unique_ptr<em::CloudPolicySettings> obtained_policy_payload_;
+
+ private:
+  base::ScopedTempDir tmp_dir_;
+
+  bool expecting_fresh_policy_fetch_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(PreSigninPolicyFetcherTest);
+};
+
+// Test that we successfully determine that the user has no policy (unmanaged
+// user). The cached policy fetch succeeds with NO_POLICY.
+// PreSigninPolicyFetcher does not attempt to fetch fresh policy.
+TEST_F(PreSigninPolicyFetcherTest, NoPolicy) {
+  ExpectTemporaryCryptohomeMount();
+  // session_manager's RetrievePolicy* methods signal that there is no policy by
+  // passing an empty string as policy blob.
+  ExpectRetrievePolicyForUserWithoutSession(std::string());
+
+  ExpectNoFreshPolicyFetchOnClient();
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::NO_POLICY,
+            obtained_policy_fetch_result_);
+  EXPECT_FALSE(obtained_policy_payload_);
+}
+
+// Test that PreSigninPolicyFetcher signals an error when the temporary
+// cryptohome mount fails.
+TEST_F(PreSigninPolicyFetcherTest, CryptohomeTemporaryMountError) {
+  ExpectTemporaryCryptohomeMount(
+      cryptohome::MountError::MOUNT_ERROR_KEY_FAILURE);
+
+  ExecuteFetchPolicy();
+
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::ERROR,
+            obtained_policy_fetch_result_);
+  EXPECT_FALSE(obtained_policy_payload_);
+}
+
+// Break the signature of cached policy. We expect that the cached policy
+// fails to validate as a consequence and thus we get a
+// PolicyFetchResult::ERROR as response. PreSigninPolicyFetcher will not
+// attempt to fetch fresh policy in this case.
+TEST_F(PreSigninPolicyFetcherTest, CachedPolicyFailsToValidate) {
+  cached_policy_.policy().mutable_policy_data_signature()->append("garbage");
+
+  StoreUserPolicyKey(cached_policy_.GetPublicSigningKeyAsString());
+
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectNoFreshPolicyFetchOnClient();
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::ERROR,
+            obtained_policy_fetch_result_);
+  EXPECT_FALSE(obtained_policy_payload_);
+}
+
+// Don't call StoreUserPolicyKey - chrome won't find a cached policy key. We
+// expect that the cached policy fails to validate and thus we get a
+// PolicyFetchResult::ERROR as response. PreSigninPolicyFetcher will not
+// attempt to fetch fresh policy in this case.
+TEST_F(PreSigninPolicyFetcherTest, NoCachedPolicyKeyAccessible) {
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectNoFreshPolicyFetchOnClient();
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::ERROR,
+            obtained_policy_fetch_result_);
+  EXPECT_FALSE(obtained_policy_payload_);
+}
+
+// Cached policy is available and validates. However, fresh policy fetch fails
+// with a CloudPolicyClient error. Expect that PreSigninPolicyFetcher will
+// report a PolicyFetchResult::SUCCESS and pass the cached policy to the
+// callback.
+TEST_F(PreSigninPolicyFetcherTest, FreshPolicyFetchFails) {
+  StoreUserPolicyKey(cached_policy_.GetPublicSigningKeyAsString());
+
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectFreshPolicyFetchOnClient(PolicyBuilder::kFakeToken,
+                                 PolicyBuilder::kFakeDeviceId);
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  // Fresh policy fetch fails with a CloudPolicyClient error.
+  cloud_policy_client_->NotifyClientError();
+
+  // Expect that we still get PolicyFetchResult::SUCCESS with the cached policy.
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::SUCCESS,
+            obtained_policy_fetch_result_);
+  EXPECT_TRUE(obtained_policy_payload_);
+  EXPECT_EQ(kCachedHomepage,
+            obtained_policy_payload_->homepagelocation().value());
+}
+
+// Cached policy is available and validates. However, fresh policy fetch fails
+// with timeout. Expect that PreSigninPolicyFetcher will report a
+// PolicyFetchResult::SUCCESS and pass the cached policy to the callback.
+TEST_F(PreSigninPolicyFetcherTest, FreshPolicyFetchTimeout) {
+  StoreUserPolicyKey(cached_policy_.GetPublicSigningKeyAsString());
+
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectFreshPolicyFetchOnClient(PolicyBuilder::kFakeToken,
+                                 PolicyBuilder::kFakeDeviceId);
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  // Fresh policy fetch times out.
+  EXPECT_TRUE(pre_signin_policy_fetcher_->ForceTimeoutForTesting());
+
+  // Expect that we still get PolicyFetchResult::SUCCESS with the cached policy.
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::SUCCESS,
+            obtained_policy_fetch_result_);
+  EXPECT_TRUE(obtained_policy_payload_);
+  EXPECT_EQ(kCachedHomepage,
+            obtained_policy_payload_->homepagelocation().value());
+}
+
+// Cached policy is available and validates. Fresh policy fetch is also
+// successful, but the fresh policy fails to validate. Expect that
+// PreSigninPolicyFetcher will report a PolicyFetchResult::SUCCESS and pass
+// the cached policy to the callback.
+TEST_F(PreSigninPolicyFetcherTest, FreshPolicyFetchFailsToValidate) {
+  StoreUserPolicyKey(cached_policy_.GetPublicSigningKeyAsString());
+
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectFreshPolicyFetchOnClient(PolicyBuilder::kFakeToken,
+                                 PolicyBuilder::kFakeDeviceId);
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  // Fresh policy fetch is successful but returns a policy blob with a broken
+  // signature, so the fresh policy fails to validate.
+  fresh_policy_.policy().mutable_policy_data_signature()->append("garbage");
+  cloud_policy_client_->SetPolicy(dm_protocol::kChromeUserPolicyType,
+                                  std::string() /* settings_entity_id */,
+                                  fresh_policy_.policy());
+  cloud_policy_client_->NotifyPolicyFetched();
+  scoped_task_environment_.RunUntilIdle();
+
+  // Expect that we still get a PolicyFetchResult::SUCCESS with cached policy.
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::SUCCESS,
+            obtained_policy_fetch_result_);
+  EXPECT_TRUE(obtained_policy_payload_);
+  EXPECT_EQ(kCachedHomepage,
+            obtained_policy_payload_->homepagelocation().value());
+}
+
+// Cached policy is available and validates. Fresh policy fetch is also
+// successful and the fresh policy validates. Expect that
+// PreSigninPolicyFetcher will report a PolicyFetchResult::SUCCESS and pass
+// the fresh policy to the callback.
+TEST_F(PreSigninPolicyFetcherTest, FreshPolicyFetchSuccess) {
+  StoreUserPolicyKey(cached_policy_.GetPublicSigningKeyAsString());
+
+  ExpectTemporaryCryptohomeMount();
+  ExpectRetrievePolicyForUserWithoutSession(cached_policy_.GetBlob());
+
+  ExpectFreshPolicyFetchOnClient(PolicyBuilder::kFakeToken,
+                                 PolicyBuilder::kFakeDeviceId);
+  ExecuteFetchPolicy();
+
+  VerifyExpectationsOnClient();
+
+  // Fresh policy fetch is successful and validates.
+  cloud_policy_client_->SetPolicy(dm_protocol::kChromeUserPolicyType,
+                                  std::string() /* settings_entity_id */,
+                                  fresh_policy_.policy());
+  cloud_policy_client_->NotifyPolicyFetched();
+  scoped_task_environment_.RunUntilIdle();
+
+  // Expect that we get PolicyFetchResult::SUCCESS with fresh policy.
+  EXPECT_TRUE(policy_retrieved_called_);
+  EXPECT_EQ(PreSigninPolicyFetcher::PolicyFetchResult::SUCCESS,
+            obtained_policy_fetch_result_);
+  EXPECT_TRUE(obtained_policy_payload_);
+  EXPECT_EQ(kFreshHomepage,
+            obtained_policy_payload_->homepagelocation().value());
+}
+
+}  // namespace
+
+}  // namespace policy
diff --git a/chrome/browser/notifications/notification_channels_provider_android.cc b/chrome/browser/notifications/notification_channels_provider_android.cc
index 8eded769..32ad274 100644
--- a/chrome/browser/notifications/notification_channels_provider_android.cc
+++ b/chrome/browser/notifications/notification_channels_provider_android.cc
@@ -10,9 +10,10 @@
 #include "base/android/jni_string.h"
 #include "base/logging.h"
 #include "base/macros.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
+#include "base/time/default_clock.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
-#include "chrome/browser/profiles/profile.h"
 #include "components/content_settings/core/browser/content_settings_details.h"
 #include "components/content_settings/core/browser/content_settings_rule.h"
 #include "components/content_settings/core/browser/content_settings_utils.h"
@@ -43,18 +44,29 @@
         AttachCurrentThread());
   }
 
-  void CreateChannel(const std::string& origin, bool enabled) override {
+  NotificationChannel CreateChannel(const std::string& origin,
+                                    const base::Time& timestamp,
+                                    bool enabled) override {
     JNIEnv* env = AttachCurrentThread();
-    Java_NotificationSettingsBridge_createChannel(
-        env, ConvertUTF8ToJavaString(env, origin), enabled);
+    ScopedJavaLocalRef<jobject> jchannel =
+        Java_NotificationSettingsBridge_createChannel(
+            env, ConvertUTF8ToJavaString(env, origin),
+            timestamp.ToInternalValue(), enabled);
+    return NotificationChannel(
+        ConvertJavaStringToUTF8(Java_SiteChannel_getId(env, jchannel)),
+        ConvertJavaStringToUTF8(Java_SiteChannel_getOrigin(env, jchannel)),
+        base::Time::FromInternalValue(
+            Java_SiteChannel_getTimestamp(env, jchannel)),
+        static_cast<NotificationChannelStatus>(
+            Java_SiteChannel_getStatus(env, jchannel)));
   }
 
   NotificationChannelStatus GetChannelStatus(
-      const std::string& origin) override {
+      const std::string& channel_id) override {
     JNIEnv* env = AttachCurrentThread();
     return static_cast<NotificationChannelStatus>(
         Java_NotificationSettingsBridge_getChannelStatus(
-            env, ConvertUTF8ToJavaString(env, origin)));
+            env, ConvertUTF8ToJavaString(env, channel_id)));
   }
 
   void DeleteChannel(const std::string& origin) override {
@@ -71,10 +83,13 @@
     std::vector<NotificationChannel> channels;
     for (jsize i = 0; i < num_channels; ++i) {
       jobject jchannel = env->GetObjectArrayElement(raw_channels.obj(), i);
-      channels.emplace_back(
+      channels.push_back(NotificationChannel(
+          ConvertJavaStringToUTF8(Java_SiteChannel_getId(env, jchannel)),
           ConvertJavaStringToUTF8(Java_SiteChannel_getOrigin(env, jchannel)),
+          base::Time::FromInternalValue(
+              Java_SiteChannel_getTimestamp(env, jchannel)),
           static_cast<NotificationChannelStatus>(
-              Java_SiteChannel_getStatus(env, jchannel)));
+              Java_SiteChannel_getStatus(env, jchannel))));
     }
     return channels;
   }
@@ -121,14 +136,27 @@
 
 }  // anonymous namespace
 
+NotificationChannel::NotificationChannel(const std::string& id,
+                                         const std::string& origin,
+                                         const base::Time& timestamp,
+                                         NotificationChannelStatus status)
+    : id(id), origin(origin), timestamp(timestamp), status(status) {}
+
+NotificationChannel::NotificationChannel(const NotificationChannel& other) =
+    default;
+
 NotificationChannelsProviderAndroid::NotificationChannelsProviderAndroid()
     : NotificationChannelsProviderAndroid(
-          base::MakeUnique<NotificationChannelsBridgeImpl>()) {}
+          base::MakeUnique<NotificationChannelsBridgeImpl>(),
+          base::MakeUnique<base::DefaultClock>()) {}
 
 NotificationChannelsProviderAndroid::NotificationChannelsProviderAndroid(
-    std::unique_ptr<NotificationChannelsBridge> bridge)
+    std::unique_ptr<NotificationChannelsBridge> bridge,
+    std::unique_ptr<base::Clock> clock)
     : bridge_(std::move(bridge)),
       should_use_channels_(bridge_->ShouldUseChannelSettings()),
+      clock_(std::move(clock)),
+      initialized_cached_channels_(false),
       weak_factory_(this) {}
 
 NotificationChannelsProviderAndroid::~NotificationChannelsProviderAndroid() =
@@ -143,25 +171,34 @@
       !should_use_channels_) {
     return nullptr;
   }
+  std::vector<NotificationChannel> channels = UpdateCachedChannels();
+  return channels.empty()
+             ? nullptr
+             : base::MakeUnique<ChannelsRuleIterator>(std::move(channels));
+}
+
+std::vector<NotificationChannel>
+NotificationChannelsProviderAndroid::UpdateCachedChannels() const {
   std::vector<NotificationChannel> channels = bridge_->GetChannels();
-  std::sort(channels.begin(), channels.end());
-  if (channels != cached_channels_) {
+  std::map<std::string, NotificationChannel> updated_channels_map;
+  for (const auto& channel : channels)
+    updated_channels_map.emplace(channel.origin, channel);
+  if (updated_channels_map != cached_channels_) {
     // This const_cast is not ideal but tolerated because it doesn't change the
     // underlying state of NotificationChannelsProviderAndroid, and allows us to
     // notify observers as soon as we detect changes to channels.
     auto* provider = const_cast<NotificationChannelsProviderAndroid*>(this);
     content::BrowserThread::GetTaskRunnerForThread(content::BrowserThread::UI)
-        ->PostTask(
-            FROM_HERE,
-            base::BindOnce(
-                &NotificationChannelsProviderAndroid::NotifyObservers,
-                provider->weak_factory_.GetWeakPtr(), ContentSettingsPattern(),
-                ContentSettingsPattern(), content_type, std::string()));
-    provider->cached_channels_ = channels;
+        ->PostTask(FROM_HERE,
+                   base::BindOnce(
+                       &NotificationChannelsProviderAndroid::NotifyObservers,
+                       provider->weak_factory_.GetWeakPtr(),
+                       ContentSettingsPattern(), ContentSettingsPattern(),
+                       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string()));
+    provider->cached_channels_ = std::move(updated_channels_map);
+    provider->initialized_cached_channels_ = true;
   }
-  return channels.empty()
-             ? nullptr
-             : base::MakeUnique<ChannelsRuleIterator>(std::move(channels));
+  return channels;
 }
 
 bool NotificationChannelsProviderAndroid::SetWebsiteSetting(
@@ -180,9 +217,13 @@
       resource_identifier.empty()) {
     return false;
   }
+
+  InitCachedChannels();
+
   url::Origin origin = url::Origin(GURL(primary_pattern.ToString()));
   DCHECK(!origin.unique());
   const std::string origin_string = origin.Serialize();
+
   ContentSetting setting = content_settings::ValueToContentSetting(value);
   switch (setting) {
     case CONTENT_SETTING_ALLOW:
@@ -193,18 +234,19 @@
       CreateChannelIfRequired(origin_string,
                               NotificationChannelStatus::BLOCKED);
       break;
-    case CONTENT_SETTING_DEFAULT:
-      bridge_->DeleteChannel(origin_string);
+    case CONTENT_SETTING_DEFAULT: {
+      auto channel_to_delete = cached_channels_.find(origin_string);
+      if (channel_to_delete != cached_channels_.end()) {
+        bridge_->DeleteChannel(channel_to_delete->second.id);
+        cached_channels_.erase(channel_to_delete);
+      }
       break;
+    }
     default:
       // We rely on notification settings being one of ALLOW/BLOCK/DEFAULT.
       NOTREACHED();
       break;
   }
-  // TODO(awdf): Maybe update cached_channels before notifying here, to
-  // avoid notifying observers unnecessarily from GetRuleIterator.
-  NotifyObservers(primary_pattern, secondary_pattern, content_type,
-                  resource_identifier);
   return true;
 }
 
@@ -216,7 +258,7 @@
   }
   std::vector<NotificationChannel> channels = bridge_->GetChannels();
   for (auto channel : channels)
-    bridge_->DeleteChannel(channel.origin);
+    bridge_->DeleteChannel(channel.id);
 
   if (channels.size() > 0) {
     NotifyObservers(ContentSettingsPattern(), ContentSettingsPattern(),
@@ -224,23 +266,65 @@
   }
 }
 
+base::Time NotificationChannelsProviderAndroid::GetWebsiteSettingLastModified(
+    const ContentSettingsPattern& primary_pattern,
+    const ContentSettingsPattern& secondary_pattern,
+    ContentSettingsType content_type,
+    const content_settings::ResourceIdentifier& resource_identifier) {
+  if (content_type != CONTENT_SETTINGS_TYPE_NOTIFICATIONS ||
+      !should_use_channels_) {
+    return base::Time();
+  }
+  url::Origin origin = url::Origin(GURL(primary_pattern.ToString()));
+  if (origin.unique())
+    return base::Time();
+  const std::string origin_string = origin.Serialize();
+
+  InitCachedChannels();
+  auto channel_entry = cached_channels_.find(origin_string);
+  if (channel_entry == cached_channels_.end())
+    return base::Time();
+
+  return channel_entry->second.timestamp;
+}
+
 void NotificationChannelsProviderAndroid::ShutdownOnUIThread() {
   RemoveAllObservers();
 }
 
+// InitCachedChannels() must be called prior to calling this method.
 void NotificationChannelsProviderAndroid::CreateChannelIfRequired(
     const std::string& origin_string,
     NotificationChannelStatus new_channel_status) {
   // TODO(awdf): Maybe check cached incognito status here to make sure
   // channels are never created in incognito mode.
-  auto old_channel_status = bridge_->GetChannelStatus(origin_string);
-  if (old_channel_status == NotificationChannelStatus::UNAVAILABLE) {
-    bridge_->CreateChannel(
-        origin_string,
+  auto channel_entry = cached_channels_.find(origin_string);
+  if (channel_entry == cached_channels_.end()) {
+    base::Time timestamp = clock_->Now();
+
+    NotificationChannel channel = bridge_->CreateChannel(
+        origin_string, timestamp,
         new_channel_status == NotificationChannelStatus::ENABLED);
+    cached_channels_.emplace(origin_string, std::move(channel));
+
+    NotifyObservers(ContentSettingsPattern(), ContentSettingsPattern(),
+                    CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string());
   } else {
+    auto old_channel_status =
+        bridge_->GetChannelStatus(channel_entry->second.id);
     // TODO(awdf): Maybe remove this DCHECK - channel status could change any
     // time so this may be vulnerable to a race condition.
-    DCHECK(old_channel_status == new_channel_status);
+    DCHECK_EQ(old_channel_status, new_channel_status);
   }
 }
+
+// This method must be called prior to accessing |cached_channels_|.
+void NotificationChannelsProviderAndroid::InitCachedChannels() {
+  if (initialized_cached_channels_)
+    return;
+  DCHECK_EQ(cached_channels_.size(), 0u);
+  std::vector<NotificationChannel> channels = bridge_->GetChannels();
+  for (auto channel : channels)
+    cached_channels_.emplace(channel.origin, std::move(channel));
+  initialized_cached_channels_ = true;
+}
diff --git a/chrome/browser/notifications/notification_channels_provider_android.h b/chrome/browser/notifications/notification_channels_provider_android.h
index 6e6dfb265..707ff62 100644
--- a/chrome/browser/notifications/notification_channels_provider_android.h
+++ b/chrome/browser/notifications/notification_channels_provider_android.h
@@ -5,12 +5,14 @@
 #ifndef CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_CHANNELS_PROVIDER_ANDROID_H_
 #define CHROME_BROWSER_NOTIFICATIONS_NOTIFICATION_CHANNELS_PROVIDER_ANDROID_H_
 
+#include <map>
 #include <string>
 #include <tuple>
 #include <vector>
 
 #include "base/macros.h"
 #include "base/memory/weak_ptr.h"
+#include "base/time/clock.h"
 #include "components/content_settings/core/browser/content_settings_observable_provider.h"
 #include "components/content_settings/core/browser/content_settings_observer.h"
 #include "components/content_settings/core/browser/content_settings_rule.h"
@@ -23,15 +25,17 @@
 enum NotificationChannelStatus { ENABLED, BLOCKED, UNAVAILABLE };
 
 struct NotificationChannel {
-  NotificationChannel(std::string origin, NotificationChannelStatus status)
-      : origin(origin), status(status) {}
+  NotificationChannel(const std::string& id,
+                      const std::string& origin,
+                      const base::Time& timestamp,
+                      NotificationChannelStatus status);
+  NotificationChannel(const NotificationChannel& other);
   bool operator==(const NotificationChannel& other) const {
     return origin == other.origin && status == other.status;
   }
-  bool operator<(const NotificationChannel& other) const {
-    return std::tie(origin, status) < std::tie(other.origin, other.status);
-  }
-  std::string origin;
+  const std::string id;
+  const std::string origin;
+  const base::Time timestamp;
   NotificationChannelStatus status = NotificationChannelStatus::UNAVAILABLE;
 };
 
@@ -47,7 +51,9 @@
    public:
     virtual ~NotificationChannelsBridge() = default;
     virtual bool ShouldUseChannelSettings() = 0;
-    virtual void CreateChannel(const std::string& origin, bool enabled) = 0;
+    virtual NotificationChannel CreateChannel(const std::string& origin,
+                                              const base::Time& timestamp,
+                                              bool enabled) = 0;
     virtual NotificationChannelStatus GetChannelStatus(
         const std::string& origin) = 0;
     virtual void DeleteChannel(const std::string& origin) = 0;
@@ -71,22 +77,48 @@
   void ClearAllContentSettingsRules(ContentSettingsType content_type) override;
   void ShutdownOnUIThread() override;
 
+  base::Time GetWebsiteSettingLastModified(
+      const ContentSettingsPattern& primary_pattern,
+      const ContentSettingsPattern& secondary_pattern,
+      ContentSettingsType content_type,
+      const content_settings::ResourceIdentifier& resource_identifier);
+
  private:
   explicit NotificationChannelsProviderAndroid(
-      std::unique_ptr<NotificationChannelsBridge> bridge);
+      std::unique_ptr<NotificationChannelsBridge> bridge,
+      std::unique_ptr<base::Clock> clock);
   friend class NotificationChannelsProviderAndroidTest;
 
+  std::vector<NotificationChannel> UpdateCachedChannels() const;
+
   void CreateChannelIfRequired(const std::string& origin_string,
                                NotificationChannelStatus new_channel_status);
 
+  void InitCachedChannels();
+
   std::unique_ptr<NotificationChannelsBridge> bridge_;
+
   bool should_use_channels_;
 
-  // This cache is updated every time GetRuleIterator is called. It is used to
-  // check if any channels have changed their status since the previous call,
-  // in order to notify observers. This is necessary to detect channels getting
-  // blocked/enabled by the user, in the absence of a callback for this event.
-  std::vector<NotificationChannel> cached_channels_;
+  std::unique_ptr<base::Clock> clock_;
+
+  // Flag to keep track of whether |cached_channels_| has been initialized yet.
+  bool initialized_cached_channels_;
+
+  // Map of origin - NotificationChannel. Channel status may be out of date.
+  // This cache is completely refreshed every time GetRuleIterator is called;
+  // entries are also added and deleted when channels are added and deleted.
+  // This cache serves three purposes:
+  //
+  // 1. For looking up the channel ID for an origin.
+  //
+  // 2. For looking up the channel creation timestamp for an origin.
+  //
+  // 3. To check if any channels have changed status since the last time
+  //    they were checked, in order to notify observers. This is necessary to
+  //    detect channels getting blocked/enabled by the user, in the absence of a
+  //    callback for this event.
+  std::map<std::string, NotificationChannel> cached_channels_;
 
   base::WeakPtrFactory<NotificationChannelsProviderAndroid> weak_factory_;
 
diff --git a/chrome/browser/notifications/notification_channels_provider_android_unittest.cc b/chrome/browser/notifications/notification_channels_provider_android_unittest.cc
index c9b31ff7..566392d 100644
--- a/chrome/browser/notifications/notification_channels_provider_android_unittest.cc
+++ b/chrome/browser/notifications/notification_channels_provider_android_unittest.cc
@@ -7,10 +7,17 @@
 #include <map>
 #include <vector>
 
+#include "base/feature_list.h"
 #include "base/memory/ptr_util.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/task_scheduler/task_scheduler.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/simple_test_clock.h"
+#include "base/time/clock.h"
+#include "base/time/default_clock.h"
 #include "base/values.h"
 #include "chrome/browser/content_settings/content_settings_mock_observer.h"
+#include "chrome/common/chrome_features.h"
 #include "components/content_settings/core/browser/content_settings_pref.h"
 #include "components/content_settings/core/browser/content_settings_rule.h"
 #include "components/content_settings/core/browser/content_settings_utils.h"
@@ -39,44 +46,46 @@
 
   void SetChannelStatus(const std::string& origin,
                         NotificationChannelStatus status) {
-    switch (status) {
-      case NotificationChannelStatus::UNAVAILABLE:
-        channels_.erase(origin);
-        return;
-      case NotificationChannelStatus::ENABLED:
-      case NotificationChannelStatus::BLOCKED:
-        auto entry = channels_.find(origin);
-        if (entry != channels_.end())
-          entry->second.status = status;
-        else
-          channels_.emplace(origin, NotificationChannel(origin, status));
-        return;
-    }
+    DCHECK_NE(NotificationChannelStatus::UNAVAILABLE, status);
+    auto it = std::find_if(
+        channels_.begin(), channels_.end(),
+        [&origin](const std::pair<std::string, NotificationChannel>& pair) {
+          return pair.second.origin == origin;
+        });
+    DCHECK(it != channels_.end())
+        << "Must call bridge.CreateChannel before SetChannelStatus.";
+    it->second.status = status;
   }
 
   // NotificationChannelsBridge methods.
 
   bool ShouldUseChannelSettings() override { return should_use_channels_; }
 
-  void CreateChannel(const std::string& origin, bool enabled) override {
-    // Note if a channel for the given origin was already created, this is a
+  NotificationChannel CreateChannel(const std::string& origin,
+                                    const base::Time& timestamp,
+                                    bool enabled) override {
+    std::string channel_id =
+        origin + base::Int64ToString(timestamp.ToInternalValue());
+    // Note if a channel with this channel ID was already created, this is a
     // no-op. This is intentional, to match the Android Channels API.
-    channels_.emplace(
-        origin, NotificationChannel(
-                    origin, enabled ? NotificationChannelStatus::ENABLED
-                                    : NotificationChannelStatus::BLOCKED));
+    NotificationChannel channel =
+        NotificationChannel(channel_id, origin, timestamp,
+                            enabled ? NotificationChannelStatus::ENABLED
+                                    : NotificationChannelStatus::BLOCKED);
+    channels_.emplace(channel_id, channel);
+    return channel;
   }
 
   NotificationChannelStatus GetChannelStatus(
-      const std::string& origin) override {
-    auto entry = channels_.find(origin);
+      const std::string& channel_id) override {
+    auto entry = channels_.find(channel_id);
     if (entry != channels_.end())
       return entry->second.status;
     return NotificationChannelStatus::UNAVAILABLE;
   }
 
-  void DeleteChannel(const std::string& origin) override {
-    channels_.erase(origin);
+  void DeleteChannel(const std::string& channel_id) override {
+    channels_.erase(channel_id);
   }
 
   std::vector<NotificationChannel> GetChannels() override {
@@ -88,6 +97,8 @@
 
  private:
   bool should_use_channels_;
+
+  // Map from channel_id - channel.
   std::map<std::string, NotificationChannel> channels_;
 
   DISALLOW_COPY_AND_ASSIGN(FakeNotificationChannelsBridge);
@@ -95,23 +106,31 @@
 
 class NotificationChannelsProviderAndroidTest : public testing::Test {
  public:
-  NotificationChannelsProviderAndroidTest() = default;
-
+  NotificationChannelsProviderAndroidTest() {
+    scoped_feature_list_.InitAndEnableFeature(
+        features::kSiteNotificationChannels);
+  }
   ~NotificationChannelsProviderAndroidTest() override {
     channels_provider_->ShutdownOnUIThread();
   }
 
  protected:
   void InitChannelsProvider(bool should_use_channels) {
+    InitChannelsProviderWithClock(should_use_channels,
+                                  base::MakeUnique<base::DefaultClock>());
+  }
+
+  void InitChannelsProviderWithClock(bool should_use_channels,
+                                     std::unique_ptr<base::Clock> clock) {
     fake_bridge_ = new FakeNotificationChannelsBridge(should_use_channels);
 
     // Can't use base::MakeUnique because the provider's constructor is private.
     channels_provider_ =
         base::WrapUnique(new NotificationChannelsProviderAndroid(
-            base::WrapUnique(fake_bridge_)));
+            base::WrapUnique(fake_bridge_), std::move(clock)));
   }
-
   content::TestBrowserThreadBundle test_browser_thread_bundle_;
+  base::test::ScopedFeatureList scoped_feature_list_;
 
   std::unique_ptr<NotificationChannelsProviderAndroid> channels_provider_;
 
@@ -120,7 +139,7 @@
 };
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingWhenChannelsShouldNotBeUsed_NoopAndReturnsFalse) {
+       SetWebsiteSettingWhenChannelsShouldNotBeUsed_ReturnsFalse) {
   this->InitChannelsProvider(false /* should_use_channels */);
   bool result = channels_provider_->SetWebsiteSetting(
       ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
@@ -131,23 +150,29 @@
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingAllowedWhenChannelUnavailable_CreatesEnabledChannel) {
+       SetWebsiteSettingAllowedCreatesOneAllowedRule) {
   InitChannelsProvider(true /* should_use_channels */);
 
   bool result = channels_provider_->SetWebsiteSetting(
       ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
       new base::Value(CONTENT_SETTING_ALLOW));
-
   EXPECT_TRUE(result);
-  EXPECT_EQ(1u, fake_bridge_->GetChannels().size());
-  EXPECT_EQ(
-      NotificationChannel(kTestOrigin, NotificationChannelStatus::ENABLED),
-      fake_bridge_->GetChannels()[0]);
+
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
+      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+                                          std::string(), false /* incognito */);
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule rule = rule_iterator->Next();
+  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
+            rule.primary_pattern);
+  EXPECT_EQ(CONTENT_SETTING_ALLOW,
+            content_settings::ValueToContentSetting(rule.value.get()));
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingBlockedWhenChannelUnavailable_CreatesDisabledChannel) {
+       SetWebsiteSettingBlockedCreatesOneBlockedRule) {
   InitChannelsProvider(true /* should_use_channels */);
 
   bool result = channels_provider_->SetWebsiteSetting(
@@ -156,75 +181,109 @@
       new base::Value(CONTENT_SETTING_BLOCK));
 
   EXPECT_TRUE(result);
-  EXPECT_EQ(1u, fake_bridge_->GetChannels().size());
-  EXPECT_EQ(
-      NotificationChannel(kTestOrigin, NotificationChannelStatus::BLOCKED),
-      fake_bridge_->GetChannels()[0]);
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
+      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+                                          std::string(), false /* incognito */);
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule rule = rule_iterator->Next();
+  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
+            rule.primary_pattern);
+  EXPECT_EQ(CONTENT_SETTING_BLOCK,
+            content_settings::ValueToContentSetting(rule.value.get()));
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingAllowedWhenChannelAllowed_NoopAndReturnsTrue) {
+       SetWebsiteSettingAllowedTwiceForSameOriginCreatesOneAllowedRule) {
   InitChannelsProvider(true /* should_use_channels */);
-  fake_bridge_->CreateChannel(kTestOrigin, true);
 
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      new base::Value(CONTENT_SETTING_ALLOW));
   bool result = channels_provider_->SetWebsiteSetting(
       ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
       new base::Value(CONTENT_SETTING_ALLOW));
 
   EXPECT_TRUE(result);
-  EXPECT_EQ(1u, fake_bridge_->GetChannels().size());
-  EXPECT_EQ(
-      NotificationChannel(kTestOrigin, NotificationChannelStatus::ENABLED),
-      fake_bridge_->GetChannels()[0]);
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
+      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+                                          std::string(), false /* incognito */);
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule rule = rule_iterator->Next();
+  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
+            rule.primary_pattern);
+  EXPECT_EQ(CONTENT_SETTING_ALLOW,
+            content_settings::ValueToContentSetting(rule.value.get()));
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingBlockedWhenChannelBlocked_NoopAndReturnsTrue) {
+       SetWebsiteSettingBlockedTwiceForSameOriginCreatesOneBlockedRule) {
   InitChannelsProvider(true /* should_use_channels */);
-  fake_bridge_->CreateChannel(kTestOrigin, false);
 
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      new base::Value(CONTENT_SETTING_BLOCK));
   bool result = channels_provider_->SetWebsiteSetting(
       ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
       new base::Value(CONTENT_SETTING_BLOCK));
 
   EXPECT_TRUE(result);
-  EXPECT_EQ(1u, fake_bridge_->GetChannels().size());
-  EXPECT_EQ(
-      NotificationChannel(kTestOrigin, NotificationChannelStatus::BLOCKED),
-      fake_bridge_->GetChannels()[0]);
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
+      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+                                          std::string(), false /* incognito */);
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule rule = rule_iterator->Next();
+  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
+            rule.primary_pattern);
+  EXPECT_EQ(CONTENT_SETTING_BLOCK,
+            content_settings::ValueToContentSetting(rule.value.get()));
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingDefault_DeletesChannelAndReturnsTrue) {
+       SetWebsiteSettingDefault_DeletesRule) {
   InitChannelsProvider(true /* should_use_channels */);
-  fake_bridge_->CreateChannel(kTestOrigin, true);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      new base::Value(CONTENT_SETTING_ALLOW));
+
   bool result = channels_provider_->SetWebsiteSetting(
       ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(), nullptr);
 
   EXPECT_TRUE(result);
-  EXPECT_EQ(0u, fake_bridge_->GetChannels().size());
+  EXPECT_FALSE(channels_provider_->GetRuleIterator(
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      false /* incognito */));
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorWhenChannelsShouldNotBeUsed) {
+       NoRulesWhenChannelsShouldNotBeUsed) {
   InitChannelsProvider(false /* should_use_channels */);
   EXPECT_FALSE(channels_provider_->GetRuleIterator(
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
       false /* incognito */));
 }
 
-TEST_F(NotificationChannelsProviderAndroidTest, GetRuleIteratorForIncognito) {
+TEST_F(NotificationChannelsProviderAndroidTest, NoRulesInIncognito) {
   InitChannelsProvider(true /* should_use_channels */);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://abc.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
   EXPECT_FALSE(
       channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
                                           std::string(), true /* incognito */));
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorWhenNoChannelsExist) {
+       NoRulesWhenNoWebsiteSettingsSet) {
   InitChannelsProvider(true /* should_use_channels */);
   EXPECT_FALSE(channels_provider_->GetRuleIterator(
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
@@ -232,72 +291,54 @@
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorWhenOneBlockedChannelExists) {
+       SetWebsiteSettingForMultipleOriginsCreatesMultipleRules) {
   InitChannelsProvider(true /* should_use_channels */);
-  fake_bridge_->CreateChannel(kTestOrigin, false);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://abc.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://xyz.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_BLOCK));
 
-  std::unique_ptr<content_settings::RuleIterator> result =
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
       channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
                                           std::string(), false /* incognito */);
-  EXPECT_TRUE(result->HasNext());
-  content_settings::Rule rule = result->Next();
-  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
-            rule.primary_pattern);
-  EXPECT_EQ(CONTENT_SETTING_BLOCK,
-            content_settings::ValueToContentSetting(rule.value.get()));
-  EXPECT_FALSE(result->HasNext());
-}
-
-TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorWhenOneAllowedChannelExists) {
-  InitChannelsProvider(true /* should_use_channels */);
-  fake_bridge_->CreateChannel(kTestOrigin, true);
-
-  std::unique_ptr<content_settings::RuleIterator> result =
-      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
-                                          std::string(), false /* incognito */);
-  EXPECT_TRUE(result->HasNext());
-  content_settings::Rule rule = result->Next();
-  EXPECT_EQ(ContentSettingsPattern::FromString(kTestOrigin),
-            rule.primary_pattern);
-  EXPECT_EQ(CONTENT_SETTING_ALLOW,
-            content_settings::ValueToContentSetting(rule.value.get()));
-  EXPECT_FALSE(result->HasNext());
-}
-
-TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorWhenMultipleChannelsExist) {
-  InitChannelsProvider(true /* should_use_channels */);
-  std::vector<NotificationChannel> channels;
-  fake_bridge_->CreateChannel("https://abc.com", true);
-  fake_bridge_->CreateChannel("https://xyz.com", false);
-
-  std::unique_ptr<content_settings::RuleIterator> result =
-      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
-                                          std::string(), false /* incognito */);
-  EXPECT_TRUE(result->HasNext());
-  content_settings::Rule first_rule = result->Next();
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule first_rule = rule_iterator->Next();
   EXPECT_EQ(ContentSettingsPattern::FromString("https://abc.com"),
             first_rule.primary_pattern);
   EXPECT_EQ(CONTENT_SETTING_ALLOW,
             content_settings::ValueToContentSetting(first_rule.value.get()));
-  EXPECT_TRUE(result->HasNext());
-  content_settings::Rule second_rule = result->Next();
+  EXPECT_TRUE(rule_iterator->HasNext());
+  content_settings::Rule second_rule = rule_iterator->Next();
   EXPECT_EQ(ContentSettingsPattern::FromString("https://xyz.com"),
             second_rule.primary_pattern);
   EXPECT_EQ(CONTENT_SETTING_BLOCK,
             content_settings::ValueToContentSetting(second_rule.value.get()));
-  EXPECT_FALSE(result->HasNext());
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       GetRuleIteratorNotifiesObserversIfStatusChanges) {
+       NotifiesObserversOnChannelStatusChanges) {
   InitChannelsProvider(true /* should_use_channels */);
   content_settings::MockObserver mock_observer;
   channels_provider_->AddObserver(&mock_observer);
 
-  // Create channel as enabled initially.
-  fake_bridge_->CreateChannel("https://example.com", true);
+  // Create channel as enabled initially - this should notify the mock observer.
+  EXPECT_CALL(
+      mock_observer,
+      OnContentSettingChanged(_, _, CONTENT_SETTINGS_TYPE_NOTIFICATIONS, ""));
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://example.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+  content::RunAllBlockingPoolTasksUntilIdle();
+
+  // Emulate user blocking the channel.
+  fake_bridge_->SetChannelStatus("https://example.com",
+                                 NotificationChannelStatus::BLOCKED);
 
   // Observer should be notified on first invocation of GetRuleIterator.
   EXPECT_CALL(
@@ -311,45 +352,23 @@
   channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
                                       std::string(), false /* incognito */);
   content::RunAllBlockingPoolTasksUntilIdle();
-
-  // Now emulate user blocking the channel.
-  fake_bridge_->SetChannelStatus("https://example.com",
-                                 NotificationChannelStatus::BLOCKED);
-  // GetRuleIterator should now notify observer.
-  EXPECT_CALL(
-      mock_observer,
-      OnContentSettingChanged(_, _, CONTENT_SETTINGS_TYPE_NOTIFICATIONS, ""));
-  channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
-                                      std::string(), false /* incognito */);
-  content::RunAllBlockingPoolTasksUntilIdle();
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       SetWebsiteSettingNotifiesObserver) {
-  InitChannelsProvider(true /* should_use_channels */);
-  content_settings::MockObserver mock_observer;
-  channels_provider_->AddObserver(&mock_observer);
-
-  EXPECT_CALL(
-      mock_observer,
-      OnContentSettingChanged(_, _, CONTENT_SETTINGS_TYPE_NOTIFICATIONS, ""));
-  channels_provider_->SetWebsiteSetting(
-      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
-      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
-      new base::Value(CONTENT_SETTING_ALLOW));
-}
-
-TEST_F(NotificationChannelsProviderAndroidTest,
-       ClearAllContentSettingsRulesDeletesChannelsAndNotifiesObservers) {
+       ClearAllContentSettingsRulesClearsRulesAndNotifiesObservers) {
   InitChannelsProvider(true /* should_use_channels */);
   content_settings::MockObserver mock_observer;
   channels_provider_->AddObserver(&mock_observer);
 
   // Set up some channels.
-  fake_bridge_->SetChannelStatus("https://abc.com",
-                                 NotificationChannelStatus::ENABLED);
-  fake_bridge_->SetChannelStatus("https://xyz.com",
-                                 NotificationChannelStatus::BLOCKED);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://abc.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://xyz.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_BLOCK));
 
   EXPECT_CALL(mock_observer,
               OnContentSettingChanged(
@@ -359,8 +378,10 @@
   channels_provider_->ClearAllContentSettingsRules(
       CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
 
-  // Check channels were deleted.
-  EXPECT_EQ(0u, fake_bridge_->GetChannels().size());
+  // Check no rules are returned.
+  EXPECT_FALSE(channels_provider_->GetRuleIterator(
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      false /* incognito */));
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
@@ -368,10 +389,14 @@
   InitChannelsProvider(true /* should_use_channels */);
 
   // Set up some channels.
-  fake_bridge_->SetChannelStatus("https://abc.com",
-                                 NotificationChannelStatus::ENABLED);
-  fake_bridge_->SetChannelStatus("https://xyz.com",
-                                 NotificationChannelStatus::BLOCKED);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://abc.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString("https://xyz.com"),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_BLOCK));
 
   channels_provider_->ClearAllContentSettingsRules(
       CONTENT_SETTINGS_TYPE_COOKIES);
@@ -380,13 +405,105 @@
   channels_provider_->ClearAllContentSettingsRules(
       CONTENT_SETTINGS_TYPE_GEOLOCATION);
 
-  // Check the channels still exist.
-  EXPECT_EQ(2u, fake_bridge_->GetChannels().size());
+  // Check two rules are still returned.
+  std::unique_ptr<content_settings::RuleIterator> rule_iterator =
+      channels_provider_->GetRuleIterator(CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+                                          std::string(), false /* incognito */);
+  EXPECT_TRUE(rule_iterator->HasNext());
+  rule_iterator->Next();
+  EXPECT_TRUE(rule_iterator->HasNext());
+  rule_iterator->Next();
+  EXPECT_FALSE(rule_iterator->HasNext());
 }
 
 TEST_F(NotificationChannelsProviderAndroidTest,
-       ClearAllContentSettingsRulesNoopsIfNotUsingChannels) {
-  InitChannelsProvider(false /* should_use_channels */);
-  channels_provider_->ClearAllContentSettingsRules(
-      CONTENT_SETTINGS_TYPE_NOTIFICATIONS);
+       GetWebsiteSettingLastModifiedReturnsNullIfNoModifications) {
+  InitChannelsProvider(true /* should_use_channels */);
+
+  auto result = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string());
+
+  EXPECT_TRUE(result.is_null());
+}
+
+TEST_F(NotificationChannelsProviderAndroidTest,
+       GetWebsiteSettingLastModifiedForOtherSettingsReturnsNull) {
+  InitChannelsProvider(true /* should_use_channels */);
+
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_NOTIFICATIONS, std::string(),
+      new base::Value(CONTENT_SETTING_ALLOW));
+
+  auto result = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_GEOLOCATION, std::string());
+
+  EXPECT_TRUE(result.is_null());
+
+  result = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(kTestOrigin), ContentSettingsPattern(),
+      CONTENT_SETTINGS_TYPE_COOKIES, std::string());
+
+  EXPECT_TRUE(result.is_null());
+}
+
+TEST_F(NotificationChannelsProviderAndroidTest,
+       GetWebsiteSettingLastModifiedReturnsMostRecentTimestamp) {
+  auto test_clock = base::MakeUnique<base::SimpleTestClock>();
+  base::Time t1 = base::Time::Now();
+  test_clock->SetNow(t1);
+  base::SimpleTestClock* clock = test_clock.get();
+  InitChannelsProviderWithClock(true /* should_use_channels */,
+                                std::move(test_clock));
+
+  // Create channel and check last-modified time is the creation time.
+  std::string first_origin = "https://example.com";
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+  clock->Advance(base::TimeDelta::FromSeconds(1));
+
+  base::Time last_modified = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string());
+  EXPECT_EQ(last_modified, t1);
+
+  // Delete and recreate the same channel after some time has passed.
+  // This simulates the user clearing data and regranting permisison.
+  clock->Advance(base::TimeDelta::FromSeconds(3));
+  base::Time t2 = clock->Now();
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), nullptr);
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+
+  // Last modified time should be updated.
+  last_modified = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string());
+  EXPECT_EQ(last_modified, t2);
+
+  // Create an unrelated channel after some more time has passed.
+  clock->Advance(base::TimeDelta::FromSeconds(5));
+  std::string second_origin = "https://other.com";
+  channels_provider_->SetWebsiteSetting(
+      ContentSettingsPattern::FromString(second_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string(), new base::Value(CONTENT_SETTING_ALLOW));
+
+  // Expect first origin's last-modified time to be unchanged.
+  last_modified = channels_provider_->GetWebsiteSettingLastModified(
+      ContentSettingsPattern::FromString(first_origin),
+      ContentSettingsPattern(), CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
+      std::string());
+  EXPECT_EQ(last_modified, t2);
 }
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 5e479c2d..349f7c5a 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -454,21 +454,16 @@
 ukm::SourceId ChromePasswordManagerClient::GetUkmSourceId() {
   // TODO(crbug.com/732846): The UKM Source should be recycled (e.g. from the
   // web contents), once the UKM framework provides a mechanism for that.
-  if (!ukm_source_id_) {
+  if (!ukm_source_id_)
     ukm_source_id_ = ukm::UkmRecorder::GetNewSourceID();
-    ukm::UkmRecorder* ukm_recorder = GetUkmRecorder();
-    if (ukm_recorder)
-      ukm_recorder->UpdateSourceURL(*ukm_source_id_, GetMainFrameURL());
-  }
   return *ukm_source_id_;
 }
 
 PasswordManagerMetricsRecorder&
 ChromePasswordManagerClient::GetMetricsRecorder() {
   if (!metrics_recorder_) {
-    metrics_recorder_.emplace(
-        PasswordManagerMetricsRecorder::CreateUkmEntryBuilder(
-            GetUkmRecorder(), GetUkmSourceId()));
+    metrics_recorder_.emplace(GetUkmRecorder(), GetUkmSourceId(),
+                              GetMainFrameURL());
   }
   return metrics_recorder_.value();
 }
diff --git a/chrome/browser/password_manager/save_password_infobar_delegate_android_unittest.cc b/chrome/browser/password_manager/save_password_infobar_delegate_android_unittest.cc
index a00ed800..0f78eff 100644
--- a/chrome/browser/password_manager/save_password_infobar_delegate_android_unittest.cc
+++ b/chrome/browser/password_manager/save_password_infobar_delegate_android_unittest.cc
@@ -185,13 +185,9 @@
   ukm::TestUkmRecorder test_ukm_recorder;
   {
     // Setup metrics recorder
-    ukm::SourceId source_id = test_ukm_recorder.GetNewSourceID();
-    static_cast<ukm::UkmRecorder*>(&test_ukm_recorder)
-        ->UpdateSourceURL(source_id, GURL("https://www.example.com/"));
     auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-        true /*is_main_frame_secure*/,
-        PasswordFormMetricsRecorder::CreateUkmEntryBuilder(&test_ukm_recorder,
-                                                           source_id));
+        true /*is_main_frame_secure*/, &test_ukm_recorder,
+        test_ukm_recorder.GetNewSourceID(), GURL("https://www.example.com/"));
 
     // Exercise delegate.
     std::unique_ptr<MockPasswordFormManager> password_form_manager(
diff --git a/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js b/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
index c1c9e24..2b5f56c 100644
--- a/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
+++ b/chrome/browser/resources/chromeos/login/oobe_screen_oauth_enrollment.js
@@ -318,6 +318,10 @@
      * screen (either the next authentication or the login screen).
      */
     cancel: function() {
+      if (this.currentStep_ == STEP_WORKING ||
+          this.currentStep_ == STEP_AD_JOIN) {
+        return;
+      }
       if (this.isCancelDisabled)
         return;
       this.isCancelDisabled = true;
@@ -440,11 +444,8 @@
           this.currentStep_ == STEP_SIGNIN && this.lastBackMessageValue_;
       this.navigation_.refreshVisible =
           this.isAtTheBeginning() && !this.isManualEnrollment_;
-      this.navigation_.closeVisible =
-          (this.currentStep_ == STEP_SIGNIN ||
-           this.currentStep_ == STEP_ERROR ||
-           this.currentStep_ == STEP_ACTIVE_DIRECTORY_JOIN_ERROR ||
-           this.currentStep_ == STEP_AD_JOIN) &&
+      this.navigation_.closeVisible = (this.currentStep_ == STEP_SIGNIN ||
+                                       this.currentStep_ == STEP_ERROR) &&
           !this.navigation_.refreshVisible;
       $('login-header-bar').updateUI_();
     }
diff --git a/chrome/browser/ui/passwords/manage_passwords_bubble_model_unittest.cc b/chrome/browser/ui/passwords/manage_passwords_bubble_model_unittest.cc
index afab0acc..6648d37e 100644
--- a/chrome/browser/ui/passwords/manage_passwords_bubble_model_unittest.cc
+++ b/chrome/browser/ui/passwords/manage_passwords_bubble_model_unittest.cc
@@ -334,13 +334,10 @@
     {
       // Setup metrics recorder
       ukm::SourceId source_id = test_ukm_recorder.GetNewSourceID();
-      static_cast<ukm::UkmRecorder*>(&test_ukm_recorder)
-          ->UpdateSourceURL(source_id, GURL("https://www.example.com/"));
-      auto recorder = base::MakeRefCounted<
-          password_manager::PasswordFormMetricsRecorder>(
-          true /*is_main_frame_secure*/,
-          password_manager::PasswordFormMetricsRecorder::CreateUkmEntryBuilder(
-              &test_ukm_recorder, source_id));
+      auto recorder =
+          base::MakeRefCounted<password_manager::PasswordFormMetricsRecorder>(
+              true /*is_main_frame_secure*/, &test_ukm_recorder, source_id,
+              GURL("https://www.example.com/"));
 
       // Exercise bubble.
       ON_CALL(*controller(), GetPasswordFormMetricsRecorder())
@@ -571,14 +568,11 @@
         ukm::TestUkmRecorder test_ukm_recorder;
         {
           // Setup metrics recorder
-          ukm::SourceId source_id = test_ukm_recorder.GetNewSourceID();
-          static_cast<ukm::UkmRecorder*>(&test_ukm_recorder)
-              ->UpdateSourceURL(source_id, GURL("https://www.example.com/"));
           auto recorder = base::MakeRefCounted<
               password_manager::PasswordFormMetricsRecorder>(
-              true /*is_main_frame_secure*/,
-              password_manager::PasswordFormMetricsRecorder::
-                  CreateUkmEntryBuilder(&test_ukm_recorder, source_id));
+              true /*is_main_frame_secure*/, &test_ukm_recorder,
+              test_ukm_recorder.GetNewSourceID(),
+              GURL("https://www.example.com/"));
 
           // Exercise bubble.
           ON_CALL(*controller(), GetPasswordFormMetricsRecorder())
diff --git a/chrome/common/url_constants.cc b/chrome/common/url_constants.cc
index 496249f..720ce306 100644
--- a/chrome/common/url_constants.cc
+++ b/chrome/common/url_constants.cc
@@ -665,6 +665,7 @@
     kChromeUIHistoryHost,
     kChromeUIInvalidationsHost,
     kChromeUILocalStateHost,
+    kChromeUIMediaEngagementHost,
     kChromeUINetExportHost,
     kChromeUINetInternalsHost,
     kChromeUINewTabHost,
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index a1ec46f7..1486f97 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -2792,6 +2792,11 @@
     ]
   },
 
+  "EcryptfsMigrationStrategy": {
+    "os": ["chromeos"],
+    "test_policy": { "EcryptfsMigrationStrategy": 1 }
+  },
+
   "----- Chrome OS device policies ---------------------------------------": {},
 
   "DevicePolicyRefreshRate": {
diff --git a/chromeos/dbus/fake_session_manager_client.cc b/chromeos/dbus/fake_session_manager_client.cc
index 181776c..d4395d64 100644
--- a/chromeos/dbus/fake_session_manager_client.cc
+++ b/chromeos/dbus/fake_session_manager_client.cc
@@ -161,6 +161,15 @@
   return RetrievePolicyResponseType::SUCCESS;
 }
 
+void FakeSessionManagerClient::RetrievePolicyForUserWithoutSession(
+    const cryptohome::Identification& cryptohome_id,
+    const RetrievePolicyCallback& callback) {
+  // This is currently not supported in FakeSessionManagerClient.
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE,
+      base::Bind(callback, nullptr, RetrievePolicyResponseType::OTHER_ERROR));
+}
+
 void FakeSessionManagerClient::RetrieveDeviceLocalAccountPolicy(
     const std::string& account_id,
     const RetrievePolicyCallback& callback) {
diff --git a/chromeos/dbus/fake_session_manager_client.h b/chromeos/dbus/fake_session_manager_client.h
index 9d205fc..fe84d29 100644
--- a/chromeos/dbus/fake_session_manager_client.h
+++ b/chromeos/dbus/fake_session_manager_client.h
@@ -52,6 +52,9 @@
   RetrievePolicyResponseType BlockingRetrievePolicyForUser(
       const cryptohome::Identification& cryptohome_id,
       std::string* policy_out) override;
+  void RetrievePolicyForUserWithoutSession(
+      const cryptohome::Identification& cryptohome_id,
+      const RetrievePolicyCallback& callback) override;
   void RetrieveDeviceLocalAccountPolicy(
       const std::string& account_id,
       const RetrievePolicyCallback& callback) override;
diff --git a/chromeos/dbus/mock_session_manager_client.h b/chromeos/dbus/mock_session_manager_client.h
index 880df89..1f3556c 100644
--- a/chromeos/dbus/mock_session_manager_client.h
+++ b/chromeos/dbus/mock_session_manager_client.h
@@ -44,6 +44,9 @@
   MOCK_METHOD2(RetrievePolicyForUser,
                void(const cryptohome::Identification&,
                     const RetrievePolicyCallback&));
+  MOCK_METHOD2(RetrievePolicyForUserWithoutSession,
+               void(const cryptohome::Identification&,
+                    const RetrievePolicyCallback&));
   MOCK_METHOD2(BlockingRetrievePolicyForUser,
                RetrievePolicyResponseType(const cryptohome::Identification&,
                                           std::string*));
diff --git a/chromeos/dbus/session_manager_client.cc b/chromeos/dbus/session_manager_client.cc
index 36ec445..d80e22da 100644
--- a/chromeos/dbus/session_manager_client.cc
+++ b/chromeos/dbus/session_manager_client.cc
@@ -144,6 +144,12 @@
              login_manager::kSessionManagerRetrievePolicyForUser) {
     UMA_HISTOGRAM_ENUMERATION("Enterprise.RetrievePolicyResponse.User",
                               response, RetrievePolicyResponseType::COUNT);
+  } else if (method_name ==
+             login_manager::
+                 kSessionManagerRetrievePolicyForUserWithoutSession) {
+    UMA_HISTOGRAM_ENUMERATION(
+        "Enterprise.RetrievePolicyResponse.UserDuringLogin", response,
+        RetrievePolicyResponseType::COUNT);
   } else {
     LOG(ERROR) << "Invalid method_name: " << method_name;
   }
@@ -313,6 +319,23 @@
         policy_out);
   }
 
+  void RetrievePolicyForUserWithoutSession(
+      const cryptohome::Identification& cryptohome_id,
+      const RetrievePolicyCallback& callback) override {
+    const std::string method_name =
+        login_manager::kSessionManagerRetrievePolicyForUserWithoutSession;
+    dbus::MethodCall method_call(login_manager::kSessionManagerInterface,
+                                 method_name);
+    dbus::MessageWriter writer(&method_call);
+    writer.AppendString(cryptohome_id.id());
+    session_manager_proxy_->CallMethodWithErrorCallback(
+        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
+        base::Bind(&SessionManagerClientImpl::OnRetrievePolicySuccess,
+                   weak_ptr_factory_.GetWeakPtr(), method_name, callback),
+        base::Bind(&SessionManagerClientImpl::OnRetrievePolicyError,
+                   weak_ptr_factory_.GetWeakPtr(), method_name, callback));
+  }
+
   void RetrieveDeviceLocalAccountPolicy(
       const std::string& account_name,
       const RetrievePolicyCallback& callback) override {
@@ -959,6 +982,11 @@
         GetFileContent(GetUserFilePath(cryptohome_id, kStubPolicyFile));
     return RetrievePolicyResponseType::SUCCESS;
   }
+  void RetrievePolicyForUserWithoutSession(
+      const cryptohome::Identification& cryptohome_id,
+      const RetrievePolicyCallback& callback) override {
+    RetrievePolicyForUser(cryptohome_id, callback);
+  }
   void RetrieveDeviceLocalAccountPolicy(
       const std::string& account_id,
       const RetrievePolicyCallback& callback) override {
diff --git a/chromeos/dbus/session_manager_client.h b/chromeos/dbus/session_manager_client.h
index b125309..da5f08c8 100644
--- a/chromeos/dbus/session_manager_client.h
+++ b/chromeos/dbus/session_manager_client.h
@@ -203,6 +203,12 @@
       const cryptohome::Identification& cryptohome_id,
       std::string* policy_out) = 0;
 
+  // Fetches the user policy blob for a hidden user home mount. |callback| is
+  // invoked upon completition.
+  virtual void RetrievePolicyForUserWithoutSession(
+      const cryptohome::Identification& cryptohome_id,
+      const RetrievePolicyCallback& callback) = 0;
+
   // Fetches the policy blob associated with the specified device-local account
   // from session manager.  |callback| is invoked up on completion.
   virtual void RetrieveDeviceLocalAccountPolicy(
diff --git a/components/ntp_tiles/most_visited_sites.cc b/components/ntp_tiles/most_visited_sites.cc
index 45a4bcdd..a1046eb 100644
--- a/components/ntp_tiles/most_visited_sites.cc
+++ b/components/ntp_tiles/most_visited_sites.cc
@@ -33,9 +33,6 @@
 const base::Feature kDisplaySuggestionsServiceTiles{
     "DisplaySuggestionsServiceTiles", base::FEATURE_ENABLED_BY_DEFAULT};
 
-// The maximum index of the home page tile.
-const size_t kMaxHomeTileIndex = 3;
-
 // Determine whether we need any tiles from PopularSites to fill up a grid of
 // |num_tiles| tiles.
 bool NeedPopularSites(const PrefService* prefs, int num_tiles) {
@@ -404,38 +401,34 @@
 
   const GURL& home_page_url = home_page_client_->GetHomePageUrl();
   NTPTilesVector new_tiles;
-  // Add the home tile to the first four tiles.
-  NTPTile home_tile;
-  home_tile.url = home_page_url;
-  home_tile.title = title;
-  home_tile.source = TileSource::HOMEPAGE;
-
   bool home_tile_added = false;
-  size_t index = 0;
 
-  while (index < tiles.size() && new_tiles.size() < num_sites_) {
-    bool hosts_are_equal = tiles[index].url.host() == home_page_url.host();
+  for (auto& tile : tiles) {
+    if (new_tiles.size() >= num_sites_) {
+      break;
+    }
 
-    // Add the home tile to the first four tiles
-    // or at the position of a tile that has the same host
-    // and is ranked higher.
     // TODO(fhorschig): Introduce a more sophisticated deduplication.
-    if (!home_tile_added && (index >= kMaxHomeTileIndex || hosts_are_equal)) {
-      new_tiles.push_back(std::move(home_tile));
+    if (tile.url.host() == home_page_url.host()) {
+      tile.source = TileSource::HOMEPAGE;
       home_tile_added = true;
-      continue;  // Do not advance the current tile index.
     }
-
-    // Add non-home page tiles.
-    if (!hosts_are_equal) {
-      new_tiles.push_back(std::move(tiles[index]));
-    }
-    ++index;
+    new_tiles.push_back(std::move(tile));
   }
 
   // Add the home page tile if there are less than 4 tiles
   // and none of them is the home page (and there is space left).
-  if (!home_tile_added && new_tiles.size() < num_sites_) {
+  if (!home_tile_added) {
+    // Make room for the home page tile.
+    if (new_tiles.size() >= num_sites_) {
+      new_tiles.pop_back();
+    }
+
+    NTPTile home_tile;
+    home_tile.url = home_page_url;
+    home_tile.title = title;
+    home_tile.source = TileSource::HOMEPAGE;
+
     new_tiles.push_back(std::move(home_tile));
   }
   return new_tiles;
diff --git a/components/ntp_tiles/most_visited_sites_unittest.cc b/components/ntp_tiles/most_visited_sites_unittest.cc
index 43bdea7..0facbe7 100644
--- a/components/ntp_tiles/most_visited_sites_unittest.cc
+++ b/components/ntp_tiles/most_visited_sites_unittest.cc
@@ -549,7 +549,7 @@
   base::RunLoop().RunUntilIdle();
 }
 
-TEST_P(MostVisitedSitesTest, ShouldReturnMostPopularPageIfOneTileRequested) {
+TEST_P(MostVisitedSitesTest, ShouldReturnHomePageIfOneTileRequested) {
   FakeHomePageClient* home_page_client = RegisterNewHomePageClient();
   home_page_client->SetHomePageEnabled(true);
   DisableRemoteSuggestions();
@@ -561,14 +561,40 @@
       .Times(AnyNumber())
       .WillRepeatedly(Return(false));
   EXPECT_CALL(mock_observer_,
-              OnMostVisitedURLsAvailable(ElementsAre(MatchesTile(
-                  "Site 1", "http://site1/", TileSource::TOP_SITES))));
+              OnMostVisitedURLsAvailable(ElementsAre(
+                  MatchesTile("", kHomePageUrl, TileSource::HOMEPAGE))));
   most_visited_sites_->SetMostVisitedURLsObserver(&mock_observer_,
                                                   /*num_sites=*/1);
   base::RunLoop().RunUntilIdle();
 }
 
-TEST_P(MostVisitedSitesTest, ShouldContainHomePageInFirstFourTiles) {
+TEST_P(MostVisitedSitesTest, ShouldReplaceLastTileWithHomePageWhenFull) {
+  FakeHomePageClient* home_page_client = RegisterNewHomePageClient();
+  home_page_client->SetHomePageEnabled(true);
+  DisableRemoteSuggestions();
+  EXPECT_CALL(*mock_top_sites_, GetMostVisitedURLs(_, false))
+      .WillRepeatedly(InvokeCallbackArgument<0>((MostVisitedURLList{
+          MakeMostVisitedURL("Site 1", "http://site1/"),
+          MakeMostVisitedURL("Site 2", "http://site2/"),
+          MakeMostVisitedURL("Site 3", "http://site3/"),
+          MakeMostVisitedURL("Site 4", "http://site4/"),
+          MakeMostVisitedURL("Site 5", "http://site5/"),
+      })));
+  EXPECT_CALL(*mock_top_sites_, SyncWithHistory());
+  EXPECT_CALL(*mock_top_sites_, IsBlacklisted(Eq(GURL(kHomePageUrl))))
+      .Times(AnyNumber())
+      .WillRepeatedly(Return(false));
+  std::vector<NTPTile> tiles;
+  EXPECT_CALL(mock_observer_, OnMostVisitedURLsAvailable(_))
+      .WillOnce(SaveArg<0>(&tiles));
+  most_visited_sites_->SetMostVisitedURLsObserver(&mock_observer_,
+                                                  /*num_sites=*/4);
+  base::RunLoop().RunUntilIdle();
+  // Assert that the home page replaces the final tile.
+  EXPECT_THAT(tiles[3], MatchesTile("", kHomePageUrl, TileSource::HOMEPAGE));
+}
+
+TEST_P(MostVisitedSitesTest, ShouldAppendHomePageWhenNotFull) {
   FakeHomePageClient* home_page_client = RegisterNewHomePageClient();
   home_page_client->SetHomePageEnabled(true);
   DisableRemoteSuggestions();
@@ -590,8 +616,8 @@
   most_visited_sites_->SetMostVisitedURLsObserver(&mock_observer_,
                                                   /*num_sites=*/8);
   base::RunLoop().RunUntilIdle();
-  // Assert that the home page tile is in the first four tiles.
-  EXPECT_THAT(tiles[3], MatchesTile("", kHomePageUrl, TileSource::HOMEPAGE));
+  // Assert that the home page is appended as the final tile.
+  EXPECT_THAT(tiles[5], MatchesTile("", kHomePageUrl, TileSource::HOMEPAGE));
 }
 
 TEST_P(MostVisitedSitesTest, ShouldDeduplicateHomePageWithTopSites) {
diff --git a/components/password_manager/core/browser/password_form_manager.cc b/components/password_manager/core/browser/password_form_manager.cc
index 9b30e37..dc20688 100644
--- a/components/password_manager/core/browser/password_form_manager.cc
+++ b/components/password_manager/core/browser/password_form_manager.cc
@@ -255,9 +255,8 @@
   metrics_recorder_ = std::move(metrics_recorder);
   if (!metrics_recorder_) {
     metrics_recorder_ = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-        client_->IsMainFrameSecure(),
-        PasswordFormMetricsRecorder::CreateUkmEntryBuilder(
-            client_->GetUkmRecorder(), client_->GetUkmSourceId()));
+        client_->IsMainFrameSecure(), client_->GetUkmRecorder(),
+        client_->GetUkmSourceId(), client_->GetMainFrameURL());
   }
 
   if (owned_form_fetcher_)
diff --git a/components/password_manager/core/browser/password_form_manager_unittest.cc b/components/password_manager/core/browser/password_form_manager_unittest.cc
index ba7573e2..429fca6 100644
--- a/components/password_manager/core/browser/password_form_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_form_manager_unittest.cc
@@ -3843,21 +3843,22 @@
       fetched_forms.push_back(test.fetched_form);
 
     ukm::TestUkmRecorder test_ukm_recorder;
-    client()->GetUkmRecorder()->UpdateSourceURL(client()->GetUkmSourceId(),
-                                                form_to_fill.origin);
-
     {
+      auto metrics_recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
+          form_to_fill.origin.SchemeIsCryptographic(), &test_ukm_recorder,
+          test_ukm_recorder.GetNewSourceID(), form_to_fill.origin);
       FakeFormFetcher fetcher;
       PasswordFormManager form_manager(
           password_manager(), client(),
           test.is_http_basic_auth ? nullptr : client()->driver(), form_to_fill,
           base::MakeUnique<NiceMock<MockFormSaver>>(), &fetcher);
-      form_manager.Init(nullptr);
+      form_manager.Init(metrics_recorder);
       fetcher.SetNonFederated(fetched_forms, 0u);
     }
 
     const auto* source =
         test_ukm_recorder.GetSourceForUrl(form_to_fill.origin.spec().c_str());
+    ASSERT_TRUE(source);
     test_ukm_recorder.ExpectMetric(*source, "PasswordForm",
                                    kUkmManagerFillEvent, test.expected_event);
   }
diff --git a/components/password_manager/core/browser/password_form_metrics_recorder.cc b/components/password_manager/core/browser/password_form_metrics_recorder.cc
index a0df4bb..c90e449 100644
--- a/components/password_manager/core/browser/password_form_metrics_recorder.cc
+++ b/components/password_manager/core/browser/password_form_metrics_recorder.cc
@@ -32,9 +32,17 @@
 
 PasswordFormMetricsRecorder::PasswordFormMetricsRecorder(
     bool is_main_frame_secure,
-    std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder)
+    ukm::UkmRecorder* ukm_recorder,
+    ukm::SourceId source_id,
+    const GURL& main_frame_url)
     : is_main_frame_secure_(is_main_frame_secure),
-      ukm_entry_builder_(std::move(ukm_entry_builder)) {}
+      ukm_recorder_(ukm_recorder),
+      source_id_(source_id),
+      main_frame_url_(main_frame_url),
+      ukm_entry_builder_(
+          ukm_recorder
+              ? ukm_recorder->GetEntryBuilder(source_id, "PasswordForm")
+              : nullptr) {}
 
 PasswordFormMetricsRecorder::~PasswordFormMetricsRecorder() {
   UMA_HISTOGRAM_ENUMERATION("PasswordManager.ActionsTakenV3", GetActionsTaken(),
@@ -76,16 +84,11 @@
 
   for (const DetailedUserAction& action : one_time_report_user_actions_)
     RecordUkmMetric(kUkmUserAction, static_cast<int64_t>(action));
-}
 
-// static
-std::unique_ptr<ukm::UkmEntryBuilder>
-PasswordFormMetricsRecorder::CreateUkmEntryBuilder(
-    ukm::UkmRecorder* ukm_recorder,
-    ukm::SourceId source_id) {
-  if (!ukm_recorder)
-    return nullptr;
-  return ukm_recorder->GetEntryBuilder(source_id, "PasswordForm");
+  // Bind |main_frame_url_| to |source_id_| directly before sending the content
+  // of |ukm_recorder_| to ensure that the binding has not been purged already.
+  if (ukm_recorder_)
+    ukm_recorder_->UpdateSourceURL(source_id_, main_frame_url_);
 }
 
 void PasswordFormMetricsRecorder::MarkGenerationAvailable() {
diff --git a/components/password_manager/core/browser/password_form_metrics_recorder.h b/components/password_manager/core/browser/password_form_metrics_recorder.h
index 43826b0..9d5203b 100644
--- a/components/password_manager/core/browser/password_form_metrics_recorder.h
+++ b/components/password_manager/core/browser/password_form_metrics_recorder.h
@@ -17,6 +17,7 @@
 #include "components/password_manager/core/browser/password_form_user_action.h"
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "services/metrics/public/cpp/ukm_recorder.h"
+#include "url/gurl.h"
 
 namespace password_manager {
 
@@ -96,19 +97,18 @@
 class PasswordFormMetricsRecorder
     : public base::RefCounted<PasswordFormMetricsRecorder> {
  public:
-  // |ukm_entry_builder| is the destination into which UKM metrics are recorded.
-  // It may be nullptr, in which case no UKM metrics are recorded. This should
-  // be created via the static CreateUkmEntryBuilder() method of this class.
-  PasswordFormMetricsRecorder(
-      bool is_main_frame_secure,
-      std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder);
-
-  // Creates a UkmEntryBuilder that can be used to record metrics into the event
-  // "PasswordForm". |source_id| should be bound the the correct URL in the
-  // |ukm_recorder| when this function is called.
-  static std::unique_ptr<ukm::UkmEntryBuilder> CreateUkmEntryBuilder(
-      ukm::UkmRecorder* ukm_recorder,
-      ukm::SourceId source_id);
+  // Records UKM metrics and reports them on destruction. The |source_id| is
+  // (re-)bound to |main_frame_url| shortly before reporting. As such it is
+  // crucial that the |source_id| is never bound to a different URL by another
+  // consumer. The reason for this late binding is that metrics can be
+  // collected for a WebContents for a long period of time and by the time the
+  // reporting happens, the binding of |source_id| to |main_frame_url| is
+  // already purged. |ukm_recorder| may be a nullptr, in which case no UKM
+  // metrics are recorded.
+  PasswordFormMetricsRecorder(bool is_main_frame_secure,
+                              ukm::UkmRecorder* ukm_recorder,
+                              ukm::SourceId source_id,
+                              const GURL& main_frame_url);
 
   // ManagerAction - What does the PasswordFormManager do with this form? Either
   // it fills it, or it doesn't. If it doesn't fill it, that's either
@@ -367,6 +367,18 @@
   // data the user has entered.
   SubmittedFormType submitted_form_type_ = kSubmittedFormTypeUnspecified;
 
+  // Recorder to which metrics are sent. Has to outlive this
+  // PasswordFormMetricsRecorder.
+  ukm::UkmRecorder* ukm_recorder_;
+
+  // A SourceId of |ukm_recorder_|. This id gets bound to |main_frame_url_| on
+  // destruction. It can be shared across multiple metrics recorders as long as
+  // they all bind it to the same URL.
+  ukm::SourceId source_id_;
+
+  // URL for which UKMs are reported.
+  GURL main_frame_url_;
+
   // Records URL keyed metrics (UKMs) and submits them on its destruction. May
   // be a nullptr in which case no recording is expected.
   std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder_;
diff --git a/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc b/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
index 7ae90546..0147cf4 100644
--- a/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
+++ b/components/password_manager/core/browser/password_form_metrics_recorder_unittest.cc
@@ -22,13 +22,12 @@
 constexpr char kTestUrl[] = "https://www.example.com/";
 
 // Create a UkmEntryBuilder with a SourceId that is initialized for kTestUrl.
-std::unique_ptr<ukm::UkmEntryBuilder> CreateUkmEntryBuilder(
+scoped_refptr<PasswordFormMetricsRecorder> CreatePasswordFormMetricsRecorder(
+    bool is_main_frame_secure,
     ukm::TestUkmRecorder* test_ukm_recorder) {
-  ukm::SourceId source_id = test_ukm_recorder->GetNewSourceID();
-  static_cast<ukm::UkmRecorder*>(test_ukm_recorder)
-      ->UpdateSourceURL(source_id, GURL(kTestUrl));
-  return PasswordFormMetricsRecorder::CreateUkmEntryBuilder(test_ukm_recorder,
-                                                            source_id);
+  return base::MakeRefCounted<PasswordFormMetricsRecorder>(
+      is_main_frame_secure, test_ukm_recorder,
+      test_ukm_recorder->GetNewSourceID(), GURL(kTestUrl));
 }
 
 // TODO(crbug.com/738921) Replace this with generalized infrastructure.
@@ -87,9 +86,8 @@
     // Use a scoped PasswordFromMetricsRecorder because some metrics are recored
     // on destruction.
     {
-      auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-          /*is_main_frame_secure*/ true,
-          CreateUkmEntryBuilder(&test_ukm_recorder));
+      auto recorder = CreatePasswordFormMetricsRecorder(
+          /*is_main_frame_secure*/ true, &test_ukm_recorder);
       if (test.generation_available)
         recorder->MarkGenerationAvailable();
       recorder->SetHasGeneratedPassword(test.has_generated_password);
@@ -247,8 +245,8 @@
     // Use a scoped PasswordFromMetricsRecorder because some metrics are recored
     // on destruction.
     {
-      auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-          test.is_main_frame_secure, CreateUkmEntryBuilder(&test_ukm_recorder));
+      auto recorder = CreatePasswordFormMetricsRecorder(
+          test.is_main_frame_secure, &test_ukm_recorder);
 
       recorder->SetManagerAction(test.manager_action);
       if (test.user_action != UserAction::kNone)
@@ -314,9 +312,8 @@
   // Use a scoped PasswordFromMetricsRecorder because some metrics are recored
   // on destruction.
   {
-    auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-        /*is_main_frame_secure*/ true,
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+    auto recorder = CreatePasswordFormMetricsRecorder(
+        true /*is_main_frame_secure*/, &test_ukm_recorder);
     recorder->SetManagerAction(
         PasswordFormMetricsRecorder::kManagerActionAutofilled);
     recorder->SetUserAction(UserAction::kChoosePslMatch);
@@ -360,8 +357,8 @@
     // Use a scoped PasswordFromMetricsRecorder because some metrics are recored
     // on destruction.
     {
-      auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-          test.is_main_frame_secure, CreateUkmEntryBuilder(&test_ukm_recorder));
+      auto recorder = CreatePasswordFormMetricsRecorder(
+          test.is_main_frame_secure, &test_ukm_recorder);
       recorder->SetSubmittedFormType(test.form_type);
     }
 
@@ -447,9 +444,8 @@
                  << ", display_disposition = " << test.display_disposition);
     ukm::TestUkmRecorder test_ukm_recorder;
     {
-      auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-          true /*is_main_frame_secure*/,
-          CreateUkmEntryBuilder(&test_ukm_recorder));
+      auto recorder = CreatePasswordFormMetricsRecorder(
+          true /*is_main_frame_secure*/, &test_ukm_recorder);
       recorder->RecordPasswordBubbleShown(test.credential_source_type,
                                           test.display_disposition);
     }
@@ -505,9 +501,8 @@
                  << ", dismissal_reason = " << test.dismissal_reason);
     ukm::TestUkmRecorder test_ukm_recorder;
     {
-      auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-          true /*is_main_frame_secure*/,
-          CreateUkmEntryBuilder(&test_ukm_recorder));
+      auto recorder = CreatePasswordFormMetricsRecorder(
+          true /*is_main_frame_secure*/, &test_ukm_recorder);
       recorder->RecordPasswordBubbleShown(
           metrics_util::CredentialSourceType::kPasswordManager,
           test.display_disposition);
@@ -530,9 +525,8 @@
   using BubbleTrigger = PasswordFormMetricsRecorder::BubbleTrigger;
   ukm::TestUkmRecorder test_ukm_recorder;
   {
-    auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-        true /*is_main_frame_secure*/,
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+    auto recorder = CreatePasswordFormMetricsRecorder(
+        true /*is_main_frame_secure*/, &test_ukm_recorder);
     // Open and confirm an automatically triggered saving prompt.
     recorder->RecordPasswordBubbleShown(
         metrics_util::CredentialSourceType::kPasswordManager,
@@ -574,9 +568,8 @@
   const DetailedUserAction kRepeatedAction = DetailedUserAction::kUnknown;
   ukm::TestUkmRecorder test_ukm_recorder;
   {
-    auto recorder = base::MakeRefCounted<PasswordFormMetricsRecorder>(
-        true /*is_main_frame_secure*/,
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+    auto recorder = CreatePasswordFormMetricsRecorder(
+        true /*is_main_frame_secure*/, &test_ukm_recorder);
     recorder->RecordDetailedUserAction(kOneTimeAction);
     recorder->RecordDetailedUserAction(kOneTimeAction);
     recorder->RecordDetailedUserAction(kRepeatedAction);
diff --git a/components/password_manager/core/browser/password_manager_client.h b/components/password_manager/core/browser/password_manager_client.h
index 907ea62..58bc5ed 100644
--- a/components/password_manager/core/browser/password_manager_client.h
+++ b/components/password_manager/core/browser/password_manager_client.h
@@ -221,7 +221,8 @@
   virtual ukm::UkmRecorder* GetUkmRecorder() = 0;
 
   // Gets a ukm::SourceId that is associated with the WebContents object
-  // and its last committed main frame navigation.
+  // and its last committed main frame navigation. Note that the URL binding
+  // has to happen by the caller at a later point.
   virtual ukm::SourceId GetUkmSourceId() = 0;
 
   // Gets a metrics recorder for the currently committed navigation.
diff --git a/components/password_manager/core/browser/password_manager_metrics_recorder.cc b/components/password_manager/core/browser/password_manager_metrics_recorder.cc
index 802a8c5c..14d2026 100644
--- a/components/password_manager/core/browser/password_manager_metrics_recorder.cc
+++ b/components/password_manager/core/browser/password_manager_metrics_recorder.cc
@@ -20,30 +20,33 @@
 const char kUkmProvisionalSaveFailure[] = "ProvisionalSaveFailure";
 
 PasswordManagerMetricsRecorder::PasswordManagerMetricsRecorder(
-    std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder)
-    : ukm_entry_builder_(std::move(ukm_entry_builder)) {}
+    ukm::UkmRecorder* ukm_recorder,
+    ukm::SourceId source_id,
+    const GURL& main_frame_url)
+    : ukm_recorder_(ukm_recorder),
+      source_id_(source_id),
+      main_frame_url_(main_frame_url),
+      ukm_entry_builder_(
+          ukm_recorder
+              ? ukm_recorder->GetEntryBuilder(source_id, "PageWithPassword")
+              : nullptr) {}
 
 PasswordManagerMetricsRecorder::PasswordManagerMetricsRecorder(
-    PasswordManagerMetricsRecorder&& that) = default;
+    PasswordManagerMetricsRecorder&& that) noexcept = default;
 
 PasswordManagerMetricsRecorder::~PasswordManagerMetricsRecorder() {
   if (user_modified_password_field_)
     RecordUkmMetric(kUkmUserModifiedPasswordField, 1);
+
+  // Bind |main_frame_url_| to |source_id_| directly before sending the content
+  // of |ukm_recorder_| to ensure that the binding has not been purged already.
+  if (ukm_recorder_)
+    ukm_recorder_->UpdateSourceURL(source_id_, main_frame_url_);
 }
 
 PasswordManagerMetricsRecorder& PasswordManagerMetricsRecorder::operator=(
     PasswordManagerMetricsRecorder&& that) = default;
 
-// static
-std::unique_ptr<ukm::UkmEntryBuilder>
-PasswordManagerMetricsRecorder::CreateUkmEntryBuilder(
-    ukm::UkmRecorder* ukm_recorder,
-    ukm::SourceId source_id) {
-  if (!ukm_recorder)
-    return nullptr;
-  return ukm_recorder->GetEntryBuilder(source_id, "PageWithPassword");
-}
-
 void PasswordManagerMetricsRecorder::RecordUserModifiedPasswordField() {
   user_modified_password_field_ = true;
 }
diff --git a/components/password_manager/core/browser/password_manager_metrics_recorder.h b/components/password_manager/core/browser/password_manager_metrics_recorder.h
index 8dd1d10..8b95daa8 100644
--- a/components/password_manager/core/browser/password_manager_metrics_recorder.h
+++ b/components/password_manager/core/browser/password_manager_metrics_recorder.h
@@ -11,6 +11,7 @@
 
 #include "base/macros.h"
 #include "services/metrics/public/cpp/ukm_recorder.h"
+#include "url/gurl.h"
 
 class GURL;
 
@@ -61,25 +62,25 @@
     MAX_FAILURE_VALUE
   };
 
-  // |ukm_entry_builder| is the destination into which UKM metrics are recorded.
-  // It may be nullptr, in which case no UKM metrics are recorded. This should
-  // be created via the static CreateUkmEntryBuilder() method of this class.
-  explicit PasswordManagerMetricsRecorder(
-      std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder);
-  explicit PasswordManagerMetricsRecorder(
-      PasswordManagerMetricsRecorder&& that);
+  // Records UKM metrics and reports them on destruction. The |source_id| is
+  // (re-)bound to |main_frame_url| shortly before reporting. As such it is
+  // crucial that the |source_id| is never bound to a different URL by another
+  // consumer.  The reason for this late binding is that metrics can be
+  // collected for a WebContents for a long period of time and by the time the
+  // reporting happens, the binding of |source_id| to |main_frame_url| is
+  // already purged.  |ukm_recorder| may be a nullptr, in which case no UKM
+  // metrics are recorded.
+  PasswordManagerMetricsRecorder(ukm::UkmRecorder* ukm_recorder,
+                                 ukm::SourceId source_id,
+                                 const GURL& main_frame_url);
+
+  PasswordManagerMetricsRecorder(
+      PasswordManagerMetricsRecorder&& that) noexcept;
   ~PasswordManagerMetricsRecorder();
 
   PasswordManagerMetricsRecorder& operator=(
       PasswordManagerMetricsRecorder&& that);
 
-  // Creates a UkmEntryBuilder that can be used to record metrics into the event
-  // "PageWithPassword". |source_id| should be bound the the correct URL in the
-  // |ukm_recorder| when this function is called.
-  static std::unique_ptr<ukm::UkmEntryBuilder> CreateUkmEntryBuilder(
-      ukm::UkmRecorder* ukm_recorder,
-      ukm::SourceId source_id);
-
   // Records that the user has modified a password field on a page. This may be
   // called multiple times but a single metric will be reported.
   void RecordUserModifiedPasswordField();
@@ -95,6 +96,18 @@
   // Records a metric into |ukm_entry_builder_| if it is not nullptr.
   void RecordUkmMetric(const char* metric_name, int64_t value);
 
+  // Recorder to which metrics are sent. Has to outlive this
+  // PasswordManagerMetircsRecorder.
+  ukm::UkmRecorder* ukm_recorder_;
+
+  // A SourceId of |ukm_recorder_|. This id gets bound to |main_frame_url_| on
+  // destruction. It can be shared across multiple metrics recorders as long as
+  // they all bind it to the same URL.
+  ukm::SourceId source_id_;
+
+  // URL for which UKMs are reported.
+  GURL main_frame_url_;
+
   // Records URL keyed metrics (UKMs) and submits them on its destruction. May
   // be a nullptr in which case no recording is expected.
   std::unique_ptr<ukm::UkmEntryBuilder> ukm_entry_builder_;
diff --git a/components/password_manager/core/browser/password_manager_metrics_recorder_unittest.cc b/components/password_manager/core/browser/password_manager_metrics_recorder_unittest.cc
index 9cb4e2f..cd8f1a21 100644
--- a/components/password_manager/core/browser/password_manager_metrics_recorder_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_metrics_recorder_unittest.cc
@@ -10,43 +10,23 @@
 #include "components/ukm/test_ukm_recorder.h"
 #include "components/ukm/ukm_source.h"
 #include "services/metrics/public/cpp/ukm_recorder.h"
+#include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
+using ::testing::Contains;
+using ::testing::Not;
+
 namespace password_manager {
 
 namespace {
+
 constexpr char kTestUrl[] = "https://www.example.com/";
 
-// Create a UkmEntryBuilder with a SourceId that is initialized for kTestUrl.
-std::unique_ptr<ukm::UkmEntryBuilder> CreateUkmEntryBuilder(
-    ukm::TestUkmRecorder* test_ukm_recorder) {
-  ukm::SourceId source_id = test_ukm_recorder->GetNewSourceID();
-  static_cast<ukm::UkmRecorder*>(test_ukm_recorder)
-      ->UpdateSourceURL(source_id, GURL(kTestUrl));
-  return PasswordManagerMetricsRecorder::CreateUkmEntryBuilder(
-      test_ukm_recorder, source_id);
-}
-
-// TODO(crbug.com/738921) Replace this with generalized infrastructure.
-// Verifies that the metric |metric_name| was recorded with value |value| in the
-// single entry of |test_ukm_recorder| exactly |expected_count| times.
-void ExpectUkmValueCount(ukm::TestUkmRecorder* test_ukm_recorder,
-                         const char* metric_name,
-                         int64_t value,
-                         int64_t expected_count) {
-  const ukm::UkmSource* source = test_ukm_recorder->GetSourceForUrl(kTestUrl);
-  ASSERT_TRUE(source);
-
-  ASSERT_EQ(1U, test_ukm_recorder->entries_count());
-  const ukm::mojom::UkmEntry* entry = test_ukm_recorder->GetEntry(0);
-
-  int64_t occurrences = 0;
-  for (const ukm::mojom::UkmMetricPtr& metric : entry->metrics) {
-    if (metric->metric_hash == base::HashMetricName(metric_name) &&
-        metric->value == value)
-      ++occurrences;
-  }
-  EXPECT_EQ(expected_count, occurrences) << metric_name << ": " << value;
+// Creates a PasswordManagerMetricsRecorder that reports metrics for kTestUrl.
+PasswordManagerMetricsRecorder CreateMetricsRecorder(
+    ukm::UkmRecorder* ukm_recorder) {
+  return PasswordManagerMetricsRecorder(
+      ukm_recorder, ukm_recorder->GetNewSourceID(), GURL(kTestUrl));
 }
 
 }  // namespace
@@ -55,31 +35,41 @@
   ukm::TestUkmRecorder test_ukm_recorder;
   {
     PasswordManagerMetricsRecorder recorder(
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+        CreateMetricsRecorder(&test_ukm_recorder));
     recorder.RecordUserModifiedPasswordField();
   }
-  ExpectUkmValueCount(&test_ukm_recorder, kUkmUserModifiedPasswordField, 1, 1);
+  const ukm::UkmSource* source = test_ukm_recorder.GetSourceForUrl(kTestUrl);
+  ASSERT_TRUE(source);
+  test_ukm_recorder.ExpectMetric(*source, "PageWithPassword",
+                                 kUkmUserModifiedPasswordField, 1);
 }
 
 TEST(PasswordManagerMetricsRecorder, UserModifiedPasswordFieldMultipleTimes) {
   ukm::TestUkmRecorder test_ukm_recorder;
   {
     PasswordManagerMetricsRecorder recorder(
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+        CreateMetricsRecorder(&test_ukm_recorder));
     // Multiple calls should not create more than one entry.
     recorder.RecordUserModifiedPasswordField();
     recorder.RecordUserModifiedPasswordField();
   }
-  ExpectUkmValueCount(&test_ukm_recorder, kUkmUserModifiedPasswordField, 1, 1);
+  const ukm::UkmSource* source = test_ukm_recorder.GetSourceForUrl(kTestUrl);
+  ASSERT_TRUE(source);
+  test_ukm_recorder.ExpectMetric(*source, "PageWithPassword",
+                                 kUkmUserModifiedPasswordField, 1);
 }
 
 TEST(PasswordManagerMetricsRecorder, UserModifiedPasswordFieldNotCalled) {
   ukm::TestUkmRecorder test_ukm_recorder;
   {
     PasswordManagerMetricsRecorder recorder(
-        CreateUkmEntryBuilder(&test_ukm_recorder));
+        CreateMetricsRecorder(&test_ukm_recorder));
   }
-  ExpectUkmValueCount(&test_ukm_recorder, kUkmUserModifiedPasswordField, 1, 0);
+  const ukm::UkmSource* source = test_ukm_recorder.GetSourceForUrl(kTestUrl);
+  ASSERT_TRUE(source);
+  EXPECT_THAT(test_ukm_recorder.GetMetrics(*source, "PageWithPassword",
+                                           kUkmUserModifiedPasswordField),
+              Not(Contains(1)));
 }
 
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/password_manager_unittest.cc b/components/password_manager/core/browser/password_manager_unittest.cc
index 12c762a4..485458a 100644
--- a/components/password_manager/core/browser/password_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_manager_unittest.cc
@@ -116,6 +116,10 @@
 }  // namespace
 
 class PasswordManagerTest : public testing::Test {
+ public:
+  PasswordManagerTest() : test_url_("https://www.example.com") {}
+  ~PasswordManagerTest() override = default;
+
  protected:
   void SetUp() override {
     store_ = new testing::StrictMock<MockPasswordStore>;
@@ -140,8 +144,7 @@
     EXPECT_CALL(client_, DidLastPageLoadEncounterSSLErrors())
         .WillRepeatedly(Return(false));
 
-    ON_CALL(client_, GetMainFrameURL())
-        .WillByDefault(ReturnRef(GURL::EmptyGURL()));
+    ON_CALL(client_, GetMainFrameURL()).WillByDefault(ReturnRef(test_url_));
   }
 
   void TearDown() override {
@@ -235,6 +238,7 @@
 
   void FormSubmitted(const PasswordForm& form) { submitted_form_ = form; }
 
+  const GURL test_url_;
   base::MessageLoop message_loop_;
   scoped_refptr<MockPasswordStore> store_;
   testing::NiceMock<MockPasswordManagerClient> client_;
diff --git a/components/password_manager/core/browser/stub_password_manager_client.cc b/components/password_manager/core/browser/stub_password_manager_client.cc
index f31ab34..14a3cbb 100644
--- a/components/password_manager/core/browser/stub_password_manager_client.cc
+++ b/components/password_manager/core/browser/stub_password_manager_client.cc
@@ -14,10 +14,7 @@
 StubPasswordManagerClient::StubPasswordManagerClient()
     : ukm_source_id_(ukm::UkmRecorder::Get()
                          ? ukm::UkmRecorder::Get()->GetNewSourceID()
-                         : 0),
-      metrics_recorder_(PasswordManagerMetricsRecorder::CreateUkmEntryBuilder(
-          ukm::UkmRecorder::Get(),
-          ukm_source_id_)) {}
+                         : 0) {}
 
 StubPasswordManagerClient::~StubPasswordManagerClient() {}
 
@@ -95,7 +92,11 @@
 
 PasswordManagerMetricsRecorder&
 StubPasswordManagerClient::GetMetricsRecorder() {
-  return metrics_recorder_;
+  if (!metrics_recorder_) {
+    metrics_recorder_.emplace(GetUkmRecorder(), GetUkmSourceId(),
+                              GetMainFrameURL());
+  }
+  return metrics_recorder_.value();
 }
 
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/stub_password_manager_client.h b/components/password_manager/core/browser/stub_password_manager_client.h
index dd5c47c0..a33c7b2c 100644
--- a/components/password_manager/core/browser/stub_password_manager_client.h
+++ b/components/password_manager/core/browser/stub_password_manager_client.h
@@ -6,6 +6,7 @@
 #define COMPONENTS_PASSWORD_MANAGER_CORE_BROWSER_STUB_PASSWORD_MANAGER_CLIENT_H_
 
 #include "base/macros.h"
+#include "base/optional.h"
 #include "components/password_manager/core/browser/password_manager_client.h"
 #include "components/password_manager/core/browser/password_manager_metrics_recorder.h"
 #include "components/password_manager/core/browser/stub_credentials_filter.h"
@@ -60,7 +61,7 @@
   const StubCredentialsFilter credentials_filter_;
   StubLogManager log_manager_;
   ukm::SourceId ukm_source_id_;
-  PasswordManagerMetricsRecorder metrics_recorder_;
+  base::Optional<PasswordManagerMetricsRecorder> metrics_recorder_;
 
   DISALLOW_COPY_AND_ASSIGN(StubPasswordManagerClient);
 };
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index b457811..dbef9ac 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -94,7 +94,7 @@
 #   numbers can be any string that does not contain ':' or '-' characters.
 #
 #   Currently supported product names:
-#       'chrome_frame', 'chrome_os', 'android', 'ios', 'webview_android',
+#       'chrome_frame', 'chrome_os', 'android', 'webview_android',
 #       'chrome.win', 'chrome.linux', 'chrome.mac', 'chrome.*'
 #     For example if 'chrome.*:5-10' is specified for a policy, then it should
 #     be read as:
@@ -143,7 +143,7 @@
 #   persistent IDs for all fields (but not for groups!) are needed. These are
 #   specified by the 'id' keys of each policy. NEVER CHANGE EXISTING IDs,
 #   because doing so would break the deployed wire format!
-#   For your editing convenience: highest ID currently used: 375
+#   For your editing convenience: highest ID currently used: 376
 #   And don't forget to also update the EnterprisePolicies enum of
 #   histograms.xml (run 'python tools/metrics/histograms/update_policies.py').
 #
@@ -402,7 +402,6 @@
         'chrome.*:8-',
         'chrome_os:11-',
         'android:30-',
-        'ios:34-47',
       ],
       'features': {
         'can_be_recommended': True,
@@ -1322,7 +1321,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'can_be_recommended': True,
@@ -1373,7 +1371,6 @@
         'chrome.*:8-',
         'chrome_os:11-',
         'android:30-',
-        'ios:34-47',
       ],
       'features': {
         'can_be_recommended': True,
@@ -1921,7 +1918,6 @@
             'chrome.*:10-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -1992,7 +1988,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2029,7 +2024,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2057,7 +2051,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2085,7 +2078,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2125,7 +2117,6 @@
         'chrome.*:18-',
         'chrome_os:18-',
         'android:30-',
-        'ios:34-47',
       ],
       'future': True,
       'features': {
@@ -2778,7 +2769,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2811,7 +2801,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2833,7 +2822,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -2857,7 +2845,6 @@
             'chrome.*:8-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -3149,7 +3136,6 @@
             'chrome.*:10-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -3290,7 +3276,6 @@
           'supported_on': [
             'chrome.*:10-',
             'chrome_os:11-',
-            'ios:34-47',
             'android:33-',
           ],
           'features': {
@@ -3510,7 +3495,6 @@
             'chrome.*:11-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -3535,7 +3519,6 @@
             'chrome.*:11-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -3560,7 +3543,6 @@
             'chrome.*:11-',
             'chrome_os:11-',
             'android:30-',
-            'ios:34-47',
           ],
           'features': {
             'dynamic_refresh': True,
@@ -3748,7 +3730,6 @@
           'supported_on': [
             'chrome.*:11-',
             'chrome_os:11-',
-            'ios:34-47',
             'android:34-',
           ],
           'features': {
@@ -3813,7 +3794,6 @@
           'supported_on': [
             'chrome.*:11-',
             'chrome_os:11-',
-            'ios:34-47',
             'android:34-',
           ],
           'features': {
@@ -4266,7 +4246,6 @@
         'chrome.*:12-',
         'chrome_os:12-',
         'android:30-',
-        'ios:34-47',
       ],
       'features': {
         'can_be_recommended': True,
@@ -4621,7 +4600,6 @@
         'chrome_os:15-',
         'android:30-',
         'webview_android:47-',
-        'ios:34-47',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -4654,7 +4632,6 @@
         'chrome_os:15-',
         'android:30-',
         'webview_android:47-',
-        'ios:34-47',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -7591,7 +7568,7 @@
       'name': 'VariationsRestrictParameter',
       'type': 'string',
       'schema': { 'type': 'string' },
-      'supported_on': ['chrome.*:27-', 'android:34-', 'ios:35-47'],
+      'supported_on': ['chrome.*:27-', 'android:34-'],
       'features': {
         'dynamic_refresh': False,
         'per_profile': False,
@@ -7899,7 +7876,6 @@
       },
       'supported_on': [
         'android:30-',
-        'ios:35-47',
         'chrome.*:37-',
         'chrome_os:37-',
       ],
@@ -8254,7 +8230,6 @@
         'chrome.*:39-43',
         'chrome_os:39-43',
         'android:39-43',
-        'ios:39-43',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -8299,7 +8274,6 @@
         'chrome.*:50-52',
         'chrome_os:50-52',
         'android:50-52',
-        'ios:50-52',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -8344,7 +8318,6 @@
         'chrome.*:58-',
         'chrome_os:58-',
         'android:58-',
-        'ios:58-',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -8398,7 +8371,6 @@
         'chrome.*:48-52',
         'chrome_os:48-52',
         'android:48-52',
-        'ios:48-52',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -8423,7 +8395,6 @@
         'chrome.*:53-57',
         'chrome_os:53-57',
         'android:53-57',
-        'ios:53-57',
       ],
       'features': {
         'dynamic_refresh': True,
@@ -9758,7 +9729,58 @@
 
       If you set this policy to 'AllowMigration', users with ecryptfs home directories will be offered to migrate these to ext4 encryption as necessary (currently when Android N becomes available on the device).
 
-      If this policy is left not set, the device will behave as if 'DisallowArc' was chosen.''',
+      This policy does not apply to kiosk apps - these are migrated automatically. If this policy is left not set, the device will behave as if 'DisallowArc' was chosen.''',
+    },
+    {
+      'name': 'EcryptfsMigrationStrategy',
+      'type': 'int-enum',
+      'schema': {
+        'type': 'integer',
+        'enum': [ 0, 1, 2, 3],
+      },
+      'items': [
+        {
+          'name': 'DisallowArc',
+          'value': 0,
+          'caption': '''Disallow data migration and ARC.''',
+        },
+        {
+          'name': 'Migrate',
+          'value': 1,
+          'caption': '''Migrate automatically, don’t ask for user consent.''',
+        },
+        {
+          'name': 'Wipe',
+          'value': 2,
+          'caption': '''Wipe the user’s ecryptfs home directory and start with a fresh ext4-encrypted home directory.''',
+        },
+        {
+          'name': 'AskUser',
+          'value': 3,
+          'caption': '''Ask the user if they would like to migrate or skip migration and disallow ARC.''',
+        },
+      ],
+      'supported_on': ['chrome_os:61-'],
+      'device_only': False,
+      'features': {
+        'dynamic_refresh': False,
+        'per_profile': False,
+      },
+      'example_value': 3,
+      'id': 376,
+      'caption': '''Migration strategy for ecryptfs''',
+      'tags': [],
+      'desc': '''Specifies the action that should be taken when the user's home directory was created with ecryptfs encryption and needs to transition to ext4 encryption.
+
+      If you set this policy to 'DisallowArc', Android apps will be disabled for the user and no migration from ecryptfs to ext4 encryption will be performed. Android apps will not be prevented from running when the home directory is already ext4-encrypted.
+
+      If you set this policy to 'Migrate', ecryptfs-encrypted home directories will be automatically migrated to ext4 encryption on sign-in without asking for user consent.
+
+      If you set this policy to 'Wipe', ecryptfs-encrypted home directories will be deleted on sign-in and new ext4-encrypted home directories will be created instead. Warning: This removes the user's local data.
+
+      If you set this policy to 'AskUser', users with ecryptfs-encrypted home directories will be offered to migrate.
+
+      This policy does not apply to kiosk users. If this policy is left not set, the device will behave as if 'DisallowArc' was chosen.''',
     },
   ],
   'messages': {
diff --git a/components/policy/tools/generate_policy_source.py b/components/policy/tools/generate_policy_source.py
index 3cc47d9..40fc527b 100755
--- a/components/policy/tools/generate_policy_source.py
+++ b/components/policy/tools/generate_policy_source.py
@@ -87,6 +87,11 @@
       if self.is_device_only and platform != 'chrome_os':
         raise RuntimeError('is_device_only is only allowed for Chrome OS: "%s"'
                            % p)
+      if platform not in ['chrome_frame', 'chrome_os',
+                          'android', 'webview_android',
+                          'chrome.win', 'chrome.linux', 'chrome.mac',
+                          'chrome.*']:
+        raise RuntimeError('Platform "%s" is not supported' % platform)
 
       split_result = version_range.split('-')
       if len(split_result) != 2:
diff --git a/components/signin/ios/browser/account_consistency_service.mm b/components/signin/ios/browser/account_consistency_service.mm
index a56cc1b..41cebe0 100644
--- a/components/signin/ios/browser/account_consistency_service.mm
+++ b/components/signin/ios/browser/account_consistency_service.mm
@@ -6,6 +6,7 @@
 
 #import <WebKit/WebKit.h>
 
+#include "base/ios/ios_util.h"
 #include "base/logging.h"
 #import "base/mac/foundation_util.h"
 #include "base/macros.h"
@@ -394,6 +395,15 @@
   }
   if (!web_view_) {
     web_view_ = BuildWKWebView();
+    if (base::ios::IsRunningOnIOS11OrLater()) {
+      // On iOS 11 WKWebView load is broken if view is not a part of the
+      // hierarchy. Add view to the hierarchy, but place it offscreen to
+      // workaround iOS bug (rdar://33184203 and crbug.com/738435)
+      // TODO(crbug.com/739390): Remove this workaround.
+      web_view_.hidden = YES;
+      web_view_.frame = CGRectMake(-100, -100, 100, 100);
+      [UIApplication.sharedApplication.keyWindow addSubview:web_view_];
+    }
     navigation_delegate_ = [[AccountConsistencyNavigationDelegate alloc]
         initWithCallback:base::Bind(&AccountConsistencyService::
                                         FinishedApplyingCookieRequest,
@@ -410,6 +420,13 @@
 void AccountConsistencyService::ResetWKWebView() {
   [web_view_ setNavigationDelegate:nil];
   [web_view_ stopLoading];
+  if (base::ios::IsRunningOnIOS10OrLater()) {
+    // On iOS 11 WKWebView load is broken if view is not a part of the
+    // hierarchy. Add view to the hierarchy, but place it offscreen to
+    // workaround iOS bug (rdar://33184203 and crbug.com/738435)
+    // TODO(crbug.com/739390): Remove this workaround.
+    [web_view_ removeFromSuperview];
+  }
   web_view_ = nil;
   navigation_delegate_ = nil;
   applying_cookie_requests_ = false;
diff --git a/components/tracing/test/heap_profiler_perftest.cc b/components/tracing/test/heap_profiler_perftest.cc
index d776e4ff..8a39695 100644
--- a/components/tracing/test/heap_profiler_perftest.cc
+++ b/components/tracing/test/heap_profiler_perftest.cc
@@ -10,7 +10,6 @@
 #include "base/strings/stringprintf.h"
 #include "base/time/time.h"
 #include "base/trace_event/heap_profiler_stack_frame_deduplicator.h"
-#include "base/trace_event/heap_profiler_string_deduplicator.h"
 #include "base/trace_event/trace_event_argument.h"
 #include "perf_test_helpers.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -125,8 +124,7 @@
 };
 
 TEST_F(HeapProfilerPerfTest, DeduplicateStackFrames) {
-  StringDeduplicator string_deduplicator;
-  StackFrameDeduplicator deduplicator(&string_deduplicator);
+  StackFrameDeduplicator deduplicator;
 
   auto variations = GetRandomBacktraceVariations();
 
@@ -137,21 +135,15 @@
 }
 
 TEST_F(HeapProfilerPerfTest, AppendStackFramesAsTraceFormat) {
-  StringDeduplicator string_deduplicator;
-  StackFrameDeduplicator deduplicator(&string_deduplicator);
+  StackFrameDeduplicator deduplicator;
 
   auto variations = GetRandomBacktraceVariations();
   InsertRandomBacktraces(&deduplicator, variations, 1000000);
 
-  auto traced_value = base::MakeUnique<TracedValue>();
-  traced_value->BeginArray("nodes");
-  deduplicator.SerializeIncrementally(traced_value.get());
-  traced_value->EndArray();
-
   {
     ScopedStopwatch stopwatch("time_to_append");
     std::string json;
-    traced_value->AppendAsTraceFormat(&json);
+    deduplicator.AppendAsTraceFormat(&json);
   }
 }
 
diff --git a/components/user_manager/user_manager_base.cc b/components/user_manager/user_manager_base.cc
index 1fdcb558..dddc4e56 100644
--- a/components/user_manager/user_manager_base.cc
+++ b/components/user_manager/user_manager_base.cc
@@ -384,6 +384,10 @@
                                             bool force_online_signin) {
   DCHECK(!task_runner_ || task_runner_->RunsTasksInCurrentSequence());
 
+  User* const user = FindUserAndModify(account_id);
+  if (user)
+    user->set_force_online_signin(force_online_signin);
+
   // Do not update local state if data stored or cached outside the user's
   // cryptohome is to be treated as ephemeral.
   if (IsUserNonCryptohomeDataEphemeral(account_id))
diff --git a/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm b/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
index b3900d3..25fa3bd 100644
--- a/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
+++ b/ios/chrome/browser/passwords/ios_chrome_password_manager_client.mm
@@ -153,9 +153,6 @@
     metrics_recorder_.reset();
     ukm_source_url_ = delegate_.lastCommittedURL;
     ukm_source_id_ = ukm::UkmRecorder::GetNewSourceID();
-    ukm::UkmRecorder* ukm_recorder = GetUkmRecorder();
-    if (ukm_recorder)
-      ukm_recorder->UpdateSourceURL(ukm_source_id_, ukm_source_url_);
   }
   return ukm_source_id_;
 }
@@ -163,9 +160,10 @@
 PasswordManagerMetricsRecorder&
 IOSChromePasswordManagerClient::GetMetricsRecorder() {
   if (!metrics_recorder_) {
-    metrics_recorder_.emplace(PasswordManagerMetricsRecorder(
-        PasswordManagerMetricsRecorder::CreateUkmEntryBuilder(
-            GetUkmRecorder(), GetUkmSourceId())));
+    // Query source_id first, because that has the side effect of initializing
+    // |ukm_source_url_|.
+    ukm::SourceId source_id = GetUkmSourceId();
+    metrics_recorder_.emplace(GetUkmRecorder(), source_id, ukm_source_url_);
   }
   return metrics_recorder_.value();
 }
diff --git a/ios/chrome/browser/tabs/tab.mm b/ios/chrome/browser/tabs/tab.mm
index 533064fb..7dd2153 100644
--- a/ios/chrome/browser/tabs/tab.mm
+++ b/ios/chrome/browser/tabs/tab.mm
@@ -479,7 +479,6 @@
 
   if (experimental_flags::IsAutoReloadEnabled())
     _autoReloadBridge = [[AutoReloadBridge alloc] initWithTab:self];
-  _printObserver = base::MakeUnique<PrintObserver>(self.webState);
 
   id<PasswordsUiDelegate> passwordsUiDelegate =
       [[PasswordsUiDelegateImpl alloc] init];
@@ -510,6 +509,13 @@
   }
 }
 
+// Attach any tab helpers which are dependent on the dispatcher having been
+// set on the tab.
+- (void)attachDispatcherDependentTabHelpers {
+  _printObserver =
+      base::MakeUnique<PrintObserver>(self.webState, self.dispatcher);
+}
+
 - (NSArray*)accessoryViewProviders {
   NSMutableArray* providers = [NSMutableArray array];
   id<FormInputAccessoryViewProvider> provider =
@@ -791,6 +797,18 @@
   overscrollActionsControllerDelegate_ = overscrollActionsControllerDelegate;
 }
 
+- (void)setDispatcher:(id<ApplicationCommands, BrowserCommands>)dispatcher {
+  if (_dispatcher == dispatcher)
+    return;
+  // The dispatcher shouldn't change once set, so at this stage the dispatcher
+  // should be nil, or the new value should be nil.
+  DCHECK(!_dispatcher || !dispatcher);
+  _dispatcher = dispatcher;
+  // If the new dispatcher is nonnull, add tab helpers.
+  if (self.dispatcher)
+    [self attachDispatcherDependentTabHelpers];
+}
+
 - (void)saveTitleToHistoryDB {
   // If incognito, don't update history.
   if (_browserState->IsOffTheRecord())
diff --git a/ios/chrome/browser/ui/activity_services/activity_service_controller.mm b/ios/chrome/browser/ui/activity_services/activity_service_controller.mm
index 39410609..6672267 100644
--- a/ios/chrome/browser/ui/activity_services/activity_service_controller.mm
+++ b/ios/chrome/browser/ui/activity_services/activity_service_controller.mm
@@ -40,7 +40,8 @@
 - (NSArray*)activityItemsForData:(ShareToData*)data;
 // Returns an array of UIActivity objects that can handle the given |data|.
 - (NSArray*)applicationActivitiesForData:(ShareToData*)data
-                              controller:(UIViewController*)controller;
+                              controller:(UIViewController*)controller
+                              dispatcher:(id<BrowserCommands>)dispatcher;
 // Processes |extensionItems| returned from App Extension invocation returning
 // the |activityType|. Calls shareDelegate_ with the processed returned items
 // and |result| of activity. Returns whether caller should reset UI.
@@ -91,6 +92,7 @@
 - (void)shareWithData:(ShareToData*)data
            controller:(UIViewController*)controller
          browserState:(ios::ChromeBrowserState*)browserState
+           dispatcher:(id<BrowserCommands>)dispatcher
       shareToDelegate:(id<ShareToDelegate>)delegate
              fromRect:(CGRect)fromRect
                inView:(UIView*)inView {
@@ -109,11 +111,12 @@
   activityViewController_ = [[UIActivityViewController alloc]
       initWithActivityItems:[self activityItemsForData:data]
       applicationActivities:[self applicationActivitiesForData:data
-                                                    controller:controller]];
+                                                    controller:controller
+                                                    dispatcher:dispatcher]];
 
   // Reading List and Print activities refer to iOS' version of these.
   // Chrome-specific implementations of these two activities are provided below
-  // in applicationActivitiesForData:controller:
+  // in applicationActivitiesForData:controller:dispatcher:
   NSArray* excludedActivityTypes = @[
     UIActivityTypeAddToReadingList, UIActivityTypePrint,
     UIActivityTypeSaveToCameraRoll
@@ -211,11 +214,12 @@
 }
 
 - (NSArray*)applicationActivitiesForData:(ShareToData*)data
-                              controller:(UIViewController*)controller {
+                              controller:(UIViewController*)controller
+                              dispatcher:(id<BrowserCommands>)dispatcher {
   NSMutableArray* applicationActivities = [NSMutableArray array];
   if (data.isPagePrintable) {
     PrintActivity* printActivity = [[PrintActivity alloc] init];
-    [printActivity setResponder:controller];
+    printActivity.dispatcher = dispatcher;
     [applicationActivities addObject:printActivity];
   }
   if (data.url.SchemeIsHTTPOrHTTPS()) {
diff --git a/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm b/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm
index 9563e878..9f544ca 100644
--- a/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm
+++ b/ios/chrome/browser/ui/activity_services/activity_service_controller_unittest.mm
@@ -25,7 +25,9 @@
 @interface ActivityServiceController (CrVisibleForTesting)
 - (NSArray*)activityItemsForData:(ShareToData*)data;
 - (NSArray*)applicationActivitiesForData:(ShareToData*)data
-                              controller:(UIViewController*)controller;
+                              controller:(UIViewController*)controller
+                              dispatcher:(id<BrowserCommands>)dispatcher;
+
 - (BOOL)processItemsReturnedFromActivity:(NSString*)activityType
                                   status:(ShareTo::ShareResult)result
                                    items:(NSArray*)extensionItems;
@@ -220,6 +222,7 @@
   [activityController shareWithData:shareData_
                          controller:parentController
                        browserState:nullptr
+                         dispatcher:nil
                     shareToDelegate:GetShareToDelegate()
                            fromRect:AnchorRect()
                              inView:AnchorView()];
@@ -439,8 +442,9 @@
                        isPagePrintable:YES
                     thumbnailGenerator:DummyThumbnailGeneratorBlock()];
 
-  NSArray* items =
-      [activityController applicationActivitiesForData:data controller:nil];
+  NSArray* items = [activityController applicationActivitiesForData:data
+                                                         controller:nil
+                                                         dispatcher:nil];
   ASSERT_EQ(2U, [items count]);
   EXPECT_EQ([PrintActivity class], [[items objectAtIndex:0] class]);
 
@@ -451,7 +455,9 @@
                        isOriginalTitle:YES
                        isPagePrintable:NO
                     thumbnailGenerator:DummyThumbnailGeneratorBlock()];
-  items = [activityController applicationActivitiesForData:data controller:nil];
+  items = [activityController applicationActivitiesForData:data
+                                                controller:nil
+                                                dispatcher:nil];
   EXPECT_EQ(1U, [items count]);
 }
 
diff --git a/ios/chrome/browser/ui/activity_services/print_activity.h b/ios/chrome/browser/ui/activity_services/print_activity.h
index 2d4d015..675e8d0 100644
--- a/ios/chrome/browser/ui/activity_services/print_activity.h
+++ b/ios/chrome/browser/ui/activity_services/print_activity.h
@@ -7,11 +7,13 @@
 
 #import <UIKit/UIKit.h>
 
+@protocol BrowserCommands;
+
 // Activity that triggers the printing service.
 @interface PrintActivity : UIActivity
 
-// The responder that receives ChromeCommands when the activity is performed.
-@property(nonatomic, weak) UIResponder* responder;
+// The dispatcher that handles when the activity is performed.
+@property(nonatomic, weak) id<BrowserCommands> dispatcher;
 
 // Identifier for the print activity.
 + (NSString*)activityIdentifier;
diff --git a/ios/chrome/browser/ui/activity_services/print_activity.mm b/ios/chrome/browser/ui/activity_services/print_activity.mm
index 3e47c9d..bfe9c339 100644
--- a/ios/chrome/browser/ui/activity_services/print_activity.mm
+++ b/ios/chrome/browser/ui/activity_services/print_activity.mm
@@ -5,9 +5,7 @@
 #import "ios/chrome/browser/ui/activity_services/print_activity.h"
 
 #include "base/logging.h"
-#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
-#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
-#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
+#include "ios/chrome/browser/ui/commands/browser_commands.h"
 #include "ios/chrome/grit/ios_strings.h"
 #include "ui/base/l10n/l10n_util_mac.h"
 
@@ -21,13 +19,8 @@
 
 }  // namespace
 
-@interface PrintActivity () {
-  UIResponder* responder_;
-}
-@end
-
 @implementation PrintActivity
-@dynamic responder;
+@synthesize dispatcher = _dispatcher;
 
 + (NSString*)activityIdentifier {
   return kPrintActivityType;
@@ -35,10 +28,6 @@
 
 #pragma mark - UIActivity
 
-- (void)setResponder:(UIResponder*)responder {
-  responder_ = responder;
-}
-
 - (NSString*)activityType {
   return [PrintActivity activityIdentifier];
 }
@@ -63,10 +52,7 @@
 }
 
 - (void)performActivity {
-  GenericChromeCommand* command =
-      [[GenericChromeCommand alloc] initWithTag:IDC_PRINT];
-  DCHECK(responder_);
-  [responder_ chromeExecuteCommand:command];
+  [self.dispatcher printTab];
   [self activityDidFinish:YES];
 }
 
diff --git a/ios/chrome/browser/ui/activity_services/share_protocol.h b/ios/chrome/browser/ui/activity_services/share_protocol.h
index b135248e..d58bc1d 100644
--- a/ios/chrome/browser/ui/activity_services/share_protocol.h
+++ b/ios/chrome/browser/ui/activity_services/share_protocol.h
@@ -8,6 +8,7 @@
 #import <Foundation/Foundation.h>
 #import <UIKit/UIKit.h>
 
+@protocol BrowserCommands;
 @class ShareToData;
 
 namespace ShareTo {
@@ -71,6 +72,7 @@
 - (void)shareWithData:(ShareToData*)data
            controller:(UIViewController*)controller
          browserState:(ios::ChromeBrowserState*)browserState
+           dispatcher:(id<BrowserCommands>)dispatcher
       shareToDelegate:(id<ShareToDelegate>)delegate
              fromRect:(CGRect)rect
                inView:(UIView*)inView;
diff --git a/ios/chrome/browser/ui/browser_view_controller.mm b/ios/chrome/browser/ui/browser_view_controller.mm
index 9b4a782..3909b80 100644
--- a/ios/chrome/browser/ui/browser_view_controller.mm
+++ b/ios/chrome/browser/ui/browser_view_controller.mm
@@ -635,8 +635,6 @@
 - (void)sharePageWithData:(ShareToData*)data;
 // Convenience method to share the current page.
 - (void)sharePage;
-// Prints the web page in the current tab.
-- (void)print;
 // Shows the Online Help Page in a tab.
 - (void)showHelpPage;
 // Show the bookmarks page.
@@ -3517,26 +3515,6 @@
   [_toolbarController dismissTabHistoryPopup];
 }
 
-- (void)print {
-  Tab* currentTab = [_model currentTab];
-  // The UI should prevent users from printing non-printable pages. However, a
-  // redirection to an un-printable page can happen before it is reflected in
-  // the UI.
-  if (![currentTab viewForPrinting]) {
-    TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeError);
-    [self showSnackbar:l10n_util::GetNSString(IDS_IOS_CANNOT_PRINT_PAGE_ERROR)];
-    return;
-  }
-  DCHECK(_browserState);
-  if (!_printController) {
-    _printController = [[PrintController alloc]
-        initWithContextGetter:_browserState->GetRequestContext()];
-  }
-  [_printController printView:[currentTab viewForPrinting]
-                    withTitle:[currentTab title]
-               viewController:self];
-}
-
 - (void)addToReadingListURL:(const GURL&)URL title:(NSString*)title {
   base::RecordAction(UserMetricsAction("MobileReadingListAdd"));
 
@@ -4072,6 +4050,26 @@
                    transition:ui::PAGE_TRANSITION_TYPED];
 }
 
+- (void)printTab {
+  Tab* currentTab = [_model currentTab];
+  // The UI should prevent users from printing non-printable pages. However, a
+  // redirection to an un-printable page can happen before it is reflected in
+  // the UI.
+  if (![currentTab viewForPrinting]) {
+    TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeError);
+    [self showSnackbar:l10n_util::GetNSString(IDS_IOS_CANNOT_PRINT_PAGE_ERROR)];
+    return;
+  }
+  DCHECK(_browserState);
+  if (!_printController) {
+    _printController = [[PrintController alloc]
+        initWithContextGetter:_browserState->GetRequestContext()];
+  }
+  [_printController printView:[currentTab viewForPrinting]
+                    withTitle:[currentTab title]
+               viewController:self];
+}
+
 #pragma mark - Command Handling
 
 - (IBAction)chromeExecuteCommand:(id)sender {
@@ -4180,9 +4178,6 @@
       DCHECK([sender isKindOfClass:[TabHistoryCell class]]);
       [self navigateToSelectedEntry:sender];
       break;
-    case IDC_PRINT:
-      [self print];
-      break;
     case IDC_ADD_READING_LIST: {
       ReadingListAddCommand* command =
           base::mac::ObjCCastStrict<ReadingListAddCommand>(sender);
@@ -4247,6 +4242,7 @@
   [controller shareWithData:data
                  controller:self
                browserState:_browserState
+                 dispatcher:self.dispatcher
             shareToDelegate:self
                    fromRect:fromRect
                      inView:inView];
diff --git a/ios/chrome/browser/ui/browser_view_controller_unittest.mm b/ios/chrome/browser/ui/browser_view_controller_unittest.mm
index 17fe03a..d6a8003 100644
--- a/ios/chrome/browser/ui/browser_view_controller_unittest.mm
+++ b/ios/chrome/browser/ui/browser_view_controller_unittest.mm
@@ -472,6 +472,7 @@
         shareWithData:[OCMArg checkWithBlock:shareDataChecker]
            controller:bvc_
          browserState:chrome_browser_state_.get()
+           dispatcher:bvc_.dispatcher
       shareToDelegate:bvc_
              fromRect:[bvc_ testing_shareButtonAnchorRect]
                inView:[OCMArg any]];
@@ -498,6 +499,7 @@
         shareWithData:[OCMArg any]
            controller:bvc_
          browserState:chrome_browser_state_.get()
+           dispatcher:bvc_.dispatcher
       shareToDelegate:bvc_
              fromRect:[bvc_ testing_shareButtonAnchorRect]
                inView:[OCMArg any]];
diff --git a/ios/chrome/browser/ui/commands/browser_commands.h b/ios/chrome/browser/ui/commands/browser_commands.h
index e6cabf672..f62bf1c 100644
--- a/ios/chrome/browser/ui/commands/browser_commands.h
+++ b/ios/chrome/browser/ui/commands/browser_commands.h
@@ -40,6 +40,9 @@
 // Opens a new tab as specified by |newTabCommand|.
 - (void)openNewTab:(OpenNewTabCommand*)newTabCommand;
 
+// Prints the currently active tab.
+- (void)printTab;
+
 @end
 
 #endif  // IOS_CHROME_BROWSER_UI_COMMANDS_BROWSER_COMMANDS_H_
diff --git a/ios/chrome/browser/ui/commands/ios_command_ids.h b/ios/chrome/browser/ui/commands/ios_command_ids.h
index bab3cdfc..a22bb7f 100644
--- a/ios/chrome/browser/ui/commands/ios_command_ids.h
+++ b/ios/chrome/browser/ui/commands/ios_command_ids.h
@@ -14,7 +14,6 @@
 
 // clang-format off
 #define IDC_VIEW_SOURCE                                35002
-#define IDC_PRINT                                      35003
 #define IDC_FIND                                       37000
 #define IDC_FIND_NEXT                                  37001
 #define IDC_FIND_PREVIOUS                              37002
diff --git a/ios/chrome/browser/ui/settings/passwords_settings_egtest.mm b/ios/chrome/browser/ui/settings/passwords_settings_egtest.mm
index c2d45dd3..b016625 100644
--- a/ios/chrome/browser/ui/settings/passwords_settings_egtest.mm
+++ b/ios/chrome/browser/ui/settings/passwords_settings_egtest.mm
@@ -4,6 +4,8 @@
 
 #include <TargetConditionals.h>
 
+#include <utility>
+
 #include "base/callback.h"
 #include "base/mac/foundation_util.h"
 #include "base/memory/ref_counted.h"
@@ -13,8 +15,12 @@
 #include "components/autofill/core/common/password_form.h"
 #include "components/keyed_service/core/service_access_type.h"
 #include "components/password_manager/core/browser/password_store.h"
+#include "components/password_manager/core/browser/password_store_consumer.h"
 #include "components/password_manager/core/common/password_manager_features.h"
+#include "components/password_manager/core/common/password_manager_pref_names.h"
+#include "components/prefs/pref_service.h"
 #include "components/strings/grit/components_strings.h"
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
 #include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h"
 #import "ios/chrome/browser/ui/settings/password_details_collection_view_controller_for_testing.h"
 #import "ios/chrome/browser/ui/settings/reauthentication_module.h"
@@ -24,10 +30,12 @@
 #include "ios/chrome/grit/ios_strings.h"
 #import "ios/chrome/test/app/chrome_test_util.h"
 #include "ios/chrome/test/earl_grey/accessibility_util.h"
+#import "ios/chrome/test/earl_grey/chrome_actions.h"
 #import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
 #import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
 #import "ios/chrome/test/earl_grey/chrome_matchers.h"
 #import "ios/chrome/test/earl_grey/chrome_test_case.h"
+#import "ios/testing/wait_util.h"
 #import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "url/gurl.h"
@@ -56,19 +64,26 @@
 // it too high could result in scrolling way past the searched element.
 constexpr int kScrollAmount = 150;
 
+// Returns the GREYElementInteraction* for the item on the password list with
+// the given |matcher|. It scrolls in |direction| if necessary to ensure that
+// the matched item is interactable. The result can be used to perform user
+// actions or checks.
+GREYElementInteraction* GetInteractionForListItem(id<GREYMatcher> matcher,
+                                                  GREYDirection direction) {
+  return [[EarlGrey
+      selectElementWithMatcher:grey_allOf(matcher, grey_interactable(), nil)]
+         usingSearchAction:grey_scrollInDirection(direction, kScrollAmount)
+      onElementWithMatcher:grey_accessibilityID(
+                               @"SavePasswordsCollectionViewController")];
+}
+
 // Returns the GREYElementInteraction* for the cell on the password list with
 // the given |username|. It scrolls down if necessary to ensure that the matched
 // cell is interactable. The result can be used to perform user actions or
 // checks.
 GREYElementInteraction* GetInteractionForPasswordEntry(NSString* username) {
-  return [[EarlGrey
-      selectElementWithMatcher:grey_allOf(
-                                   ButtonWithAccessibilityLabel(username),
-                                   grey_interactable(), nil)]
-         usingSearchAction:grey_scrollInDirection(kGREYDirectionDown,
-                                                  kScrollAmount)
-      onElementWithMatcher:grey_accessibilityID(
-                               @"SavePasswordsCollectionViewController")];
+  return GetInteractionForListItem(ButtonWithAccessibilityLabel(username),
+                                   kGREYDirectionDown);
 }
 
 // Returns the GREYElementInteraction* for the item on the detail view
@@ -220,12 +235,70 @@
       ServiceAccessType::EXPLICIT_ACCESS);
 }
 
+// This class is used to obtain results from the PasswordStore and hence both
+// check the success of store updates and ensure that store has finished
+// processing.
+class TestStoreConsumer : public password_manager::PasswordStoreConsumer {
+ public:
+  void OnGetPasswordStoreResults(
+      std::vector<std::unique_ptr<autofill::PasswordForm>> obtained) override {
+    obtained_ = std::move(obtained);
+  }
+
+  const std::vector<autofill::PasswordForm>& GetStoreResults() {
+    results_.clear();
+    ResetObtained();
+    GetPasswordStore()->GetAutofillableLogins(this);
+    bool responded = testing::WaitUntilConditionOrTimeout(1.0, ^bool {
+      return !AreObtainedReset();
+    });
+    GREYAssert(responded, @"Obtaining fillable items took too long.");
+    AppendObtainedToResults();
+    GetPasswordStore()->GetBlacklistLogins(this);
+    responded = testing::WaitUntilConditionOrTimeout(1.0, ^bool {
+      return !AreObtainedReset();
+    });
+    GREYAssert(responded, @"Obtaining blacklisted items took too long.");
+    AppendObtainedToResults();
+    return results_;
+  }
+
+ private:
+  // Puts |obtained_| in a known state not corresponding to any PasswordStore
+  // state.
+  void ResetObtained() {
+    obtained_.clear();
+    obtained_.emplace_back(nullptr);
+  }
+
+  // Returns true if |obtained_| are in the reset state.
+  bool AreObtainedReset() { return obtained_.size() == 1 && !obtained_[0]; }
+
+  void AppendObtainedToResults() {
+    for (const auto& source : obtained_) {
+      results_.emplace_back(*source);
+    }
+    ResetObtained();
+  }
+
+  // Temporary cache of obtained store results.
+  std::vector<std::unique_ptr<autofill::PasswordForm>> obtained_;
+
+  // Combination of fillable and blacklisted credentials from the store.
+  std::vector<autofill::PasswordForm> results_;
+};
+
 // Saves |form| to the password store and waits until the async processing is
 // done.
 void SaveToPasswordStore(const PasswordForm& form) {
   GetPasswordStore()->AddLogin(form);
-  // Allow the PasswordStore to process this on the DB thread.
-  [[GREYUIThreadExecutor sharedInstance] drainUntilIdle];
+  // Check the result and ensure PasswordStore processed this.
+  TestStoreConsumer consumer;
+  for (const auto& result : consumer.GetStoreResults()) {
+    if (result == form)
+      return;
+  }
+  GREYFail(@"Stored form was not found in the PasswordStore results.");
 }
 
 // Saves an example form in the store.
@@ -242,8 +315,9 @@
 void ClearPasswordStore() {
   GetPasswordStore()->RemoveLoginsCreatedBetween(base::Time(), base::Time(),
                                                  base::Closure());
-  // Allow the PasswordStore to process this on the DB thread.
-  [[GREYUIThreadExecutor sharedInstance] drainUntilIdle];
+  TestStoreConsumer consumer;
+  GREYAssert(consumer.GetStoreResults().empty(),
+             @"PasswordStore was not cleared.");
 }
 
 // Opens the passwords page from the NTP. It requires no menus to be open.
@@ -251,6 +325,13 @@
   [ChromeEarlGreyUI openSettingsMenu];
   [ChromeEarlGreyUI
       tapSettingsMenuButton:chrome_test_util::SettingsMenuPasswordsButton()];
+  // The settings page requested results from PasswordStore. Make sure they
+  // have already been delivered by posting a task to PasswordStore's
+  // background task runner and waits until it is finished. Because the
+  // background task runner is sequenced, this means that previously posted
+  // tasks are also finished when this function exits.
+  TestStoreConsumer consumer;
+  consumer.GetStoreResults();
 }
 
 // Tap Edit in any settings view.
@@ -472,7 +553,10 @@
 
 // Checks that deleting a password from password details view goes back to the
 // list-of-passwords view.
-- (void)testDeletion {
+// TODO(crbug.com/515462) This test stopped working after
+// https://chromium-review.googlesource.com/c/573381/ landed. Needs to be
+// investigated and fixed.
+- (void)DISABLED_testDeletion {
   base::test::ScopedFeatureList scoped_feature_list;
   scoped_feature_list.InitAndEnableFeature(
       password_manager::features::kViewPasswords);
@@ -980,4 +1064,71 @@
   ClearPasswordStore();
 }
 
+// Check that stored entries are shown no matter what the preference for saving
+// passwords is.
+// TODO(crbug.com/515462): Fix the scrolling.
+- (void)DISABLED_testStoredEntriesAlwaysShown {
+  SaveExamplePasswordForm();
+
+  PasswordForm blacklisted;
+  blacklisted.origin = GURL("https://blacklisted.com");
+  blacklisted.signon_realm = blacklisted.origin.spec();
+  blacklisted.blacklisted_by_user = true;
+  SaveToPasswordStore(blacklisted);
+
+  OpenPasswordSettings();
+
+  // Toggle the "Save Passwords" control off and back on and check that stored
+  // items are still present.
+  constexpr BOOL kExpectedState[] = {YES, NO};
+  for (BOOL expected_state : kExpectedState) {
+    // Toggle the switch. It is located near the top, so if not interactable,
+    // try scrolling up.
+    [GetInteractionForListItem(chrome_test_util::CollectionViewSwitchCell(
+                                   @"savePasswordsItem_switch", expected_state),
+                               kGREYDirectionUp)
+        performAction:chrome_test_util::TurnCollectionViewSwitchOn(
+                          !expected_state)];
+    // Check the stored items. Scroll down if needed.
+    [GetInteractionForPasswordEntry(@"example.com, concrete username")
+        assertWithMatcher:grey_notNil()];
+    [GetInteractionForPasswordEntry(@"blacklisted.com")
+        assertWithMatcher:grey_notNil()];
+  }
+
+  [[EarlGrey selectElementWithMatcher:SettingsMenuBackButton()]
+      performAction:grey_tap()];
+  TapDone();
+  ClearPasswordStore();
+}
+
+// Check that toggling the switch for the "save passwords" preference changes
+// the settings.
+- (void)testPrefToggle {
+  OpenPasswordSettings();
+
+  // Toggle the "Save Passwords" control off and back on and check the
+  // preferences.
+  constexpr BOOL kExpectedState[] = {YES, NO};
+  for (BOOL expected_initial_state : kExpectedState) {
+    [[EarlGrey
+        selectElementWithMatcher:chrome_test_util::CollectionViewSwitchCell(
+                                     @"savePasswordsItem_switch",
+                                     expected_initial_state)]
+        performAction:chrome_test_util::TurnCollectionViewSwitchOn(
+                          !expected_initial_state)];
+    ios::ChromeBrowserState* browserState =
+        chrome_test_util::GetOriginalBrowserState();
+    const bool expected_final_state = !expected_initial_state;
+    GREYAssertEqual(expected_final_state,
+                    browserState->GetPrefs()->GetBoolean(
+                        password_manager::prefs::kPasswordManagerSavingEnabled),
+                    @"State of the UI toggle differs from real preferences.");
+  }
+
+  [[EarlGrey selectElementWithMatcher:SettingsMenuBackButton()]
+      performAction:grey_tap()];
+  TapDone();
+}
+
 @end
diff --git a/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm b/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm
index 6de6ac7f..53f05be8 100644
--- a/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm
+++ b/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm
@@ -204,34 +204,32 @@
       toSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch];
 
   // Saved passwords.
-  if ([passwordManagerEnabled_ value]) {
-    if (!savedForms_.empty()) {
-      [model addSectionWithIdentifier:SectionIdentifierSavedPasswords];
-      CollectionViewTextItem* headerItem =
-          [[CollectionViewTextItem alloc] initWithType:ItemTypeHeader];
-      headerItem.text =
-          l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING);
-      headerItem.textColor = [[MDCPalette greyPalette] tint500];
-      [model setHeader:headerItem
-          forSectionWithIdentifier:SectionIdentifierSavedPasswords];
-      for (const auto& form : savedForms_) {
-        [model addItem:[self savedFormItemWithForm:form.get()]
-            toSectionWithIdentifier:SectionIdentifierSavedPasswords];
-      }
+  if (!savedForms_.empty()) {
+    [model addSectionWithIdentifier:SectionIdentifierSavedPasswords];
+    CollectionViewTextItem* headerItem =
+        [[CollectionViewTextItem alloc] initWithType:ItemTypeHeader];
+    headerItem.text =
+        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING);
+    headerItem.textColor = [[MDCPalette greyPalette] tint500];
+    [model setHeader:headerItem
+        forSectionWithIdentifier:SectionIdentifierSavedPasswords];
+    for (const auto& form : savedForms_) {
+      [model addItem:[self savedFormItemWithForm:form.get()]
+          toSectionWithIdentifier:SectionIdentifierSavedPasswords];
     }
-    if (!blacklistedForms_.empty()) {
-      [model addSectionWithIdentifier:SectionIdentifierBlacklist];
-      CollectionViewTextItem* headerItem =
-          [[CollectionViewTextItem alloc] initWithType:ItemTypeHeader];
-      headerItem.text =
-          l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING);
-      headerItem.textColor = [[MDCPalette greyPalette] tint500];
-      [model setHeader:headerItem
-          forSectionWithIdentifier:SectionIdentifierBlacklist];
-      for (const auto& form : blacklistedForms_) {
-        [model addItem:[self blacklistedFormItemWithForm:form.get()]
-            toSectionWithIdentifier:SectionIdentifierBlacklist];
-      }
+  }
+  if (!blacklistedForms_.empty()) {
+    [model addSectionWithIdentifier:SectionIdentifierBlacklist];
+    CollectionViewTextItem* headerItem =
+        [[CollectionViewTextItem alloc] initWithType:ItemTypeHeader];
+    headerItem.text =
+        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING);
+    headerItem.textColor = [[MDCPalette greyPalette] tint500];
+    [model setHeader:headerItem
+        forSectionWithIdentifier:SectionIdentifierBlacklist];
+    for (const auto& form : blacklistedForms_) {
+      [model addItem:[self blacklistedFormItemWithForm:form.get()]
+          toSectionWithIdentifier:SectionIdentifierBlacklist];
     }
   }
 }
@@ -256,6 +254,7 @@
           initWithType:ItemTypeSavePasswordsSwitch];
   savePasswordsItem.text = l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS);
   savePasswordsItem.on = [passwordManagerEnabled_ value];
+  savePasswordsItem.accessibilityIdentifier = @"savePasswordsItem_switch";
   return savePasswordsItem;
 }
 
@@ -378,10 +377,9 @@
   // Update the cell.
   [self reconfigureCellsForItems:@[ savePasswordsItem_ ]];
 
-  // Update the rest of the UI.
+  // Update the edit button.
   [self.editor setEditing:NO];
   [self updateEditButton];
-  [self reloadData];
 }
 
 #pragma mark - Actions
@@ -393,10 +391,9 @@
   // Update the item.
   savePasswordsItem_.on = [passwordManagerEnabled_ value];
 
-  // Update the rest of the UI.
+  // Update the edit button.
   [self.editor setEditing:NO];
   [self updateEditButton];
-  [self reloadData];
 }
 
 #pragma mark - Private methods
diff --git a/ios/chrome/browser/ui/tools_menu/tools_popup_controller.mm b/ios/chrome/browser/ui/tools_menu/tools_popup_controller.mm
index 9df29c6..5fa1256 100644
--- a/ios/chrome/browser/ui/tools_menu/tools_popup_controller.mm
+++ b/ios/chrome/browser/ui/tools_menu/tools_popup_controller.mm
@@ -218,9 +218,6 @@
     case TOOLS_STOP_ITEM:
       base::RecordAction(UserMetricsAction("MobileMenuStop"));
       break;
-    case IDC_PRINT:
-      base::RecordAction(UserMetricsAction("MobileMenuPrint"));
-      break;
     case IDC_REPORT_AN_ISSUE:
       self.containerView.hidden = YES;
       base::RecordAction(UserMetricsAction("MobileMenuReportAnIssue"));
diff --git a/ios/chrome/browser/web/print_observer.h b/ios/chrome/browser/web/print_observer.h
index e47a88d..548c32f 100644
--- a/ios/chrome/browser/web/print_observer.h
+++ b/ios/chrome/browser/web/print_observer.h
@@ -7,6 +7,7 @@
 
 #include "ios/web/public/web_state/web_state_observer.h"
 
+@protocol BrowserCommands;
 namespace base {
 class DictionaryValue;
 }  // namespace base
@@ -16,7 +17,7 @@
 // Handles print requests from JavaScript window.print.
 class PrintObserver : public web::WebStateObserver {
  public:
-  explicit PrintObserver(web::WebState* web_state);
+  PrintObserver(web::WebState* web_state, id<BrowserCommands> dispatcher);
   ~PrintObserver() override;
 
  private:
@@ -27,6 +28,8 @@
   bool OnPrintCommand(const base::DictionaryValue&, const GURL&, bool);
   // Stops handling print requests from the web page.
   void Detach();
+
+  __weak id<BrowserCommands> dispatcher_;
 };
 
 #endif  // IOS_CHROME_BROWSER_WEB_PRINT_OBSERVER_H_
diff --git a/ios/chrome/browser/web/print_observer.mm b/ios/chrome/browser/web/print_observer.mm
index c883008..3c98184b 100644
--- a/ios/chrome/browser/web/print_observer.mm
+++ b/ios/chrome/browser/web/print_observer.mm
@@ -7,9 +7,7 @@
 #include "base/bind.h"
 #include "base/bind_helpers.h"
 #include "base/values.h"
-#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
-#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
-#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
+#include "ios/chrome/browser/ui/commands/browser_commands.h"
 #import "ios/web/public/web_state/web_state.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
@@ -21,8 +19,9 @@
 const char kPrintCommandPrefix[] = "print";
 }
 
-PrintObserver::PrintObserver(web::WebState* web_state)
-    : web::WebStateObserver(web_state) {
+PrintObserver::PrintObserver(web::WebState* web_state,
+                             id<BrowserCommands> dispatcher)
+    : web::WebStateObserver(web_state), dispatcher_(dispatcher) {
   web_state->AddScriptCommandCallback(
       base::Bind(&PrintObserver::OnPrintCommand, base::Unretained(this)),
       kPrintCommandPrefix);
@@ -39,9 +38,7 @@
 bool PrintObserver::OnPrintCommand(const base::DictionaryValue&,
                                    const GURL&,
                                    bool) {
-  GenericChromeCommand* print_command =
-      [[GenericChromeCommand alloc] initWithTag:IDC_PRINT];
-  [web_state()->GetView() chromeExecuteCommand:print_command];
+  [dispatcher_ printTab];
   return true;
 }
 
diff --git a/ios/clean/chrome/browser/ui/tab/BUILD.gn b/ios/clean/chrome/browser/ui/tab/BUILD.gn
index 3788862..3e44ed1 100644
--- a/ios/clean/chrome/browser/ui/tab/BUILD.gn
+++ b/ios/clean/chrome/browser/ui/tab/BUILD.gn
@@ -40,6 +40,7 @@
   ]
   deps = [
     "//ios/clean/chrome/browser/ui",
+    "//ios/clean/chrome/browser/ui/transitions",
     "//ios/clean/chrome/browser/ui/transitions/animators",
     "//ios/clean/chrome/browser/ui/transitions/presenters",
   ]
diff --git a/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.h b/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.h
index 6a1aacc..299c33ba 100644
--- a/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.h
+++ b/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.h
@@ -10,6 +10,8 @@
 #import "ios/clean/chrome/browser/ui/transitions/animators/zoom_transition_delegate.h"
 #import "ios/clean/chrome/browser/ui/transitions/presenters/menu_presentation_delegate.h"
 
+@protocol ContainmentTransitioningDelegate;
+
 // Abstract base class for a view controller that contains several views,
 // each managed by their own view controllers.
 // Subclasses manage the specific layout of these view controllers.
@@ -39,6 +41,11 @@
 // currently closed.
 @property(nonatomic, strong) UIViewController* findBarViewController;
 
+// Transitioning delegate for containment animations. By default it's the
+// tab container view controller itself.
+@property(nonatomic, weak) id<ContainmentTransitioningDelegate>
+    containmentTransitioningDelegate;
+
 @end
 
 // Tab container which positions the toolbar at the top.
diff --git a/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.mm b/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.mm
index b4b56b1..77fcdf7 100644
--- a/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.mm
+++ b/ios/clean/chrome/browser/ui/tab/tab_container_view_controller.mm
@@ -4,6 +4,9 @@
 
 #import "ios/clean/chrome/browser/ui/tab/tab_container_view_controller.h"
 
+#import "ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h"
+#import "ios/clean/chrome/browser/ui/transitions/containment_transition_context.h"
+#import "ios/clean/chrome/browser/ui/transitions/containment_transitioning_delegate.h"
 #import "ios/clean/chrome/browser/ui/ui_types.h"
 
 #if !defined(__has_feature) || !__has_feature(objc_arc)
@@ -15,7 +18,7 @@
 CGFloat kTabStripHeight = 120.0f;
 }
 
-@interface TabContainerViewController ()
+@interface TabContainerViewController ()<ContainmentTransitioningDelegate>
 
 // Container views for child view controllers. The child view controller's
 // view is added as a subview that fills its container view via autoresizing.
@@ -53,11 +56,14 @@
 @synthesize toolbarHeightConstraint = _toolbarHeightConstraint;
 @synthesize actionToForward = _actionToForward;
 @synthesize forwardingTarget = _forwardingTarget;
+@synthesize containmentTransitioningDelegate =
+    _containmentTransitioningDelegate;
 
 #pragma mark - UIViewController
 
 - (void)viewDidLoad {
   [super viewDidLoad];
+  self.containmentTransitioningDelegate = self;
   self.findBarView = [[UIView alloc] init];
   self.tabStripView = [[UIView alloc] init];
   self.toolbarView = [[UIView alloc] init];
@@ -67,10 +73,11 @@
   self.toolbarView.translatesAutoresizingMaskIntoConstraints = NO;
   self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
   self.view.backgroundColor = [UIColor blackColor];
-  self.findBarView.backgroundColor = [UIColor greenColor];
+  self.findBarView.backgroundColor = [UIColor clearColor];
   self.tabStripView.backgroundColor = [UIColor blackColor];
   self.toolbarView.backgroundColor = [UIColor blackColor];
   self.contentView.backgroundColor = [UIColor blackColor];
+  self.findBarView.clipsToBounds = YES;
 
   // Views that are added last have the highest z-order.
   [self.view addSubview:self.tabStripView];
@@ -115,12 +122,31 @@
   if (self.findBarViewController == findBarViewController)
     return;
   if ([self isViewLoaded]) {
-    [self detachChildViewController:self.findBarViewController];
-    [self addChildViewController:findBarViewController
-                       toSubview:self.findBarView];
+    self.findBarView.hidden = NO;
+    findBarViewController.view.translatesAutoresizingMaskIntoConstraints = YES;
+    findBarViewController.view.autoresizingMask =
+        UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+
+    ContainmentTransitionContext* context =
+        [[ContainmentTransitionContext alloc]
+            initWithFromViewController:self.findBarViewController
+                      toViewController:findBarViewController
+                  parentViewController:self
+                                inView:self.findBarView
+                            completion:^(BOOL finished) {
+                              self.findBarView.hidden =
+                                  (findBarViewController == nil);
+                            }];
+    id<UIViewControllerAnimatedTransitioning> animator =
+        [self.containmentTransitioningDelegate
+            animationControllerForAddingChildController:findBarViewController
+                                removingChildController:
+                                    self.findBarViewController
+                                           toController:self];
+    [context prepareTransitionWithAnimator:animator];
+    [animator animateTransition:context];
   }
   _findBarViewController = findBarViewController;
-  self.findBarView.hidden = (_findBarViewController == nil);
 }
 
 - (void)setToolbarViewController:(UIViewController*)toolbarViewController {
@@ -241,6 +267,15 @@
   return nil;
 }
 
+#pragma mark - ContainmentTransitioningDelegate
+
+- (id<UIViewControllerAnimatedTransitioning>)
+animationControllerForAddingChildController:(UIViewController*)addedChild
+                    removingChildController:(UIViewController*)removedChild
+                               toController:(UIViewController*)parent {
+  return [[SwapFromAboveAnimator alloc] init];
+}
+
 @end
 
 @implementation TopToolbarTabViewController
diff --git a/ios/clean/chrome/browser/ui/transitions/BUILD.gn b/ios/clean/chrome/browser/ui/transitions/BUILD.gn
index 0fb5b0b..7bcdbc2 100644
--- a/ios/clean/chrome/browser/ui/transitions/BUILD.gn
+++ b/ios/clean/chrome/browser/ui/transitions/BUILD.gn
@@ -4,6 +4,9 @@
 
 source_set("transitions") {
   sources = [
+    "containment_transition_context.h",
+    "containment_transition_context.mm",
+    "containment_transitioning_delegate.h",
     "zoom_transition_controller.h",
     "zoom_transition_controller.mm",
     "zooming_menu_transition_controller.h",
@@ -14,7 +17,23 @@
 
   deps = [
     "//base",
+    "//ios/chrome/browser",
     "//ios/clean/chrome/browser/ui/transitions/animators",
     "//ios/clean/chrome/browser/ui/transitions/presenters",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+
+  sources = [
+    "containment_transition_context_unittest.mm",
+  ]
+
+  configs += [ "//build/config/compiler:enable_arc" ]
+
+  deps = [
+    ":transitions",
+    "//testing/gtest",
+  ]
+}
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/BUILD.gn b/ios/clean/chrome/browser/ui/transitions/animators/BUILD.gn
index 32c610a..c4fba63 100644
--- a/ios/clean/chrome/browser/ui/transitions/animators/BUILD.gn
+++ b/ios/clean/chrome/browser/ui/transitions/animators/BUILD.gn
@@ -4,6 +4,8 @@
 
 source_set("animators") {
   sources = [
+    "swap_from_above_animator.h",
+    "swap_from_above_animator.mm",
     "zoom_transition_animator.h",
     "zoom_transition_animator.mm",
     "zoom_transition_delegate.h",
@@ -15,3 +17,21 @@
     "//base",
   ]
 }
+
+source_set("unit_tests") {
+  testonly = true
+
+  sources = [
+    "swap_from_above_animator_unittest.mm",
+    "test_transition_context.h",
+    "test_transition_context.mm",
+  ]
+
+  configs += [ "//build/config/compiler:enable_arc" ]
+
+  deps = [
+    ":animators",
+    "//base/test:test_support",
+    "//testing/gtest",
+  ]
+}
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h
new file mode 100644
index 0000000..b08b926
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h
@@ -0,0 +1,16 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_SWAP_FROM_ABOVE_ANIMATOR_H_
+#define IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_SWAP_FROM_ABOVE_ANIMATOR_H_
+
+#import <UIKit/UIKit.h>
+
+// An animation controller that animates in the new view controller from the top
+// and animates out the old view controller to the top.
+@interface SwapFromAboveAnimator
+    : NSObject<UIViewControllerAnimatedTransitioning>
+@end
+
+#endif  // IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_SWAP_FROM_ABOVE_ANIMATOR_H_
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.mm b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.mm
new file mode 100644
index 0000000..f1a50609
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.mm
@@ -0,0 +1,43 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@implementation SwapFromAboveAnimator
+
+#pragma mark - UIViewControllerAnimatedTransitioning
+
+- (NSTimeInterval)transitionDuration:
+    (id<UIViewControllerContextTransitioning>)transitionContext {
+  return 0.25;
+}
+
+- (void)animateTransition:
+    (id<UIViewControllerContextTransitioning>)transitionContext {
+  UIViewController* toViewController = [transitionContext
+      viewControllerForKey:UITransitionContextToViewControllerKey];
+  UIViewController* fromViewController = [transitionContext
+      viewControllerForKey:UITransitionContextFromViewControllerKey];
+  [transitionContext.containerView addSubview:toViewController.view];
+  CGRect insideFrame = transitionContext.containerView.bounds;
+  CGRect outsideFrame =
+      CGRectOffset(insideFrame, 0, -CGRectGetHeight(insideFrame));
+  toViewController.view.frame = outsideFrame;
+
+  [UIView animateWithDuration:[self transitionDuration:transitionContext]
+      animations:^{
+        fromViewController.view.frame = outsideFrame;
+        toViewController.view.frame = insideFrame;
+      }
+      completion:^(BOOL finished) {
+        [transitionContext
+            completeTransition:![transitionContext transitionWasCancelled]];
+      }];
+}
+
+@end
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator_unittest.mm b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator_unittest.mm
new file mode 100644
index 0000000..60aa8a0
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator_unittest.mm
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/clean/chrome/browser/ui/transitions/animators/swap_from_above_animator.h"
+
+#import "base/test/ios/wait_util.h"
+#import "ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+TEST(SwapFromAboveAnimatorTest, AnimationCallsCompleteTransitionOnContext) {
+  SwapFromAboveAnimator* animator = [[SwapFromAboveAnimator alloc] init];
+  TestTransitionContext* context = [[TestTransitionContext alloc] init];
+
+  [animator animateTransition:context];
+
+  base::test::ios::WaitUntilCondition(^bool {
+    return context.completeTransitionCount == 1;
+  });
+}
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.h b/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.h
new file mode 100644
index 0000000..a523ba3
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.h
@@ -0,0 +1,19 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_TEST_TRANSITION_CONTEXT_H_
+#define IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_TEST_TRANSITION_CONTEXT_H_
+
+#import <UIKit/UIKit.h>
+
+// A concrete transitioning context for tests.
+@interface TestTransitionContext
+    : NSObject<UIViewControllerContextTransitioning>
+
+// Counts the number of times |completeTransition:| was called.
+@property(nonatomic) NSUInteger completeTransitionCount;
+
+@end
+
+#endif  // IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_ANIMATORS_TEST_TRANSITION_CONTEXT_H_
diff --git a/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.mm b/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.mm
new file mode 100644
index 0000000..4324b86f
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.mm
@@ -0,0 +1,55 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/clean/chrome/browser/ui/transitions/animators/test_transition_context.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@implementation TestTransitionContext
+@synthesize completeTransitionCount = _completeTransitionCount;
+@synthesize animated = _animated;
+@synthesize interactive = _interactive;
+@synthesize transitionWasCancelled = _transitionWasCancelled;
+@synthesize presentationStyle = _presentationStyle;
+@synthesize targetTransform = _targetTransform;
+@synthesize containerView = _containerView;
+
+#pragma mark - UIViewControllerContextTransitioning
+
+- (void)updateInteractiveTransition:(CGFloat)percentComplete {
+}
+
+- (void)finishInteractiveTransition {
+}
+
+- (void)cancelInteractiveTransition {
+}
+
+- (void)pauseInteractiveTransition {
+}
+
+- (void)completeTransition:(BOOL)didComplete {
+  self.completeTransitionCount++;
+}
+
+- (UIViewController*)viewControllerForKey:
+    (UITransitionContextViewControllerKey)key {
+  return nil;
+}
+
+- (UIView*)viewForKey:(UITransitionContextViewKey)key {
+  return nil;
+}
+
+- (CGRect)initialFrameForViewController:(UIViewController*)vc {
+  return CGRectZero;
+}
+
+- (CGRect)finalFrameForViewController:(UIViewController*)vc {
+  return CGRectZero;
+}
+
+@end
diff --git a/ios/clean/chrome/browser/ui/transitions/containment_transition_context.h b/ios/clean/chrome/browser/ui/transitions/containment_transition_context.h
new file mode 100644
index 0000000..746f424
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/containment_transition_context.h
@@ -0,0 +1,55 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITION_CONTEXT_H_
+#define IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITION_CONTEXT_H_
+
+#import <UIKit/UIKit.h>
+
+#import "ios/chrome/browser/procedural_block_types.h"
+
+// A transition context specific to child view controller containment
+// transitions. For all other transitions (push/pop, present/dismiss, tab bar
+// change), it's usually UIKit that provides such a context.  Since UIKit
+// doesn't provide containment transitions API
+// ContainmentTransitionContext is introduced.
+//
+// How to use it:
+// When needing to animate in, animate out or swap child view controllers,
+// create a context with the "from" and "to" view controllers, the common parent
+// view controller, and the container view in which the transition animation
+// will take stage. Then call |prepareTransitionWithAnimator:| before passing
+// the context to an animation controller.
+//
+// Note that like with UIKit transitions API, the "from" view controller, if
+// provided, needs to be a child of the parent. On the other side, the "to" view
+// controller must not be a child already. It will be added automatically by the
+// context when |startTransition| is called.
+// Also, like with UIKit transitions API, it is necessary that the animation
+// controller calls |completeTransition:| on the context. This will update the
+// view controller hierarchy to reflect the transition.
+@interface ContainmentTransitionContext
+    : NSObject<UIViewControllerContextTransitioning>
+
+// Instantiates a transition context for child view controller containment.
+// The "from" and "to" can be nil (for animating in, animating out), but not at
+// the same time.
+- (instancetype)initWithFromViewController:(UIViewController*)fromViewController
+                          toViewController:(UIViewController*)toViewController
+                      parentViewController:
+                          (UIViewController*)parentViewController
+                                    inView:(UIView*)containerView
+                                completion:(ProceduralBlockWithBool)completion;
+
+// Prepare the process of transitioning between view controllers. It calls the
+// appropriate |addChildViewController:| and |willMoveFromParentViewController:|
+// required before animating their views.
+// |animator| is optional and will only be used to call |animationEnded:| on it
+// (if it responds to it) when the transition ends.
+- (void)prepareTransitionWithAnimator:
+    (id<UIViewControllerAnimatedTransitioning>)animator;
+
+@end
+
+#endif  // IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITION_CONTEXT_H_
diff --git a/ios/clean/chrome/browser/ui/transitions/containment_transition_context.mm b/ios/clean/chrome/browser/ui/transitions/containment_transition_context.mm
new file mode 100644
index 0000000..5d5e584c
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/containment_transition_context.mm
@@ -0,0 +1,135 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/clean/chrome/browser/ui/transitions/containment_transition_context.h"
+
+#include "base/logging.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@interface ContainmentTransitionContext ()
+@property(nonatomic, strong) UIView* containerView;
+@property(nonatomic, strong) UIViewController* parentViewController;
+@property(nonatomic, copy) ProceduralBlockWithBool completion;
+@property(nonatomic, weak) id<UIViewControllerAnimatedTransitioning> animator;
+@property(nonatomic, strong)
+    NSDictionary<UITransitionContextViewControllerKey, UIViewController*>*
+        viewControllers;
+@property(nonatomic, strong)
+    NSDictionary<UITransitionContextViewKey, UIView*>* views;
+@end
+
+@implementation ContainmentTransitionContext
+@synthesize animated = _animated;
+@synthesize interactive = _interactive;
+@synthesize transitionWasCancelled = _transitionWasCancelled;
+@synthesize presentationStyle = _presentationStyle;
+@synthesize targetTransform = _targetTransform;
+@synthesize containerView = _containerView;
+@synthesize parentViewController = _parentViewController;
+@synthesize completion = _completion;
+@synthesize animator = _animator;
+@synthesize viewControllers = _viewControllers;
+@synthesize views = _views;
+
+- (instancetype)initWithFromViewController:(UIViewController*)fromViewController
+                          toViewController:(UIViewController*)toViewController
+                      parentViewController:
+                          (UIViewController*)parentViewController
+                                    inView:(UIView*)containerView
+                                completion:(ProceduralBlockWithBool)completion {
+  self = [super init];
+  if (self) {
+    DCHECK(parentViewController);
+    DCHECK(!fromViewController ||
+           fromViewController.parentViewController == parentViewController);
+    DCHECK(!toViewController || toViewController.parentViewController == nil);
+    _animated = YES;
+    _interactive = NO;
+    _presentationStyle = UIModalPresentationCustom;
+    _targetTransform = CGAffineTransformIdentity;
+    NSMutableDictionary<UITransitionContextViewControllerKey,
+                        UIViewController*>* viewControllers =
+        [NSMutableDictionary dictionary];
+    NSMutableDictionary<UITransitionContextViewKey, UIView*>* views =
+        [NSMutableDictionary dictionary];
+    if (fromViewController) {
+      viewControllers[UITransitionContextFromViewControllerKey] =
+          fromViewController;
+      views[UITransitionContextFromViewKey] = fromViewController.view;
+    }
+    if (toViewController) {
+      viewControllers[UITransitionContextToViewControllerKey] =
+          toViewController;
+      views[UITransitionContextToViewKey] = toViewController.view;
+    }
+    _viewControllers = [viewControllers copy];
+    _views = [views copy];
+    _parentViewController = parentViewController;
+    _containerView = containerView;
+    _completion = [completion copy];
+  }
+  return self;
+}
+
+- (void)prepareTransitionWithAnimator:
+    (id<UIViewControllerAnimatedTransitioning>)animator {
+  self.animator = animator;
+  [self.viewControllers[UITransitionContextFromViewControllerKey]
+      willMoveToParentViewController:nil];
+  UIViewController* toViewController =
+      self.viewControllers[UITransitionContextToViewControllerKey];
+  if (toViewController) {
+    [self.parentViewController addChildViewController:toViewController];
+  }
+}
+
+#pragma mark - UIViewControllerContextTransitioning
+
+- (void)updateInteractiveTransition:(CGFloat)percentComplete {
+}
+
+- (void)finishInteractiveTransition {
+}
+
+- (void)cancelInteractiveTransition {
+}
+
+- (void)pauseInteractiveTransition {
+}
+
+- (void)completeTransition:(BOOL)didComplete {
+  [[self viewForKey:UITransitionContextFromViewKey] removeFromSuperview];
+  [[self viewControllerForKey:UITransitionContextFromViewControllerKey]
+      removeFromParentViewController];
+  [[self viewControllerForKey:UITransitionContextToViewControllerKey]
+      didMoveToParentViewController:self.parentViewController];
+  if ([self.animator respondsToSelector:@selector(animationEnded:)]) {
+    [self.animator animationEnded:didComplete];
+  }
+  if (self.completion) {
+    self.completion(didComplete);
+  }
+}
+
+- (UIViewController*)viewControllerForKey:
+    (UITransitionContextViewControllerKey)key {
+  return self.viewControllers[key];
+}
+
+- (UIView*)viewForKey:(UITransitionContextViewKey)key {
+  return self.views[key];
+}
+
+- (CGRect)initialFrameForViewController:(UIViewController*)viewController {
+  return CGRectZero;
+}
+
+- (CGRect)finalFrameForViewController:(UIViewController*)viewController {
+  return CGRectZero;
+}
+
+@end
diff --git a/ios/clean/chrome/browser/ui/transitions/containment_transition_context_unittest.mm b/ios/clean/chrome/browser/ui/transitions/containment_transition_context_unittest.mm
new file mode 100644
index 0000000..3e901c5
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/containment_transition_context_unittest.mm
@@ -0,0 +1,254 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/clean/chrome/browser/ui/transitions/containment_transition_context.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+#error "This file requires ARC support."
+#endif
+
+@interface FakeAnimator : NSObject<UIViewControllerAnimatedTransitioning>
+
+@property(nonatomic) BOOL transitionDurationCalled;
+@property(nonatomic) BOOL animateTransitionCalled;
+@property(nonatomic) NSUInteger animationEndedCount;
+@property(nonatomic) BOOL animationEndedArgument;
+
+@end
+
+@implementation FakeAnimator
+@synthesize transitionDurationCalled = _transitionDurationCalled;
+@synthesize animateTransitionCalled = _animateTransitionCalled;
+@synthesize animationEndedCount = _animationEndedCount;
+@synthesize animationEndedArgument = _animationEndedArgument;
+
+- (NSTimeInterval)transitionDuration:
+    (id<UIViewControllerContextTransitioning>)transitionContext {
+  self.transitionDurationCalled = YES;
+  return 0;
+}
+
+- (void)animateTransition:
+    (id<UIViewControllerContextTransitioning>)transitionContext {
+  self.animateTransitionCalled = YES;
+}
+
+- (void)animationEnded:(BOOL)transitionCompleted {
+  self.animationEndedArgument = transitionCompleted;
+  self.animationEndedCount++;
+}
+
+@end
+
+@interface TestViewController : UIViewController
+// Defaults to NO. Set to start recording calls to containment calls.
+@property(nonatomic, getter=isRecordingCalls) BOOL recordingCalls;
+@property(nonatomic) NSUInteger willMoveToParentCount;
+@property(nonatomic) UIViewController* willMoveToParentArgument;
+@property(nonatomic) NSUInteger didMoveToParentCount;
+@property(nonatomic) UIViewController* didMoveToParentArgument;
+@end
+
+@implementation TestViewController
+@synthesize recordingCalls = _recordingCalls;
+@synthesize willMoveToParentCount = _willMoveToParentCount;
+@synthesize willMoveToParentArgument = _willMoveToParentArgument;
+@synthesize didMoveToParentCount = _didMoveToParentCount;
+@synthesize didMoveToParentArgument = _didMoveToParentArgument;
+
+- (void)willMoveToParentViewController:(UIViewController*)parent {
+  [super willMoveToParentViewController:parent];
+  if (self.recordingCalls) {
+    self.willMoveToParentCount++;
+    self.willMoveToParentArgument = parent;
+  }
+}
+
+- (void)didMoveToParentViewController:(UIViewController*)parent {
+  [super didMoveToParentViewController:parent];
+  if (self.recordingCalls) {
+    self.didMoveToParentCount++;
+    self.didMoveToParentArgument = parent;
+  }
+}
+
+@end
+
+// Tests that the view controllers are properly returned by the context after
+// initialization.
+TEST(ContainmentTransitionContextTest, InitializationChildViewControllers) {
+  UIViewController* fromViewController = [[UIViewController alloc] init];
+  UIViewController* toViewController = [[UIViewController alloc] init];
+  UIViewController* parentViewController = [[UIViewController alloc] init];
+  [parentViewController addChildViewController:fromViewController];
+  [parentViewController.view addSubview:fromViewController.view];
+  [fromViewController didMoveToParentViewController:parentViewController];
+
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:fromViewController
+                toViewController:toViewController
+            parentViewController:parentViewController
+                          inView:nil
+                      completion:nil];
+
+  EXPECT_EQ(
+      fromViewController,
+      [context viewControllerForKey:UITransitionContextFromViewControllerKey]);
+  EXPECT_EQ(
+      toViewController,
+      [context viewControllerForKey:UITransitionContextToViewControllerKey]);
+  EXPECT_EQ(fromViewController.view,
+            [context viewForKey:UITransitionContextFromViewKey]);
+  EXPECT_EQ(toViewController.view,
+            [context viewForKey:UITransitionContextToViewKey]);
+}
+
+// Tests that the container view is properly returned by the context after
+// initialization.
+TEST(ContainmentTransitionContextTest, InitializationContainerView) {
+  UIView* containerView = [[UIView alloc] init];
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:nil
+                toViewController:nil
+            parentViewController:[[UIViewController alloc] init]
+                          inView:containerView
+                      completion:nil];
+  EXPECT_EQ(containerView, context.containerView);
+}
+
+// Tests that the view controllers receive the appropriate calls to perform a
+// containment transition when prepareTransitionWithAnimator: and
+// completeTransition: are called.
+TEST(ContainmentTransitionContextTest, PrepareAndTransition) {
+  TestViewController* fromViewController = [[TestViewController alloc] init];
+  TestViewController* toViewController = [[TestViewController alloc] init];
+  UIViewController* parentViewController = [[UIViewController alloc] init];
+  [parentViewController addChildViewController:fromViewController];
+  [parentViewController.view addSubview:fromViewController.view];
+  [fromViewController didMoveToParentViewController:parentViewController];
+  fromViewController.recordingCalls = YES;
+  toViewController.recordingCalls = YES;
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:fromViewController
+                toViewController:toViewController
+            parentViewController:parentViewController
+                          inView:nil
+                      completion:nil];
+
+  [context prepareTransitionWithAnimator:nil];
+
+  // Check that both view controllers are children of the parent.
+  EXPECT_EQ(parentViewController, fromViewController.parentViewController);
+  EXPECT_EQ(parentViewController, toViewController.parentViewController);
+  // Check that fromViewController received the appropriate call to prepare for
+  // being removed from its parent.
+  EXPECT_EQ(1U, fromViewController.willMoveToParentCount);
+  EXPECT_EQ(nil, fromViewController.willMoveToParentArgument);
+  // Check that the from view is still parented.
+  EXPECT_NE(nil, fromViewController.view.superview);
+  // Check that the to view is not yet parented.
+  EXPECT_EQ(nil, toViewController.view.superview);
+  // Check that toViewController didn't yet receive confirmation call for being
+  // added to the parent.
+  EXPECT_EQ(0U, toViewController.didMoveToParentCount);
+
+  [context completeTransition:YES];
+
+  // Check that only toViewController is a child of the parent.
+  EXPECT_EQ(nil, fromViewController.parentViewController);
+  EXPECT_EQ(parentViewController, toViewController.parentViewController);
+  // Check that fromViewController didn't get more calls to prepare for being
+  // removed from its parent.
+  EXPECT_EQ(1U, fromViewController.willMoveToParentCount);
+  // Check that the from view is not parented anymore.
+  EXPECT_EQ(nil, fromViewController.view.superview);
+  // Check that toViewController received the confirmation call for being added
+  // to the parent.
+  EXPECT_EQ(1U, toViewController.didMoveToParentCount);
+  EXPECT_EQ(parentViewController, toViewController.didMoveToParentArgument);
+}
+
+TEST(ContainmentTransitionContextTest, CompletionBlockCalledOnCompletion) {
+  __block NSUInteger count = 0;
+  __block BOOL didComplete = NO;
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:nil
+                toViewController:nil
+            parentViewController:[[UIViewController alloc] init]
+                          inView:nil
+                      completion:^(BOOL innerDidComplete) {
+                        count++;
+                        didComplete = innerDidComplete;
+                      }];
+  [context completeTransition:YES];
+  EXPECT_EQ(1U, count);
+  EXPECT_EQ(YES, didComplete);
+}
+
+TEST(ContainmentTransitionContextTest, CompletionBlockCalledOnCancellation) {
+  __block NSUInteger count = 0;
+  __block BOOL didComplete = NO;
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:nil
+                toViewController:nil
+            parentViewController:[[UIViewController alloc] init]
+                          inView:nil
+                      completion:^(BOOL innerDidComplete) {
+                        count++;
+                        didComplete = innerDidComplete;
+                      }];
+  [context completeTransition:NO];
+  EXPECT_EQ(1U, count);
+  EXPECT_EQ(NO, didComplete);
+}
+
+TEST(ContainmentTransitionContextTest, AnimationEndedCalledOnCompletion) {
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:nil
+                toViewController:nil
+            parentViewController:[[UIViewController alloc] init]
+                          inView:nil
+                      completion:nil];
+  FakeAnimator* animator = [[FakeAnimator alloc] init];
+  [context prepareTransitionWithAnimator:animator];
+  EXPECT_EQ(0U, animator.animationEndedCount);
+  EXPECT_FALSE(animator.animationEndedArgument);
+
+  [context completeTransition:YES];
+
+  // Check that animation ended was called as expected.
+  EXPECT_EQ(1U, animator.animationEndedCount);
+  EXPECT_TRUE(animator.animationEndedArgument);
+
+  // Check that the animator is never asked its duration or to animate. It's not
+  // the context job to call them.
+  EXPECT_FALSE(animator.transitionDurationCalled);
+  EXPECT_FALSE(animator.animateTransitionCalled);
+}
+
+TEST(ContainmentTransitionContextTest, AnimationEndedCalledOnCancellation) {
+  ContainmentTransitionContext* context = [[ContainmentTransitionContext alloc]
+      initWithFromViewController:nil
+                toViewController:nil
+            parentViewController:[[UIViewController alloc] init]
+                          inView:nil
+                      completion:nil];
+  FakeAnimator* animator = [[FakeAnimator alloc] init];
+  [context prepareTransitionWithAnimator:animator];
+  EXPECT_EQ(0U, animator.animationEndedCount);
+  EXPECT_FALSE(animator.animationEndedArgument);
+
+  [context completeTransition:NO];
+
+  // Check that animation ended was called as expected.
+  EXPECT_EQ(1U, animator.animationEndedCount);
+  EXPECT_FALSE(animator.animationEndedArgument);
+
+  // Check that the animator is never asked its duration or to animate. It's not
+  // the context job to call them.
+  EXPECT_FALSE(animator.transitionDurationCalled);
+  EXPECT_FALSE(animator.animateTransitionCalled);
+}
diff --git a/ios/clean/chrome/browser/ui/transitions/containment_transitioning_delegate.h b/ios/clean/chrome/browser/ui/transitions/containment_transitioning_delegate.h
new file mode 100644
index 0000000..6d3d04f
--- /dev/null
+++ b/ios/clean/chrome/browser/ui/transitions/containment_transitioning_delegate.h
@@ -0,0 +1,24 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITIONING_DELEGATE_H_
+#define IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITIONING_DELEGATE_H_
+
+// Delegate protocol to retrieve an animation controller when performing a view
+// controller containment animation.
+@protocol ContainmentTransitioningDelegate
+
+// Retrieves the animation controller to use when performing the transition from
+// |removedChild| to |addedChild| in the parent view controller.
+// |addedChild| may be nil, corresponding to just animating out |removedChild|.
+// |removedChild| may be nil, corresponding to just animating in |addedChild|.
+// |parent| is the view controller whose delegate is receiving this call.
+- (id<UIViewControllerAnimatedTransitioning>)
+animationControllerForAddingChildController:(UIViewController*)addedChild
+                    removingChildController:(UIViewController*)removedChild
+                               toController:(UIViewController*)parent;
+
+@end
+
+#endif  // IOS_CLEAN_CHROME_BROWSER_UI_TRANSITIONS_CONTAINMENT_TRANSITIONING_DELEGATE_H_
diff --git a/ios/clean/chrome/test/BUILD.gn b/ios/clean/chrome/test/BUILD.gn
index 446e0c6c..d48ff38 100644
--- a/ios/clean/chrome/test/BUILD.gn
+++ b/ios/clean/chrome/test/BUILD.gn
@@ -34,6 +34,8 @@
     "//ios/clean/chrome/browser/ui/tab_grid:unit_tests",
     "//ios/clean/chrome/browser/ui/toolbar:unit_tests",
     "//ios/clean/chrome/browser/ui/tools:unit_tests",
+    "//ios/clean/chrome/browser/ui/transitions:unit_tests",
+    "//ios/clean/chrome/browser/ui/transitions/animators:unit_tests",
     "//ios/clean/chrome/browser/ui/web_contents:unit_tests",
   ]
 }
diff --git a/media/gpu/args.gni b/media/gpu/args.gni
index c1511d9..0c4f3316 100644
--- a/media/gpu/args.gni
+++ b/media/gpu/args.gni
@@ -9,4 +9,7 @@
   # Indicates if Video4Linux2 codec is used. This is used for all CrOS
   # platforms which have v4l2 hardware encoder / decoder.
   use_v4l2_codec = false
+
+  # Indicates if VA-API-based hardware acceleration is to be used.
+  use_vaapi = false
 }
diff --git a/media/midi/task_service_unittest.cc b/media/midi/task_service_unittest.cc
index f69e74e..de0e35e 100644
--- a/media/midi/task_service_unittest.cc
+++ b/media/midi/task_service_unittest.cc
@@ -104,14 +104,14 @@
   }
 
   void SignalEvent() {
-    midi::SignalEvent();
     IncrementCount();
+    midi::SignalEvent();
   }
 
   void WaitEvent() {
+    IncrementCount();
     wait_task_event_->Signal();
     midi::WaitEvent();
-    IncrementCount();
   }
 
   base::Lock lock_;
diff --git a/third_party/WebKit/LayoutTests/TestExpectations b/third_party/WebKit/LayoutTests/TestExpectations
index 0b9b0d1..c4919c2 100644
--- a/third_party/WebKit/LayoutTests/TestExpectations
+++ b/third_party/WebKit/LayoutTests/TestExpectations
@@ -3102,3 +3102,6 @@
 crbug.com/746128 [ Win7 ] media/controls/video-enter-exit-fullscreen-without-hovering-doesnt-show-controls.html [ Failure ]
 
 crbug.com/626703 [ Win7 ] external/wpt/image-decodes/image-decode-path-changes.html [ Pass Crash ]
+
+# Sheriff failures 2017-07-20
+crbug.com/746926 [ Win Debug ] virtual/outofblink-cors/webexposed/global-interface-listing.html [ Timeout ]
diff --git a/third_party/WebKit/LayoutTests/http/tests/xmlhttprequest/resources/access-control-allow-lists.php b/third_party/WebKit/LayoutTests/http/tests/xmlhttprequest/resources/access-control-allow-lists.php
index e28fda8c..84001ae 100644
--- a/third_party/WebKit/LayoutTests/http/tests/xmlhttprequest/resources/access-control-allow-lists.php
+++ b/third_party/WebKit/LayoutTests/http/tests/xmlhttprequest/resources/access-control-allow-lists.php
@@ -26,6 +26,4 @@
     }
 }
 
-$headers['get_value'] = isset($_GET['get_value']) ? $_GET['get_value'] : '';
-
 echo json_encode( $headers );
diff --git a/third_party/WebKit/Source/core/dom/Modulator.h b/third_party/WebKit/Source/core/dom/Modulator.h
index ef910f66..975707d 100644
--- a/third_party/WebKit/Source/core/dom/Modulator.h
+++ b/third_party/WebKit/Source/core/dom/Modulator.h
@@ -23,6 +23,7 @@
 class ModuleScript;
 class ModuleScriptFetchRequest;
 class ModuleScriptLoaderClient;
+class ModuleTreeReachedUrlSet;
 class ScriptModuleResolver;
 class ScriptState;
 class ScriptValue;
@@ -91,6 +92,7 @@
   virtual void FetchTreeInternal(const ModuleScriptFetchRequest&,
                                  const AncestorList&,
                                  ModuleGraphLevel,
+                                 ModuleTreeReachedUrlSet*,
                                  ModuleTreeClient*) = 0;
 
   // Asynchronously retrieve a module script from the module map, or fetch it
diff --git a/third_party/WebKit/Source/core/dom/ModulatorImpl.cpp b/third_party/WebKit/Source/core/dom/ModulatorImpl.cpp
index 2cbe420e..d0bc44b 100644
--- a/third_party/WebKit/Source/core/dom/ModulatorImpl.cpp
+++ b/third_party/WebKit/Source/core/dom/ModulatorImpl.cpp
@@ -63,7 +63,7 @@
 
   AncestorList empty_ancestor_list;
   FetchTreeInternal(request, empty_ancestor_list,
-                    ModuleGraphLevel::kTopLevelModuleFetch, client);
+                    ModuleGraphLevel::kTopLevelModuleFetch, nullptr, client);
 
   // Step 2. When the internal module script graph fetching procedure
   // asynchronously completes with result, asynchronously complete this
@@ -74,12 +74,14 @@
 void ModulatorImpl::FetchTreeInternal(const ModuleScriptFetchRequest& request,
                                       const AncestorList& ancestor_list,
                                       ModuleGraphLevel level,
+                                      ModuleTreeReachedUrlSet* reached_url_set,
                                       ModuleTreeClient* client) {
   // We ensure module-related code is not executed without the flag.
   // https://crbug.com/715376
   CHECK(RuntimeEnabledFeatures::ModuleScriptsEnabled());
 
-  tree_linker_registry_->Fetch(request, ancestor_list, level, this, client);
+  tree_linker_registry_->Fetch(request, ancestor_list, level, this,
+                               reached_url_set, client);
 }
 
 void ModulatorImpl::FetchDescendantsForInlineScript(ModuleScript* module_script,
diff --git a/third_party/WebKit/Source/core/dom/ModulatorImpl.h b/third_party/WebKit/Source/core/dom/ModulatorImpl.h
index 9a87c58..b2116adf 100644
--- a/third_party/WebKit/Source/core/dom/ModulatorImpl.h
+++ b/third_party/WebKit/Source/core/dom/ModulatorImpl.h
@@ -18,6 +18,7 @@
 class ModuleMap;
 class ModuleScriptLoaderRegistry;
 class ModuleTreeLinkerRegistry;
+class ModuleTreeReachedUrlSet;
 class ResourceFetcher;
 class ScriptState;
 class WebTaskRunner;
@@ -51,6 +52,7 @@
   void FetchTreeInternal(const ModuleScriptFetchRequest&,
                          const AncestorList&,
                          ModuleGraphLevel,
+                         ModuleTreeReachedUrlSet*,
                          ModuleTreeClient*) override;
   void FetchSingle(const ModuleScriptFetchRequest&,
                    ModuleGraphLevel,
diff --git a/third_party/WebKit/Source/core/editing/LayoutSelection.cpp b/third_party/WebKit/Source/core/editing/LayoutSelection.cpp
index 13834e5..e014516 100644
--- a/third_party/WebKit/Source/core/editing/LayoutSelection.cpp
+++ b/third_party/WebKit/Source/core/editing/LayoutSelection.cpp
@@ -237,46 +237,6 @@
   DISALLOW_NEW();
 
  public:
-  class Iterator
-      : public std::iterator<std::input_iterator_tag, LayoutObject*> {
-   public:
-    explicit Iterator(const SelectionMarkingRange* range) {
-      if (!range) {
-        current_ = nullptr;
-        return;
-      }
-      range_ = range;
-      current_ = range->StartLayoutObject();
-    }
-    Iterator(const Iterator&) = default;
-    bool operator==(const Iterator& other) const {
-      return current_ == other.current_;
-    }
-    bool operator!=(const Iterator& other) const { return !operator==(other); }
-    Iterator& operator++() {
-      DCHECK(current_);
-      for (current_ = current_->NextInPreOrder();
-           current_ && current_ != range_->traverse_stop_;
-           current_ = current_->NextInPreOrder()) {
-        if (current_->CanBeSelectionLeaf())
-          return *this;
-      }
-
-      current_ = nullptr;
-      return *this;
-    }
-    LayoutObject* operator*() const {
-      DCHECK(current_);
-      return current_;
-    }
-
-   private:
-    const SelectionMarkingRange* range_;
-    LayoutObject* current_;
-  };
-  Iterator begin() const { return Iterator(this); };
-  Iterator end() const { return Iterator(nullptr); };
-
   SelectionMarkingRange() = default;
   SelectionMarkingRange(LayoutObject* start_layout_object,
                         int start_offset,
@@ -288,9 +248,6 @@
         end_offset_(end_offset) {
     DCHECK(start_layout_object_);
     DCHECK(end_layout_object_);
-    traverse_stop_ = end_layout_object_->ChildAt(end_offset);
-    if (!traverse_stop_)
-      traverse_stop_ = end_layout_object_->NextInPreOrderAfterChildren();
     if (start_layout_object_ != end_layout_object_)
       return;
     DCHECK_LT(start_offset_, end_offset_);
@@ -324,7 +281,6 @@
   int start_offset_ = -1;
   LayoutObject* end_layout_object_ = nullptr;
   int end_offset_ = -1;
-  LayoutObject* traverse_stop_ = nullptr;
 };
 
 // Update the selection status of all LayoutObjects between |start| and |end|.
@@ -341,7 +297,7 @@
     range.EndLayoutObject()->SetSelectionStateIfNeeded(SelectionState::kEnd);
   }
 
-  for (LayoutObject* runner : range) {
+  for (LayoutObject* runner : range.ToPaintRange()) {
     if (runner != range.StartLayoutObject() &&
         runner != range.EndLayoutObject() && runner->CanBeSelectionLeaf())
       runner->SetSelectionStateIfNeeded(SelectionState::kInside);
diff --git a/third_party/WebKit/Source/core/editing/VisibleSelection.cpp b/third_party/WebKit/Source/core/editing/VisibleSelection.cpp
index 4ee9cb8..d334ed93 100644
--- a/third_party/WebKit/Source/core/editing/VisibleSelection.cpp
+++ b/third_party/WebKit/Source/core/editing/VisibleSelection.cpp
@@ -574,6 +574,18 @@
   return nullptr;
 }
 
+template <typename Strategy>
+static bool ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+    const PositionTemplate<Strategy>& position,
+    const ContainerNode* editable_root,
+    const Element* base_editable_ancestor) {
+  if (editable_root)
+    return true;
+  Element* const editable_ancestor =
+      LowestEditableAncestor(position.ComputeContainerNode());
+  return editable_ancestor != base_editable_ancestor;
+}
+
 // The selection ends in editable content or non-editable content inside a
 // different editable ancestor, move backward until non-editable content inside
 // the same lowest editable ancestor is reached.
@@ -582,9 +594,8 @@
     const PositionTemplate<Strategy>& end,
     ContainerNode* end_root,
     Element* base_editable_ancestor) {
-  Element* const end_editable_ancestor =
-      LowestEditableAncestor(end.ComputeContainerNode());
-  if (end_root || end_editable_ancestor != base_editable_ancestor) {
+  if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+          end, end_root, base_editable_ancestor)) {
     PositionTemplate<Strategy> position =
         PreviousVisuallyDistinctCandidate(end);
     Element* shadow_ancestor = end_root ? end_root->OwnerShadowHost() : nullptr;
@@ -616,9 +627,8 @@
     const PositionTemplate<Strategy>& start,
     ContainerNode* start_root,
     Element* base_editable_ancestor) {
-  Element* const start_editable_ancestor =
-      LowestEditableAncestor(start.ComputeContainerNode());
-  if (start_root || start_editable_ancestor != base_editable_ancestor) {
+  if (ShouldAdjustPositionToAvoidCrossingEditingBoundaries(
+          start, start_root, base_editable_ancestor)) {
     PositionTemplate<Strategy> position = NextVisuallyDistinctCandidate(start);
     Element* shadow_ancestor =
         start_root ? start_root->OwnerShadowHost() : nullptr;
diff --git a/third_party/WebKit/Source/core/inspector/InspectorNetworkAgent.cpp b/third_party/WebKit/Source/core/inspector/InspectorNetworkAgent.cpp
index 9116d5a2..99107ba 100644
--- a/third_party/WebKit/Source/core/inspector/InspectorNetworkAgent.cpp
+++ b/third_party/WebKit/Source/core/inspector/InspectorNetworkAgent.cpp
@@ -248,7 +248,7 @@
 
 String BuildBlockedReason(ResourceRequestBlockedReason reason) {
   switch (reason) {
-    case ResourceRequestBlockedReason::CSP:
+    case ResourceRequestBlockedReason::kCSP:
       return protocol::Network::BlockedReasonEnum::Csp;
     case ResourceRequestBlockedReason::kMixedContent:
       return protocol::Network::BlockedReasonEnum::MixedContent;
diff --git a/third_party/WebKit/Source/core/loader/BUILD.gn b/third_party/WebKit/Source/core/loader/BUILD.gn
index 96a5d00..86c8ada 100644
--- a/third_party/WebKit/Source/core/loader/BUILD.gn
+++ b/third_party/WebKit/Source/core/loader/BUILD.gn
@@ -88,6 +88,7 @@
     "modulescript/ModuleTreeLinker.h",
     "modulescript/ModuleTreeLinkerRegistry.cpp",
     "modulescript/ModuleTreeLinkerRegistry.h",
+    "modulescript/ModuleTreeReachedUrlSet.h",
     "private/CrossOriginPreflightResultCache.cpp",
     "private/CrossOriginPreflightResultCache.h",
     "private/FrameClientHintsPreferencesContext.cpp",
diff --git a/third_party/WebKit/Source/core/loader/BaseFetchContext.cpp b/third_party/WebKit/Source/core/loader/BaseFetchContext.cpp
index de64a5b..24eef731 100644
--- a/third_party/WebKit/Source/core/loader/BaseFetchContext.cpp
+++ b/third_party/WebKit/Source/core/loader/BaseFetchContext.cpp
@@ -148,7 +148,7 @@
                                 options.integrity_metadata,
                                 options.parser_disposition, redirect_status,
                                 reporting_policy, check_header_type)) {
-    return ResourceRequestBlockedReason::CSP;
+    return ResourceRequestBlockedReason::kCSP;
   }
   return ResourceRequestBlockedReason::kNone;
 }
@@ -222,15 +222,15 @@
   if (CheckCSPForRequest(
           resource_request, url, options, reporting_policy, redirect_status,
           ContentSecurityPolicy::CheckHeaderType::kCheckEnforce) ==
-      ResourceRequestBlockedReason::CSP) {
-    return ResourceRequestBlockedReason::CSP;
+      ResourceRequestBlockedReason::kCSP) {
+    return ResourceRequestBlockedReason::kCSP;
   }
 
   if (type == Resource::kScript || type == Resource::kImportResource) {
     if (!AllowScriptFromSource(url)) {
       // TODO(estark): Use a different ResourceRequestBlockedReason here, since
       // this check has nothing to do with CSP. https://crbug.com/600795
-      return ResourceRequestBlockedReason::CSP;
+      return ResourceRequestBlockedReason::kCSP;
     }
   }
 
diff --git a/third_party/WebKit/Source/core/loader/BaseFetchContextTest.cpp b/third_party/WebKit/Source/core/loader/BaseFetchContextTest.cpp
index eef0b19..9fdc852 100644
--- a/third_party/WebKit/Source/core/loader/BaseFetchContextTest.cpp
+++ b/third_party/WebKit/Source/core/loader/BaseFetchContextTest.cpp
@@ -271,7 +271,7 @@
 
   ResourceLoaderOptions options;
 
-  EXPECT_EQ(ResourceRequestBlockedReason::CSP,
+  EXPECT_EQ(ResourceRequestBlockedReason::kCSP,
             fetch_context_->CanFollowRedirect(
                 Resource::kScript, resource_request, url, options,
                 SecurityViolationReportingPolicy::kReport,
@@ -298,7 +298,7 @@
 
   ResourceLoaderOptions options;
 
-  EXPECT_EQ(ResourceRequestBlockedReason::CSP,
+  EXPECT_EQ(ResourceRequestBlockedReason::kCSP,
             fetch_context_->AllowResponse(Resource::kScript, resource_request,
                                           url, options));
   EXPECT_EQ(2u, policy->violation_reports_sent_.size());
diff --git a/third_party/WebKit/Source/core/loader/DocumentLoader.cpp b/third_party/WebKit/Source/core/loader/DocumentLoader.cpp
index 56eeb34..367282c 100644
--- a/third_party/WebKit/Source/core/loader/DocumentLoader.cpp
+++ b/third_party/WebKit/Source/core/loader/DocumentLoader.cpp
@@ -816,7 +816,12 @@
       kSandboxAll &
       ~(kSandboxPopups | kSandboxPropagatesToAuxiliaryBrowsingContexts));
 
-  CommitData(main_resource->Data()->Data(), main_resource->Data()->size());
+  RefPtr<SharedBuffer> data(main_resource->Data());
+  data->ForEachSegment(
+      [this](const char* segment, size_t segment_size, size_t segment_offset) {
+        CommitData(segment, segment_size);
+        return true;
+      });
   return true;
 }
 
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.cpp b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.cpp
index 7211247..0af80648 100644
--- a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.cpp
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.cpp
@@ -9,6 +9,7 @@
 #include "core/dom/ModuleScript.h"
 #include "core/loader/modulescript/ModuleScriptFetchRequest.h"
 #include "core/loader/modulescript/ModuleTreeLinkerRegistry.h"
+#include "core/loader/modulescript/ModuleTreeReachedUrlSet.h"
 #include "platform/WebTaskRunner.h"
 #include "platform/bindings/V8ThrowException.h"
 #include "platform/loader/fetch/ResourceLoadingLog.h"
@@ -22,13 +23,15 @@
     const AncestorList& ancestor_list,
     ModuleGraphLevel level,
     Modulator* modulator,
+    ModuleTreeReachedUrlSet* reached_url_set,
     ModuleTreeLinkerRegistry* registry,
     ModuleTreeClient* client) {
   AncestorList ancestor_list_with_url = ancestor_list;
   ancestor_list_with_url.insert(request.Url());
 
-  ModuleTreeLinker* fetcher = new ModuleTreeLinker(
-      ancestor_list_with_url, level, modulator, registry, client);
+  ModuleTreeLinker* fetcher =
+      new ModuleTreeLinker(ancestor_list_with_url, level, modulator,
+                           reached_url_set, registry, client);
   fetcher->FetchSelf(request);
   return fetcher;
 }
@@ -47,7 +50,7 @@
   // 4. "Fetch the descendants of script (using an empty ancestor list)."
   ModuleTreeLinker* fetcher = new ModuleTreeLinker(
       empty_ancestor_list, ModuleGraphLevel::kTopLevelModuleFetch, modulator,
-      registry, client);
+      nullptr, registry, client);
   fetcher->module_script_ = module_script;
   fetcher->AdvanceState(State::kFetchingSelf);
 
@@ -68,21 +71,29 @@
 ModuleTreeLinker::ModuleTreeLinker(const AncestorList& ancestor_list_with_url,
                                    ModuleGraphLevel level,
                                    Modulator* modulator,
+                                   ModuleTreeReachedUrlSet* reached_url_set,
                                    ModuleTreeLinkerRegistry* registry,
                                    ModuleTreeClient* client)
     : modulator_(modulator),
+      reached_url_set_(
+          level == ModuleGraphLevel::kTopLevelModuleFetch
+              ? ModuleTreeReachedUrlSet::CreateFromTopLevelAncestorList(
+                    ancestor_list_with_url)
+              : reached_url_set),
       registry_(registry),
       client_(client),
       ancestor_list_with_url_(ancestor_list_with_url),
       level_(level),
       module_script_(this, nullptr) {
   CHECK(modulator);
+  CHECK(reached_url_set_);
   CHECK(registry);
   CHECK(client);
 }
 
 DEFINE_TRACE(ModuleTreeLinker) {
   visitor->Trace(modulator_);
+  visitor->Trace(reached_url_set_);
   visitor->Trace(registry_);
   visitor->Trace(client_);
   visitor->Trace(module_script_);
@@ -288,10 +299,23 @@
                             "return either a valid url or null.";
 
     // Step 6.3. if ancestor list does not contain url, append url to urls.
-    if (!ancestor_list_with_url_.Contains(url)) {
-      urls.push_back(url);
-      positions.push_back(module_request.position);
+    if (ancestor_list_with_url_.Contains(url))
+      continue;
+
+    // [unspec] If we already have started a sub-graph fetch for the |url| for
+    // this top-level module graph starting from |top_level_linker_|, we can
+    // safely rely on other module graph node ModuleTreeLinker to handle it.
+    if (reached_url_set_->IsAlreadyBeingFetched(url)) {
+      // We can't skip any sub-graph fetches directly made from the top level
+      // module, as we may end up proceeding to ModuleDeclarationInstantiation()
+      // with part of the graph still fetching.
+      CHECK_NE(level_, ModuleGraphLevel::kTopLevelModuleFetch);
+
+      continue;
     }
+
+    urls.push_back(url);
+    positions.push_back(module_request.position);
   }
 
   // Step 7. For each url in urls, perform the internal module script graph
@@ -321,6 +345,7 @@
   for (size_t i = 0; i < urls.size(); ++i) {
     DependencyModuleClient* dependency_client =
         DependencyModuleClient::Create(this);
+    reached_url_set_->ObserveModuleTreeLink(urls[i]);
     dependency_clients_.insert(dependency_client);
 
     ModuleScriptFetchRequest request(
@@ -329,7 +354,7 @@
         module_script_->BaseURL().GetString(), positions[i]);
     modulator_->FetchTreeInternal(request, ancestor_list_with_url_,
                                   ModuleGraphLevel::kDependentModuleFetch,
-                                  dependency_client);
+                                  reached_url_set_.Get(), dependency_client);
   }
 
   // Asynchronously continue processing after NotifyOneDescendantFinished() is
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.h b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.h
index b4a4346..f6bb121 100644
--- a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.h
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinker.h
@@ -14,8 +14,8 @@
 namespace blink {
 
 class ModuleScriptFetchRequest;
-enum class ModuleGraphLevel;
 class ModuleTreeLinkerRegistry;
+class ModuleTreeReachedUrlSet;
 
 // A ModuleTreeLinker is responsible for running and keeping intermediate states
 // for "internal module script graph fetching procedure" for a module graph tree
@@ -27,6 +27,7 @@
                                  const AncestorList&,
                                  ModuleGraphLevel,
                                  Modulator*,
+                                 ModuleTreeReachedUrlSet*,
                                  ModuleTreeLinkerRegistry*,
                                  ModuleTreeClient*);
   static ModuleTreeLinker* FetchDescendantsForInlineScript(
@@ -48,6 +49,7 @@
   ModuleTreeLinker(const AncestorList& ancestor_list_with_url,
                    ModuleGraphLevel,
                    Modulator*,
+                   ModuleTreeReachedUrlSet*,
                    ModuleTreeLinkerRegistry*,
                    ModuleTreeClient*);
 
@@ -79,6 +81,7 @@
   friend class DependencyModuleClient;
 
   const Member<Modulator> modulator_;
+  const Member<ModuleTreeReachedUrlSet> reached_url_set_;
   const Member<ModuleTreeLinkerRegistry> registry_;
   const Member<ModuleTreeClient> client_;
   const HashSet<KURL> ancestor_list_with_url_;
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.cpp b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.cpp
index cde4f58..9fb1ebb 100644
--- a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.cpp
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.cpp
@@ -24,9 +24,10 @@
     const AncestorList& ancestor_list,
     ModuleGraphLevel level,
     Modulator* modulator,
+    ModuleTreeReachedUrlSet* reached_url_set,
     ModuleTreeClient* client) {
   ModuleTreeLinker* fetcher = ModuleTreeLinker::Fetch(
-      request, ancestor_list, level, modulator, this, client);
+      request, ancestor_list, level, modulator, reached_url_set, this, client);
   DCHECK(fetcher->IsFetching());
   active_tree_linkers_.insert(
       TraceWrapperMember<ModuleTreeLinker>(this, fetcher));
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.h b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.h
index aa4a5cca..f96879f 100644
--- a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.h
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerRegistry.h
@@ -19,6 +19,7 @@
 class ModuleTreeLinker;
 enum class ModuleGraphLevel;
 class ModuleScript;
+class ModuleTreeReachedUrlSet;
 
 // ModuleTreeLinkerRegistry keeps active ModuleTreeLinkers alive.
 class CORE_EXPORT ModuleTreeLinkerRegistry
@@ -35,6 +36,7 @@
                           const AncestorList&,
                           ModuleGraphLevel,
                           Modulator*,
+                          ModuleTreeReachedUrlSet*,
                           ModuleTreeClient*);
   ModuleTreeLinker* FetchDescendantsForInlineScript(ModuleScript*,
                                                     Modulator*,
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerTest.cpp b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerTest.cpp
index d2cc943..bc2d125 100644
--- a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerTest.cpp
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeLinkerTest.cpp
@@ -11,6 +11,7 @@
 #include "core/dom/ModuleScript.h"
 #include "core/loader/modulescript/ModuleScriptFetchRequest.h"
 #include "core/loader/modulescript/ModuleTreeLinkerRegistry.h"
+#include "core/loader/modulescript/ModuleTreeReachedUrlSet.h"
 #include "core/testing/DummyModulator.h"
 #include "core/testing/DummyPageHolder.h"
 #include "platform/bindings/ScriptState.h"
@@ -167,6 +168,7 @@
   void FetchTreeInternal(const ModuleScriptFetchRequest& request,
                          const AncestorList& list,
                          ModuleGraphLevel level,
+                         ModuleTreeReachedUrlSet* reached_url_set,
                          ModuleTreeClient* client) override {
     const auto& url = request.Url();
 
@@ -174,6 +176,7 @@
     EXPECT_TRUE(ancestor_result.is_new_entry);
 
     EXPECT_EQ(ModuleGraphLevel::kDependentModuleFetch, level);
+    EXPECT_TRUE(reached_url_set);
 
     auto result_map = pending_tree_client_map_.insert(url, client);
     EXPECT_TRUE(result_map.is_new_entry);
@@ -274,7 +277,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -298,7 +301,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -326,7 +329,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -350,7 +353,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -383,7 +386,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -433,7 +436,7 @@
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(module_request, AncestorList(),
                   ModuleGraphLevel::kTopLevelModuleFetch, GetModulator(),
-                  client);
+                  nullptr, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -493,11 +496,14 @@
   KURL url(kParsedURLString, "http://example.com/depth1.js");
   ModuleScriptFetchRequest module_request(
       url, String(), kParserInserted, WebURLRequest::kFetchCredentialsModeOmit);
+  ModuleTreeReachedUrlSet* reached_url_set =
+      ModuleTreeReachedUrlSet::CreateFromTopLevelAncestorList(AncestorList());
   TestModuleTreeClient* client = new TestModuleTreeClient;
   registry->Fetch(
       module_request,
       AncestorList{KURL(kParsedURLString, "http://example.com/root.js")},
-      ModuleGraphLevel::kDependentModuleFetch, GetModulator(), client);
+      ModuleGraphLevel::kDependentModuleFetch, GetModulator(), reached_url_set,
+      client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -528,11 +534,13 @@
   KURL url(kParsedURLString, "http://example.com/a.js");
   ModuleScriptFetchRequest module_request(
       url, String(), kParserInserted, WebURLRequest::kFetchCredentialsModeOmit);
+  AncestorList ancestor_list{KURL(kParsedURLString, "http://example.com/a.js")};
+  ModuleTreeReachedUrlSet* reached_url_set =
+      ModuleTreeReachedUrlSet::CreateFromTopLevelAncestorList(ancestor_list);
   TestModuleTreeClient* client = new TestModuleTreeClient;
-  registry->Fetch(
-      module_request,
-      AncestorList{KURL(kParsedURLString, "http://example.com/a.js")},
-      ModuleGraphLevel::kDependentModuleFetch, GetModulator(), client);
+  registry->Fetch(module_request, ancestor_list,
+                  ModuleGraphLevel::kDependentModuleFetch, GetModulator(),
+                  reached_url_set, client);
 
   EXPECT_FALSE(client->WasNotifyFinished())
       << "ModuleTreeLinker should always finish asynchronously.";
@@ -541,8 +549,8 @@
   GetModulator()->ResolveSingleModuleScriptFetch(
       url, {"./a.js"}, ScriptModuleState::kUninstantiated);
 
-  auto ancestor_list = GetModulator()->GetAncestorListForTreeFetch(url);
-  EXPECT_EQ(0u, ancestor_list.size());
+  auto ancestor_list2 = GetModulator()->GetAncestorListForTreeFetch(url);
+  EXPECT_EQ(0u, ancestor_list2.size());
 
   EXPECT_TRUE(client->WasNotifyFinished());
   ASSERT_TRUE(client->GetModuleScript());
diff --git a/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeReachedUrlSet.h b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeReachedUrlSet.h
new file mode 100644
index 0000000..c178a5db
--- /dev/null
+++ b/third_party/WebKit/Source/core/loader/modulescript/ModuleTreeReachedUrlSet.h
@@ -0,0 +1,59 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ModuleTreeReachedUrlSet_h
+#define ModuleTreeReachedUrlSet_h
+
+#include "core/CoreExport.h"
+#include "core/dom/AncestorList.h"
+#include "platform/heap/Handle.h"
+#include "platform/weborigin/KURL.h"
+#include "platform/weborigin/KURLHash.h"
+#include "platform/wtf/HashSet.h"
+
+namespace blink {
+
+// ModuleTreeReachedUrlSet aims to reduce number of ModuleTreeLinker
+// involved in loading a module graph.
+//
+// ModuleTreeReachedUrlSet is created per top-level ModuleTreeLinker
+// invocations. The instance is shared among all descendants fetch
+// ModuleTreeLinker to track a set of module sub-graph root URLs
+// which we have started ModuleTreeLinker on.
+//
+// We consult this ModuleTreeReachedUrlSet before creating
+// ModuleTreeLinker for a descendant module sub-graph.
+// If the ModuleTreeReachedUrlSet indicates that the sub-graph
+// fetch is already taken care of by a existing ModuleTreeLinker,
+// a parent ModuleTreeLinker will defer to it and not instantiate a new
+// ModuleTreeLinker.
+class CORE_EXPORT ModuleTreeReachedUrlSet
+    : public GarbageCollectedFinalized<ModuleTreeReachedUrlSet> {
+ public:
+  static ModuleTreeReachedUrlSet* CreateFromTopLevelAncestorList(
+      const AncestorList& list) {
+    ModuleTreeReachedUrlSet* set = new ModuleTreeReachedUrlSet;
+    CHECK_LE(list.size(), 1u);
+    set->set_ = list;
+    return set;
+  }
+
+  void ObserveModuleTreeLink(const KURL& url) {
+    auto result = set_.insert(url);
+    CHECK(result.is_new_entry);
+  }
+
+  bool IsAlreadyBeingFetched(const KURL& url) const {
+    return set_.Contains(url);
+  }
+
+  DEFINE_INLINE_TRACE() {}
+
+ private:
+  HashSet<KURL> set_;
+};
+
+}  // namespace blink
+
+#endif  // ModuleTreeReachedUrlSet_h
diff --git a/third_party/WebKit/Source/core/loader/resource/FontResource.cpp b/third_party/WebKit/Source/core/loader/resource/FontResource.cpp
index dc74069..29a64ab 100644
--- a/third_party/WebKit/Source/core/loader/resource/FontResource.cpp
+++ b/third_party/WebKit/Source/core/loader/resource/FontResource.cpp
@@ -55,10 +55,11 @@
 };
 
 static FontPackageFormat PackageFormatOf(SharedBuffer* buffer) {
-  if (buffer->size() < 4)
+  static constexpr size_t kMaxHeaderSize = 4;
+  char data[kMaxHeaderSize];
+  if (!buffer->GetBytes(data, kMaxHeaderSize))
     return kPackageFormatUnknown;
 
-  const char* data = buffer->Data();
   if (data[0] == 'w' && data[1] == 'O' && data[2] == 'F' && data[3] == 'F')
     return kPackageFormatWOFF;
   if (data[0] == 'w' && data[1] == 'O' && data[2] == 'F' && data[3] == '2')
diff --git a/third_party/WebKit/Source/core/testing/DummyModulator.cpp b/third_party/WebKit/Source/core/testing/DummyModulator.cpp
index ac51a15..0cdf87ed 100644
--- a/third_party/WebKit/Source/core/testing/DummyModulator.cpp
+++ b/third_party/WebKit/Source/core/testing/DummyModulator.cpp
@@ -71,6 +71,7 @@
 void DummyModulator::FetchTreeInternal(const ModuleScriptFetchRequest&,
                                        const AncestorList&,
                                        ModuleGraphLevel,
+                                       ModuleTreeReachedUrlSet*,
                                        ModuleTreeClient*) {
   NOTREACHED();
 };
diff --git a/third_party/WebKit/Source/core/testing/DummyModulator.h b/third_party/WebKit/Source/core/testing/DummyModulator.h
index e7f123a0..e492eb0 100644
--- a/third_party/WebKit/Source/core/testing/DummyModulator.h
+++ b/third_party/WebKit/Source/core/testing/DummyModulator.h
@@ -41,6 +41,7 @@
   void FetchTreeInternal(const ModuleScriptFetchRequest&,
                          const AncestorList&,
                          ModuleGraphLevel,
+                         ModuleTreeReachedUrlSet*,
                          ModuleTreeClient*) override;
   void FetchSingle(const ModuleScriptFetchRequest&,
                    ModuleGraphLevel,
diff --git a/third_party/WebKit/Source/platform/PartitionAllocMemoryDumpProvider.cpp b/third_party/WebKit/Source/platform/PartitionAllocMemoryDumpProvider.cpp
index ff2bb46..59269a2 100644
--- a/third_party/WebKit/Source/platform/PartitionAllocMemoryDumpProvider.cpp
+++ b/third_party/WebKit/Source/platform/PartitionAllocMemoryDumpProvider.cpp
@@ -135,7 +135,17 @@
 
   MemoryDumpLevelOfDetail level_of_detail = args.level_of_detail;
   if (allocation_register_.is_enabled()) {
-    memory_dump->DumpHeapUsage(allocation_register_, kPartitionAllocDumpName);
+    // Overhead should always be reported, regardless of light vs. heavy.
+    base::trace_event::TraceEventMemoryOverhead overhead;
+    std::unordered_map<base::trace_event::AllocationContext,
+                       base::trace_event::AllocationMetrics>
+        metrics_by_context;
+    // Dump only the overhead estimation in non-detailed dumps.
+    if (level_of_detail == MemoryDumpLevelOfDetail::DETAILED) {
+      allocation_register_.UpdateAndReturnsMetrics(metrics_by_context);
+    }
+    allocation_register_.EstimateTraceMemoryOverhead(&overhead);
+    memory_dump->DumpHeapUsage(metrics_by_context, overhead, "partition_alloc");
   }
 
   PartitionStatsDumperImpl partition_stats_dumper(memory_dump, level_of_detail);
diff --git a/third_party/WebKit/Source/platform/heap/BlinkGCMemoryDumpProvider.cpp b/third_party/WebKit/Source/platform/heap/BlinkGCMemoryDumpProvider.cpp
index 5856bb25..d4077870 100644
--- a/third_party/WebKit/Source/platform/heap/BlinkGCMemoryDumpProvider.cpp
+++ b/third_party/WebKit/Source/platform/heap/BlinkGCMemoryDumpProvider.cpp
@@ -66,7 +66,16 @@
   DumpMemoryTotals(memory_dump);
 
   if (allocation_register_.is_enabled()) {
-    memory_dump->DumpHeapUsage(allocation_register_, "blink_gc");
+    // Overhead should always be reported, regardless of light vs. heavy.
+    base::trace_event::TraceEventMemoryOverhead overhead;
+    std::unordered_map<base::trace_event::AllocationContext,
+                       base::trace_event::AllocationMetrics>
+        metrics_by_context;
+    if (level_of_detail == MemoryDumpLevelOfDetail::DETAILED) {
+      allocation_register_.UpdateAndReturnsMetrics(metrics_by_context);
+    }
+    allocation_register_.EstimateTraceMemoryOverhead(&overhead);
+    memory_dump->DumpHeapUsage(metrics_by_context, overhead, "blink_gc");
   }
 
   // Merge all dumps collected by ThreadHeap::collectGarbage.
diff --git a/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.cc b/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.cc
index b3eee7d..6049d65c 100644
--- a/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.cc
+++ b/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.cc
@@ -7,6 +7,7 @@
 #include "base/memory/discardable_memory.h"
 #include "base/memory/ptr_util.h"
 #include "base/strings/stringprintf.h"
+#include "base/trace_event/heap_profiler_heap_dump_writer.h"
 #include "base/trace_event/process_memory_dump.h"
 #include "base/trace_event/trace_event_argument.h"
 #include "base/trace_event/trace_event_memory_overhead.h"
@@ -173,4 +174,14 @@
   return CreateWebMemoryAllocatorDump(dump);
 }
 
+void WebProcessMemoryDump::DumpHeapUsage(
+    const std::unordered_map<base::trace_event::AllocationContext,
+                             base::trace_event::AllocationMetrics>&
+        metrics_by_context,
+    base::trace_event::TraceEventMemoryOverhead& overhead,
+    const char* allocator_name) {
+  process_memory_dump_->DumpHeapUsage(metrics_by_context, overhead,
+                                      allocator_name);
+}
+
 }  // namespace content
diff --git a/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.h b/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.h
index 423ddf1..ed02763f 100644
--- a/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.h
+++ b/third_party/WebKit/Source/platform/instrumentation/tracing/web_process_memory_dump.h
@@ -26,6 +26,7 @@
 namespace trace_event {
 class MemoryAllocatorDump;
 class ProcessMemoryDump;
+class TraceEventMemoryOverhead;
 }  // namespace base
 }  // namespace trace_event
 
@@ -123,6 +124,15 @@
       const std::string& name,
       base::DiscardableMemory* discardable);
 
+  // Dumps heap memory usage. |allocatorName| is used as an absolute name for
+  // base::trace_event::ProcessMemoryDump::DumpHeapUsage().
+  void DumpHeapUsage(
+      const std::unordered_map<base::trace_event::AllocationContext,
+                               base::trace_event::AllocationMetrics>&
+          metrics_by_context,
+      base::trace_event::TraceEventMemoryOverhead& overhead,
+      const char* allocator_name);
+
  private:
   FRIEND_TEST_ALL_PREFIXES(WebProcessMemoryDumpTest, IntegrationTest);
 
diff --git a/third_party/WebKit/Source/platform/loader/fetch/ResourceRequest.h b/third_party/WebKit/Source/platform/loader/fetch/ResourceRequest.h
index bc17824..58adb65 100644
--- a/third_party/WebKit/Source/platform/loader/fetch/ResourceRequest.h
+++ b/third_party/WebKit/Source/platform/loader/fetch/ResourceRequest.h
@@ -45,7 +45,7 @@
 namespace blink {
 
 enum class ResourceRequestBlockedReason {
-  CSP,
+  kCSP,
   kMixedContent,
   kOrigin,
   kInspector,
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
index 91182b8e..4e70077 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
@@ -227,7 +227,7 @@
             _log.warning('Checkout is dirty; aborting.')
             return False
         _, local_commits = self.run(
-            ['git', 'log', '--oneline', 'origin/master..HEAD'])[1]
+            ['git', 'log', '--oneline', 'origin/master..HEAD'])
         if local_commits:
             _log.warning('Checkout has local commits before import.')
         return True
diff --git a/tools/gn/bootstrap/bootstrap.py b/tools/gn/bootstrap/bootstrap.py
index 231ff03..1390560f 100755
--- a/tools/gn/bootstrap/bootstrap.py
+++ b/tools/gn/bootstrap/bootstrap.py
@@ -537,7 +537,6 @@
       'base/trace_event/heap_profiler_event_writer.cc',
       'base/trace_event/heap_profiler_serialization_state.cc',
       'base/trace_event/heap_profiler_stack_frame_deduplicator.cc',
-      'base/trace_event/heap_profiler_string_deduplicator.cc',
       'base/trace_event/heap_profiler_type_name_deduplicator.cc',
       'base/trace_event/malloc_dump_provider.cc',
       'base/trace_event/memory_allocator_dump.cc',
diff --git a/tools/grit/grit/format/data_pack.py b/tools/grit/grit/format/data_pack.py
index f9bfc84..15d977e 100755
--- a/tools/grit/grit/format/data_pack.py
+++ b/tools/grit/grit/format/data_pack.py
@@ -21,9 +21,7 @@
 from grit.node import structure
 
 
-PACK_FILE_VERSION = 4
-HEADER_LENGTH = 2 * 4 + 1  # Two uint32s. (file version, number of entries) and
-                           # one uint8 (encoding of text resources)
+PACK_FILE_VERSION = 5
 BINARY, UTF8, UTF16 = range(3)
 
 
@@ -31,6 +29,10 @@
   pass
 
 
+class CorruptDataPack(Exception):
+  pass
+
+
 DataPackContents = collections.namedtuple(
     'DataPackContents', 'resources encoding')
 
@@ -49,56 +51,100 @@
 
 
 def ReadDataPack(input_file):
+  return ReadDataPackFromString(util.ReadFile(input_file, util.BINARY))
+
+
+def ReadDataPackFromString(data):
   """Reads a data pack file and returns a dictionary."""
-  data = util.ReadFile(input_file, util.BINARY)
   original_data = data
 
   # Read the header.
-  version, num_entries, encoding = struct.unpack('<IIB', data[:HEADER_LENGTH])
-  if version != PACK_FILE_VERSION:
-    print 'Wrong file version in ', input_file
-    raise WrongFileVersion
+  version = struct.unpack('<I', data[:4])[0]
+  if version == 4:
+    resource_count, encoding = struct.unpack('<IB', data[4:9])
+    alias_count = 0
+    data = data[9:]
+  elif version == 5:
+    encoding, resource_count, alias_count = struct.unpack('<BxxxHH', data[4:12])
+    data = data[12:]
+  else:
+    raise WrongFileVersion('Found version: ' + str(version))
 
   resources = {}
-  if num_entries == 0:
-    return DataPackContents(resources, encoding)
-
-  # Read the index and data.
-  data = data[HEADER_LENGTH:]
   kIndexEntrySize = 2 + 4  # Each entry is a uint16 and a uint32.
-  for _ in range(num_entries):
-    id, offset = struct.unpack('<HI', data[:kIndexEntrySize])
-    data = data[kIndexEntrySize:]
-    next_id, next_offset = struct.unpack('<HI', data[:kIndexEntrySize])
-    resources[id] = original_data[offset:next_offset]
+  def entry_at_index(idx):
+    offset = idx * kIndexEntrySize
+    return struct.unpack('<HI', data[offset:offset + kIndexEntrySize])
+
+  prev_resource_id, prev_offset = entry_at_index(0)
+  for i in xrange(1, resource_count + 1):
+    resource_id, offset = entry_at_index(i)
+    resources[prev_resource_id] = original_data[prev_offset:offset]
+    prev_resource_id, prev_offset = resource_id, offset
+
+  # Read the alias table.
+  alias_data = data[(resource_count + 1) * kIndexEntrySize:]
+  kAliasEntrySize = 2 + 2  # uint16, uint16
+  def alias_at_index(idx):
+    offset = idx * kAliasEntrySize
+    return struct.unpack('<HH', alias_data[offset:offset + kAliasEntrySize])
+
+  for i in xrange(alias_count):
+    resource_id, index = alias_at_index(i)
+    aliased_id = entry_at_index(index)[0]
+    resources[resource_id] = resources[aliased_id]
 
   return DataPackContents(resources, encoding)
 
 
 def WriteDataPackToString(resources, encoding):
   """Returns a string with a map of id=>data in the data pack format."""
-  ids = sorted(resources.keys())
   ret = []
 
+  # Compute alias map.
+  resource_ids = sorted(resources)
+  # Use reversed() so that for duplicates lower IDs clobber higher ones.
+  id_by_data = {resources[k]: k for k in reversed(resource_ids)}
+  # Map of resource_id -> resource_id, where value < key.
+  alias_map = {k: id_by_data[v] for k, v in resources.iteritems()
+               if id_by_data[v] != k}
+
   # Write file header.
-  ret.append(struct.pack('<IIB', PACK_FILE_VERSION, len(ids), encoding))
-  HEADER_LENGTH = 2 * 4 + 1            # Two uint32s and one uint8.
+  resource_count = len(resources) - len(alias_map)
+  # Padding bytes added for alignment.
+  ret.append(struct.pack('<IBxxxHH', PACK_FILE_VERSION, encoding,
+                         resource_count, len(alias_map)))
+  HEADER_LENGTH = 4 + 4 + 2 + 2
 
-  # Each entry is a uint16 + a uint32s. We have one extra entry for the last
-  # item.
-  index_length = (len(ids) + 1) * (2 + 4)
+  # Each main table entry is: uint16 + uint32 (and an extra entry at the end).
+  # Each alias table entry is: uint16 + uint16.
+  data_offset = HEADER_LENGTH + (resource_count + 1) * 6 + len(alias_map) * 4
 
-  # Write index.
-  data_offset = HEADER_LENGTH + index_length
-  for id in ids:
-    ret.append(struct.pack('<HI', id, data_offset))
-    data_offset += len(resources[id])
+  # Write main table.
+  index_by_id = {}
+  deduped_data = []
+  index = 0
+  for resource_id in resource_ids:
+    if resource_id in alias_map:
+      continue
+    data = resources[resource_id]
+    index_by_id[resource_id] = index
+    ret.append(struct.pack('<HI', resource_id, data_offset))
+    data_offset += len(data)
+    deduped_data.append(data)
+    index += 1
 
+  assert index == resource_count
+  # Add an extra entry at the end.
   ret.append(struct.pack('<HI', 0, data_offset))
 
+  # Write alias table.
+  for resource_id in sorted(alias_map):
+    index = index_by_id[alias_map[resource_id]]
+    ret.append(struct.pack('<HH', resource_id, index))
+
   # Write data.
-  for id in ids:
-    ret.append(resources[id])
+  ret.extend(deduped_data)
   return ''.join(ret)
 
 
diff --git a/tools/grit/grit/format/data_pack_unittest.py b/tools/grit/grit/format/data_pack_unittest.py
index f6e9edc..28a7b29 100755
--- a/tools/grit/grit/format/data_pack_unittest.py
+++ b/tools/grit/grit/format/data_pack_unittest.py
@@ -17,8 +17,8 @@
 
 
 class FormatDataPackUnittest(unittest.TestCase):
-  def testWriteDataPack(self):
-    expected = (
+  def testReadDataPackV4(self):
+    expected_data = (
         '\x04\x00\x00\x00'                  # header(version
         '\x04\x00\x00\x00'                  #        no. entries,
         '\x01'                              #        encoding)
@@ -28,9 +28,42 @@
         '\x0a\x00\x3f\x00\x00\x00'          # index entry 10
         '\x00\x00\x3f\x00\x00\x00'          # extra entry for the size of last
         'this is id 4this is id 6')         # data
-    input = {1: '', 4: 'this is id 4', 6: 'this is id 6', 10: ''}
-    output = data_pack.WriteDataPackToString(input, data_pack.UTF8)
-    self.failUnless(output == expected)
+    expected_resources = {
+        1: '',
+        4: 'this is id 4',
+        6: 'this is id 6',
+        10: '',
+    }
+    expected_data_pack = data_pack.DataPackContents(
+        expected_resources, data_pack.UTF8)
+    loaded = data_pack.ReadDataPackFromString(expected_data)
+    self.assertEquals(loaded, expected_data_pack)
+
+  def testReadWriteDataPackV5(self):
+    expected_data = (
+        '\x05\x00\x00\x00'                  # version
+        '\x01\x00\x00\x00'                  # encoding & padding
+        '\x03\x00'                          # resource_count
+        '\x01\x00'                          # alias_count
+        '\x01\x00\x28\x00\x00\x00'          # index entry 1
+        '\x04\x00\x28\x00\x00\x00'          # index entry 4
+        '\x06\x00\x34\x00\x00\x00'          # index entry 6
+        '\x00\x00\x40\x00\x00\x00'          # extra entry for the size of last
+        '\x0a\x00\x01\x00'                  # alias table
+        'this is id 4this is id 6')         # data
+    expected_resources = {
+        1: '',
+        4: 'this is id 4',
+        6: 'this is id 6',
+        10: 'this is id 4',
+    }
+    data = data_pack.WriteDataPackToString(expected_resources, data_pack.UTF8)
+    self.assertEquals(data, expected_data)
+
+    expected_data_pack = data_pack.DataPackContents(
+        expected_resources, data_pack.UTF8)
+    loaded = data_pack.ReadDataPackFromString(expected_data)
+    self.assertEquals(loaded, expected_data_pack)
 
   def testRePackUnittest(self):
     expected_with_whitelist = {
@@ -50,12 +83,14 @@
               in inputs]
 
     # RePack using whitelist
-    output, _ = data_pack.RePackFromDataPackStrings(inputs, whitelist)
+    output, _ = data_pack.RePackFromDataPackStrings(
+        inputs, whitelist, suppress_removed_key_output=True)
     self.assertDictEqual(expected_with_whitelist, output,
                          'Incorrect resource output')
 
     # RePack a None whitelist
-    output, _ = data_pack.RePackFromDataPackStrings(inputs, None)
+    output, _ = data_pack.RePackFromDataPackStrings(
+        inputs, None, suppress_removed_key_output=True)
     self.assertDictEqual(expected_without_whitelist, output,
                          'Incorrect resource output')
 
diff --git a/tools/grit/grit/format/policy_templates/policy_template_generator.py b/tools/grit/grit/format/policy_templates/policy_template_generator.py
index a0bacaf8..1444a8d 100755
--- a/tools/grit/grit/format/policy_templates/policy_template_generator.py
+++ b/tools/grit/grit/format/policy_templates/policy_template_generator.py
@@ -92,7 +92,6 @@
           'webview_android': ('webview',       'android'),
           'chrome_os':       ('chrome_os',     'chrome_os'),
           'chrome_frame':    ('chrome_frame',  'win'),
-          'ios':             ('chrome',        'ios'),
         }[product_platform_part]
         platforms = [platform]
       since_version, until_version = version_part.split('-')
diff --git a/tools/grit/grit/format/policy_templates/writers/doc_writer.py b/tools/grit/grit/format/policy_templates/writers/doc_writer.py
index d5eedda..9956d82 100755
--- a/tools/grit/grit/format/policy_templates/writers/doc_writer.py
+++ b/tools/grit/grit/format/policy_templates/writers/doc_writer.py
@@ -688,7 +688,6 @@
       'linux': 'Linux',
       'chrome_os': self.config['os_name'],
       'android': 'Android',
-      'ios': 'iOS',
     }
     # Human-readable names of supported products.
     self._PRODUCT_MAP = {
diff --git a/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py
index b7a39d2..91c2c76 100755
--- a/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py
+++ b/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py
@@ -428,11 +428,6 @@
         'platforms': ['android'],
         'since_version': '47',
         'until_version': '',
-      }, {
-        'product': 'chrome',
-        'platforms': ['ios'],
-        'since_version': '34',
-        'until_version': '',
       }],
       'features': {'dynamic_refresh': False},
       'example_value': False,
@@ -459,7 +454,6 @@
           '<li>Chrome (Windows, Mac, Linux) ...8...</li>'
           '<li>Chrome (Android) ...30...</li>'
           '<li>WebView (Android) ...47...</li>'
-          '<li>Chrome (iOS) ...34...</li>'
         '</ul>'
       '</dd>'
       '<dt style="style_dt;">_test_supported_features</dt>'
@@ -584,11 +578,6 @@
         'platforms': ['android'],
         'since_version': '30',
         'until_version': '',
-      }, {
-        'product': 'chrome',
-        'platforms': ['ios'],
-        'since_version': '34',
-        'until_version': '',
       }],
       'features': {
         'dynamic_refresh': False,
@@ -615,7 +604,6 @@
         '<ul style="style_ul;">'
           '<li>Chrome (Windows, Mac, Linux) ...8...</li>'
           '<li>Chrome (Android) ...30...</li>'
-          '<li>Chrome (iOS) ...34...</li>'
         '</ul>'
       '</dd>'
       '<dt style="style_dt;">_test_supported_features</dt>'
diff --git a/tools/grit/pak_util.py b/tools/grit/pak_util.py
index e98f9bd3..dd98049 100755
--- a/tools/grit/pak_util.py
+++ b/tools/grit/pak_util.py
@@ -33,6 +33,7 @@
 
 def _PrintMain(args):
   pak = data_pack.ReadDataPack(args.pak_file)
+  id_map = {id(v): k for k, v in sorted(pak.resources.items(), reverse=True)}
   encoding = 'binary'
   if pak.encoding == 1:
     encoding = 'utf-8'
@@ -57,8 +58,13 @@
       except UnicodeDecodeError:
         pass
     sha1 = hashlib.sha1(data).hexdigest()[:10]
-    line = u'Entry(id={}, len={}, sha1={}): {}'.format(
-        resource_id, len(data), sha1, desc)
+    canonical_id = id_map[id(data)]
+    if resource_id == canonical_id:
+      line = u'Entry(id={}, len={}, sha1={}): {}'.format(
+          resource_id, len(data), sha1, desc)
+    else:
+      line = u'Entry(id={}, alias_of={}): {}'.format(
+          resource_id, canonical_id, desc)
     print line.encode('utf-8')
 
 
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 3fbafed..c021b88 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -11083,6 +11083,7 @@
   <int value="373" label="UseSystemDefaultPrinterAsDefault"/>
   <int value="374" label="DeviceEcryptfsMigrationStrategy"/>
   <int value="375" label="SafeBrowsingForTrustedSourcesEnabled"/>
+  <int value="376" label="EcryptfsMigrationStrategy"/>
 </enum>
 
 <enum name="EnterprisePolicyInvalidations">
diff --git a/ui/base/BUILD.gn b/ui/base/BUILD.gn
index 119d0fa..b326fb2 100644
--- a/ui/base/BUILD.gn
+++ b/ui/base/BUILD.gn
@@ -788,6 +788,7 @@
     "material_design/material_design_controller_unittest.cc",
     "models/tree_node_iterator_unittest.cc",
     "resource/data_pack_literal.cc",
+    "resource/data_pack_literal.h",
     "resource/data_pack_unittest.cc",
     "resource/resource_bundle_unittest.cc",
     "resource/scale_factor_unittest.cc",
diff --git a/ui/base/resource/data_pack.cc b/ui/base/resource/data_pack.cc
index 3d07d985..511149d 100644
--- a/ui/base/resource/data_pack.cc
+++ b/ui/base/resource/data_pack.cc
@@ -24,31 +24,14 @@
 
 namespace {
 
-static const uint32_t kFileFormatVersion = 4;
-// Length of file header: version, entry count and text encoding type.
-static const size_t kHeaderLength = 2 * sizeof(uint32_t) + sizeof(uint8_t);
-
-#pragma pack(push, 2)
-struct DataPackEntry {
-  uint16_t resource_id;
-  uint32_t file_offset;
-
-  static int CompareById(const void* void_key, const void* void_entry) {
-    uint16_t key = *reinterpret_cast<const uint16_t*>(void_key);
-    const DataPackEntry* entry =
-        reinterpret_cast<const DataPackEntry*>(void_entry);
-    if (key < entry->resource_id) {
-      return -1;
-    } else if (key > entry->resource_id) {
-      return 1;
-    } else {
-      return 0;
-    }
-  }
-};
-#pragma pack(pop)
-
-static_assert(sizeof(DataPackEntry) == 6, "size of entry must be six");
+static const uint32_t kFileFormatV4 = 4;
+static const uint32_t kFileFormatV5 = 5;
+// int32(version), int32(resource_count), int8(encoding)
+static const size_t kHeaderLengthV4 = 2 * sizeof(uint32_t) + sizeof(uint8_t);
+// int32(version), int8(encoding), 3 bytes padding,
+// int16(resource_count), int16(alias_count)
+static const size_t kHeaderLengthV5 =
+    sizeof(uint32_t) + sizeof(uint8_t) * 4 + sizeof(uint16_t) * 2;
 
 // We're crashing when trying to load a pak file on Windows.  Add some error
 // codes for logging.
@@ -102,6 +85,30 @@
 
 namespace ui {
 
+#pragma pack(push, 2)
+struct DataPack::Entry {
+  uint16_t resource_id;
+  uint32_t file_offset;
+
+  static int CompareById(const void* void_key, const void* void_entry) {
+    uint16_t key = *reinterpret_cast<const uint16_t*>(void_key);
+    const Entry* entry = reinterpret_cast<const Entry*>(void_entry);
+    return key - entry->resource_id;
+  }
+};
+
+struct DataPack::Alias {
+  uint16_t resource_id;
+  uint16_t entry_index;
+
+  static int CompareById(const void* void_key, const void* void_entry) {
+    uint16_t key = *reinterpret_cast<const uint16_t*>(void_key);
+    const Alias* entry = reinterpret_cast<const Alias*>(void_entry);
+    return key - entry->resource_id;
+  }
+};
+#pragma pack(pop)
+
 // Abstraction of a data source (memory mapped file or in-memory buffer).
 class DataPack::DataSource {
  public:
@@ -149,9 +156,15 @@
 };
 
 DataPack::DataPack(ui::ScaleFactor scale_factor)
-    : resource_count_(0),
+    : resource_table_(nullptr),
+      resource_count_(0),
+      alias_table_(nullptr),
+      alias_count_(0),
       text_encoding_type_(BINARY),
       scale_factor_(scale_factor) {
+  // Static assert must be within a DataPack member to appease visiblity rules.
+  static_assert(sizeof(Entry) == 6, "size of Entry must be 6");
+  static_assert(sizeof(Alias) == 4, "size of Alias must be 4");
 }
 
 DataPack::~DataPack() {
@@ -193,29 +206,37 @@
 }
 
 bool DataPack::LoadImpl(std::unique_ptr<DataPack::DataSource> data_source) {
-  // Sanity check the header of the file.
-  if (kHeaderLength > data_source->GetLength()) {
+  const uint8_t* data = data_source->GetData();
+  size_t data_length = data_source->GetLength();
+  // Parse the version and check for truncated header.
+  uint32_t version = 0;
+  if (data_length > sizeof(version))
+    version = reinterpret_cast<const uint32_t*>(data)[0];
+  size_t header_length =
+      version == kFileFormatV4 ? kHeaderLengthV4 : kHeaderLengthV5;
+  if (version == 0 || data_length < header_length) {
     DLOG(ERROR) << "Data pack file corruption: incomplete file header.";
     LogDataPackError(HEADER_TRUNCATED);
     return false;
   }
 
   // Parse the header of the file.
-  // First uint32_t: version; second: resource count;
-  const uint32_t* ptr =
-      reinterpret_cast<const uint32_t*>(data_source->GetData());
-  uint32_t version = ptr[0];
-  if (version != kFileFormatVersion) {
+  if (version == kFileFormatV4) {
+    resource_count_ = reinterpret_cast<const uint32_t*>(data)[1];
+    alias_count_ = 0;
+    text_encoding_type_ = static_cast<TextEncodingType>(data[8]);
+  } else if (version == kFileFormatV5) {
+    // Version 5 added the alias table and changed the header format.
+    text_encoding_type_ = static_cast<TextEncodingType>(data[4]);
+    resource_count_ = reinterpret_cast<const uint16_t*>(data)[4];
+    alias_count_ = reinterpret_cast<const uint16_t*>(data)[5];
+  } else {
     LOG(ERROR) << "Bad data pack version: got " << version << ", expected "
-               << kFileFormatVersion;
+               << kFileFormatV4 << " or " << kFileFormatV5;
     LogDataPackError(BAD_VERSION);
     return false;
   }
-  resource_count_ = ptr[1];
 
-  // third: text encoding.
-  const uint8_t* ptr_encoding = reinterpret_cast<const uint8_t*>(ptr + 2);
-  text_encoding_type_ = static_cast<TextEncodingType>(*ptr_encoding);
   if (text_encoding_type_ != UTF8 && text_encoding_type_ != UTF16 &&
       text_encoding_type_ != BINARY) {
     LOG(ERROR) << "Bad data pack text encoding: got " << text_encoding_type_
@@ -227,35 +248,63 @@
   // Sanity check the file.
   // 1) Check we have enough entries. There's an extra entry after the last item
   // which gives the length of the last item.
-  if (kHeaderLength + (resource_count_ + 1) * sizeof(DataPackEntry) >
-      data_source->GetLength()) {
-    LOG(ERROR) << "Data pack file corruption: too short for number of "
-                  "entries specified.";
+  size_t resource_table_size = (resource_count_ + 1) * sizeof(Entry);
+  size_t alias_table_size = alias_count_ * sizeof(Alias);
+  if (header_length + resource_table_size + alias_table_size > data_length) {
+    LOG(ERROR) << "Data pack file corruption: "
+               << "too short for number of entries.";
     LogDataPackError(INDEX_TRUNCATED);
     return false;
   }
+
+  resource_table_ = reinterpret_cast<const Entry*>(&data[header_length]);
+  alias_table_ = reinterpret_cast<const Alias*>(
+      &data[header_length + resource_table_size]);
+
   // 2) Verify the entries are within the appropriate bounds. There's an extra
   // entry after the last item which gives us the length of the last item.
   for (size_t i = 0; i < resource_count_ + 1; ++i) {
-    const DataPackEntry* entry = reinterpret_cast<const DataPackEntry*>(
-        data_source->GetData() + kHeaderLength + (i * sizeof(DataPackEntry)));
-    if (entry->file_offset > data_source->GetLength()) {
-      LOG(ERROR) << "Entry #" << i << " in data pack points off end of file. "
-                 << "Was the file corrupted?";
+    if (resource_table_[i].file_offset > data_length) {
+      LOG(ERROR) << "Data pack file corruption: "
+                 << "Entry #" << i << " past end.";
+      LogDataPackError(ENTRY_NOT_FOUND);
+      return false;
+    }
+  }
+
+  // 3) Verify the aliases are within the appropriate bounds.
+  for (size_t i = 0; i < alias_count_; ++i) {
+    if (alias_table_[i].entry_index >= resource_count_) {
+      LOG(ERROR) << "Data pack file corruption: "
+                 << "Alias #" << i << " past end.";
       LogDataPackError(ENTRY_NOT_FOUND);
       return false;
     }
   }
 
   data_source_ = std::move(data_source);
-
   return true;
 }
 
+const DataPack::Entry* DataPack::LookupEntryById(uint16_t resource_id) const {
+  // Search the resource table first as most resources will be in there.
+  const Entry* ret = reinterpret_cast<const Entry*>(
+      bsearch(&resource_id, resource_table_, resource_count_, sizeof(Entry),
+              Entry::CompareById));
+  if (ret == nullptr) {
+    // Search the alias table for the ~10% of entries which are aliases.
+    const Alias* alias = reinterpret_cast<const Alias*>(
+        bsearch(&resource_id, alias_table_, alias_count_, sizeof(Alias),
+                Alias::CompareById));
+    if (alias != nullptr) {
+      ret = &resource_table_[alias->entry_index];
+    }
+  }
+  return ret;
+}
+
 bool DataPack::HasResource(uint16_t resource_id) const {
-  return !!bsearch(&resource_id, data_source_->GetData() + kHeaderLength,
-                   resource_count_, sizeof(DataPackEntry),
-                   DataPackEntry::CompareById);
+  return !!LookupEntryById(resource_id);
 }
 
 bool DataPack::GetStringPiece(uint16_t resource_id,
@@ -271,20 +320,19 @@
   #error DataPack assumes little endian
 #endif
 
-  const DataPackEntry* target = reinterpret_cast<const DataPackEntry*>(bsearch(
-      &resource_id, data_source_->GetData() + kHeaderLength, resource_count_,
-      sizeof(DataPackEntry), DataPackEntry::CompareById));
-  if (!target) {
+  const Entry* target = LookupEntryById(resource_id);
+  if (!target)
     return false;
-  }
 
-  const DataPackEntry* next_entry = target + 1;
+  const Entry* next_entry = target + 1;
   // If the next entry points beyond the end of the file this data pack's entry
   // table is corrupt. Log an error and return false. See
   // http://crbug.com/371301.
-  if (next_entry->file_offset > data_source_->GetLength()) {
-    size_t entry_index = target - reinterpret_cast<const DataPackEntry*>(
-                                      data_source_->GetData() + kHeaderLength);
+  size_t entry_offset =
+      reinterpret_cast<const uint8_t*>(next_entry) - data_source_->GetData();
+  size_t pak_size = data_source_->GetLength();
+  if (entry_offset > pak_size || next_entry->file_offset > pak_size) {
+    size_t entry_index = target - resource_table_;
     LOG(ERROR) << "Entry #" << entry_index << " in data pack points off end "
                << "of file. This should have been caught when loading. Was the "
                << "file modified?";
@@ -320,9 +368,7 @@
 void DataPack::CheckForDuplicateResources(
     const std::vector<std::unique_ptr<ResourceHandle>>& packs) {
   for (size_t i = 0; i < resource_count_ + 1; ++i) {
-    const DataPackEntry* entry = reinterpret_cast<const DataPackEntry*>(
-        data_source_->GetData() + kHeaderLength + (i * sizeof(DataPackEntry)));
-    const uint16_t resource_id = entry->resource_id;
+    const uint16_t resource_id = resource_table_[i].resource_id;
     const float resource_scale = GetScaleForScaleFactor(scale_factor_);
     for (const auto& handle : packs) {
       if (GetScaleForScaleFactor(handle->GetScaleFactor()) != resource_scale)
@@ -339,56 +385,44 @@
 bool DataPack::WritePack(const base::FilePath& path,
                          const std::map<uint16_t, base::StringPiece>& resources,
                          TextEncodingType textEncodingType) {
-  FILE* file = base::OpenFile(path, "wb");
-  if (!file)
-    return false;
-
-  if (fwrite(&kFileFormatVersion, sizeof(kFileFormatVersion), 1, file) != 1) {
-    LOG(ERROR) << "Failed to write file version";
-    base::CloseFile(file);
-    return false;
-  }
-
-  // Note: the python version of this function explicitly sorted keys, but
-  // std::map is a sorted associative container, we shouldn't have to do that.
-  uint32_t entry_count = resources.size();
-  if (fwrite(&entry_count, sizeof(entry_count), 1, file) != 1) {
-    LOG(ERROR) << "Failed to write entry count";
-    base::CloseFile(file);
-    return false;
-  }
-
   if (textEncodingType != UTF8 && textEncodingType != UTF16 &&
       textEncodingType != BINARY) {
     LOG(ERROR) << "Invalid text encoding type, got " << textEncodingType
                << ", expected between " << BINARY << " and " << UTF16;
-    base::CloseFile(file);
     return false;
   }
 
-  uint8_t write_buffer = static_cast<uint8_t>(textEncodingType);
-  if (fwrite(&write_buffer, sizeof(uint8_t), 1, file) != 1) {
-    LOG(ERROR) << "Failed to write file text resources encoding";
+  FILE* file = base::OpenFile(path, "wb");
+  if (!file)
+    return false;
+
+  uint32_t encoding = static_cast<uint32_t>(textEncodingType);
+  // Note: the python version of this function explicitly sorted keys, but
+  // std::map is a sorted associative container, we shouldn't have to do that.
+  uint16_t entry_count = resources.size();
+  // Don't bother computing aliases (revisit if it becomes worth it).
+  uint16_t alias_count = 0;
+
+  if (fwrite(&kFileFormatV5, sizeof(kFileFormatV5), 1, file) != 1 ||
+      fwrite(&encoding, sizeof(uint32_t), 1, file) != 1 ||
+      fwrite(&entry_count, sizeof(entry_count), 1, file) != 1 ||
+      fwrite(&alias_count, sizeof(alias_count), 1, file) != 1) {
+    LOG(ERROR) << "Failed to write header";
     base::CloseFile(file);
     return false;
   }
 
   // Each entry is a uint16_t + a uint32_t. We have an extra entry after the
   // last item so we can compute the size of the list item.
-  uint32_t index_length = (entry_count + 1) * sizeof(DataPackEntry);
-  uint32_t data_offset = kHeaderLength + index_length;
+  uint32_t index_length = (entry_count + 1) * sizeof(Entry);
+  uint32_t data_offset = kHeaderLengthV5 + index_length;
   for (std::map<uint16_t, base::StringPiece>::const_iterator it =
            resources.begin();
        it != resources.end(); ++it) {
     uint16_t resource_id = it->first;
-    if (fwrite(&resource_id, sizeof(resource_id), 1, file) != 1) {
-      LOG(ERROR) << "Failed to write id for " << resource_id;
-      base::CloseFile(file);
-      return false;
-    }
-
-    if (fwrite(&data_offset, sizeof(data_offset), 1, file) != 1) {
-      LOG(ERROR) << "Failed to write offset for " << resource_id;
+    if (fwrite(&resource_id, sizeof(resource_id), 1, file) != 1 ||
+        fwrite(&data_offset, sizeof(data_offset), 1, file) != 1) {
+      LOG(ERROR) << "Failed to write entry for " << resource_id;
       base::CloseFile(file);
       return false;
     }
diff --git a/ui/base/resource/data_pack.h b/ui/base/resource/data_pack.h
index 56d30d2..ffd47a4 100644
--- a/ui/base/resource/data_pack.h
+++ b/ui/base/resource/data_pack.h
@@ -75,6 +75,8 @@
 #endif
 
  private:
+  struct Entry;
+  struct Alias;
   class DataSource;
   class BufferDataSource;
   class MemoryMappedDataSource;
@@ -82,11 +84,14 @@
   // Does the actual loading of a pack file.
   // Called by Load and LoadFromFile and LoadFromBuffer.
   bool LoadImpl(std::unique_ptr<DataSource> data_source);
+  const Entry* LookupEntryById(uint16_t resource_id) const;
 
   std::unique_ptr<DataSource> data_source_;
 
-  // Number of resources in the data.
+  const Entry* resource_table_;
   size_t resource_count_;
+  const Alias* alias_table_;
+  size_t alias_count_;
 
   // Type of encoding for text resources.
   TextEncodingType text_encoding_type_;
diff --git a/ui/base/resource/data_pack_literal.cc b/ui/base/resource/data_pack_literal.cc
index cf490868..3218f848 100644
--- a/ui/base/resource/data_pack_literal.cc
+++ b/ui/base/resource/data_pack_literal.cc
@@ -4,57 +4,71 @@
 
 #include <stddef.h>
 
+#include "ui/base/resource/data_pack_literal.h"
+
 namespace ui {
 
-extern const char kSamplePakContents[] = {
-    0x04, 0x00, 0x00, 0x00,               // header(version
-    0x04, 0x00, 0x00, 0x00,               //        no. entries
-    0x01,                                 //        encoding)
-    0x01, 0x00, 0x27, 0x00, 0x00, 0x00,   // index entry 1
-    0x04, 0x00, 0x27, 0x00, 0x00, 0x00,   // index entry 4
-    0x06, 0x00, 0x33, 0x00, 0x00, 0x00,   // index entry 6
-    0x0a, 0x00, 0x3f, 0x00, 0x00, 0x00,   // index entry 10
-    0x00, 0x00, 0x3f, 0x00, 0x00, 0x00,   // extra entry for the size of last
-    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '4',
-    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '6'
+const char kSamplePakContentsV4[] = {
+    0x04, 0x00, 0x00, 0x00,              // header(version
+    0x04, 0x00, 0x00, 0x00,              //        no. entries
+    0x01,                                //        encoding)
+    0x01, 0x00, 0x27, 0x00, 0x00, 0x00,  // index entry 1
+    0x04, 0x00, 0x27, 0x00, 0x00, 0x00,  // index entry 4
+    0x06, 0x00, 0x33, 0x00, 0x00, 0x00,  // index entry 6
+    0x0a, 0x00, 0x3f, 0x00, 0x00, 0x00,  // index entry 10
+    0x00, 0x00, 0x3f, 0x00, 0x00, 0x00,  // extra entry for the size of last
+    't',  'h',  'i',  's',  ' ',  'i',  's', ' ', 'i', 'd', ' ', '4',
+    't',  'h',  'i',  's',  ' ',  'i',  's', ' ', 'i', 'd', ' ', '6'};
+
+const size_t kSamplePakSizeV4 = sizeof(kSamplePakContentsV4);
+
+const char kSamplePakContentsV5[] = {
+    0x05, 0x00, 0x00, 0x00,              // version
+    0x01, 0x00, 0x00, 0x00,              // encoding + padding
+    0x03, 0x00, 0x01, 0x00,              // num_resources, num_aliases
+    0x01, 0x00, 0x28, 0x00, 0x00, 0x00,  // index entry 1
+    0x04, 0x00, 0x28, 0x00, 0x00, 0x00,  // index entry 4
+    0x06, 0x00, 0x34, 0x00, 0x00, 0x00,  // index entry 6
+    0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // extra entry for the size of last
+    0x0a, 0x00, 0x01, 0x00,              // alias table
+    't',  'h',  'i',  's',  ' ',  'i',  's', ' ', 'i', 'd', ' ', '4',
+    't',  'h',  'i',  's',  ' ',  'i',  's', ' ', 'i', 'd', ' ', '6'};
+
+const size_t kSamplePakSizeV5 = sizeof(kSamplePakContentsV5);
+
+const char kSampleCorruptPakContents[] = {
+    0x04, 0x00, 0x00, 0x00,              // header(version
+    0x04, 0x00, 0x00, 0x00,              //        no. entries
+    0x01,                                //        encoding)
+    0x01, 0x00, 0x27, 0x00, 0x00, 0x00,  // index entry 1
+    0x04, 0x00, 0x27, 0x00, 0x00, 0x00,  // index entry 4
+    0x06, 0x00, 0x33, 0x00, 0x00, 0x00,  // index entry 6
+    0x0a, 0x00, 0x3f, 0x00, 0x00, 0x00,  // index entry 10
+    0x00, 0x00, 0x40, 0x00, 0x00, 0x00,  // extra entry for the size of last,
+                                         // extends past END OF FILE.
+    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '4', 't', 'h', 'i',
+    's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '6'};
+
+const size_t kSampleCorruptPakSize = sizeof(kSampleCorruptPakContents);
+
+const char kSamplePakContents2x[] = {
+    0x04, 0x00, 0x00, 0x00,              // header(version
+    0x01, 0x00, 0x00, 0x00,              //        no. entries
+    0x01,                                //        encoding)
+    0x04, 0x00, 0x15, 0x00, 0x00, 0x00,  // index entry 4
+    0x00, 0x00, 0x24, 0x00, 0x00, 0x00,  // extra entry for the size of last
+    't',  'h',  'i',  's',  ' ',  'i',  's', ' ',
+    'i',  'd',  ' ',  '4',  ' ',  '2',  'x'};
+
+const size_t kSamplePakSize2x = sizeof(kSamplePakContents2x);
+
+const char kEmptyPakContents[] = {
+    0x04, 0x00, 0x00, 0x00,             // header(version
+    0x00, 0x00, 0x00, 0x00,             //        no. entries
+    0x01,                               //        encoding)
+    0x00, 0x00, 0x0f, 0x00, 0x00, 0x00  // extra entry for the size of last
 };
 
-extern const size_t kSamplePakSize = sizeof(kSamplePakContents);
-
-extern const char kSampleCorruptPakContents[] = {
-    0x04, 0x00, 0x00, 0x00,               // header(version
-    0x04, 0x00, 0x00, 0x00,               //        no. entries
-    0x01,                                 //        encoding)
-    0x01, 0x00, 0x27, 0x00, 0x00, 0x00,   // index entry 1
-    0x04, 0x00, 0x27, 0x00, 0x00, 0x00,   // index entry 4
-    0x06, 0x00, 0x33, 0x00, 0x00, 0x00,   // index entry 6
-    0x0a, 0x00, 0x3f, 0x00, 0x00, 0x00,   // index entry 10
-    0x00, 0x00, 0x40, 0x00, 0x00, 0x00,   // extra entry for the size of last,
-                                          // extends past END OF FILE.
-    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '4',
-    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '6'
-};
-
-extern const size_t kSampleCorruptPakSize = sizeof(kSampleCorruptPakContents);
-
-extern const char kSamplePakContents2x[] = {
-    0x04, 0x00, 0x00, 0x00,               // header(version
-    0x01, 0x00, 0x00, 0x00,               //        no. entries
-    0x01,                                 //        encoding)
-    0x04, 0x00, 0x15, 0x00, 0x00, 0x00,   // index entry 4
-    0x00, 0x00, 0x24, 0x00, 0x00, 0x00,   // extra entry for the size of last
-    't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'i', 'd', ' ', '4', ' ', '2', 'x'
-};
-
-extern const size_t kSamplePakSize2x = sizeof(kSamplePakContents2x);
-
-extern const char kEmptyPakContents[] = {
-    0x04, 0x00, 0x00, 0x00,               // header(version
-    0x00, 0x00, 0x00, 0x00,               //        no. entries
-    0x01,                                 //        encoding)
-    0x00, 0x00, 0x0f, 0x00, 0x00, 0x00    // extra entry for the size of last
-};
-
-extern const size_t kEmptyPakSize = sizeof(kEmptyPakContents);
+const size_t kEmptyPakSize = sizeof(kEmptyPakContents);
 
 }  // namespace ui
diff --git a/ui/base/resource/data_pack_literal.h b/ui/base/resource/data_pack_literal.h
new file mode 100644
index 0000000..0e74647
--- /dev/null
+++ b/ui/base/resource/data_pack_literal.h
@@ -0,0 +1,23 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef UI_BASE_RESOURCE_DATA_PACK_LITERAL_H_
+#define UI_BASE_RESOURCE_DATA_PACK_LITERAL_H_
+
+namespace ui {
+
+extern const char kSamplePakContentsV4[];
+extern const size_t kSamplePakSizeV4;
+extern const char kSamplePakContentsV5[];
+extern const size_t kSamplePakSizeV5;
+extern const char kSamplePakContents2x[];
+extern const size_t kSamplePakSize2x;
+extern const char kEmptyPakContents[];
+extern const size_t kEmptyPakSize;
+extern const char kSampleCorruptPakContents[];
+extern const size_t kSampleCorruptPakSize;
+
+}  // namespace ui
+
+#endif  // UI_BASE_RESOURCE_DATA_PACK_LITERAL_H_
diff --git a/ui/base/resource/data_pack_unittest.cc b/ui/base/resource/data_pack_unittest.cc
index 4d53196..d2615b0 100644
--- a/ui/base/resource/data_pack_unittest.cc
+++ b/ui/base/resource/data_pack_unittest.cc
@@ -17,6 +17,7 @@
 #include "base/strings/string_piece.h"
 #include "build/build_config.h"
 #include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/resource/data_pack_literal.h"
 #include "ui/base/ui_base_paths.h"
 
 namespace ui {
@@ -27,11 +28,6 @@
   DataPackTest() {}
 };
 
-extern const char kSamplePakContents[];
-extern const char kSampleCorruptPakContents[];
-extern const size_t kSamplePakSize;
-extern const size_t kSampleCorruptPakSize;
-
 TEST(DataPackTest, LoadFromPath) {
   base::ScopedTempDir dir;
   ASSERT_TRUE(dir.CreateUniqueTempDir());
@@ -39,8 +35,8 @@
       dir.GetPath().Append(FILE_PATH_LITERAL("sample.pak"));
 
   // Dump contents into the pak file.
-  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContents, kSamplePakSize),
-            static_cast<int>(kSamplePakSize));
+  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContentsV4, kSamplePakSizeV4),
+            static_cast<int>(kSamplePakSizeV4));
 
   // Load the file through the data pack API.
   DataPack pack(SCALE_FACTOR_100P);
@@ -72,8 +68,8 @@
       dir.GetPath().Append(FILE_PATH_LITERAL("sample.pak"));
 
   // Dump contents into the pak file.
-  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContents, kSamplePakSize),
-            static_cast<int>(kSamplePakSize));
+  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContentsV4, kSamplePakSizeV4),
+            static_cast<int>(kSamplePakSizeV4));
 
   base::File file(data_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
   ASSERT_TRUE(file.IsValid());
@@ -112,15 +108,15 @@
   const char kPadding[5678] = {0};
   ASSERT_EQ(static_cast<int>(sizeof(kPadding)),
             base::WriteFile(data_path, kPadding, sizeof(kPadding)));
-  ASSERT_TRUE(base::AppendToFile(
-      data_path, kSamplePakContents, kSamplePakSize));
+  ASSERT_TRUE(
+      base::AppendToFile(data_path, kSamplePakContentsV4, kSamplePakSizeV4));
 
   base::File file(data_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
   ASSERT_TRUE(file.IsValid());
 
   // Load the file through the data pack API.
   DataPack pack(SCALE_FACTOR_100P);
-  base::MemoryMappedFile::Region region = {sizeof(kPadding), kSamplePakSize};
+  base::MemoryMappedFile::Region region = {sizeof(kPadding), kSamplePakSizeV4};
   ASSERT_TRUE(pack.LoadFromFileRegion(std::move(file), region));
 
   base::StringPiece data;
@@ -142,11 +138,11 @@
   ASSERT_FALSE(pack.GetStringPiece(140, &data));
 }
 
-TEST(DataPackTest, LoadFromBuffer) {
+TEST(DataPackTest, LoadFromBufferV4) {
   DataPack pack(SCALE_FACTOR_100P);
 
   ASSERT_TRUE(pack.LoadFromBuffer(
-      base::StringPiece(kSamplePakContents, kSamplePakSize)));
+      base::StringPiece(kSamplePakContentsV4, kSamplePakSizeV4)));
 
   base::StringPiece data;
   ASSERT_TRUE(pack.HasResource(4));
@@ -167,6 +163,31 @@
   ASSERT_FALSE(pack.GetStringPiece(140, &data));
 }
 
+TEST(DataPackTest, LoadFromBufferV5) {
+  DataPack pack(SCALE_FACTOR_100P);
+
+  ASSERT_TRUE(pack.LoadFromBuffer(
+      base::StringPiece(kSamplePakContentsV5, kSamplePakSizeV5)));
+
+  base::StringPiece data;
+  ASSERT_TRUE(pack.HasResource(4));
+  ASSERT_TRUE(pack.GetStringPiece(4, &data));
+  EXPECT_EQ("this is id 4", data);
+  ASSERT_TRUE(pack.HasResource(6));
+  ASSERT_TRUE(pack.GetStringPiece(6, &data));
+  EXPECT_EQ("this is id 6", data);
+
+  // Try reading zero-length data blobs, just in case.
+  ASSERT_TRUE(pack.GetStringPiece(1, &data));
+  EXPECT_EQ(0U, data.length());
+  ASSERT_TRUE(pack.GetStringPiece(10, &data));
+  EXPECT_EQ("this is id 4", data);
+
+  // Try looking up an invalid key.
+  ASSERT_FALSE(pack.HasResource(140));
+  ASSERT_FALSE(pack.GetStringPiece(140, &data));
+}
+
 INSTANTIATE_TEST_CASE_P(WriteBINARY, DataPackTest, ::testing::Values(
     DataPack::BINARY));
 INSTANTIATE_TEST_CASE_P(WriteUTF8, DataPackTest, ::testing::Values(
@@ -228,8 +249,8 @@
       dir.GetPath().Append(FILE_PATH_LITERAL("sample.pak"));
 
   // Dump contents into the pak file.
-  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContents, kSamplePakSize),
-            static_cast<int>(kSamplePakSize));
+  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContentsV4, kSamplePakSizeV4),
+            static_cast<int>(kSamplePakSizeV4));
 
   base::File file(data_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
   ASSERT_TRUE(file.IsValid());
diff --git a/ui/base/resource/resource_bundle_unittest.cc b/ui/base/resource/resource_bundle_unittest.cc
index df11f1a..363a0e9 100644
--- a/ui/base/resource/resource_bundle_unittest.cc
+++ b/ui/base/resource/resource_bundle_unittest.cc
@@ -23,6 +23,7 @@
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/base/layout.h"
 #include "ui/base/resource/data_pack.h"
+#include "ui/base/resource/data_pack_literal.h"
 #include "ui/gfx/codec/png_codec.h"
 #include "ui/gfx/font_list.h"
 #include "ui/gfx/image/image_skia.h"
@@ -38,14 +39,6 @@
 using ::testing::ReturnArg;
 
 namespace ui {
-
-extern const char kSamplePakContents[];
-extern const size_t kSamplePakSize;
-extern const char kSamplePakContents2x[];
-extern const size_t kSamplePakSize2x;
-extern const char kEmptyPakContents[];
-extern const size_t kEmptyPakSize;
-
 namespace {
 
 const unsigned char kPngMagic[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 };
@@ -416,8 +409,8 @@
       dir_path().Append(FILE_PATH_LITERAL("sample_2x.pak"));
 
   // Dump contents into the pak files.
-  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContents,
-      kSamplePakSize), static_cast<int>(kSamplePakSize));
+  ASSERT_EQ(base::WriteFile(data_path, kSamplePakContentsV4, kSamplePakSizeV4),
+            static_cast<int>(kSamplePakSizeV4));
   ASSERT_EQ(base::WriteFile(data_2x_path, kSamplePakContents2x,
       kSamplePakSize2x), static_cast<int>(kSamplePakSize2x));
 
diff --git a/ui/strings/ui_strings.grd b/ui/strings/ui_strings.grd
index e7d71bd..4323568 100644
--- a/ui/strings/ui_strings.grd
+++ b/ui/strings/ui_strings.grd
@@ -643,6 +643,9 @@
       <message name="IDS_MESSAGE_CENTER_NOTIFICATION_PROGRESS_PERCENTAGE" desc="The summary text in a notification that is shown with progress bar.">
         <ph name="NUMBER">$1<ex>75</ex></ph> %
       </message>
+      <message name="IDS_MESSAGE_CENTER_NOTIFICATION_CHROMEOS_SYSTEM" desc="The label to describe the notification comes from ChromeOS system.">
+        <ph name="IDS_SHORT_PRODUCT_OS_NAME">$1<ex>Chrome OS</ex></ph> system
+      </message>
       <message name="IDS_MESSAGE_NOTIFICATION_NOW_STRING_SHORTEST" desc="A string denoting the current point in time that should be as short as possible. Should be same as AndroidPlatform msgId 8912796667087856402. (frameworks/base/core/res/res/values/strings.xml:string:now_string_shortest)">
         now
       </message>
diff --git a/ui/webui/resources/cr_elements/chromeos/cr_picture/cr_picture_pane.js b/ui/webui/resources/cr_elements/chromeos/cr_picture/cr_picture_pane.js
index 31012d7b..4ebfba1 100644
--- a/ui/webui/resources/cr_elements/chromeos/cr_picture/cr_picture_pane.js
+++ b/ui/webui/resources/cr_elements/chromeos/cr_picture/cr_picture_pane.js
@@ -93,13 +93,16 @@
   },
 
   /**
-   * Returns the 2x (high dpi) image to use for 'srcset'. Note: 'src' will still
-   * be used as the 1x candidate as per the HTML spec.
+   * Returns the 2x (high dpi) image to use for 'srcset' for chrome://theme
+   * images. Note: 'src' will still be used as the 1x candidate as per the HTML
+   * spec.
    * @param {string} url
    * @return {string}
    * @private
    */
   getImgSrc2x_: function(url) {
+    if (url.indexOf('chrome://theme') != 0)
+      return '';
     return url + '@2x 2x';
   },
 });