[go: nahoru, domu]

cc: Add image decode queue functionality to image manager.

This patch adds an ability to request an image decode from
the compositor. The caller should specify which image
(SkImage) to decode and a callback. In return, it gets an
id. When the image is decoded, the callback is issued with
this id. The decode is then kept alive for 2 compositor
frames.

Right now, only unittests are using this functionality.

R=enne@chromium.org, ericrk@chromium.org, danakj@chromium.org
CQ_INCLUDE_TRYBOTS=master.tryserver.blink:linux_trusty_blink_rel

Review-Url: https://codereview.chromium.org/2537683002
Cr-Original-Commit-Position: refs/heads/master@{#443085}
Committed: https://chromium.googlesource.com/chromium/src/+/bb991d41668aa0f37aa6d6712e3bf9b52a5480ae
Review-Url: https://codereview.chromium.org/2537683002
Cr-Commit-Position: refs/heads/master@{#443341}
diff --git a/cc/BUILD.gn b/cc/BUILD.gn
index 7b8bb2b..d16ec63 100644
--- a/cc/BUILD.gn
+++ b/cc/BUILD.gn
@@ -422,6 +422,8 @@
     "scheduler/scheduler_state_machine.cc",
     "scheduler/scheduler_state_machine.h",
     "scheduler/video_frame_controller.h",
+    "tiles/decoded_image_tracker.cc",
+    "tiles/decoded_image_tracker.h",
     "tiles/eviction_tile_priority_queue.cc",
     "tiles/eviction_tile_priority_queue.h",
     "tiles/gpu_image_decode_cache.cc",
@@ -885,6 +887,7 @@
     "test/mock_helper_unittest.cc",
     "test/ordered_simple_task_runner_unittest.cc",
     "test/test_web_graphics_context_3d_unittest.cc",
+    "tiles/decoded_image_tracker_unittest.cc",
     "tiles/gpu_image_decode_cache_unittest.cc",
     "tiles/image_controller_unittest.cc",
     "tiles/mipmap_util_unittest.cc",
diff --git a/cc/playback/image_hijack_canvas_unittest.cc b/cc/playback/image_hijack_canvas_unittest.cc
index 214de87..76186eea 100644
--- a/cc/playback/image_hijack_canvas_unittest.cc
+++ b/cc/playback/image_hijack_canvas_unittest.cc
@@ -28,6 +28,8 @@
   MOCK_METHOD0(ReduceCacheUsage, void());
   MOCK_METHOD1(SetShouldAggressivelyFreeResources,
                void(bool aggressively_free_resources));
+  MOCK_METHOD2(GetOutOfRasterDecodeTaskForImageAndRef,
+               bool(const DrawImage& image, scoped_refptr<TileTask>* task));
 };
 
 TEST(ImageHijackCanvasTest, NonLazyImagesSkipped) {
diff --git a/cc/raster/task.cc b/cc/raster/task.cc
index 94e413f..94486fd5 100644
--- a/cc/raster/task.cc
+++ b/cc/raster/task.cc
@@ -18,6 +18,10 @@
          "CANCELED state.";
 }
 
+bool TaskState::IsNew() const {
+  return value_ == Value::NEW;
+}
+
 bool TaskState::IsScheduled() const {
   return value_ == Value::SCHEDULED;
 }
@@ -38,6 +42,23 @@
   value_ = Value::NEW;
 }
 
+std::string TaskState::ToString() const {
+  switch (value_) {
+    case Value::NEW:
+      return "NEW";
+    case Value::SCHEDULED:
+      return "SCHEDULED";
+    case Value::RUNNING:
+      return "RUNNING";
+    case Value::FINISHED:
+      return "FINISHED";
+    case Value::CANCELED:
+      return "CANCELED";
+  }
+  NOTREACHED();
+  return "";
+}
+
 void TaskState::DidSchedule() {
   DCHECK(value_ == Value::NEW)
       << "Task should be in NEW state to get scheduled.";
diff --git a/cc/raster/task.h b/cc/raster/task.h
index 2c406077..01c16a0 100644
--- a/cc/raster/task.h
+++ b/cc/raster/task.h
@@ -7,6 +7,7 @@
 
 #include <stdint.h>
 
+#include <string>
 #include <vector>
 
 #include "base/memory/ref_counted.h"
@@ -38,6 +39,7 @@
 //    └─────────┘         ╚══════════╝
 class CC_EXPORT TaskState {
  public:
+  bool IsNew() const;
   bool IsScheduled() const;
   bool IsRunning() const;
   bool IsFinished() const;
@@ -53,6 +55,8 @@
   void DidFinish();
   void DidCancel();
 
+  std::string ToString() const;
+
  private:
   friend class Task;
 
diff --git a/cc/test/fake_layer_tree_host_impl.cc b/cc/test/fake_layer_tree_host_impl.cc
index d6b2cd0..63a32fd 100644
--- a/cc/test/fake_layer_tree_host_impl.cc
+++ b/cc/test/fake_layer_tree_host_impl.cc
@@ -31,7 +31,8 @@
                         &stats_instrumentation_,
                         task_graph_runner,
                         AnimationHost::CreateForTesting(ThreadInstance::IMPL),
-                        0),
+                        0,
+                        nullptr),
       notify_tile_state_changed_called_(false) {
   // Explicitly clear all debug settings.
   SetDebugState(LayerTreeDebugState());
diff --git a/cc/test/fake_tile_manager.cc b/cc/test/fake_tile_manager.cc
index 0f2b418a..24457b51 100644
--- a/cc/test/fake_tile_manager.cc
+++ b/cc/test/fake_tile_manager.cc
@@ -30,32 +30,18 @@
 
 }  // namespace
 
-FakeTileManager::FakeTileManager(TileManagerClient* client)
-    : TileManager(client,
-                  base::ThreadTaskRunnerHandle::Get().get(),
-                  std::numeric_limits<size_t>::max(),
-                  false /* use_partial_raster */,
-                  false /* check_tile_priority_inversion */),
-      image_decode_cache_(
-          ResourceFormat::RGBA_8888,
-          LayerTreeSettings().software_decoded_image_budget_bytes) {
-  SetResources(
-      nullptr, &image_decode_cache_, g_synchronous_task_graph_runner.Pointer(),
-      g_fake_raster_buffer_provider.Pointer(),
-      std::numeric_limits<size_t>::max(), false /* use_gpu_rasterization */);
-  SetTileTaskManagerForTesting(base::MakeUnique<FakeTileTaskManagerImpl>());
-}
-
 FakeTileManager::FakeTileManager(TileManagerClient* client,
                                  ResourcePool* resource_pool)
     : TileManager(client,
                   base::ThreadTaskRunnerHandle::Get().get(),
+                  nullptr,
                   std::numeric_limits<size_t>::max(),
                   false /* use_partial_raster */,
                   false /* check_tile_priority_inversion */),
       image_decode_cache_(
           ResourceFormat::RGBA_8888,
           LayerTreeSettings().software_decoded_image_budget_bytes) {
+  SetDecodedImageTracker(&decoded_image_tracker_);
   SetResources(resource_pool, &image_decode_cache_,
                g_synchronous_task_graph_runner.Pointer(),
                g_fake_raster_buffer_provider.Pointer(),
diff --git a/cc/test/fake_tile_manager.h b/cc/test/fake_tile_manager.h
index 07380079..0e1d715b 100644
--- a/cc/test/fake_tile_manager.h
+++ b/cc/test/fake_tile_manager.h
@@ -15,8 +15,8 @@
 
 class FakeTileManager : public TileManager {
  public:
-  explicit FakeTileManager(TileManagerClient* client);
-  FakeTileManager(TileManagerClient* client, ResourcePool* resource_pool);
+  FakeTileManager(TileManagerClient* client,
+                  ResourcePool* resource_pool = nullptr);
   ~FakeTileManager() override;
 
   bool HasBeenAssignedMemory(Tile* tile);
@@ -27,6 +27,7 @@
 
  private:
   SoftwareImageDecodeCache image_decode_cache_;
+  DecodedImageTracker decoded_image_tracker_;
 };
 
 }  // namespace cc
diff --git a/cc/test/layer_tree_test.cc b/cc/test/layer_tree_test.cc
index 7177231..474399d 100644
--- a/cc/test/layer_tree_test.cc
+++ b/cc/test/layer_tree_test.cc
@@ -117,7 +117,8 @@
                           stats_instrumentation,
                           task_graph_runner,
                           AnimationHost::CreateForTesting(ThreadInstance::IMPL),
-                          0),
+                          0,
+                          nullptr),
         test_hooks_(test_hooks),
         block_notify_ready_to_activate_for_testing_(false),
         notify_ready_to_activate_was_blocked_(false) {}
