Mozart: Improve tracing and backpressure.

Refactored the GPU Output to clarify the separation of concerns
between the output and rasterizer classes.  GpuOutput maintains
the queue of frames to draw and keeps track of how many frames
are outstanding.  GpuRasterizer manages the GL context and issues
the actual drawing commands.

Simplified the policy for dropping frames so it only happen in
one spot in the compositor.  The compositor maintains a queue
of drawn frames awaiting finish.  When that queue's size exceeds
the maximum allowable pipeline depth (configurable using
the --pipeline-depth argument) a pending frame will be discarded.

Added more trace events to help understand the behavior of the
compositor.

Normalized the nomenclature for different stages in composition.
They are now called...

  - Present: accept and validate scene updates published by apps
  - Snapshot: resolve scene dependencies and capture the state
              of the scene graph for traversal
  - Paint: record drawing commands for a frame (as an SkPicture)
  - Submit: enqueue a frame to be rasterized
  - Draw: rasterize the frame

There's still much to be improved here.

BUG=
R=mikejurka@google.com

Review URL: https://codereview.chromium.org/1995873002 .
diff --git a/services/gfx/compositor/backend/gpu_output.cc b/services/gfx/compositor/backend/gpu_output.cc
index 37a51e4..531f2ee 100644
--- a/services/gfx/compositor/backend/gpu_output.cc
+++ b/services/gfx/compositor/backend/gpu_output.cc
@@ -7,159 +7,297 @@
 #include <utility>
 
 #include "base/bind.h"
+#include "base/command_line.h"
 #include "base/location.h"
 #include "base/logging.h"
 #include "base/message_loop/message_loop.h"
+#include "base/strings/string_number_conversions.h"
 #include "base/trace_event/trace_event.h"