diff --git a/cc/tiles/decoded_image_tracker.cc b/cc/tiles/decoded_image_tracker.cc
new file mode 100644
index 0000000..cdfa39bb
--- /dev/null
+++ b/cc/tiles/decoded_image_tracker.cc
@@ -0,0 +1,50 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "cc/tiles/decoded_image_tracker.h"
+
+namespace cc {
+namespace {
+const int kNumFramesToLock = 2;
+}  // namespace
+
+DecodedImageTracker::DecodedImageTracker() = default;
+DecodedImageTracker::~DecodedImageTracker() {
+  for (auto& pair : locked_images_)
+    image_controller_->UnlockImageDecode(pair.first);
+}
+
+void DecodedImageTracker::QueueImageDecode(sk_sp<const SkImage> image,
+                                           const base::Closure& callback) {
+  DCHECK(image_controller_);
+  // Queue the decode in the image controller, but switch out the callback for
+  // our own.
+  image_controller_->QueueImageDecode(
+      std::move(image), base::Bind(&DecodedImageTracker::ImageDecodeFinished,
+                                   base::Unretained(this), callback));
+}
+
+void DecodedImageTracker::NotifyFrameFinished() {
+  // Go through the images and if the frame ref count goes to 0, unlock the
+  // image in the controller.
+  for (auto it = locked_images_.begin(); it != locked_images_.end();) {
+    auto id = it->first;
+    int& ref_count = it->second;
+    if (--ref_count != 0) {
+      ++it;
+      continue;
+    }
+    image_controller_->UnlockImageDecode(id);
+    it = locked_images_.erase(it);
+  }
+}
+
+void DecodedImageTracker::ImageDecodeFinished(
+    const base::Closure& callback,
+    ImageController::ImageDecodeRequestId id) {
+  locked_images_.push_back(std::make_pair(id, kNumFramesToLock));
+  callback.Run();
+}
+
+}  // namespace cc
diff --git a/cc/tiles/decoded_image_tracker.h b/cc/tiles/decoded_image_tracker.h
new file mode 100644
index 0000000..2d303ee
--- /dev/null
+++ b/cc/tiles/decoded_image_tracker.h
@@ -0,0 +1,56 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CC_TILES_DECODED_IMAGE_TRACKER_H_
+#define CC_TILES_DECODED_IMAGE_TRACKER_H_
+
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "cc/base/cc_export.h"
+#include "cc/tiles/image_controller.h"
+
+class SkImage;
+
+namespace cc {
+
+// This class is the main interface for the rest of the system to request
+// decodes. It is responsible for keeping the decodes locked for a number of
+// frames, specified as |kNumFramesToLock| in the implementation file.
+//
+// Note that it is safe to replace ImageController's cache without doing
+// anything special with this class, since it retains only ids to the decode
+// requests. When defunct ids are then used to try and unlock the image, they
+// are silently ignored.
+class CC_EXPORT DecodedImageTracker {
+ public:
+  DecodedImageTracker();
+  ~DecodedImageTracker();
+
+  void QueueImageDecode(sk_sp<const SkImage> image,
+                        const base::Closure& callback);
+  void NotifyFrameFinished();
+
+ private:
+  friend class TileManager;
+  friend class DecodedImageTrackerTest;
+
+  void set_image_controller(ImageController* controller) {
+    image_controller_ = controller;
+  }
+
+  void ImageDecodeFinished(const base::Closure& callback,
+                           ImageController::ImageDecodeRequestId id);
+
+  ImageController* image_controller_ = nullptr;
+  std::vector<std::pair<ImageController::ImageDecodeRequestId, int>>
+      locked_images_;
+
+  DISALLOW_COPY_AND_ASSIGN(DecodedImageTracker);
+};
+
+}  // namespace cc
+
+#endif  // CC_TILES_DECODED_IMAGE_TRACKER_H_
diff --git a/cc/tiles/decoded_image_tracker_unittest.cc b/cc/tiles/decoded_image_tracker_unittest.cc
new file mode 100644
index 0000000..eff5f18
--- /dev/null
+++ b/cc/tiles/decoded_image_tracker_unittest.cc
@@ -0,0 +1,90 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <vector>
+
+#include "base/bind.h"
+#include "cc/tiles/decoded_image_tracker.h"
+#include "cc/tiles/image_controller.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cc {
+
+class TestImageController : public ImageController {
+ public:
+  TestImageController() : ImageController(nullptr, nullptr) {}
+
+  void UnlockImageDecode(ImageDecodeRequestId id) override {
+    auto it = std::find(locked_ids_.begin(), locked_ids_.end(), id);
+    ASSERT_FALSE(it == locked_ids_.end());
+    locked_ids_.erase(it);
+  }
+
+  ImageDecodeRequestId QueueImageDecode(
+      sk_sp<const SkImage> image,
+      const ImageDecodedCallback& callback) override {
+    auto id = next_id_++;
+    locked_ids_.push_back(id);
+    callback.Run(id);
+    return id;
+  }
+
+  size_t num_locked_images() { return locked_ids_.size(); }
+
+ private:
+  ImageDecodeRequestId next_id_ = 1;
+  std::vector<ImageDecodeRequestId> locked_ids_;
+};
+
+class DecodedImageTrackerTest : public testing::Test {
+ public:
+  void SetUp() override {
+    decoded_image_tracker_.set_image_controller(image_controller());
+  }
+
+  TestImageController* image_controller() { return &image_controller_; }
+  DecodedImageTracker* decoded_image_tracker() {
+    return &decoded_image_tracker_;
+  }
+
+ private:
+  TestImageController image_controller_;
+  DecodedImageTracker decoded_image_tracker_;
+};
+
+TEST_F(DecodedImageTrackerTest, QueueImageLocksImages) {
+  bool locked = false;
+  decoded_image_tracker()->QueueImageDecode(
+      nullptr, base::Bind([](bool* locked) { *locked = true; },
+                          base::Unretained(&locked)));
+  EXPECT_TRUE(locked);
+  EXPECT_EQ(1u, image_controller()->num_locked_images());
+}
+
+TEST_F(DecodedImageTrackerTest, NotifyFrameFinishedUnlocksImages) {
+  bool locked = false;
+  decoded_image_tracker()->QueueImageDecode(
+      nullptr, base::Bind([](bool* locked) { *locked = true; },
+                          base::Unretained(&locked)));
+  EXPECT_TRUE(locked);
+  EXPECT_EQ(1u, image_controller()->num_locked_images());
+
+  decoded_image_tracker()->NotifyFrameFinished();
+  EXPECT_EQ(1u, image_controller()->num_locked_images());
+
+  locked = false;
+  decoded_image_tracker()->QueueImageDecode(
+      nullptr, base::Bind([](bool* locked) { *locked = true; },
+                          base::Unretained(&locked)));
+  EXPECT_TRUE(locked);
+  EXPECT_EQ(2u, image_controller()->num_locked_images());
+
+  decoded_image_tracker()->NotifyFrameFinished();
+  EXPECT_EQ(1u, image_controller()->num_locked_images());
+
+  decoded_image_tracker()->NotifyFrameFinished();
+  EXPECT_EQ(0u, image_controller()->num_locked_images());
+}
+
+}  // namespace cc
diff --git a/cc/tiles/gpu_image_decode_cache.cc b/cc/tiles/gpu_image_decode_cache.cc
index 3ae2645..7f75c88 100644
--- a/cc/tiles/gpu_image_decode_cache.cc
+++ b/cc/tiles/gpu_image_decode_cache.cc
@@ -149,11 +149,13 @@
  public:
   ImageDecodeTaskImpl(GpuImageDecodeCache* cache,
                       const DrawImage& draw_image,
-                      const ImageDecodeCache::TracingInfo& tracing_info)
+                      const ImageDecodeCache::TracingInfo& tracing_info,
+                      GpuImageDecodeCache::DecodeTaskType task_type)
       : TileTask(true),
         cache_(cache),
         image_(draw_image),
-        tracing_info_(tracing_info) {
+        tracing_info_(tracing_info),
+        task_type_(task_type) {
     DCHECK(!SkipImage(draw_image));
   }
 
@@ -169,7 +171,7 @@
 
   // Overridden from TileTask:
   void OnTaskCompleted() override {
-    cache_->OnImageDecodeTaskCompleted(image_);
+    cache_->OnImageDecodeTaskCompleted(image_, task_type_);
   }
 
  protected:
@@ -179,6 +181,7 @@
   GpuImageDecodeCache* cache_;
   DrawImage image_;
   const ImageDecodeCache::TracingInfo tracing_info_;
+  const GpuImageDecodeCache::DecodeTaskType task_type_;
 
   DISALLOW_COPY_AND_ASSIGN(ImageDecodeTaskImpl);
 };
@@ -380,6 +383,22 @@
 bool GpuImageDecodeCache::GetTaskForImageAndRef(const DrawImage& draw_image,
                                                 const TracingInfo& tracing_info,
                                                 scoped_refptr<TileTask>* task) {
+  return GetTaskForImageAndRefInternal(
+      draw_image, tracing_info, DecodeTaskType::PART_OF_UPLOAD_TASK, task);
+}
+
+bool GpuImageDecodeCache::GetOutOfRasterDecodeTaskForImageAndRef(
+    const DrawImage& draw_image,
+    scoped_refptr<TileTask>* task) {
+  return GetTaskForImageAndRefInternal(
+      draw_image, TracingInfo(), DecodeTaskType::STAND_ALONE_DECODE_TASK, task);
+}
+
+bool GpuImageDecodeCache::GetTaskForImageAndRefInternal(
+    const DrawImage& draw_image,
+    const TracingInfo& tracing_info,
+    DecodeTaskType task_type,
+    scoped_refptr<TileTask>* task) {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
                "GpuImageDecodeCache::GetTaskForImageAndRef");
   if (SkipImage(draw_image)) {
@@ -408,11 +427,18 @@
     RefImage(draw_image);
     *task = nullptr;
     return true;
-  } else if (image_data->upload.task) {
+  } else if (task_type == DecodeTaskType::PART_OF_UPLOAD_TASK &&
+             image_data->upload.task) {
     // We had an existing upload task, ref the image and return the task.
     RefImage(draw_image);
     *task = image_data->upload.task;
     return true;
+  } else if (task_type == DecodeTaskType::STAND_ALONE_DECODE_TASK &&
+             image_data->decode.stand_alone_task) {
+    // We had an existing out of raster task, ref the image and return the task.
+    RefImage(draw_image);
+    *task = image_data->decode.stand_alone_task;
+    return true;
   }
 
   // Ensure that the image we're about to decode/upload will fit in memory.
@@ -427,13 +453,18 @@
   if (new_data)
     persistent_cache_.Put(image_id, std::move(new_data));
 
-  // Ref image and create a upload and decode tasks. We will release this ref
-  // in UploadTaskCompleted.
-  RefImage(draw_image);
-  *task = make_scoped_refptr(new ImageUploadTaskImpl(
-      this, draw_image, GetImageDecodeTaskAndRef(draw_image, tracing_info),
-      tracing_info));
-  image_data->upload.task = *task;
+  if (task_type == DecodeTaskType::PART_OF_UPLOAD_TASK) {
+    // Ref image and create a upload and decode tasks. We will release this ref
+    // in UploadTaskCompleted.
+    RefImage(draw_image);
+    *task = make_scoped_refptr(new ImageUploadTaskImpl(
+        this, draw_image,
+        GetImageDecodeTaskAndRef(draw_image, tracing_info, task_type),
+        tracing_info));
+    image_data->upload.task = *task;
+  } else {
+    *task = GetImageDecodeTaskAndRef(draw_image, tracing_info, task_type);
+  }
 
   // Ref the image again - this ref is owned by the caller, and it is their
   // responsibility to release it by calling UnrefImage.
@@ -647,15 +678,22 @@
 }
 
 void GpuImageDecodeCache::OnImageDecodeTaskCompleted(
-    const DrawImage& draw_image) {
+    const DrawImage& draw_image,
+    DecodeTaskType task_type) {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
                "GpuImageDecodeCache::OnImageDecodeTaskCompleted");
   base::AutoLock lock(lock_);
   // Decode task is complete, remove our reference to it.
   ImageData* image_data = GetImageDataForDrawImage(draw_image);
   DCHECK(image_data);
-  DCHECK(image_data->decode.task);
-  image_data->decode.task = nullptr;
+  if (task_type == DecodeTaskType::PART_OF_UPLOAD_TASK) {
+    DCHECK(image_data->decode.task);
+    image_data->decode.task = nullptr;
+  } else {
+    DCHECK(task_type == DecodeTaskType::STAND_ALONE_DECODE_TASK);
+    DCHECK(image_data->decode.stand_alone_task);
+    image_data->decode.stand_alone_task = nullptr;
+  }
 
   // While the decode task is active, we keep a ref on the decoded data.
   // Release that ref now.
@@ -684,14 +722,16 @@
 // the requested decode.
 scoped_refptr<TileTask> GpuImageDecodeCache::GetImageDecodeTaskAndRef(
     const DrawImage& draw_image,
-    const TracingInfo& tracing_info) {
+    const TracingInfo& tracing_info,
+    DecodeTaskType task_type) {
   TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("cc.debug"),
                "GpuImageDecodeCache::GetImageDecodeTaskAndRef");
   lock_.AssertAcquired();
 
   // This ref is kept alive while an upload task may need this decode. We
   // release this ref in UploadTaskCompleted.
-  RefImageDecode(draw_image);
+  if (task_type == DecodeTaskType::PART_OF_UPLOAD_TASK)
+    RefImageDecode(draw_image);
 
   ImageData* image_data = GetImageDataForDrawImage(draw_image);
   DCHECK(image_data);
@@ -704,13 +744,16 @@
   }
 
   // We didn't have an existing locked image, create a task to lock or decode.
-  scoped_refptr<TileTask>& existing_task = image_data->decode.task;
+  scoped_refptr<TileTask>& existing_task =
+      (task_type == DecodeTaskType::PART_OF_UPLOAD_TASK)
+          ? image_data->decode.task
+          : image_data->decode.stand_alone_task;
   if (!existing_task) {
     // Ref image decode and create a decode task. This ref will be released in
     // DecodeTaskCompleted.
     RefImageDecode(draw_image);
     existing_task = make_scoped_refptr(
-        new ImageDecodeTaskImpl(this, draw_image, tracing_info));
+        new ImageDecodeTaskImpl(this, draw_image, tracing_info, task_type));
   }
   return existing_task;
 }
@@ -857,10 +900,11 @@
   // We should unlock the discardable memory for the image in two cases:
   // 1) The image is no longer being used (no decode or upload refs).
   // 2) This is a GPU backed image that has already been uploaded (no decode
-  //    refs).
+  //    refs, and we actually already have an image).
   bool should_unlock_discardable =