-#include "services/gfx/compositor/backend/gpu_rasterizer.h"
 #include "services/gfx/compositor/render/render_frame.h"
 
 namespace compositor {
 namespace {
-// Maximum number of frames to hold in the queue for rendering.
-constexpr size_t kMaxPipelineDepth = 1;
-}
+constexpr const char* kPipelineDepthSwitch = "pipeline-depth";
+constexpr uint32_t kDefaultPipelineDepth = 2u;  // ideally should be 1
+constexpr uint32_t kMinPipelineDepth = 1u;
+constexpr uint32_t kMaxPipelineDepth = 10u;
 
-template <typename T>
-static void Drop(scoped_ptr<T> ptr) {}
-
-static scoped_ptr<base::MessagePump> CreateMessagePumpMojo() {
+scoped_ptr<base::MessagePump> CreateMessagePumpMojo() {
   return base::MessageLoop::CreateMessagePumpForType(
       base::MessageLoop::TYPE_DEFAULT);
 }
+}  // namespace
 
 GpuOutput::GpuOutput(
     mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
     const SchedulerCallbacks& scheduler_callbacks,
     const base::Closure& error_callback)
-    : scheduler_(new VsyncScheduler(base::MessageLoop::current()->task_runner(),
-                                    scheduler_callbacks)),
-      rasterizer_delegate_(make_scoped_ptr(new RasterizerDelegate())) {
+    : compositor_task_runner_(base::MessageLoop::current()->task_runner()),
+      vsync_scheduler_(
+          new VsyncScheduler(compositor_task_runner_, scheduler_callbacks)),
+      rasterizer_thread_(new base::Thread("gpu_rasterizer")),
+      rasterizer_initialized_(true, false) {
   DCHECK(context_provider);
 
-  rasterizer_delegate_->PostInitialize(
-      std::move(context_provider), scheduler_,
-      base::MessageLoop::current()->task_runner(), error_callback);
+  pipeline_depth_ = kDefaultPipelineDepth;
+  auto command_line = base::CommandLine::ForCurrentProcess();
+  if (command_line->HasSwitch(kPipelineDepthSwitch)) {
+    std::string str(command_line->GetSwitchValueASCII(kPipelineDepthSwitch));
+    unsigned value;
+    if (base::StringToUint(str, &value) && value >= kMinPipelineDepth &&
+        value <= kMaxPipelineDepth) {
+      pipeline_depth_ = value;
+    } else {
+      LOG(ERROR) << "Invalid value for --" << kPipelineDepthSwitch << ": \""
+                 << str << "\"";
+      PostErrorCallback();
+    }
+  }
+  DVLOG(2) << "Using pipeline depth " << pipeline_depth_;
+
+  base::Thread::Options options;
+  options.message_pump_factory = base::Bind(&CreateMessagePumpMojo);
+  rasterizer_thread_->StartWithOptions(options);
+  rasterizer_task_runner_ = rasterizer_thread_->message_loop()->task_runner();
+  rasterizer_task_runner_->PostTask(
+      FROM_HERE,
+      base::Bind(&GpuOutput::InitializeRasterizer, base::Unretained(this),
+                 base::Passed(std::move(context_provider))));
+  rasterizer_initialized_.Wait();
+  DCHECK(rasterizer_);
 }
 
 GpuOutput::~GpuOutput() {
-  // Ensure destruction happens on the correct thread.
-  rasterizer_delegate_->PostDestroy(rasterizer_delegate_.Pass());
+  // Ensure rasterizer destruction happens on the rasterizer thread.
+  rasterizer_task_runner_->PostTask(
+      FROM_HERE,
+      base::Bind(&GpuOutput::DestroyRasterizer, base::Unretained(this)));
+  rasterizer_thread_->Stop();
+  DCHECK(!rasterizer_);
 }
 
 Scheduler* GpuOutput::GetScheduler() {
-  return scheduler_.get();
+  return vsync_scheduler_.get();
 }
 
 void GpuOutput::SubmitFrame(const scoped_refptr<RenderFrame>& frame) {
-  rasterizer_delegate_->PostFrame(frame);
-}
+  DCHECK(frame);
+  TRACE_EVENT0("gfx", "GpuOutput::SubmitFrame");
 
-GpuOutput::RasterizerDelegate::RasterizerDelegate() {
-  base::Thread::Options options;
-  options.message_pump_factory = base::Bind(&CreateMessagePumpMojo);
-
-  thread_.reset(new base::Thread("gpu_rasterizer"));
-  thread_->StartWithOptions(options);
-  task_runner_ = thread_->message_loop()->task_runner();
-}
-
-GpuOutput::RasterizerDelegate::~RasterizerDelegate() {}
-
-void GpuOutput::RasterizerDelegate::PostInitialize(
-    mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
-    const scoped_refptr<VsyncScheduler>& scheduler,
-    const scoped_refptr<base::TaskRunner>& task_runner,
-    const base::Closure& error_callback) {
-  task_runner_->PostTask(
-      FROM_HERE,
-      base::Bind(&RasterizerDelegate::InitializeTask, base::Unretained(this),
-                 base::Passed(std::move(context_provider)), scheduler,
-                 base::MessageLoop::current()->task_runner(), error_callback));
-}
-
-void GpuOutput::RasterizerDelegate::PostDestroy(
-    scoped_ptr<RasterizerDelegate> self) {
-  task_runner_->PostTask(
-      FROM_HERE, base::Bind(&Drop<RasterizerDelegate>, base::Passed(&self)));
-}
-
-void GpuOutput::RasterizerDelegate::PostFrame(
-    const scoped_refptr<RenderFrame>& frame) {
-  bool was_empty;
-  scoped_refptr<RenderFrame> dropped_frame;
+  const int64_t submit_time = MojoGetTimeTicksNow();
+  scoped_refptr<FrameData> frame_data(
+      new FrameData(frame, submit_time));  // drop outside lock
   {
-    std::lock_guard<std::mutex> lock(mutex_);
-    was_empty = frames_.empty();
-    if (frames_.size() == kMaxPipelineDepth) {
-      // TODO(jeffbrown): Adjust scheduler behavior to compensate.
-      LOG(ERROR) << "Renderer pipeline stalled, dropping a frame to catch up.";
-      dropped_frame = frames_.front();  // drop an old frame outside the lock
-      frames_.pop();
+    std::lock_guard<std::mutex> lock(shared_state_.mutex);
+
+    TRACE_EVENT_FLOW_BEGIN0("gfx", "Frame Queued", frame_data.get());
+    shared_state_.current_frame_data.swap(frame_data);
+    if (frame_data && !frame_data->drawn) {
+      // Dropped an undrawn frame.
+      DVLOG(2) << "Rasterizer stalled, dropping frame to catch up.";
+      TRACE_EVENT_FLOW_END1("gfx", "Frame Queued", frame_data.get(), "drawn",
+                            false);
     }
-    frames_.push(frame);
+
+    // TODO(jeffbrown): If the draw queue is full, we should pause
+    // scheduling until the queue drains.
+    if (shared_state_.rasterizer_ready &&
+        shared_state_.drawn_frames_awaiting_finish.size() < pipeline_depth_)
+      ScheduleDrawLocked();
+  }
+}
+
+void GpuOutput::OnRasterizerReady(int64_t vsync_timebase,
+                                  int64_t vsync_interval) {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+
+  // TODO(jeffbrown): This shouldn't be hardcoded.
+  // Need to do some real tuning and possibly determine values adaptively.
+  // We should probably split the Start() method in two to separate the
+  // process of setting parameters from starting / stopping scheduling.
+  const int64_t update_phase = -vsync_interval;
+  const int64_t snapshot_phase = -vsync_interval / 6;
+  // TODO(jeffbrown): Determine the presentation phase based on queue depth.
+  const int64_t presentation_phase = vsync_interval * pipeline_depth_;
+  if (!vsync_scheduler_->Start(vsync_timebase, vsync_interval, update_phase,
+                               snapshot_phase, presentation_phase)) {
+    LOG(ERROR) << "Received invalid vsync parameters: timebase="
+               << vsync_timebase << ", interval=" << vsync_interval;
+    PostErrorCallback();
+    return;
   }
 
-  if (was_empty)
-    PostSubmit();
-}
-
-void GpuOutput::RasterizerDelegate::PostSubmit() {
-  TRACE_EVENT0("gfx", "GpuOutput::RasterizerDelegate::PostSubmit");
-  task_runner_->PostTask(FROM_HERE, base::Bind(&RasterizerDelegate::SubmitTask,
-                                               base::Unretained(this)));
-}
-
-void GpuOutput::RasterizerDelegate::InitializeTask(
-    mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
-    const scoped_refptr<VsyncScheduler>& scheduler,
-    const scoped_refptr<base::TaskRunner>& task_runner,
-    const base::Closure& error_callback) {
-  rasterizer_.reset(new GpuRasterizer(
-      mojo::ContextProviderPtr::Create(std::move(context_provider)), scheduler,
-      task_runner, error_callback));
-}
-
-void GpuOutput::RasterizerDelegate::SubmitTask() {
-  TRACE_EVENT0("gfx", "GpuOutput::RasterizerDelegate::SubmitTask");
-  bool have_more;
-  scoped_refptr<RenderFrame> frame;
   {
-    std::lock_guard<std::mutex> lock(mutex_);
-    DCHECK(!frames_.empty());
-    frame = frames_.front();
-    frames_.pop();
-    have_more = !frames_.empty();
+    std::lock_guard<std::mutex> lock(shared_state_.mutex);
+
+    if (shared_state_.rasterizer_ready)
+      return;
+
+    DCHECK(shared_state_.drawn_frames_awaiting_finish.empty());
+    shared_state_.rasterizer_ready = true;
+
+    if (!shared_state_.current_frame_data)
+      return;
+
+    shared_state_.current_frame_data->Recycle();
+    TRACE_EVENT_FLOW_BEGIN0("gfx", "Frame Queued",
+                            shared_state_.current_frame_data.get());
+    ScheduleDrawLocked();
   }
-
-  if (have_more)
-    PostSubmit();
-
-  int64_t submit_time = MojoGetTimeTicksNow();
-  rasterizer_->SubmitFrame(
-      frame, base::Bind(&RasterizerDelegate::OnFrameSubmitted,
-                        base::Unretained(this), frame->frame_info().frame_time,
-                        frame->frame_info().presentation_time, submit_time));
 }
 
-void GpuOutput::RasterizerDelegate::OnFrameSubmitted(int64_t frame_time,
-                                                     int64_t presentation_time,
-                                                     int64_t submit_time,
-                                                     bool presented) {
-  TRACE_EVENT0("gfx", "GpuOutput::RasterizerDelegate::OnFrameSubmitted");
-  // TODO(jeffbrown): Adjust scheduler behavior based on observed timing.
-  // Note: These measurements don't account for systematic downstream delay
-  // in the display pipeline (how long it takes pixels to actually light up).
-  int64_t complete_time = MojoGetTimeTicksNow();
-  if (presented) {
-    DVLOG(3) << "Frame presented: submission latency "
-             << (submit_time - frame_time) << " us, rasterization latency "
-             << (complete_time - submit_time) << " us, total latency "
-             << (complete_time - frame_time) << " us, presentation time error "
-             << (complete_time - presentation_time);
-  } else {
-    DVLOG(3) << "Frame deferred.";
+void GpuOutput::OnRasterizerSuspended() {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+
+  vsync_scheduler_->Stop();
+
+  {
+    std::lock_guard<std::mutex> lock(shared_state_.mutex);
+
+    if (!shared_state_.rasterizer_ready)
+      return;
+
+    shared_state_.rasterizer_ready = false;
   }
 }
 
+void GpuOutput::OnRasterizerError() {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+
+  PostErrorCallback();
+}
+
+void GpuOutput::ScheduleDrawLocked() {
+  DCHECK(shared_state_.current_frame_data);
+  DCHECK(!shared_state_.current_frame_data->drawn);
+
+  if (shared_state_.draw_scheduled)
+    return;
+
+  shared_state_.draw_scheduled = true;
+  rasterizer_task_runner_->PostTask(
+      FROM_HERE, base::Bind(&GpuOutput::OnDraw, base::Unretained(this)));
+}
+
+void GpuOutput::OnDraw() {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+  TRACE_EVENT0("gfx", "GpuOutput::OnDraw");
+
+  scoped_refptr<FrameData> frame_data;  // used outside lock
+  {
+    std::lock_guard<std::mutex> lock(shared_state_.mutex);
+
+    DCHECK(shared_state_.draw_scheduled);
+    DCHECK(shared_state_.current_frame_data);
+    DCHECK(!shared_state_.current_frame_data->drawn);
+
+    shared_state_.draw_scheduled = false;
+
+    if (!shared_state_.rasterizer_ready ||
+        shared_state_.drawn_frames_awaiting_finish.size() >= pipeline_depth_)
+      return;
+
+    frame_data = shared_state_.current_frame_data;
+    frame_data->drawn = true;
+    frame_data->draw_time = MojoGetTimeTicksNow();
+    TRACE_EVENT_FLOW_END1("gfx", "Frame Queued", frame_data.get(), "drawn",
+                          true);
+
+    TRACE_EVENT_ASYNC_BEGIN0("gfx", "Rasterize", frame_data.get());
+    shared_state_.drawn_frames_awaiting_finish.emplace(frame_data);
+  }
+
+  rasterizer_->DrawFrame(frame_data->frame);
+  frame_data->wait_time = MojoGetTimeTicksNow();
+}
+
+void GpuOutput::OnRasterizerFinishedDraw(bool presented) {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+  TRACE_EVENT0("gfx", "GpuOutput::OnRasterizerFinishedDraw");
+
+  const int64_t finish_time = MojoGetTimeTicksNow();
+  scoped_refptr<FrameData> frame_data;  // drop outside lock
+  {
+    std::lock_guard<std::mutex> lock(shared_state_.mutex);
+
+    DCHECK(shared_state_.rasterizer_ready);
+    DCHECK(!shared_state_.drawn_frames_awaiting_finish.empty());
+    size_t draw_queue_depth = shared_state_.drawn_frames_awaiting_finish.size();
+    shared_state_.drawn_frames_awaiting_finish.front().swap(frame_data);
+    shared_state_.drawn_frames_awaiting_finish.pop();
+    DCHECK(frame_data);
+    DCHECK(frame_data->drawn);
+    TRACE_EVENT_ASYNC_END1("gfx", "Rasterize", frame_data.get(), "presented",
+                           presented);
+
+    // TODO(jeffbrown): Adjust scheduler behavior based on observed timing.
+    // Note: These measurements don't account for systematic downstream delay
+    // in the display pipeline (how long it takes pixels to actually light up).
+    if (presented) {
+      const RenderFrame::Metadata& frame_metadata =
+          frame_data->frame->metadata();
+      const mojo::gfx::composition::FrameInfo& frame_info =
+          frame_metadata.frame_info();
+      const int64_t frame_time = frame_info.frame_time;
+      const int64_t presentation_time = frame_info.presentation_time;
+      const int64_t composition_time = frame_metadata.composition_time();
+      const int64_t draw_time = frame_data->draw_time;
+      const int64_t wait_time = frame_data->wait_time;
+      const int64_t submit_time = frame_data->submit_time;
+
+      DVLOG(2) << "Presented frame: composition latency "
+               << (composition_time - frame_time) << " us, submission latency "
+               << (submit_time - composition_time) << " us, queue latency "
+               << (draw_time - submit_time) << " us, draw latency "
+               << (wait_time - draw_time) << " us, GPU latency "
+               << (finish_time - wait_time) << " us, total latency "
+               << (finish_time - frame_time) << " us, presentation time error "
+               << (finish_time - presentation_time) << " us"
+               << ", draw queue depth " << draw_queue_depth;
+    } else {
+      DVLOG(2) << "Rasterizer dropped frame.";
+    }
+
+    DCHECK(shared_state_.current_frame_data);
+    if (!shared_state_.current_frame_data->drawn)
+      ScheduleDrawLocked();
+  }
+}
+
+void GpuOutput::InitializeRasterizer(
+    mojo::InterfaceHandle<mojo::ContextProvider> context_provider) {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+  DCHECK(!rasterizer_);
+  TRACE_EVENT0("gfx", "GpuOutput::InitializeRasterizer");
+
+  rasterizer_.reset(new GpuRasterizer(
+      mojo::ContextProviderPtr::Create(std::move(context_provider)), this));
+  rasterizer_initialized_.Signal();
+}
+
+void GpuOutput::DestroyRasterizer() {
+  DCHECK(rasterizer_task_runner_->RunsTasksOnCurrentThread());
+  DCHECK(rasterizer_);
+  TRACE_EVENT0("gfx", "GpuOutput::DestroyRasterizer");
+
+  rasterizer_.reset();
+  rasterizer_initialized_.Reset();
+}
+
+void GpuOutput::PostErrorCallback() {
+  compositor_task_runner_->PostTask(FROM_HERE, error_callback_);
+}
+
+GpuOutput::FrameData::FrameData(const scoped_refptr<RenderFrame>& frame,
+                                int64_t submit_time)
+    : frame(frame), submit_time(submit_time) {}
+
+GpuOutput::FrameData::~FrameData() {}
+
+void GpuOutput::FrameData::Recycle() {
+  drawn = false;
+  draw_time = 0;
+  wait_time = 0;
+}
+
 }  // namespace compositor
diff --git a/services/gfx/compositor/backend/gpu_output.h b/services/gfx/compositor/backend/gpu_output.h
index 8b0aa87..d7fe5f5 100644
--- a/services/gfx/compositor/backend/gpu_output.h
+++ b/services/gfx/compositor/backend/gpu_output.h
@@ -12,19 +12,18 @@
 #include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
-#include "base/memory/scoped_ptr.h"
+#include "base/synchronization/waitable_event.h"
 #include "base/task_runner.h"
 #include "base/threading/thread.h"
 #include "mojo/services/gpu/interfaces/context_provider.mojom.h"
+#include "services/gfx/compositor/backend/gpu_rasterizer.h"
 #include "services/gfx/compositor/backend/output.h"
 #include "services/gfx/compositor/backend/vsync_scheduler.h"
 
 namespace compositor {
 
-class GpuRasterizer;
-
 // Renderer backed by a ContextProvider.
-class GpuOutput : public Output {
+class GpuOutput : public Output, private GpuRasterizer::Callbacks {
  public:
   GpuOutput(mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
             const SchedulerCallbacks& scheduler_callbacks,
@@ -35,55 +34,71 @@
   void SubmitFrame(const scoped_refptr<RenderFrame>& frame) override;
 
  private:
-  // Wrapper around state which is made available to the rasterizer thread.
-  class RasterizerDelegate {
-   public:
-    RasterizerDelegate();
-    ~RasterizerDelegate();
+  struct FrameData : public base::RefCountedThreadSafe<FrameData> {
+    FrameData(const scoped_refptr<RenderFrame>& frame, int64_t submit_time);
 
-    void PostInitialize(
-        mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
-        const scoped_refptr<VsyncScheduler>& scheduler,
-        const scoped_refptr<base::TaskRunner>& task_runner,
-        const base::Closure& error_callback);
+    void Recycle();
 
-    void PostDestroy(scoped_ptr<RasterizerDelegate> self);
-
-    void PostFrame(const scoped_refptr<RenderFrame>& frame);
+    const scoped_refptr<RenderFrame> frame;
+    const int64_t submit_time;
+    bool drawn = false;     // set when DrawFrame is called
+    int64_t draw_time = 0;  // 0 if not drawn
+    int64_t wait_time = 0;  // 0 if not drawn
 
    private:
-    void PostSubmit();
+    friend class base::RefCountedThreadSafe<FrameData>;
 
-    // Called on rasterizer thread.
-    void InitializeTask(
-        mojo::InterfaceHandle<mojo::ContextProvider> context_provider,
-        const scoped_refptr<VsyncScheduler>& scheduler,
-        const scoped_refptr<base::TaskRunner>& task_runner,
-        const base::Closure& error_callback);
+    ~FrameData();
 
-    // Called on rasterizer thread.
-    void SubmitTask();
-
-    // Called on rasterizer thread.
-    void OnFrameSubmitted(int64_t frame_time,
-                          int64_t presentation_time,
-                          int64_t submit_time,
-                          bool presented);
-
-    std::unique_ptr<base::Thread> thread_;
-    scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
-    std::unique_ptr<GpuRasterizer> rasterizer_;
-
-    std::mutex mutex_;
-    std::queue<scoped_refptr<RenderFrame>> frames_;  // guarded by |mutex_|
-
-    DISALLOW_COPY_AND_ASSIGN(RasterizerDelegate);
+    DISALLOW_COPY_AND_ASSIGN(FrameData);
   };
 
-  scoped_refptr<VsyncScheduler> scheduler_;
-  scoped_ptr<RasterizerDelegate> rasterizer_delegate_;  // can't use unique_ptr
-                                                        // here due to
-                                                        // base::Bind (sadness)
+  // |GpuRasterizer::Callbacks|:
+  void OnRasterizerReady(int64_t vsync_timebase,
+                         int64_t vsync_interval) override;
+  void OnRasterizerSuspended() override;
+  void OnRasterizerFinishedDraw(bool presented) override;
+  void OnRasterizerError() override;
+
+  void ScheduleDrawLocked();
+  void OnDraw();
+
+  void InitializeRasterizer(
+      mojo::InterfaceHandle<mojo::ContextProvider> context_provider);
+  void DestroyRasterizer();
+  void PostErrorCallback();
+
+  scoped_refptr<base::SingleThreadTaskRunner> compositor_task_runner_;
+  scoped_refptr<VsyncScheduler> vsync_scheduler_;
+  base::Closure error_callback_;
+
+  // Maximum number of frames to hold in the drawing pipeline.
+  // Any more than this and we start dropping them.
+  uint32_t pipeline_depth_;
+
+  // The rasterizer itself runs on its own thread.
+  std::unique_ptr<base::Thread> rasterizer_thread_;
+  scoped_refptr<base::SingleThreadTaskRunner> rasterizer_task_runner_;
+  base::WaitableEvent rasterizer_initialized_;
+  std::unique_ptr<GpuRasterizer> rasterizer_;
+
+  // Holds state shared between the compositor and rasterizer threads.
+  struct {
+    std::mutex mutex;  // guards all shared state
+
+    // The most recently submitted frame.
+    // Only null until the first frame has been submitted.
+    scoped_refptr<FrameData> current_frame_data;
+
+    // Frames drawn and awaiting completion by the rasterizer.
+    std::queue<scoped_refptr<FrameData>> drawn_frames_awaiting_finish;
+
+    // Set to true when the rasterizer is ready to draw.
+    bool rasterizer_ready = false;
+
+    // Set to true when a request to draw has been scheduled.
+    bool draw_scheduled = false;
+  } shared_state_;
 
   DISALLOW_COPY_AND_ASSIGN(GpuOutput);
 };
diff --git a/services/gfx/compositor/backend/gpu_rasterizer.cc b/services/gfx/compositor/backend/gpu_rasterizer.cc
index 4625490..85c5553 100644
--- a/services/gfx/compositor/backend/gpu_rasterizer.cc
+++ b/services/gfx/compositor/backend/gpu_rasterizer.cc
@@ -13,7 +13,6 @@
 #include <MGL/mgl.h>
 #include <MGL/mgl_echo.h>
 #include <MGL/mgl_onscreen.h>
-#include <MGL/mgl_signal_sync_point.h>
 
 #include "base/bind.h"
 #include "base/location.h"
@@ -21,29 +20,27 @@
 #include "base/message_loop/message_loop.h"
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
-#include "services/gfx/compositor/backend/vsync_scheduler.h"
 #include "services/gfx/compositor/render/render_frame.h"
 
 namespace compositor {
 namespace {
+// Timeout for receiving initial viewport parameters from the GPU service.
 constexpr int64_t kViewportParameterTimeoutMs = 1000;
+
+// Default vsync interval when the GPU service failed to provide viewport
+// parameters promptly.
 constexpr int64_t kDefaultVsyncIntervalUs = 100000;  // deliberately sluggish
 }
 
 GpuRasterizer::GpuRasterizer(mojo::ContextProviderPtr context_provider,
-                             const scoped_refptr<VsyncScheduler>& scheduler,
-                             const scoped_refptr<base::TaskRunner>& task_runner,
-                             const base::Closure& error_callback)
+                             Callbacks* callbacks)
     : context_provider_(context_provider.Pass()),
-      scheduler_(scheduler),
-      task_runner_(task_runner),
-      error_callback_(error_callback),
+      callbacks_(callbacks),
       viewport_parameter_listener_binding_(this),
       viewport_parameter_timeout_(false, false),
       weak_ptr_factory_(this) {
   DCHECK(context_provider_);
-  DCHECK(scheduler_);
-  DCHECK(task_runner_);
+  DCHECK(callbacks_);
 
   context_provider_.set_connection_error_handler(
       base::Bind(&GpuRasterizer::OnContextProviderConnectionError,
@@ -76,12 +73,13 @@
 
   if (!command_buffer) {
     LOG(ERROR) << "Could not create GL context.";
-    PostErrorCallback();
+    callbacks_->OnRasterizerError();
     return;
   }
 
   gl_context_ = mojo::GLContext::CreateFromCommandBuffer(
       mojo::CommandBufferPtr::Create(std::move(command_buffer)));
+  DCHECK(!gl_context_->is_lost());
   gl_context_->AddObserver(this);
   ganesh_context_ = new mojo::skia::GaneshContext(gl_context_);
 
@@ -94,19 +92,20 @@
         base::Bind(&GpuRasterizer::OnViewportParameterTimeout,
                    base::Unretained(this)));
   }
-
-  if (frame_)
-    Draw();
 }
 
 void GpuRasterizer::AbandonContext() {
-  if (gl_context_)
-    scheduler_->Stop();
-
   if (viewport_parameter_listener_binding_.is_bound()) {
     viewport_parameter_timeout_.Stop();
     viewport_parameter_listener_binding_.Close();
   }
+
+  if (ready_) {
+    while (frames_in_progress_)
+      DrawFinished(false /*presented*/);
+    ready_ = false;
+    callbacks_->OnRasterizerSuspended();
+  }
 }
 
 void GpuRasterizer::DestroyContext() {
@@ -124,15 +123,14 @@
 
 void GpuRasterizer::OnContextProviderConnectionError() {
   LOG(ERROR) << "Context provider connection lost.";
-  PostErrorCallback();
+
+  callbacks_->OnRasterizerError();
 }
 
 void GpuRasterizer::OnContextLost() {
   LOG(WARNING) << "GL context lost!";
 
   AbandonContext();
-  frames_pending_ = 0u;
-
   base::MessageLoop::current()->PostTask(
       FROM_HERE, base::Bind(&GpuRasterizer::RecreateContextAfterLoss,
                             weak_ptr_factory_.GetWeakPtr()));
@@ -167,71 +165,33 @@
   }
   vsync_timebase_ = timebase;
   vsync_interval_ = interval;
-
-  if (gl_context_ && !gl_context_->is_lost())
-    ApplyViewportParameters();
+  ApplyViewportParameters();
 }
 
 void GpuRasterizer::ApplyViewportParameters() {
   DCHECK(have_viewport_parameters_);
-  DCHECK(gl_context_);
 
-  // TODO(jeffbrown): This shouldn't be hardcoded.
-  // Need to do some real tuning and possibly determine values adaptively.
-  int64_t update_phase = -vsync_interval_;
-  int64_t snapshot_phase = -vsync_interval_ / 6;
-  int64_t presentation_phase = vsync_interval_;
-  if (!scheduler_->Start(vsync_timebase_, vsync_interval_, update_phase,
-                         snapshot_phase, presentation_phase)) {
-    LOG(ERROR) << "Received invalid vsync parameters: timebase="
-               << vsync_timebase_ << ", interval=" << vsync_interval_;
-    PostErrorCallback();
+  if (gl_context_ && !gl_context_->is_lost()) {
+    ready_ = true;
+    callbacks_->OnRasterizerReady(vsync_timebase_, vsync_interval_);
   }
 }
 
-constexpr uint32_t kMaxFramesPending = 2;
-
-void GpuRasterizer::SubmitFrame(const scoped_refptr<RenderFrame>& frame,
-                                const FrameCallback& frame_callback) {
-  TRACE_EVENT0("gfx", "GpuRasterizer::SubmitFrame");
+void GpuRasterizer::DrawFrame(const scoped_refptr<RenderFrame>& frame) {
   DCHECK(frame);
-
-  if (frame_ && !frame_callback_.is_null())
-    frame_callback_.Run(false);  // frame discarded
-
-  frame_ = frame;
-  frame_callback_ = frame_callback;
-
-  if (gl_context_ && !gl_context_->is_lost()) {
-    if (frames_pending_ == kMaxFramesPending) {
-      TRACE_EVENT_INSTANT0("gfx", "GpuRasterizer dropping",
-                           TRACE_EVENT_SCOPE_THREAD);
-      LOG(ERROR) << "too many frames pending, dropping";
-      frame_callback_.Run(false);
-      return;
-    }
-    Draw();
-  } else
-    frame_callback_.Run(false);
-}
-
-static void DidEcho(void* context) {
-  TRACE_EVENT_ASYNC_END0("gfx", "SwapBuffers Echo", context);
-  auto cb = static_cast<base::Closure*>(context);
-  cb->Run();
-  delete cb;
-}
-
-void GpuRasterizer::Draw() {
-  TRACE_EVENT0("gfx", "GpuRasterizer::Draw");
+  DCHECK(ready_);
   DCHECK(gl_context_);
+  DCHECK(!gl_context_->is_lost());
   DCHECK(ganesh_context_);
-  DCHECK(frame_);
+
+  uint32_t frame_number = total_frames_++;
+  frames_in_progress_++;
+  TRACE_EVENT1("gfx", "GpuRasterizer::DrawFrame", "num", frame_number);
 
   mojo::GLContext::Scope gl_scope(gl_context_);
 
   // Update the viewport.
-  const SkIRect& viewport = frame_->viewport();
+  const SkIRect& viewport = frame->viewport();
   bool stale_surface = false;
   if (!ganesh_surface_ ||
       ganesh_surface_->surface()->width() != viewport.width() ||
@@ -241,7 +201,7 @@
     stale_surface = true;
   }
 
-  // Paint the frame.
+  // Draw the frame content.
   {
     mojo::skia::GaneshContext::Scope ganesh_scope(ganesh_context_);
 
@@ -250,38 +210,36 @@
           new mojo::skia::GaneshFramebufferSurface(ganesh_scope));
     }
 
-    frame_->Paint(ganesh_surface_->canvas());
+    frame->Draw(ganesh_surface_->canvas());
   }
 
-  // Swap buffers and listen for completion.
-  // TODO: Investigate using |MGLSignalSyncPoint| to wait for completion.
+  // Swap buffers.
   {
     TRACE_EVENT0("gfx", "MGLSwapBuffers");
     MGLSwapBuffers();
   }
-  base::Closure* echo_callback = new base::Closure(
-      base::Bind(&GpuRasterizer::DidEchoCallback,
-                 weak_ptr_factory_.GetWeakPtr(), frame_callback_));
-  frame_callback_.Reset();
-  TRACE_EVENT_ASYNC_BEGIN0("gfx", "SwapBuffers Echo", echo_callback);
-  MGLEcho(DidEcho, echo_callback);
-  frames_pending_++;
-  TRACE_COUNTER1("gfx", "GpuRasterizer::frames_pending_", frames_pending_);
+
+  // Listen for completion.
+  TRACE_EVENT_ASYNC_BEGIN0("gfx", "MGLEcho", frame_number);
+  MGLEcho(&GpuRasterizer::OnMGLEchoReply, this);
 }
 
-void GpuRasterizer::DidEchoCallback(FrameCallback frame_callback) {
-  frames_pending_--;
-  TRACE_COUNTER1("gfx", "GpuRasterizer::frames_pending_", frames_pending_);
-  TRACE_EVENT0("gfx", "GpuRasterizer::DidEchoCallback");
-  // Signal pending callback for backpressure.
-  if (!frame_callback.is_null()) {
-    frame_callback.Run(true);
-    frame_callback.Reset();
-  }
+void GpuRasterizer::DrawFinished(bool presented) {
+  DCHECK(frames_in_progress_);
+
+  uint32_t frame_number = total_frames_ - frames_in_progress_;
+  frames_in_progress_--;
+  TRACE_EVENT2("gfx", "GpuRasterizer::DrawFinished", "num", frame_number,
+               "presented", presented);
+  TRACE_EVENT_ASYNC_END0("gfx", "MGLEcho", frame_number);
+
+  callbacks_->OnRasterizerFinishedDraw(presented);
 }
 
-void GpuRasterizer::PostErrorCallback() {
-  task_runner_->PostTask(FROM_HERE, error_callback_);
+void GpuRasterizer::OnMGLEchoReply(void* context) {
+  auto rasterizer = static_cast<GpuRasterizer*>(context);
+  if (rasterizer->ready_)
+    rasterizer->DrawFinished(true /*presented*/);
 }
 
 }  // namespace compositor
diff --git a/services/gfx/compositor/backend/gpu_rasterizer.h b/services/gfx/compositor/backend/gpu_rasterizer.h
index 12d780c..303b86e 100644
--- a/services/gfx/compositor/backend/gpu_rasterizer.h
+++ b/services/gfx/compositor/backend/gpu_rasterizer.h
@@ -7,11 +7,9 @@
 
 #include <memory>
 
-#include "base/callback.h"
 #include "base/macros.h"
 #include "base/memory/ref_counted.h"
 #include "base/memory/weak_ptr.h"
-#include "base/task_runner.h"
 #include "base/timer/timer.h"
 #include "mojo/gpu/gl_context.h"
 #include "mojo/public/cpp/bindings/binding.h"
@@ -22,30 +20,48 @@
 namespace compositor {
 
 class RenderFrame;
-class VsyncScheduler;
 
-// Ganesh-based rasterizer which runs on a separate thread from the compositor.
-// Calls into this object, including its creation, must be posted to the
-// correct message loop by the output.
+// Ganesh-based rasterizer.
+// Maintains a GL context and draws frames on demand.
+//
+// This object runs on a separate thread from the rest of the compositor.
+// It is not threadsafe; all calls into this object, including its creation,
+// must run on the rasterizer thread.
 class GpuRasterizer : public mojo::ViewportParameterListener,
                       public mojo::GLContext::Observer {
  public:
-  // Callback invoked when a frame completes.
-  // |presented| is true if the frame was actually presented, false if
-  // the frame was discarded.
-  using FrameCallback = base::Callback<void(bool presented)>;
+  // Callbacks from the rasterizer.
+  // These calls always run on the rasterizer thread.
+  class Callbacks {
+   public:
+    virtual ~Callbacks() {}
+
+    // Called when the rasterizer is ready to start drawing.
+    // May be called repeatedly with new parameters.
+    virtual void OnRasterizerReady(int64_t vsync_timebase,
+                                   int64_t vsync_interval) = 0;
+
+    // Called when the rasterizer can't draw anymore.
+    virtual void OnRasterizerSuspended() = 0;
+
+    // Called when the rasterizer finished drawing a frame.
+    // |presented| is true if the frame was actually presented, false if
+    // the frame was discarded.
+    virtual void OnRasterizerFinishedDraw(bool presented) = 0;
+
+    // Called when an unrecoverable error occurs and the rasterizer needs
+    // to be shut down soon.
+    virtual void OnRasterizerError() = 0;
+  };
 
   GpuRasterizer(mojo::ContextProviderPtr context_provider,
-                const scoped_refptr<VsyncScheduler>& scheduler,
-                const scoped_refptr<base::TaskRunner>& task_runner,
-                const base::Closure& error_callback);
+                Callbacks* callbacks);
   ~GpuRasterizer() override;
 
-  // Submits a frame to be drawn.
-  // If the GL context isn't ready yet, the frame will be retained unless
-  // superceded by another frame.
-  void SubmitFrame(const scoped_refptr<RenderFrame>& frame,
-                   const FrameCallback& frame_callback);
+  // Draws the specified frame.
+  // Each frame will be acknowledged by a called to |OnRasterizerFinishedDraw|
+  // in the order submitted.  The rasterizer must be in a ready state.
+  void DrawFrame(const scoped_refptr<RenderFrame>& frame);
 
  private:
   // |ViewportParameterListener|:
@@ -63,29 +79,25 @@
   void OnViewportParameterTimeout();
   void ApplyViewportParameters();
 
-  void DidEchoCallback(FrameCallback frame_callback);
-  void Draw();
-
-  void PostErrorCallback();
+  void DrawFinished(bool presented);
+  static void OnMGLEchoReply(void* context);
 
   mojo::ContextProviderPtr context_provider_;
-  scoped_refptr<VsyncScheduler> scheduler_;
-  scoped_refptr<base::TaskRunner> task_runner_;
-  base::Closure error_callback_;
+  Callbacks* callbacks_;
 
   scoped_refptr<mojo::GLContext> gl_context_;
   scoped_refptr<mojo::skia::GaneshContext> ganesh_context_;
   std::unique_ptr<mojo::skia::GaneshFramebufferSurface> ganesh_surface_;
 
-  scoped_refptr<RenderFrame> frame_;
-  FrameCallback frame_callback_;
-
   mojo::Binding<ViewportParameterListener> viewport_parameter_listener_binding_;
   base::Timer viewport_parameter_timeout_;
   bool have_viewport_parameters_ = false;
   int64_t vsync_timebase_ = 0u;
   int64_t vsync_interval_ = 0u;
-  uint32_t frames_pending_ = 0u;
+
+  bool ready_ = false;
+  uint32_t total_frames_ = 0u;
+  uint32_t frames_in_progress_ = 0u;
 
   base::WeakPtrFactory<GpuRasterizer> weak_ptr_factory_;
 
diff --git a/services/gfx/compositor/compositor_engine.cc b/services/gfx/compositor/compositor_engine.cc
index 8ad5b0f..bc05f2a 100644
--- a/services/gfx/compositor/compositor_engine.cc
+++ b/services/gfx/compositor/compositor_engine.cc
@@ -11,6 +11,7 @@
 #include "base/bind.h"
 #include "base/bind_helpers.h"
 #include "base/time/time.h"
+#include "base/trace_event/trace_event.h"
 #include "mojo/services/gfx/composition/cpp/formatting.h"
 #include "mojo/skia/type_converters.h"
 #include "services/gfx/compositor/backend/gpu_output.h"
@@ -350,11 +351,29 @@
   return disposition;
 }
 
+void CompositorEngine::ComposeRenderer(
+    RendererState* renderer_state,
+    const mojo::gfx::composition::FrameInfo& frame_info) {
+  DCHECK(IsRendererStateRegisteredDebug(renderer_state));
+  DVLOG(2) << "ComposeRenderer: renderer_state=" << renderer_state;
+
+  TRACE_EVENT1("gfx", "CompositorEngine::ComposeRenderer", "renderer",
+               renderer_state->FormattedLabel());
+
+  int64_t composition_time = MojoGetTimeTicksNow();
+  PresentRenderer(renderer_state, frame_info.presentation_time);
+  SnapshotRenderer(renderer_state);
+  PaintRenderer(renderer_state, frame_info, composition_time);
+}
+
 void CompositorEngine::PresentRenderer(RendererState* renderer_state,
                                        int64_t presentation_time) {
   DCHECK(IsRendererStateRegisteredDebug(renderer_state));
   DVLOG(2) << "PresentRenderer: renderer_state=" << renderer_state;
 
+  TRACE_EVENT1("gfx", "CompositorEngine::PresentRenderer", "renderer",
+               renderer_state->FormattedLabel());
+
   // TODO(jeffbrown): Be more selective and do this work only for scenes
   // associated with the renderer that actually have pending updates.
   std::vector<SceneState*> dead_scenes;
@@ -369,12 +388,13 @@
     DestroyScene(scene_state);
 }
 
-void CompositorEngine::SnapshotRenderer(
-    RendererState* renderer_state,
-    const mojo::gfx::composition::FrameInfo& frame_info) {
+void CompositorEngine::SnapshotRenderer(RendererState* renderer_state) {
   DCHECK(IsRendererStateRegisteredDebug(renderer_state));
   DVLOG(2) << "SnapshotRenderer: renderer_state=" << renderer_state;
 
+  TRACE_EVENT1("gfx", "CompositorEngine::SnapshotRenderer", "renderer",
+               renderer_state->FormattedLabel());
+
   if (VLOG_IS_ON(2)) {
     std::ostringstream block_log;
     SnapshotRendererInner(renderer_state, &block_log);
@@ -391,19 +411,6 @@
   } else {
     SnapshotRendererInner(renderer_state, nullptr);
   }
-
-  if (renderer_state->visible_snapshot()) {
-    DCHECK(!renderer_state->visible_snapshot()->is_blocked());
-    renderer_state->output()->SubmitFrame(
-        renderer_state->visible_snapshot()->CreateFrame(
-            renderer_state->root_scene_viewport(), frame_info));
-  } else {
-    SkIRect viewport = renderer_state->root_scene_viewport().To<SkIRect>();
-    if (!viewport.isEmpty()) {
-      renderer_state->output()->SubmitFrame(
-          new RenderFrame(viewport, frame_info));
-    }
-  }
 }
 
 void CompositorEngine::SnapshotRendererInner(RendererState* renderer_state,
@@ -420,6 +427,34 @@
                               renderer_state->root_scene_version(), block_log));
 }
 
+void CompositorEngine::PaintRenderer(
+    RendererState* renderer_state,
+    const mojo::gfx::composition::FrameInfo& frame_info,
+    int64_t composition_time) {
+  DCHECK(IsRendererStateRegisteredDebug(renderer_state));
+  DVLOG(2) << "PaintRenderer: renderer_state=" << renderer_state;
+
+  TRACE_EVENT1("gfx", "CompositorEngine::PaintRenderer", "renderer",
+               renderer_state->FormattedLabel());
+
+  RenderFrame::Metadata frame_metadata(frame_info, composition_time);
+
+  if (renderer_state->visible_snapshot()) {
+    // The renderer has snapshotted content; paint and submit it.
+    DCHECK(!renderer_state->visible_snapshot()->is_blocked());
+    renderer_state->output()->SubmitFrame(
+        renderer_state->visible_snapshot()->Paint(
+            frame_metadata, renderer_state->root_scene_viewport()));
+  } else {
+    // The renderer does not have any content; submit an empty (black) frame.
+    SkIRect viewport = renderer_state->root_scene_viewport().To<SkIRect>();
+    if (!viewport.isEmpty()) {
+      renderer_state->output()->SubmitFrame(
+          new RenderFrame(frame_metadata, viewport));
+    }
+  }
+}
+
 void CompositorEngine::ScheduleFrameForRenderer(
     RendererState* renderer_state,
     Scheduler::SchedulingMode scheduling_mode) {
@@ -463,8 +498,7 @@
     return;
   DCHECK(IsRendererStateRegisteredDebug(renderer_state));
 
-  PresentRenderer(renderer_state, frame_info.presentation_time);
-  SnapshotRenderer(renderer_state, frame_info);
+  ComposeRenderer(renderer_state, frame_info);
 }
 
 void CompositorEngine::OnPresentScene(
diff --git a/services/gfx/compositor/compositor_engine.h b/services/gfx/compositor/compositor_engine.h
index 05c3e8d..e4abaad 100644
--- a/services/gfx/compositor/compositor_engine.h
+++ b/services/gfx/compositor/compositor_engine.h
@@ -89,12 +89,29 @@
   SceneDef::Disposition PresentScene(SceneState* scene_state,
                                      int64_t presentation_time);
 
+  // Starts the process of composing the contents of the renderer to
+  // produce a new frame.
+  void ComposeRenderer(RendererState* renderer_state,
+                       const mojo::gfx::composition::FrameInfo& frame_info);
+
+  // Applies and validates scene updates from all scenes which are included
+  // in the renderer's scene graph.
   void PresentRenderer(RendererState* renderer_state,
                        int64_t presentation_time);
-  void SnapshotRenderer(RendererState* renderer_state,
-                        const mojo::gfx::composition::FrameInfo& frame_info);
+
+  // Resolves scene dependencies and captures a snapshot of the current
+  // state of the renderer's scene graph.
+  void SnapshotRenderer(RendererState* renderer_state);
   void SnapshotRendererInner(RendererState* renderer_state,
                              std::ostream* block_log);
+
+  // Paints the renderer's current snapshot and submits a frame of content
+  // to the output for display.
+  void PaintRenderer(RendererState* renderer_state,
+                     const mojo::gfx::composition::FrameInfo& frame_info,
+                     int64_t composition_time);
+
+  // Schedules the next frame to be rendered, if needed.
   void ScheduleFrameForRenderer(RendererState* renderer_state,
                                 Scheduler::SchedulingMode scheduling_mode);
 
diff --git a/services/gfx/compositor/graph/nodes.cc b/services/gfx/compositor/graph/nodes.cc
index b25575c..1db819a 100644
--- a/services/gfx/compositor/graph/nodes.cc
+++ b/services/gfx/compositor/graph/nodes.cc
@@ -195,9 +195,9 @@
   }
 }
 
-void Node::RecordPicture(const SceneContent* content,
-                         const Snapshot* snapshot,
-                         SkCanvas* canvas) const {
+void Node::Paint(const SceneContent* content,
+                 const Snapshot* snapshot,
+                 SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -211,15 +211,15 @@
       canvas->clipRect(content_clip_->To<SkRect>());
   }
 
-  RecordPictureInner(content, snapshot, canvas);
+  PaintInner(content, snapshot, canvas);
 
   if (must_save)
     canvas->restore();
 }
 
-void Node::RecordPictureInner(const SceneContent* content,
-                              const Snapshot* snapshot,
-                              SkCanvas* canvas) const {
+void Node::PaintInner(const SceneContent* content,
+                      const Snapshot* snapshot,
+                      SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -227,7 +227,7 @@
   TraverseSnapshottedChildren(
       content, snapshot,
       [this, content, snapshot, canvas](const Node* child_node) -> bool {
-        child_node->RecordPicture(content, snapshot, canvas);
+        child_node->Paint(content, snapshot, canvas);
         return true;
       });
 }
@@ -340,9 +340,9 @@
 
 RectNode::~RectNode() {}
 
-void RectNode::RecordPictureInner(const SceneContent* content,
-                                  const Snapshot* snapshot,
-                                  SkCanvas* canvas) const {
+void RectNode::PaintInner(const SceneContent* content,
+                          const Snapshot* snapshot,
+                          SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -351,7 +351,7 @@
   paint.setColor(MakeSkColor(color_));
   canvas->drawRect(content_rect_.To<SkRect>(), paint);
 
-  Node::RecordPictureInner(content, snapshot, canvas);
+  Node::PaintInner(content, snapshot, canvas);
 }
 
 ImageNode::ImageNode(
@@ -386,9 +386,9 @@
                                   node_id());
 }
 
-void ImageNode::RecordPictureInner(const SceneContent* content,
-                                   const Snapshot* snapshot,
-                                   SkCanvas* canvas) const {
+void ImageNode::PaintInner(const SceneContent* content,
+                           const Snapshot* snapshot,
+                           SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -407,7 +407,7 @@
                                              image_resource->image()->height()),
                         content_rect_.To<SkRect>(), &paint);
 
-  Node::RecordPictureInner(content, snapshot, canvas);
+  Node::PaintInner(content, snapshot, canvas);
 }
 
 SceneNode::SceneNode(
@@ -451,9 +451,9 @@
   return Node::RecordSnapshot(content, builder);
 }
 
-void SceneNode::RecordPictureInner(const SceneContent* content,
-                                   const Snapshot* snapshot,
-                                   SkCanvas* canvas) const {
+void SceneNode::PaintInner(const SceneContent* content,
+                           const Snapshot* snapshot,
+                           SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -461,9 +461,9 @@
   const SceneContent* resolved_content =
       snapshot->GetResolvedSceneContent(this);
   DCHECK(resolved_content);
-  resolved_content->RecordPicture(snapshot, canvas);
+  resolved_content->Paint(snapshot, canvas);
 
-  Node::RecordPictureInner(content, snapshot, canvas);
+  Node::PaintInner(content, snapshot, canvas);
 }
 
 bool SceneNode::HitTestInner(
@@ -515,9 +515,9 @@
 
 LayerNode::~LayerNode() {}
 
-void LayerNode::RecordPictureInner(const SceneContent* content,
-                                   const Snapshot* snapshot,
-                                   SkCanvas* canvas) const {
+void LayerNode::PaintInner(const SceneContent* content,
+                           const Snapshot* snapshot,
+                           SkCanvas* canvas) const {
   DCHECK(content);
   DCHECK(snapshot);
   DCHECK(canvas);
@@ -526,7 +526,7 @@
   SetPaintForBlend(&paint, blend_.get());
 
   canvas->saveLayer(layer_rect_.To<SkRect>(), &paint);
-  Node::RecordPictureInner(content, snapshot, canvas);
+  Node::PaintInner(content, snapshot, canvas);
   canvas->restore();
 }
 
diff --git a/services/gfx/compositor/graph/nodes.h b/services/gfx/compositor/graph/nodes.h
index 27fba4c..74dd8e5 100644
--- a/services/gfx/compositor/graph/nodes.h
+++ b/services/gfx/compositor/graph/nodes.h
@@ -70,10 +70,10 @@
   virtual Snapshot::Disposition RecordSnapshot(const SceneContent* content,
                                                SnapshotBuilder* builder) const;
 
-  // Called to record drawing commands from a snapshot.
-  void RecordPicture(const SceneContent* content,
-                     const Snapshot* snapshot,
-                     SkCanvas* canvas) const;
+  // Paints the content of the node to a recording canvas.
+  void Paint(const SceneContent* content,
+             const Snapshot* snapshot,
+             SkCanvas* canvas) const;
 
   // Performs a hit test at the specified point.
   // The |point| is the hit tested point in the parent's coordinate space.
@@ -100,9 +100,9 @@
                                    const Snapshot* snapshot,
                                    const Func& func) const;
 
-  virtual void RecordPictureInner(const SceneContent* content,
-                                  const Snapshot* snapshot,
-                                  SkCanvas* canvas) const;
+  virtual void PaintInner(const SceneContent* content,
+                          const Snapshot* snapshot,
+                          SkCanvas* canvas) const;
 
   virtual bool HitTestInner(
       const SceneContent* content,
@@ -148,9 +148,9 @@
  protected:
   ~RectNode() override;
 
-  void RecordPictureInner(const SceneContent* content,
-                          const Snapshot* snapshot,
-                          SkCanvas* canvas) const override;
+  void PaintInner(const SceneContent* content,
+                  const Snapshot* snapshot,
+                  SkCanvas* canvas) const override;
 
  private:
   mojo::RectF const content_rect_;
@@ -185,9 +185,9 @@
  protected:
   ~ImageNode() override;
 
-  void RecordPictureInner(const SceneContent* content,
-                          const Snapshot* snapshot,
-                          SkCanvas* canvas) const override;
+  void PaintInner(const SceneContent* content,
+                  const Snapshot* snapshot,
+                  SkCanvas* canvas) const override;
 
  private:
   mojo::RectF const content_rect_;
@@ -223,9 +223,9 @@
  protected:
   ~SceneNode() override;
 
-  void RecordPictureInner(const SceneContent* content,
-                          const Snapshot* snapshot,
-                          SkCanvas* canvas) const override;
+  void PaintInner(const SceneContent* content,
+                  const Snapshot* snapshot,
+                  SkCanvas* canvas) const override;
 
   bool HitTestInner(
       const SceneContent* content,
@@ -261,9 +261,9 @@
  protected:
   ~LayerNode() override;
 
-  void RecordPictureInner(const SceneContent* content,
-                          const Snapshot* snapshot,
-                          SkCanvas* canvas) const override;
+  void PaintInner(const SceneContent* content,
+                  const Snapshot* snapshot,
+                  SkCanvas* canvas) const override;
 
  private:
   mojo::RectF const layer_rect_;
diff --git a/services/gfx/compositor/graph/scene_content.cc b/services/gfx/compositor/graph/scene_content.cc
index eba6cb9..69c4244 100644
--- a/services/gfx/compositor/graph/scene_content.cc
+++ b/services/gfx/compositor/graph/scene_content.cc
@@ -30,11 +30,10 @@
          version_ == mojo::gfx::composition::kSceneVersionNone;
 }
 
-void SceneContent::RecordPicture(const Snapshot* snapshot,
-                                 SkCanvas* canvas) const {
+void SceneContent::Paint(const Snapshot* snapshot, SkCanvas* canvas) const {
   const Node* root = GetRootNodeIfExists();
   if (root)
-    root->RecordPicture(this, snapshot, canvas);
+    root->Paint(this, snapshot, canvas);
 }
 
 bool SceneContent::HitTest(
diff --git a/services/gfx/compositor/graph/scene_content.h b/services/gfx/compositor/graph/scene_content.h
index c78f5ed..28ac609 100644
--- a/services/gfx/compositor/graph/scene_content.h
+++ b/services/gfx/compositor/graph/scene_content.h
@@ -59,8 +59,8 @@
   // Returns true if this content satisfies a request for the specified version.
   bool MatchesVersion(uint32_t requested_version) const;
 
-  // Called to record drawing commands from a snapshot.
-  void RecordPicture(const Snapshot* snapshot, SkCanvas* canvas) const;
+  // Paints the content of the scene to a recording canvas.
+  void Paint(const Snapshot* snapshot, SkCanvas* canvas) const;
 
   // Performs a hit test at the specified point.
   // The |scene_point| is the hit tested point in the scene's coordinate space.
diff --git a/services/gfx/compositor/graph/snapshot.cc b/services/gfx/compositor/graph/snapshot.cc
index 30eab2d..b8051bd 100644
--- a/services/gfx/compositor/graph/snapshot.cc
+++ b/services/gfx/compositor/graph/snapshot.cc
@@ -8,7 +8,6 @@
 #include "mojo/services/gfx/composition/cpp/formatting.h"
 #include "mojo/skia/type_converters.h"
 #include "services/gfx/compositor/graph/scene_content.h"
-#include "services/gfx/compositor/render/render_frame.h"
 #include "third_party/skia/include/core/SkPictureRecorder.h"
 #include "third_party/skia/include/core/SkRect.h"
 #include "third_party/skia/include/utils/SkMatrix44.h"
@@ -24,9 +23,9 @@
   return dependencies_.find(scene_token.value) != dependencies_.end();
 }
 
-scoped_refptr<RenderFrame> Snapshot::CreateFrame(
-    const mojo::Rect& viewport,
-    const mojo::gfx::composition::FrameInfo& frame_info) const {
+scoped_refptr<RenderFrame> Snapshot::Paint(
+    const RenderFrame::Metadata& metadata,
+    const mojo::Rect& viewport) const {
   DCHECK(!is_blocked());
   DCHECK(root_scene_content_);
 
@@ -34,8 +33,8 @@
 
   SkPictureRecorder recorder;
   recorder.beginRecording(SkRect::Make(sk_viewport));
-  root_scene_content_->RecordPicture(this, recorder.getRecordingCanvas());
-  return new RenderFrame(sk_viewport, frame_info,
+  root_scene_content_->Paint(this, recorder.getRecordingCanvas());
+  return new RenderFrame(metadata, sk_viewport,
                          skia::AdoptRef(recorder.endRecordingAsPicture()));
 }
 
diff --git a/services/gfx/compositor/graph/snapshot.h b/services/gfx/compositor/graph/snapshot.h
index 7f3043d..8012fd2 100644
--- a/services/gfx/compositor/graph/snapshot.h
+++ b/services/gfx/compositor/graph/snapshot.h
@@ -14,13 +14,13 @@
 #include "mojo/services/geometry/interfaces/geometry.mojom.h"
 #include "mojo/services/gfx/composition/interfaces/hit_tests.mojom.h"
 #include "mojo/services/gfx/composition/interfaces/scheduling.mojom.h"
+#include "services/gfx/compositor/render/render_frame.h"
 
 namespace compositor {
 
 class Node;
 class SceneContent;
 class SceneNode;
-class RenderFrame;
 
 // Describes a single frame snapshot of the scene graph, sufficient for
 // rendering and hit testing.  When the snapshot is made, all predicated and
@@ -63,11 +63,10 @@
   bool HasDependency(
       const mojo::gfx::composition::SceneToken& scene_token) const;
 
-  // Creates a frame for rendering.
+  // Paints the content of the snapshot to produce a frame to be rendered.
   // Only valid if |!is_blocked()|.
-  scoped_refptr<RenderFrame> CreateFrame(
-      const mojo::Rect& viewport,
-      const mojo::gfx::composition::FrameInfo& frame_info) const;
+  scoped_refptr<RenderFrame> Paint(const RenderFrame::Metadata& metadata,
+                                   const mojo::Rect& viewport) const;
 
   // Performs a hit test at the specified point, populating the result.
   // Only valid if |!is_blocked()|.
diff --git a/services/gfx/compositor/render/render_frame.cc b/services/gfx/compositor/render/render_frame.cc
index 6fedcea..d39753b 100644
--- a/services/gfx/compositor/render/render_frame.cc
+++ b/services/gfx/compositor/render/render_frame.cc
@@ -5,29 +5,30 @@
 #include "services/gfx/compositor/render/render_frame.h"
 
 #include "base/logging.h"
+#include "base/trace_event/trace_event.h"
 #include "skia/ext/refptr.h"
 #include "third_party/skia/include/core/SkCanvas.h"
 #include "third_party/skia/include/core/SkPicture.h"
 
 namespace compositor {
 
-RenderFrame::RenderFrame(const SkIRect& viewport,
-                         const mojo::gfx::composition::FrameInfo& frame_info)
-    : viewport_(viewport), frame_info_(frame_info) {
+RenderFrame::RenderFrame(const Metadata& metadata, const SkIRect& viewport)
+    : metadata_(metadata), viewport_(viewport) {
   DCHECK(!viewport_.isEmpty());
 }
 
-RenderFrame::RenderFrame(const SkIRect& viewport,
-                         const mojo::gfx::composition::FrameInfo& frame_info,
+RenderFrame::RenderFrame(const Metadata& metadata,
+                         const SkIRect& viewport,
                          const skia::RefPtr<SkPicture>& picture)
-    : viewport_(viewport), frame_info_(frame_info), picture_(picture) {
+    : metadata_(metadata), viewport_(viewport), picture_(picture) {
   DCHECK(!viewport_.isEmpty());
 }
 
 RenderFrame::~RenderFrame() {}
 
-void RenderFrame::Paint(SkCanvas* canvas) const {
+void RenderFrame::Draw(SkCanvas* canvas) const {
   DCHECK(canvas);
+  TRACE_EVENT0("gfx", "RenderFrame::Draw");
 
   // TODO: Consider using GrDrawContext instead of SkCanvas.
   canvas->clear(SK_ColorBLACK);
@@ -35,4 +36,11 @@
     canvas->drawPicture(picture_.get());
 }
 
+RenderFrame::Metadata::Metadata(
+    const mojo::gfx::composition::FrameInfo& frame_info,
+    int64_t composition_time)
+    : frame_info_(frame_info), composition_time_(composition_time) {}
+
+RenderFrame::Metadata::~Metadata() {}
+
 }  // namespace compositor
diff --git a/services/gfx/compositor/render/render_frame.h b/services/gfx/compositor/render/render_frame.h
index 334e690..f468fb9 100644
--- a/services/gfx/compositor/render/render_frame.h
+++ b/services/gfx/compositor/render/render_frame.h
@@ -22,28 +22,43 @@
 // They have no direct references to the scene graph.
 class RenderFrame : public base::RefCountedThreadSafe<RenderFrame> {
  public:
+  // Contains metadata about a particular |RenderFrame| used for tracing
+  // and statistics.
+  class Metadata {
+   public:
+    Metadata(const mojo::gfx::composition::FrameInfo& frame_info,
+             int64_t composition_time);
+    ~Metadata();
+
+    const mojo::gfx::composition::FrameInfo& frame_info() const {
+      return frame_info_;
+    }
+    int64_t composition_time() const { return composition_time_; }
+
+   private:
+    mojo::gfx::composition::FrameInfo frame_info_;
+    int64_t composition_time_;
+  };
+
   // Creates an empty render frame with no content.
-  RenderFrame(const SkIRect& viewport,
-              const mojo::gfx::composition::FrameInfo& frame_info);
+  RenderFrame(const Metadata& metadata, const SkIRect& viewport);
 
   // Creates render frame backed by a picture.
-  RenderFrame(const SkIRect& viewport,
-              const mojo::gfx::composition::FrameInfo& frame_info,
+  RenderFrame(const Metadata& metadata,
+              const SkIRect& viewport,
               const skia::RefPtr<SkPicture>& picture);
 
+  // Gets metadata about the frame.
+  const Metadata& metadata() const { return metadata_; }
+
   // Gets the frame's viewport in pixels.
   const SkIRect& viewport() const { return viewport_; }
 
-  // Gets information about the frame to be rendered.
-  const mojo::gfx::composition::FrameInfo& frame_info() const {
-    return frame_info_;
-  }
-
   // Gets the underlying picture to rasterize, or null if the frame is empty.
   const skia::RefPtr<SkPicture>& picture() const { return picture_; }
 
-  // Paints the frame to a canvas.
-  void Paint(SkCanvas* canvas) const;
+  // Draws the contents of the frame to a canvas.
+  void Draw(SkCanvas* canvas) const;
 
  private:
   friend class base::RefCountedThreadSafe<RenderFrame>;
@@ -51,8 +66,8 @@
 
   ~RenderFrame();
 
+  Metadata metadata_;
   SkIRect viewport_;
-  mojo::gfx::composition::FrameInfo frame_info_;
   skia::RefPtr<SkPicture> picture_;
 
   DISALLOW_COPY_AND_ASSIGN(RenderFrame);