-      !has_any_refs || (image_data->mode == DecodedDataMode::GPU &&
-                        !image_data->decode.ref_count);
+      !has_any_refs ||
+      (image_data->mode == DecodedDataMode::GPU &&
+       !image_data->decode.ref_count && image_data->upload.image());
 
   if (should_unlock_discardable && image_data->decode.is_locked()) {
     DCHECK(image_data->decode.data());
diff --git a/cc/tiles/gpu_image_decode_cache.h b/cc/tiles/gpu_image_decode_cache.h
index 34f8dca..08dd660 100644
--- a/cc/tiles/gpu_image_decode_cache.h
+++ b/cc/tiles/gpu_image_decode_cache.h
@@ -99,6 +99,8 @@
       public base::trace_event::MemoryDumpProvider,
       public base::MemoryCoordinatorClient {
  public:
+  enum class DecodeTaskType { PART_OF_UPLOAD_TASK, STAND_ALONE_DECODE_TASK };
+
   explicit GpuImageDecodeCache(ContextProvider* context,
                                ResourceFormat decode_format,
                                size_t max_gpu_image_bytes);
@@ -111,6 +113,9 @@
   bool GetTaskForImageAndRef(const DrawImage& image,
                              const TracingInfo& tracing_info,
                              scoped_refptr<TileTask>* task) override;
+  bool GetOutOfRasterDecodeTaskForImageAndRef(
+      const DrawImage& image,
+      scoped_refptr<TileTask>* task) override;
   void UnrefImage(const DrawImage& image) override;
   DecodedDrawImage GetDecodedImageForDraw(const DrawImage& draw_image) override;
   void DrawWithImageFinished(const DrawImage& image,
@@ -131,7 +136,8 @@
   void UploadImage(const DrawImage& image);
 
   // Called by Decode / Upload tasks when tasks are finished.
-  void OnImageDecodeTaskCompleted(const DrawImage& image);
+  void OnImageDecodeTaskCompleted(const DrawImage& image,
+                                  DecodeTaskType task_type);
   void OnImageUploadTaskCompleted(const DrawImage& image);
 
   // For testing only.
@@ -167,6 +173,9 @@
     bool decode_failure = false;
     // If non-null, this is the pending decode task for this image.
     scoped_refptr<TileTask> task;
+    // Similar to above, but only is generated if there is no associated upload
+    // generated for this task (ie, this is an out-of-raster request for decode.
+    scoped_refptr<TileTask> stand_alone_task;
 
    private:
     struct UsageStats {
@@ -262,7 +271,15 @@
   // rather than the upload task, if necessary.
   scoped_refptr<TileTask> GetImageDecodeTaskAndRef(
       const DrawImage& image,
-      const TracingInfo& tracing_info);
+      const TracingInfo& tracing_info,
+      DecodeTaskType task_type);
+
+  // Note that this function behaves as if it was public (all of the same locks
+  // need to be acquired).
+  bool GetTaskForImageAndRefInternal(const DrawImage& image,
+                                     const TracingInfo& tracing_info,
+                                     DecodeTaskType task_type,
+                                     scoped_refptr<TileTask>* task);
 
   void RefImageDecode(const DrawImage& draw_image);
   void UnrefImageDecode(const DrawImage& draw_image);
diff --git a/cc/tiles/gpu_image_decode_cache_unittest.cc b/cc/tiles/gpu_image_decode_cache_unittest.cc
index 072dfd9..34d2b29 100644
--- a/cc/tiles/gpu_image_decode_cache_unittest.cc
+++ b/cc/tiles/gpu_image_decode_cache_unittest.cc
@@ -497,6 +497,7 @@
   EXPECT_TRUE(need_unref);
   EXPECT_TRUE(task);
 
+  ASSERT_GT(task->dependencies().size(), 0u);
   TestTileTaskRunner::ProcessTask(task->dependencies()[0].get());
 
   scoped_refptr<TileTask> another_task;
@@ -509,6 +510,10 @@
   TestTileTaskRunner::CancelTask(task.get());
   TestTileTaskRunner::CompleteTask(task.get());
 
+  // 2 Unrefs, so that the decode is unlocked as well.
+  cache.UnrefImage(draw_image);
+  cache.UnrefImage(draw_image);
+
   // Note that here, everything is reffed, but a new task is created. This is
   // possible with repeated schedule/cancel operations.
   scoped_refptr<TileTask> third_task;
@@ -518,12 +523,11 @@
   EXPECT_TRUE(third_task);
   EXPECT_FALSE(third_task.get() == task.get());
 
+  ASSERT_GT(third_task->dependencies().size(), 0u);
   TestTileTaskRunner::ProcessTask(third_task->dependencies()[0].get());
   TestTileTaskRunner::ProcessTask(third_task.get());
 
-  // 3 Unrefs!
-  cache.UnrefImage(draw_image);
-  cache.UnrefImage(draw_image);
+  // Unref!
   cache.UnrefImage(draw_image);
 }
 
diff --git a/cc/tiles/image_controller.cc b/cc/tiles/image_controller.cc
index 9572956..f0d1bb9 100644
--- a/cc/tiles/image_controller.cc
+++ b/cc/tiles/image_controller.cc
@@ -4,15 +4,122 @@
 
 #include "cc/tiles/image_controller.h"
 
+#include "base/bind.h"
+#include "base/task_scheduler/post_task.h"
+#include "base/task_scheduler/task_traits.h"
+#include "base/threading/thread_restrictions.h"
+#include "base/trace_event/trace_event.h"
+#include "cc/base/completion_event.h"
+#include "cc/tiles/tile_task_manager.h"
+
 namespace cc {
 
-ImageController::ImageController() = default;
-ImageController::~ImageController() = default;
+ImageController::ImageDecodeRequestId
+    ImageController::s_next_image_decode_queue_id_ = 1;
+
+ImageController::ImageController(
+    base::SequencedTaskRunner* origin_task_runner,
+    scoped_refptr<base::SequencedTaskRunner> worker_task_runner)
+    : origin_task_runner_(origin_task_runner),
+      worker_task_runner_(std::move(worker_task_runner)),
+      weak_ptr_factory_(this) {}
+
+ImageController::~ImageController() {
+  StopWorkerTasks();
+}
+
+void ImageController::StopWorkerTasks() {
+  // We can't have worker threads without a cache_ or a worker_task_runner_, so
+  // terminate early.
+  if (!cache_ || !worker_task_runner_)
+    return;
+
+  // Abort all tasks that are currently scheduled to run (we'll wait for them to
+  // finish next).
+  {
+    base::AutoLock hold(lock_);
+    abort_tasks_ = true;
+  }
+
+  // Post a task that will simply signal a completion event to ensure that we
+  // "flush" any scheduled tasks (they will abort).
+  CompletionEvent completion_event;
+  worker_task_runner_->PostTask(
+      FROM_HERE, base::Bind([](CompletionEvent* event) { event->Signal(); },
+                            base::Unretained(&completion_event)));
+  completion_event.Wait();
+
+  // Reset the abort flag so that new tasks can be scheduled.
+  {
+    base::AutoLock hold(lock_);
+    abort_tasks_ = false;
+  }
+
+  // Now that we flushed everything, if there was a task running and it
+  // finished, it would have posted a completion callback back to the compositor
+  // thread. We don't want that, so invalidate the weak ptrs again. Note that
+  // nothing can start running between wait and this invalidate, since it would
+  // only run on the current (compositor) thread.
+  weak_ptr_factory_.InvalidateWeakPtrs();
+
+  // Now, begin cleanup.
+
+  // Unlock all of the locked images (note that this vector would only be
+  // populated if we actually need to unref the image.
+  for (auto image_pair : requested_locked_images_)
+    cache_->UnrefImage(image_pair.second);
+  requested_locked_images_.clear();
+
+  // Now, complete the tasks that already ran but haven't completed. These would
+  // be posted in the run loop, but since we invalidated the weak ptrs, we need
+  // to run everything manually.
+  for (auto& request_to_complete : requests_needing_completion_) {
+    ImageDecodeRequestId id = request_to_complete.first;
+    ImageDecodeRequest& request = request_to_complete.second;
+
+    // The task (if one exists) would have run already, so we just need to
+    // complete it.
+    if (request.task)
+      request.task->DidComplete();
+
+    // Issue the callback, and unref the image immediately. This is so that any
+    // code waiting on the callback can proceed, although we're breaking the
+    // promise of having this image decoded. This is unfortunate, but it seems
+    // like the least complexity to process an image decode controller becoming
+    // nullptr.
+    request.callback.Run(id);
+    if (request.need_unref)
+      cache_->UnrefImage(request.draw_image);
+  }
+  requests_needing_completion_.clear();
+
+  // Finally, complete all of the tasks that never started running. This is
+  // similar to the |requests_needing_completion_|, but happens at a different
+  // stage in the pipeline.
+  for (auto& request_pair : image_decode_queue_) {
+    ImageDecodeRequestId id = request_pair.first;
+    ImageDecodeRequest& request = request_pair.second;
+
+    if (request.task) {
+      // This task may have run via a different request, so only cancel it if
+      // it's "new". That is, the same task could have been referenced by
+      // several different image deque requests for the same image.
+      if (request.task->state().IsNew())
+        request.task->state().DidCancel();
+      request.task->DidComplete();
+    }
+    // Run the callback and unref the image.
+    request.callback.Run(id);
+    cache_->UnrefImage(request.draw_image);
+  }
+  image_decode_queue_.clear();
+}
 
 void ImageController::SetImageDecodeCache(ImageDecodeCache* cache) {
   if (!cache) {
     SetPredecodeImages(std::vector<DrawImage>(),
                        ImageDecodeCache::TracingInfo());
+    StopWorkerTasks();
   }
   cache_ = cache;
 }
@@ -56,4 +163,155 @@
   return new_tasks;
 }
 
+ImageController::ImageDecodeRequestId ImageController::QueueImageDecode(
+    sk_sp<const SkImage> image,
+    const ImageDecodedCallback& callback) {
+  // We must not receive any image requests if we have no worker.
+  CHECK(worker_task_runner_);
+
+  // Generate the next id.
+  ImageDecodeRequestId id = s_next_image_decode_queue_id_++;
+
+  DCHECK(image);
+  auto image_bounds = image->bounds();
+  DrawImage draw_image(std::move(image), image_bounds, kNone_SkFilterQuality,
+                       SkMatrix::I());
+
+  // Get the tasks for this decode.
+  scoped_refptr<TileTask> task;
+  bool need_unref =
+      cache_->GetOutOfRasterDecodeTaskForImageAndRef(draw_image, &task);
+  // If we don't need to unref this, we don't actually have a task.
+  DCHECK(need_unref || !task);
+
+  // Schedule the task and signal that there is more work.
+  base::AutoLock hold(lock_);
+  image_decode_queue_[id] =
+      ImageDecodeRequest(id, draw_image, callback, std::move(task), need_unref);
+
+  // If this is the only image decode request, schedule a task to run.
+  // Otherwise, the task will be scheduled in the previou task's completion.
+  if (image_decode_queue_.size() == 1) {
+    // Post a worker task.
+    worker_task_runner_->PostTask(
+        FROM_HERE,
+        base::Bind(&ImageController::ProcessNextImageDecodeOnWorkerThread,
+                   base::Unretained(this)));
+  }
+
+  return id;
+}
+
+void ImageController::UnlockImageDecode(ImageDecodeRequestId id) {
+  // If the image exists, ie we actually need to unlock it, then do so.
+  auto it = requested_locked_images_.find(id);
+  if (it == requested_locked_images_.end())
+    return;
+
+  UnrefImages({it->second});
+  requested_locked_images_.erase(it);
+}
+
+void ImageController::ProcessNextImageDecodeOnWorkerThread() {
+  TRACE_EVENT0("cc", "ImageController::ProcessNextImageDecodeOnWorkerThread");
+  ImageDecodeRequest decode;
+  {
+    base::AutoLock hold(lock_);
+
+    // If we don't have any work, abort.
+    if (image_decode_queue_.empty() || abort_tasks_)
+      return;
+
+    // Take the next request from the queue.
+    auto decode_it = image_decode_queue_.begin();
+    DCHECK(decode_it != image_decode_queue_.end());
+    decode = std::move(decode_it->second);
+    image_decode_queue_.erase(decode_it);
+
+    // Notify that the task will need completion. Note that there are two cases
+    // where we process this. First, we might complete this task as a response
+    // to the posted task below. Second, we might complete it in
+    // StopWorkerTasks(). In either case, the task would have already run
+    // (either post task happens after running, or the thread was already joined
+    // which means the task ran). This means that we can put the decode into
+    // |requests_needing_completion_| here before actually running the task.
+    requests_needing_completion_[decode.id] = decode;
+  }
+
+  // Run the task if we need to run it. If the task state isn't new, then
+  // there is another task that is responsible for finishing it and cleaning
+  // up (and it already ran); we just need to post a completion callback.
+  // Note that the other tasks's completion will also run first, since the
+  // requests are ordered. So, when we process this task's completion, we
+  // won't actually do anything with the task and simply issue the callback.
+  if (decode.task && decode.task->state().IsNew()) {
+    decode.task->state().DidSchedule();
+    decode.task->state().DidStart();
+    decode.task->RunOnWorkerThread();
+    decode.task->state().DidFinish();
+  }
+  origin_task_runner_->PostTask(
+      FROM_HERE, base::Bind(&ImageController::ImageDecodeCompleted,
+                            weak_ptr_factory_.GetWeakPtr(), decode.id));
+}
+
+void ImageController::ImageDecodeCompleted(ImageDecodeRequestId id) {
+  ImageDecodedCallback callback;
+  {
+    base::AutoLock hold(lock_);
+
+    auto request_it = requests_needing_completion_.find(id);
+    DCHECK(request_it != requests_needing_completion_.end());
+    id = request_it->first;
+    ImageDecodeRequest& request = request_it->second;
+
+    // If we need to unref this decode, then we have to put it into the locked
+    // images vector.
+    if (request.need_unref)
+      requested_locked_images_[id] = std::move(request.draw_image);
+
+    // If we have a task that isn't completed yet, we need to complete it.
+    if (request.task && !request.task->HasCompleted()) {
+      request.task->OnTaskCompleted();
+      request.task->DidComplete();
+    }
+    // Finally, save the callback so we can run it without the lock, and erase
+    // the request from |requests_needing_completion_|.
+    callback = std::move(request.callback);
+    requests_needing_completion_.erase(request_it);
+  }
+
+  // Post another task to run.
+  worker_task_runner_->PostTask(
+      FROM_HERE,
+      base::Bind(&ImageController::ProcessNextImageDecodeOnWorkerThread,
+                 base::Unretained(this)));
+
+  // Finally run the requested callback.
+  callback.Run(id);
+}
+
+ImageController::ImageDecodeRequest::ImageDecodeRequest() = default;
+ImageController::ImageDecodeRequest::ImageDecodeRequest(
+    ImageDecodeRequestId id,
+    const DrawImage& draw_image,
+    const ImageDecodedCallback& callback,
+    scoped_refptr<TileTask> task,
+    bool need_unref)
+    : id(id),
+      draw_image(draw_image),
+      callback(callback),
+      task(std::move(task)),
+      need_unref(need_unref) {}
+ImageController::ImageDecodeRequest::ImageDecodeRequest(
+    ImageDecodeRequest&& other) = default;
+ImageController::ImageDecodeRequest::ImageDecodeRequest(
+    const ImageDecodeRequest& other) = default;
+ImageController::ImageDecodeRequest::~ImageDecodeRequest() = default;
+
+ImageController::ImageDecodeRequest& ImageController::ImageDecodeRequest::
+operator=(ImageDecodeRequest&& other) = default;
+ImageController::ImageDecodeRequest& ImageController::ImageDecodeRequest::
+operator=(const ImageDecodeRequest& other) = default;
+
 }  // namespace cc
diff --git a/cc/tiles/image_controller.h b/cc/tiles/image_controller.h
index d64282f8e..7fb45b8 100644
--- a/cc/tiles/image_controller.h
+++ b/cc/tiles/image_controller.h
@@ -5,11 +5,15 @@
 #ifndef CC_TILES_IMAGE_CONTROLLER_H_
 #define CC_TILES_IMAGE_CONTROLLER_H_
 
+#include <set>
 #include <vector>
 
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
+#include "base/memory/weak_ptr.h"
+#include "base/threading/simple_thread.h"
 #include "cc/base/cc_export.h"
+#include "cc/base/unique_notifier.h"
 #include "cc/playback/draw_image.h"
 #include "cc/raster/tile_task.h"
 #include "cc/tiles/image_decode_cache.h"
@@ -18,8 +22,12 @@
 
 class CC_EXPORT ImageController {
  public:
-  ImageController();
-  ~ImageController();
+  using ImageDecodeRequestId = uint64_t;
+  using ImageDecodedCallback = base::Callback<void(ImageDecodeRequestId)>;
+  explicit ImageController(
+      base::SequencedTaskRunner* origin_task_runner,
+      scoped_refptr<base::SequencedTaskRunner> worker_task_runner);
+  virtual ~ImageController();
 
   void SetImageDecodeCache(ImageDecodeCache* cache);
   void GetTasksForImagesAndRef(
@@ -32,10 +40,66 @@
       std::vector<DrawImage> predecode_images,
       const ImageDecodeCache::TracingInfo& tracing_info);
 
+  // Virtual for testing.
+  virtual void UnlockImageDecode(ImageDecodeRequestId id);
+
+  // This function requests that the given image be decoded and locked. Once the
+  // callback has been issued, it is passed an ID, which should be used to
+  // unlock this image. It is up to the caller to ensure that the image is later
+  // unlocked using UnlockImageDecode.
+  // Virtual for testing.
+  virtual ImageDecodeRequestId QueueImageDecode(
+      sk_sp<const SkImage> image,
+      const ImageDecodedCallback& callback);
+
  private:
+  struct ImageDecodeRequest {
+    ImageDecodeRequest();
+    ImageDecodeRequest(ImageDecodeRequestId id,
+                       const DrawImage& draw_image,
+                       const ImageDecodedCallback& callback,
+                       scoped_refptr<TileTask> task,
+                       bool need_unref);
+    ImageDecodeRequest(ImageDecodeRequest&& other);
+    ImageDecodeRequest(const ImageDecodeRequest& other);
+    ~ImageDecodeRequest();
+
+    ImageDecodeRequest& operator=(ImageDecodeRequest&& other);
+    ImageDecodeRequest& operator=(const ImageDecodeRequest& other);
+
+    ImageDecodeRequestId id;
+    DrawImage draw_image;
+    ImageDecodedCallback callback;
+    scoped_refptr<TileTask> task;
+    bool need_unref;
+  };
+
+  void StopWorkerTasks();
+
+  // Called from the worker thread.
+  void ProcessNextImageDecodeOnWorkerThread();
+
+  void ImageDecodeCompleted(ImageDecodeRequestId id);
+
   ImageDecodeCache* cache_ = nullptr;
   std::vector<DrawImage> predecode_locked_images_;
 
+  static ImageDecodeRequestId s_next_image_decode_queue_id_;
+  std::unordered_map<ImageDecodeRequestId, DrawImage> requested_locked_images_;
+
+  base::SequencedTaskRunner* origin_task_runner_ = nullptr;
+  scoped_refptr<base::SequencedTaskRunner> worker_task_runner_;
+
+  // The variables defined below this lock (aside from weak_ptr_factory_) can
+  // only be accessed when the lock is acquired.
+  base::Lock lock_;
+  std::map<ImageDecodeRequestId, ImageDecodeRequest> image_decode_queue_;
+  std::map<ImageDecodeRequestId, ImageDecodeRequest>
+      requests_needing_completion_;
+  bool abort_tasks_ = false;
+
+  base::WeakPtrFactory<ImageController> weak_ptr_factory_;
+
   DISALLOW_COPY_AND_ASSIGN(ImageController);
 };
 
diff --git a/cc/tiles/image_controller_unittest.cc b/cc/tiles/image_controller_unittest.cc
index fd9841c..b963ff1 100644
--- a/cc/tiles/image_controller_unittest.cc
+++ b/cc/tiles/image_controller_unittest.cc
@@ -2,18 +2,102 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "base/bind.h"
+#include "base/optional.h"
+#include "base/run_loop.h"
+#include "base/test/test_simple_task_runner.h"
+#include "base/threading/sequenced_task_runner_handle.h"
 #include "cc/tiles/image_controller.h"
 #include "cc/tiles/image_decode_cache.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace cc {
+namespace {
 
+class TestWorkerThread : public base::SimpleThread {
+ public:
+  TestWorkerThread()
+      : base::SimpleThread("test_worker_thread"), condition_(&lock_) {}
+
+  void Run() override {
+    for (;;) {
+      base::AutoLock hold(lock_);
+      if (shutdown_)
+        break;
+
+      if (queue_.empty()) {
+        condition_.Wait();
+        continue;
+      }
+
+      queue_.front().Run();
+      queue_.erase(queue_.begin());
+    }
+  }
+
+  void Shutdown() {
+    base::AutoLock hold(lock_);
+    shutdown_ = true;
+    condition_.Signal();
+  }
+
+  void PostTask(const base::Closure& task) {
+    base::AutoLock hold(lock_);
+    queue_.push_back(task);
+    condition_.Signal();
+  }
+
+ private:
+  base::Lock lock_;
+  base::ConditionVariable condition_;
+  std::vector<base::Closure> queue_;
+  bool shutdown_ = false;
+};
+
+class WorkerTaskRunner : public base::SequencedTaskRunner {
+ public:
+  WorkerTaskRunner() { thread_.Start(); }
+
+  bool PostNonNestableDelayedTask(const tracked_objects::Location& from_here,
+                                  const base::Closure& task,
+                                  base::TimeDelta delay) override {
+    return PostDelayedTask(from_here, task, delay);
+  }
+
+  bool PostDelayedTask(const tracked_objects::Location& from_here,
+                       const base::Closure& task,
+                       base::TimeDelta delay) override {
+    thread_.PostTask(task);
+    return true;
+  }
+
+  bool RunsTasksOnCurrentThread() const override { return false; }
+
+ protected:
+  ~WorkerTaskRunner() override {
+    thread_.Shutdown();
+    thread_.Join();
+  }
+
+  TestWorkerThread thread_;
+};
+
+// Image decode cache with introspection!
 class TestableCache : public ImageDecodeCache {
  public:
+  ~TestableCache() override { EXPECT_EQ(number_of_refs_, 0); }
+
   bool GetTaskForImageAndRef(const DrawImage& image,
                              const TracingInfo& tracing_info,
                              scoped_refptr<TileTask>* task) override {
-    *task = nullptr;
+    *task = task_to_use_;
+    ++number_of_refs_;
+    return true;
+  }
+  bool GetOutOfRasterDecodeTaskForImageAndRef(
+      const DrawImage& image,
+      scoped_refptr<TileTask>* task) override {
+    *task = task_to_use_;
     ++number_of_refs_;
     return true;
   }
@@ -32,26 +116,332 @@
       bool aggressively_free_resources) override {}
 
   int number_of_refs() const { return number_of_refs_; }
+  void SetTaskToUse(scoped_refptr<TileTask> task) { task_to_use_ = task; }
 
  private:
   int number_of_refs_ = 0;
+  scoped_refptr<TileTask> task_to_use_;
 };
 
-TEST(ImageControllerTest, NullCacheUnrefsImages) {
-  TestableCache cache;
-  ImageController controller;
-  controller.SetImageDecodeCache(&cache);
+// A simple class that can receive decode callbacks.
+class DecodeClient {
+ public:
+  DecodeClient() {}
+  void Callback(const base::Closure& quit_closure,
+                ImageController::ImageDecodeRequestId id) {
+    id_ = id;
+    quit_closure.Run();
+  }
 
+  ImageController::ImageDecodeRequestId id() { return id_; }
+
+ private:
+  ImageController::ImageDecodeRequestId id_ = 0;
+};
+
+// A dummy task that does nothing.
+class SimpleTask : public TileTask {
+ public:
+  SimpleTask() : TileTask(true /* supports_concurrent_execution */) {
+    EXPECT_TRUE(thread_checker_.CalledOnValidThread());
+  }
+
+  void RunOnWorkerThread() override {
+    EXPECT_FALSE(HasCompleted());
+    has_run_ = true;
+  }
+  void OnTaskCompleted() override {
+    EXPECT_TRUE(thread_checker_.CalledOnValidThread());
+  }
+
+  bool has_run() { return has_run_; }
+
+ private:
+  ~SimpleTask() override = default;
+
+  base::ThreadChecker thread_checker_;
+  bool has_run_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(SimpleTask);
+};
+
+// A task that blocks until instructed otherwise.
+class BlockingTask : public TileTask {
+ public:
+  BlockingTask()
+      : TileTask(true /* supports_concurrent_execution */), run_cv_(&lock_) {
+    EXPECT_TRUE(thread_checker_.CalledOnValidThread());
+  }
+
+  void RunOnWorkerThread() override {
+    EXPECT_FALSE(HasCompleted());
+    EXPECT_FALSE(thread_checker_.CalledOnValidThread());
+    base::AutoLock hold(lock_);
+    if (!can_run_)
+      run_cv_.Wait();
+    has_run_ = true;
+  }
+
+  void OnTaskCompleted() override {
+    EXPECT_TRUE(thread_checker_.CalledOnValidThread());
+  }
+
+  void AllowToRun() {
+    base::AutoLock hold(lock_);
+    can_run_ = true;
+    run_cv_.Signal();
+  }
+
+  bool has_run() { return has_run_; }
+
+ private:
+  ~BlockingTask() override = default;
+
+  base::ThreadChecker thread_checker_;
+  bool has_run_ = false;
+  base::Lock lock_;
+  base::ConditionVariable run_cv_;
+  bool can_run_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(BlockingTask);
+};
+
+// For tests that exercise image controller's thread, this is the timeout value
+// to
+// allow the worker thread to do its work.
+int kDefaultTimeoutSeconds = 10;
+
+class ImageControllerTest : public testing::Test {
+ public:
+  ImageControllerTest() : task_runner_(base::SequencedTaskRunnerHandle::Get()) {
+    bitmap_.allocN32Pixels(1, 1);
+    image_ = SkImage::MakeFromBitmap(bitmap_);
+  }
+  ~ImageControllerTest() override = default;
+
+  void SetUp() override {
+    worker_task_runner_ = make_scoped_refptr(new WorkerTaskRunner);
+    controller_.reset(
+        new ImageController(task_runner_.get(), worker_task_runner_));
+    cache_ = TestableCache();
+    controller_->SetImageDecodeCache(&cache_);
+  }
+
+  void TearDown() override {
+    controller_.reset();
+    worker_task_runner_ = nullptr;
+  }
+
+  base::SequencedTaskRunner* task_runner() { return task_runner_.get(); }
+  ImageController* controller() { return controller_.get(); }
+  TestableCache* cache() { return &cache_; }
+  sk_sp<const SkImage> image() const { return image_; }
+
+  // Timeout callback, which errors and exits the runloop.
+  static void Timeout(base::RunLoop* run_loop) {
+    ADD_FAILURE() << "Timeout.";
+    run_loop->Quit();
+  }
+
+  // Convenience method to run the run loop with a timeout.
+  void RunOrTimeout(base::RunLoop* run_loop) {
+    task_runner_->PostDelayedTask(
+        FROM_HERE,
+        base::Bind(&ImageControllerTest::Timeout, base::Unretained(run_loop)),
+        base::TimeDelta::FromSeconds(kDefaultTimeoutSeconds));
+    run_loop->Run();
+  }
+
+ private:
+  scoped_refptr<base::SequencedTaskRunner> task_runner_;
+  scoped_refptr<WorkerTaskRunner> worker_task_runner_;
+  TestableCache cache_;
+  std::unique_ptr<ImageController> controller_;
+  SkBitmap bitmap_;
+  sk_sp<const SkImage> image_;
+};
+
+TEST_F(ImageControllerTest, NullControllerUnrefsImages) {
   std::vector<DrawImage> images(10);
   ImageDecodeCache::TracingInfo tracing_info;
 
   ASSERT_EQ(10u, images.size());
-  auto tasks = controller.SetPredecodeImages(std::move(images), tracing_info);
+  auto tasks =
+      controller()->SetPredecodeImages(std::move(images), tracing_info);
   EXPECT_EQ(0u, tasks.size());
-  EXPECT_EQ(10, cache.number_of_refs());
+  EXPECT_EQ(10, cache()->number_of_refs());
 
-  controller.SetImageDecodeCache(nullptr);
-  EXPECT_EQ(0, cache.number_of_refs());
+  controller()->SetImageDecodeCache(nullptr);
+  EXPECT_EQ(0, cache()->number_of_refs());
 }
 
+TEST_F(ImageControllerTest, QueueImageDecode) {
+  base::RunLoop run_loop;
+  DecodeClient decode_client;
+  EXPECT_EQ(image()->bounds().width(), 1);
+  ImageController::ImageDecodeRequestId expected_id =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client),
+                     run_loop.QuitClosure()));
+  RunOrTimeout(&run_loop);
+  EXPECT_EQ(expected_id, decode_client.id());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeMultipleImages) {
+  base::RunLoop run_loop;
+  DecodeClient decode_client1;
+  ImageController::ImageDecodeRequestId expected_id1 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client1),
+                     base::Bind([] {})));
+  DecodeClient decode_client2;
+  ImageController::ImageDecodeRequestId expected_id2 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client2),
+                     base::Bind([] {})));
+  DecodeClient decode_client3;
+  ImageController::ImageDecodeRequestId expected_id3 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client3),
+                     run_loop.QuitClosure()));
+  RunOrTimeout(&run_loop);
+  EXPECT_EQ(expected_id1, decode_client1.id());
+  EXPECT_EQ(expected_id2, decode_client2.id());
+  EXPECT_EQ(expected_id3, decode_client3.id());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeWithTask) {
+  scoped_refptr<SimpleTask> task(new SimpleTask);
+  cache()->SetTaskToUse(task);
+
+  base::RunLoop run_loop;
+  DecodeClient decode_client;
+  ImageController::ImageDecodeRequestId expected_id =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client),
+                     run_loop.QuitClosure()));
+  RunOrTimeout(&run_loop);
+  EXPECT_EQ(expected_id, decode_client.id());
+  EXPECT_TRUE(task->has_run());
+  EXPECT_TRUE(task->HasCompleted());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeMultipleImagesSameTask) {
+  scoped_refptr<SimpleTask> task(new SimpleTask);
+  cache()->SetTaskToUse(task);
+
+  base::RunLoop run_loop;
+  DecodeClient decode_client1;
+  ImageController::ImageDecodeRequestId expected_id1 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client1),
+                     base::Bind([] {})));
+  DecodeClient decode_client2;
+  ImageController::ImageDecodeRequestId expected_id2 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client2),
+                     base::Bind([] {})));
+  DecodeClient decode_client3;
+  ImageController::ImageDecodeRequestId expected_id3 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client3),
+                     run_loop.QuitClosure()));
+  RunOrTimeout(&run_loop);
+  EXPECT_EQ(expected_id1, decode_client1.id());
+  EXPECT_EQ(expected_id2, decode_client2.id());
+  EXPECT_EQ(expected_id3, decode_client3.id());
+  EXPECT_TRUE(task->has_run());
+  EXPECT_TRUE(task->HasCompleted());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeChangeControllerWithTaskQueued) {
+  scoped_refptr<BlockingTask> task_one(new BlockingTask);
+  cache()->SetTaskToUse(task_one);
+
+  DecodeClient decode_client1;
+  ImageController::ImageDecodeRequestId expected_id1 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client1),
+                     base::Bind([] {})));
+
+  scoped_refptr<BlockingTask> task_two(new BlockingTask);
+  cache()->SetTaskToUse(task_two);
+
+  base::RunLoop run_loop;
+  DecodeClient decode_client2;
+  ImageController::ImageDecodeRequestId expected_id2 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client2),
+                     run_loop.QuitClosure()));
+
+  task_one->AllowToRun();
+  task_two->AllowToRun();
+  controller()->SetImageDecodeCache(nullptr);
+
+  RunOrTimeout(&run_loop);
+
+  EXPECT_TRUE(task_one->state().IsCanceled() || task_one->HasCompleted());
+  EXPECT_TRUE(task_two->state().IsCanceled() || task_two->HasCompleted());
+  EXPECT_EQ(expected_id1, decode_client1.id());
+  EXPECT_EQ(expected_id2, decode_client2.id());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeImageAlreadyLocked) {
+  scoped_refptr<SimpleTask> task(new SimpleTask);
+  cache()->SetTaskToUse(task);
+
+  base::RunLoop run_loop1;
+  DecodeClient decode_client1;
+  ImageController::ImageDecodeRequestId expected_id1 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client1),
+                     run_loop1.QuitClosure()));
+  RunOrTimeout(&run_loop1);
+  EXPECT_EQ(expected_id1, decode_client1.id());
+  EXPECT_TRUE(task->has_run());
+
+  cache()->SetTaskToUse(nullptr);
+  base::RunLoop run_loop2;
+  DecodeClient decode_client2;
+  ImageController::ImageDecodeRequestId expected_id2 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client2),
+                     run_loop2.QuitClosure()));
+  RunOrTimeout(&run_loop2);
+  EXPECT_EQ(expected_id2, decode_client2.id());
+}
+
+TEST_F(ImageControllerTest, QueueImageDecodeLockedImageControllerChange) {
+  scoped_refptr<SimpleTask> task(new SimpleTask);
+  cache()->SetTaskToUse(task);
+
+  base::RunLoop run_loop1;
+  DecodeClient decode_client1;
+  ImageController::ImageDecodeRequestId expected_id1 =
+      controller()->QueueImageDecode(
+          image(),
+          base::Bind(&DecodeClient::Callback, base::Unretained(&decode_client1),
+                     run_loop1.QuitClosure()));
+  RunOrTimeout(&run_loop1);
+  EXPECT_EQ(expected_id1, decode_client1.id());
+  EXPECT_TRUE(task->has_run());
+  EXPECT_EQ(1, cache()->number_of_refs());
+
+  controller()->SetImageDecodeCache(nullptr);
+  EXPECT_EQ(0, cache()->number_of_refs());
+}
+
+}  // namespace
 }  // namespace cc
diff --git a/cc/tiles/image_decode_cache.h b/cc/tiles/image_decode_cache.h
index d20ed37..4cba352d 100644
--- a/cc/tiles/image_decode_cache.h
+++ b/cc/tiles/image_decode_cache.h
@@ -61,6 +61,15 @@
   virtual bool GetTaskForImageAndRef(const DrawImage& image,
                                      const TracingInfo& tracing_info,
                                      scoped_refptr<TileTask>* task) = 0;
+  // Similar to GetTaskForImageAndRef, except that it returns tasks that are not
+  // meant to be run as part of raster. That is, this is part of a predecode
+  // API. Note that this should only return a task responsible for decoding (and
+  // not uploading), since it will be run on a worker thread which may not have
+  // the right GPU context for upload.
+  virtual bool GetOutOfRasterDecodeTaskForImageAndRef(
+      const DrawImage& image,
+      scoped_refptr<TileTask>* task) = 0;
+
   // Unrefs an image. When the tile is finished, this should be called for every
   // GetTaskForImageAndRef call that returned true.
   virtual void UnrefImage(const DrawImage& image) = 0;
diff --git a/cc/tiles/software_image_decode_cache.cc b/cc/tiles/software_image_decode_cache.cc
index a69e1bc..aa206659 100644
--- a/cc/tiles/software_image_decode_cache.cc
+++ b/cc/tiles/software_image_decode_cache.cc
@@ -94,11 +94,13 @@
   ImageDecodeTaskImpl(SoftwareImageDecodeCache* cache,
                       const SoftwareImageDecodeCache::ImageKey& image_key,
                       const DrawImage& image,
+                      const SoftwareImageDecodeCache::DecodeTaskType task_type,
                       const ImageDecodeCache::TracingInfo& tracing_info)
       : TileTask(true),
         cache_(cache),
         image_key_(image_key),
         image_(image),
+        task_type_(task_type),
         tracing_info_(tracing_info) {}
 
   // Overridden from Task:
@@ -109,11 +111,13 @@
     devtools_instrumentation::ScopedImageDecodeTask image_decode_task(
         image_.image().get(),
         devtools_instrumentation::ScopedImageDecodeTask::SOFTWARE);
-    cache_->DecodeImage(image_key_, image_);
+    cache_->DecodeImage(image_key_, image_, task_type_);
   }
 
   // Overridden from TileTask:
-  void OnTaskCompleted() override { cache_->RemovePendingTask(image_key_); }
+  void OnTaskCompleted() override {
+    cache_->RemovePendingTask(image_key_, task_type_);
+  }
 
  protected:
   ~ImageDecodeTaskImpl() override {}
@@ -122,6 +126,7 @@
   SoftwareImageDecodeCache* cache_;
   SoftwareImageDecodeCache::ImageKey image_key_;
   DrawImage image_;
+  SoftwareImageDecodeCache::DecodeTaskType task_type_;
   const ImageDecodeCache::TracingInfo tracing_info_;
 
   DISALLOW_COPY_AND_ASSIGN(ImageDecodeTaskImpl);
@@ -208,6 +213,22 @@
     const DrawImage& image,
     const TracingInfo& tracing_info,
     scoped_refptr<TileTask>* task) {
+  return GetTaskForImageAndRefInternal(
+      image, tracing_info, DecodeTaskType::USE_IN_RASTER_TASKS, task);
+}
+
+bool SoftwareImageDecodeCache::GetOutOfRasterDecodeTaskForImageAndRef(
+    const DrawImage& image,
+    scoped_refptr<TileTask>* task) {
+  return GetTaskForImageAndRefInternal(
+      image, TracingInfo(), DecodeTaskType::USE_OUT_OF_RASTER_TASKS, task);
+}
+
+bool SoftwareImageDecodeCache::GetTaskForImageAndRefInternal(
+    const DrawImage& image,
+    const TracingInfo& tracing_info,
+    DecodeTaskType task_type,
+    scoped_refptr<TileTask>* task) {
   // If the image already exists or if we're going to create a task for it, then
   // we'll likely need to ref this image (the exception is if we're prerolling
   // the image only). That means the image is or will be in the cache. When the
@@ -258,8 +279,15 @@
     }
   }
 
-  // If the task exists, return it.
-  scoped_refptr<TileTask>& existing_task = pending_image_tasks_[key];
+  DCHECK(task_type == DecodeTaskType::USE_IN_RASTER_TASKS ||
+         task_type == DecodeTaskType::USE_OUT_OF_RASTER_TASKS);
+  // If the task exists, return it. Note that if we always need to create a new
+  // task, then just set |existing_task| to reference the passed in task (which
+  // is set to nullptr above).
+  scoped_refptr<TileTask>& existing_task =
+      (task_type == DecodeTaskType::USE_IN_RASTER_TASKS)
+          ? pending_in_raster_image_tasks_[key]
+          : pending_out_of_raster_image_tasks_[key];
   if (existing_task) {
     RefImage(key);
     *task = existing_task;
@@ -283,7 +311,7 @@
   // ref.
   RefImage(key);
   existing_task = make_scoped_refptr(
-      new ImageDecodeTaskImpl(this, key, image, tracing_info));
+      new ImageDecodeTaskImpl(this, key, image, task_type, tracing_info));
   *task = existing_task;
   SanityCheckState(__LINE__, true);
   return true;
@@ -334,11 +362,16 @@
 }
 
 void SoftwareImageDecodeCache::DecodeImage(const ImageKey& key,
-                                           const DrawImage& image) {
+                                           const DrawImage& image,
+                                           DecodeTaskType task_type) {
   TRACE_EVENT1("cc", "SoftwareImageDecodeCache::DecodeImage", "key",
                key.ToString());
   base::AutoLock lock(lock_);
-  AutoRemoveKeyFromTaskMap remove_key_from_task_map(&pending_image_tasks_, key);
+  AutoRemoveKeyFromTaskMap remove_key_from_task_map(
+      (task_type == DecodeTaskType::USE_IN_RASTER_TASKS)
+          ? &pending_in_raster_image_tasks_
+          : &pending_out_of_raster_image_tasks_,
+      key);
 
   // We could have finished all of the raster tasks (cancelled) while the task
   // was just starting to run. Since this task already started running, it
@@ -768,9 +801,17 @@
   }
 }
 
-void SoftwareImageDecodeCache::RemovePendingTask(const ImageKey& key) {
+void SoftwareImageDecodeCache::RemovePendingTask(const ImageKey& key,
+                                                 DecodeTaskType task_type) {
   base::AutoLock lock(lock_);
-  pending_image_tasks_.erase(key);
+  switch (task_type) {
+    case DecodeTaskType::USE_IN_RASTER_TASKS:
+      pending_in_raster_image_tasks_.erase(key);
+      break;
+    case DecodeTaskType::USE_OUT_OF_RASTER_TASKS:
+      pending_out_of_raster_image_tasks_.erase(key);
+      break;
+  }
 }
 
 bool SoftwareImageDecodeCache::OnMemoryDump(
@@ -837,7 +878,10 @@
       DCHECK(ref_it != decoded_images_ref_counts_.end()) << line;
     } else {
       DCHECK(ref_it == decoded_images_ref_counts_.end() ||
-             pending_image_tasks_.find(key) != pending_image_tasks_.end())
+             pending_in_raster_image_tasks_.find(key) !=
+                 pending_in_raster_image_tasks_.end() ||
+             pending_out_of_raster_image_tasks_.find(key) !=
+                 pending_out_of_raster_image_tasks_.end())
           << line;
     }
   }
diff --git a/cc/tiles/software_image_decode_cache.h b/cc/tiles/software_image_decode_cache.h
index 9befb55..1f3d72f 100644
--- a/cc/tiles/software_image_decode_cache.h
+++ b/cc/tiles/software_image_decode_cache.h
@@ -109,6 +109,8 @@
   using ImageKey = ImageDecodeCacheKey;
   using ImageKeyHash = ImageDecodeCacheKeyHash;
 
+  enum class DecodeTaskType { USE_IN_RASTER_TASKS, USE_OUT_OF_RASTER_TASKS };
+
   SoftwareImageDecodeCache(ResourceFormat format,
                            size_t locked_memory_limit_bytes);
   ~SoftwareImageDecodeCache() override;
@@ -117,6 +119,9 @@
   bool GetTaskForImageAndRef(const DrawImage& image,
                              const TracingInfo& tracing_info,
                              scoped_refptr<TileTask>* task) override;
+  bool GetOutOfRasterDecodeTaskForImageAndRef(
+      const DrawImage& image,
+      scoped_refptr<TileTask>* task) override;
   void UnrefImage(const DrawImage& image) override;
   DecodedDrawImage GetDecodedImageForDraw(const DrawImage& image) override;
   void DrawWithImageFinished(const DrawImage& image,
@@ -128,9 +133,11 @@
 
   // Decode the given image and store it in the cache. This is only called by an
   // image decode task from a worker thread.
-  void DecodeImage(const ImageKey& key, const DrawImage& image);
+  void DecodeImage(const ImageKey& key,
+                   const DrawImage& image,
+                   DecodeTaskType task_type);
 
-  void RemovePendingTask(const ImageKey& key);
+  void RemovePendingTask(const ImageKey& key, DecodeTaskType task_type);
 
   // MemoryDumpProvider overrides.
   bool OnMemoryDump(const base::trace_event::MemoryDumpArgs& args,
@@ -267,8 +274,17 @@
   // Overriden from base::MemoryCoordinatorClient.
   void OnMemoryStateChange(base::MemoryState state) override;
 
+  // Helper method to get the different tasks. Note that this should be used as
+  // if it was public (ie, all of the locks need to be properly acquired).
+  bool GetTaskForImageAndRefInternal(const DrawImage& image,
+                                     const TracingInfo& tracing_info,
+                                     DecodeTaskType type,
+                                     scoped_refptr<TileTask>* task);
+
   std::unordered_map<ImageKey, scoped_refptr<TileTask>, ImageKeyHash>
-      pending_image_tasks_;
+      pending_in_raster_image_tasks_;
+  std::unordered_map<ImageKey, scoped_refptr<TileTask>, ImageKeyHash>
+      pending_out_of_raster_image_tasks_;
 
   // The members below this comment can only be accessed if the lock is held to
   // ensure that they are safe to access on multiple threads.
diff --git a/cc/tiles/tile_manager.cc b/cc/tiles/tile_manager.cc
index 1e948f1e..02cffea 100644
--- a/cc/tiles/tile_manager.cc
+++ b/cc/tiles/tile_manager.cc
@@ -342,13 +342,15 @@
   return std::move(state);
 }
 
-TileManager::TileManager(TileManagerClient* client,
-                         base::SequencedTaskRunner* task_runner,
-                         size_t scheduled_raster_task_limit,
-                         bool use_partial_raster,
-                         bool check_tile_priority_inversion)
+TileManager::TileManager(
+    TileManagerClient* client,
+    base::SequencedTaskRunner* origin_task_runner,
+    scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner,
+    size_t scheduled_raster_task_limit,
+    bool use_partial_raster,
+    bool check_tile_priority_inversion)
     : client_(client),
-      task_runner_(task_runner),
+      task_runner_(origin_task_runner),
       resource_pool_(nullptr),
       tile_task_manager_(nullptr),
       scheduled_raster_task_limit_(scheduled_raster_task_limit),
@@ -357,6 +359,8 @@
       all_tiles_that_need_to_be_rasterized_are_scheduled_(true),
       did_check_for_completed_tasks_since_last_schedule_tasks_(true),
       did_oom_on_last_assign_(false),
+      image_controller_(origin_task_runner,
+                        std::move(image_worker_task_runner)),
       more_tiles_need_prepare_check_notifier_(
           task_runner_,
           base::Bind(&TileManager::CheckIfMoreTilesNeedToBePrepared,
@@ -1064,6 +1068,13 @@
   client_->NotifyTileStateChanged(tile);
 }
 
+void TileManager::SetDecodedImageTracker(
+    DecodedImageTracker* decoded_image_tracker) {
+  // TODO(vmpstr): If the tile manager needs to request out-of-raster decodes,
+  // it should retain and use |decoded_image_tracker| here.
+  decoded_image_tracker->set_image_controller(&image_controller_);
+}
+
 std::unique_ptr<Tile> TileManager::CreateTile(const Tile::CreateInfo& info,
                                               int layer_id,
                                               int source_frame_number,
diff --git a/cc/tiles/tile_manager.h b/cc/tiles/tile_manager.h
index 91ae0a8b..ea71587b 100644
--- a/cc/tiles/tile_manager.h
+++ b/cc/tiles/tile_manager.h
@@ -21,6 +21,7 @@
 #include "cc/raster/raster_buffer_provider.h"
 #include "cc/resources/memory_history.h"
 #include "cc/resources/resource_pool.h"
+#include "cc/tiles/decoded_image_tracker.h"
 #include "cc/tiles/eviction_tile_priority_queue.h"
 #include "cc/tiles/image_controller.h"
 #include "cc/tiles/raster_tile_priority_queue.h"
@@ -98,7 +99,8 @@
 class CC_EXPORT TileManager {
  public:
   TileManager(TileManagerClient* client,
-              base::SequencedTaskRunner* task_runner,
+              base::SequencedTaskRunner* origin_task_runner,
+              scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner,
               size_t scheduled_raster_task_limit,
               bool use_partial_raster,
               bool check_tile_priority_inversion);
@@ -206,6 +208,8 @@
                              Resource* resource,
                              bool was_canceled);
 
+  void SetDecodedImageTracker(DecodedImageTracker* decoded_image_tracker);
+
  protected:
   friend class Tile;
   // Must be called by tile during destruction.
diff --git a/cc/trees/layer_tree_host_impl.cc b/cc/trees/layer_tree_host_impl.cc
index f4ce94d..ab2763b 100644
--- a/cc/trees/layer_tree_host_impl.cc
+++ b/cc/trees/layer_tree_host_impl.cc
@@ -190,10 +190,12 @@
     RenderingStatsInstrumentation* rendering_stats_instrumentation,
     TaskGraphRunner* task_graph_runner,
     std::unique_ptr<MutatorHost> mutator_host,
-    int id) {
+    int id,
+    scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner) {
   return base::WrapUnique(new LayerTreeHostImpl(
       settings, client, task_runner_provider, rendering_stats_instrumentation,
-      task_graph_runner, std::move(mutator_host), id));
+      task_graph_runner, std::move(mutator_host), id,
+      std::move(image_worker_task_runner)));
 }
 
 LayerTreeHostImpl::LayerTreeHostImpl(
@@ -203,7 +205,8 @@
     RenderingStatsInstrumentation* rendering_stats_instrumentation,
     TaskGraphRunner* task_graph_runner,
     std::unique_ptr<MutatorHost> mutator_host,
-    int id)
+    int id,
+    scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner)
     : client_(client),
       task_runner_provider_(task_runner_provider),
       current_begin_frame_tracker_(BEGINFRAMETRACKER_FROM_HERE),
@@ -229,6 +232,7 @@
       // task_runner_provider_.
       tile_manager_(this,
                     GetTaskRunner(),
+                    std::move(image_worker_task_runner),
                     is_synchronous_single_threaded_
                         ? std::numeric_limits<size_t>::max()
                         : settings.scheduled_raster_task_limit,
@@ -273,6 +277,8 @@
   browser_controls_offset_manager_ = BrowserControlsOffsetManager::Create(
       this, settings.top_controls_show_threshold,
       settings.top_controls_hide_threshold);
+
+  tile_manager_.SetDecodedImageTracker(&decoded_image_tracker_);
 }
 
 LayerTreeHostImpl::~LayerTreeHostImpl() {
@@ -1837,6 +1843,7 @@
 
 void LayerTreeHostImpl::DidFinishImplFrame() {
   current_begin_frame_tracker_.Finish();
+  decoded_image_tracker_.NotifyFrameFinished();
 }
 
 void LayerTreeHostImpl::UpdateViewportContainerSizes() {
diff --git a/cc/trees/layer_tree_host_impl.h b/cc/trees/layer_tree_host_impl.h
index 2bbf782..d51f307 100644
--- a/cc/trees/layer_tree_host_impl.h
+++ b/cc/trees/layer_tree_host_impl.h
@@ -36,6 +36,7 @@
 #include "cc/scheduler/commit_earlyout_reason.h"
 #include "cc/scheduler/draw_result.h"
 #include "cc/scheduler/video_frame_controller.h"
+#include "cc/tiles/decoded_image_tracker.h"
 #include "cc/tiles/image_decode_cache.h"
 #include "cc/tiles/tile_manager.h"
 #include "cc/trees/layer_tree_mutator.h"
@@ -144,7 +145,8 @@
       RenderingStatsInstrumentation* rendering_stats_instrumentation,
       TaskGraphRunner* task_graph_runner,
       std::unique_ptr<MutatorHost> mutator_host,
-      int id);
+      int id,
+      scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner);
   ~LayerTreeHostImpl() override;
 
   // InputHandler implementation
@@ -594,7 +596,8 @@
       RenderingStatsInstrumentation* rendering_stats_instrumentation,
       TaskGraphRunner* task_graph_runner,
       std::unique_ptr<MutatorHost> mutator_host,
-      int id);
+      int id,
+      scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner);
 
   // Virtual for testing.
   virtual bool AnimateLayers(base::TimeTicks monotonic_time);
@@ -746,6 +749,7 @@
 
   const bool is_synchronous_single_threaded_;
   TileManager tile_manager_;
+  DecodedImageTracker decoded_image_tracker_;
 
   gfx::Vector2dF accumulated_root_overscroll_;
 
diff --git a/cc/trees/layer_tree_host_impl_unittest.cc b/cc/trees/layer_tree_host_impl_unittest.cc
index f0afd40..761b4a6b 100644
--- a/cc/trees/layer_tree_host_impl_unittest.cc
+++ b/cc/trees/layer_tree_host_impl_unittest.cc
@@ -199,7 +199,7 @@
     host_impl_ = LayerTreeHostImpl::Create(
         settings, this, task_runner_provider, &stats_instrumentation_,
         &task_graph_runner_,
-        AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0);
+        AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0, nullptr);
     compositor_frame_sink_ = std::move(compositor_frame_sink);
     host_impl_->SetVisible(true);
     bool init = host_impl_->InitializeRenderer(compositor_frame_sink_.get());
@@ -2709,7 +2709,8 @@
                           rendering_stats_instrumentation,
                           task_graph_runner,
                           AnimationHost::CreateForTesting(ThreadInstance::IMPL),
-                          0) {}
+                          0,
+                          nullptr) {}
 
   BeginFrameArgs CurrentBeginFrameArgs() const override {
     return CreateBeginFrameArgsForTesting(BEGINFRAME_FROM_HERE, 0, 1,
@@ -7579,7 +7580,7 @@
       LayerTreeHostImpl::Create(
           settings, this, &task_runner_provider_, &stats_instrumentation_,
           &task_graph_runner_,
-          AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0);
+          AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0, nullptr);
   layer_tree_host_impl->SetVisible(true);
   layer_tree_host_impl->InitializeRenderer(compositor_frame_sink.get());
   layer_tree_host_impl->WillBeginImplFrame(
@@ -7708,7 +7709,7 @@
   std::unique_ptr<LayerTreeHostImpl> my_host_impl = LayerTreeHostImpl::Create(
       settings, client, task_runner_provider, stats_instrumentation,
       task_graph_runner, AnimationHost::CreateForTesting(ThreadInstance::IMPL),
-      0);
+      0, nullptr);
   my_host_impl->SetVisible(true);
   my_host_impl->InitializeRenderer(compositor_frame_sink);
   my_host_impl->WillBeginImplFrame(
@@ -8176,7 +8177,7 @@
   host_impl_ = LayerTreeHostImpl::Create(
       settings, this, &task_runner_provider_, &stats_instrumentation_,
       &task_graph_runner_,
-      AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0);
+      AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0, nullptr);
 
   // Gpu compositing.
   compositor_frame_sink_ =
@@ -11605,7 +11606,7 @@
   host_impl_ = LayerTreeHostImpl::Create(
       settings, this, &task_runner_provider_, &stats_instrumentation_,
       &task_graph_runner_,
-      AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0);
+      AnimationHost::CreateForTesting(ThreadInstance::IMPL), 0, nullptr);
   host_impl_->SetVisible(true);
 
   // InitializeRenderer with a gpu-raster enabled output surface.
diff --git a/cc/trees/layer_tree_host_in_process.cc b/cc/trees/layer_tree_host_in_process.cc
index 8d5ba2d..29cba59 100644
--- a/cc/trees/layer_tree_host_in_process.cc
+++ b/cc/trees/layer_tree_host_in_process.cc
@@ -122,7 +122,8 @@
       did_complete_scale_animation_(false),
       id_(s_layer_tree_host_sequence_number.GetNext() + 1),
       task_graph_runner_(params->task_graph_runner),
-      image_serialization_processor_(params->image_serialization_processor) {
+      image_serialization_processor_(params->image_serialization_processor),
+      image_worker_task_runner_(params->image_worker_task_runner) {
   DCHECK(task_graph_runner_);
   DCHECK(layer_tree_);
   DCHECK_NE(compositor_mode_, CompositorMode::REMOTE);
@@ -446,7 +447,7 @@
   std::unique_ptr<LayerTreeHostImpl> host_impl = LayerTreeHostImpl::Create(
       settings_, client, task_runner_provider_.get(),
       rendering_stats_instrumentation_.get(), task_graph_runner_,
-      std::move(mutator_host_impl), id_);
+      std::move(mutator_host_impl), id_, std::move(image_worker_task_runner_));
   host_impl->SetHasGpuRasterizationTrigger(has_gpu_rasterization_trigger_);
   host_impl->SetContentIsSuitableForGpuRasterization(
       content_is_suitable_for_gpu_rasterization_);
diff --git a/cc/trees/layer_tree_host_in_process.h b/cc/trees/layer_tree_host_in_process.h
index eec43948..909dce7 100644
--- a/cc/trees/layer_tree_host_in_process.h
+++ b/cc/trees/layer_tree_host_in_process.h
@@ -74,7 +74,8 @@
     LayerTreeSettings const* settings = nullptr;
     scoped_refptr<base::SingleThreadTaskRunner> main_task_runner;
     ImageSerializationProcessor* image_serialization_processor = nullptr;
-    MutatorHost* mutator_host;
+    MutatorHost* mutator_host = nullptr;
+    scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner;
 
     InitParams();
     ~InitParams();
@@ -317,6 +318,8 @@
   // should get these PropertyTrees directly from blink?
   std::unique_ptr<ReflectedMainFrameState> reflected_main_frame_state_;
 
+  scoped_refptr<base::SequencedTaskRunner> image_worker_task_runner_;
+
   DISALLOW_COPY_AND_ASSIGN(LayerTreeHostInProcess);
 };