[scenic] Isolate FrameScheduler

This CL:
* Collect scheduling logic in FrameScheduler
* Decouple Session updates from calls to RenderFrame()
* Simplify program flow from Present() to RenderFrame():
     Previously: Client Session -> SessionHandler -> gfx::Session
              -> SessionManager -> Engine -> FrameScheduler -> Engine
              -> SessionManager -> gfx::Session

     Now: Client Session -> SessionHandler -> gfx::Session
          -> FrameScheduler -> Engine -> gfx::Session

Change-Id: I3000b19fe7836a34595354eea57fe34744fa45eb
diff --git a/garnet/lib/ui/gfx/BUILD.gn b/garnet/lib/ui/gfx/BUILD.gn
index aea4a5c..67837d5 100644
--- a/garnet/lib/ui/gfx/BUILD.gn
+++ b/garnet/lib/ui/gfx/BUILD.gn
@@ -67,13 +67,14 @@
     "displays/display_manager.h",
     "displays/display_watcher.cc",
     "displays/display_watcher.h",
+    "engine/default_frame_scheduler.cc",
+    "engine/default_frame_scheduler.h",
     "engine/engine.cc",
     "engine/engine.h",
     "engine/engine_renderer.cc",
     "engine/engine_renderer.h",
     "engine/engine_renderer_visitor.cc",
     "engine/engine_renderer_visitor.h",
-    "engine/frame_scheduler.cc",
     "engine/frame_scheduler.h",
     "engine/frame_timings.cc",
     "engine/frame_timings.h",
@@ -98,7 +99,6 @@
     "engine/session_manager.h",
     "engine/unresolved_imports.cc",
     "engine/unresolved_imports.h",
-    "engine/update_scheduler.h",
     "gfx_system.cc",
     "gfx_system.h",
     "id.cc",
diff --git a/garnet/lib/ui/gfx/displays/display.h b/garnet/lib/ui/gfx/displays/display.h
index e3072176..511bf83 100644
--- a/garnet/lib/ui/gfx/displays/display.h
+++ b/garnet/lib/ui/gfx/displays/display.h
@@ -47,7 +47,7 @@
   // Temporary friendship to allow FrameScheduler to feed back the Vsync timings
   // gleaned from EventTimestamper.  This should go away once we receive real
   // VSync times from the display driver.
-  friend class FrameScheduler;
+  friend class DefaultFrameScheduler;
   void set_last_vsync_time(zx_time_t vsync_time);
 
   zx_time_t last_vsync_time_;
diff --git a/garnet/lib/ui/gfx/engine/frame_scheduler.cc b/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
similarity index 79%
rename from garnet/lib/ui/gfx/engine/frame_scheduler.cc
rename to garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
index 9631f77f..907b0e7 100644
--- a/garnet/lib/ui/gfx/engine/frame_scheduler.cc
+++ b/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
@@ -1,8 +1,8 @@
-// Copyright 2017 The Fuchsia Authors. All rights reserved.
+// Copyright 2019 The Fuchsia 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 "garnet/lib/ui/gfx/engine/frame_scheduler.h"
+#include "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
 
 #include <lib/async/cpp/task.h>
 #include <lib/async/default.h>
@@ -17,16 +17,16 @@
 namespace scenic_impl {
 namespace gfx {
 
-FrameScheduler::FrameScheduler(Display* display)
+DefaultFrameScheduler::DefaultFrameScheduler(Display* display)
     : dispatcher_(async_get_default_dispatcher()),
       display_(display),
       weak_factory_(this) {
   outstanding_frames_.reserve(kMaxOutstandingFrames);
 }
 
-FrameScheduler::~FrameScheduler() {}
+DefaultFrameScheduler::~DefaultFrameScheduler() {}
 
-void FrameScheduler::RequestFrame(zx_time_t presentation_time) {
+void DefaultFrameScheduler::RequestFrame(zx_time_t presentation_time) {
   const bool should_schedule_frame =
       requested_presentation_times_.empty() ||
       requested_presentation_times_.top() > presentation_time;
@@ -36,14 +36,14 @@
   }
 }
 
-void FrameScheduler::SetRenderContinuously(bool render_continuously) {
+void DefaultFrameScheduler::SetRenderContinuously(bool render_continuously) {
   render_continuously_ = render_continuously;
   if (render_continuously_) {
     RequestFrame(0);
   }
 }
 
-zx_time_t FrameScheduler::PredictRequiredFrameRenderTime() const {
+zx_time_t DefaultFrameScheduler::PredictRequiredFrameRenderTime() const {
   // TODO(MZ-400): more sophisticated prediction.  This might require more info,
   // e.g. about how many compositors will be rendering scenes, at what
   // resolutions, etc.
@@ -52,14 +52,14 @@
 }
 
 std::pair<zx_time_t, zx_time_t>
-FrameScheduler::ComputeNextPresentationAndWakeupTimes() const {
+DefaultFrameScheduler::ComputeNextPresentationAndWakeupTimes() const {
   FXL_DCHECK(!requested_presentation_times_.empty());
   return ComputeTargetPresentationAndWakeupTimes(
       requested_presentation_times_.top());
 }
 
 std::pair<zx_time_t, zx_time_t>
-FrameScheduler::ComputeTargetPresentationAndWakeupTimes(
+DefaultFrameScheduler::ComputeTargetPresentationAndWakeupTimes(
     const zx_time_t requested_presentation_time) const {
   const zx_time_t last_vsync_time = display_->GetLastVsyncTime();
   const zx_time_t vsync_interval = display_->GetVsyncInterval();
@@ -140,7 +140,7 @@
 #endif
 }
 
-void FrameScheduler::ScheduleFrame() {
+void DefaultFrameScheduler::ScheduleFrame() {
   FXL_DCHECK(!requested_presentation_times_.empty());
 
   auto times = ComputeNextPresentationAndWakeupTimes();
@@ -156,8 +156,8 @@
       zx::time(0) + zx::nsec(wakeup_time));
 }
 
-void FrameScheduler::MaybeRenderFrame(zx_time_t presentation_time,
-                                      zx_time_t wakeup_time) {
+void DefaultFrameScheduler::MaybeRenderFrame(zx_time_t presentation_time,
+                                             zx_time_t wakeup_time) {
   if (requested_presentation_times_.empty()) {
     // No frame was requested, so none needs to be rendered.  More precisely, a
     // frame must have been requested (otherwise ScheduleFrame() would not
@@ -169,8 +169,9 @@
   if (TooMuchBackPressure()) {
     // No need to request another frame; ScheduleFrame() will be called
     // when the back-pressure is relieved.
-    FXL_VLOG(2) << "FrameScheduler::MaybeRenderFrame(): dropping frame, too "
-                   "much back-pressure.";
+    FXL_VLOG(2)
+        << "DefaultFrameScheduler::MaybeRenderFrame(): dropping frame, too "
+           "much back-pressure.";
     return;
   }
 
@@ -193,14 +194,24 @@
     requested_presentation_times_.pop();
   }
 
-  // Go render the frame.
-  if (delegate_) {
+  if (delegate_.frame_renderer && delegate_.session_updater) {
     FXL_DCHECK(outstanding_frames_.size() < kMaxOutstandingFrames);
+
     auto frame_timings = fxl::MakeRefCounted<FrameTimings>(
         this, ++frame_number_, presentation_time);
-    if (delegate_->RenderFrame(frame_timings, presentation_time,
-                               display_->GetVsyncInterval(),
-                               render_continuously_)) {
+
+    // Apply all updates
+    bool any_updates_were_applied = ApplyScheduledSessionUpdates(
+        frame_timings->frame_number(), presentation_time,
+        display_->GetVsyncInterval());
+
+    if (!any_updates_were_applied && !render_continuously_) {
+      return;
+    }
+
+    // Render the frame
+    if (delegate_.frame_renderer->RenderFrame(frame_timings, presentation_time,
+                                              display_->GetVsyncInterval())) {
       outstanding_frames_.push_back(frame_timings);
     }
   }
@@ -211,7 +222,40 @@
   }
 }
 
-void FrameScheduler::OnFramePresented(FrameTimings* timings) {
+void DefaultFrameScheduler::ScheduleUpdateForSession(
+    uint64_t presentation_time, scenic_impl::SessionId session_id) {
+  updatable_sessions_.push({presentation_time, session_id});
+  RequestFrame(presentation_time);
+}
+
+bool DefaultFrameScheduler::ApplyScheduledSessionUpdates(
+    uint64_t frame_number, uint64_t presentation_time,
+    uint64_t presentation_interval) {
+  FXL_DCHECK(delegate_.session_updater);
+
+  TRACE_DURATION("gfx", "ApplyScheduledSessionUpdates", "time",
+                 presentation_time, "interval", presentation_interval);
+
+  std::vector<scenic_impl::SessionId> sessions_to_update;
+  while (!updatable_sessions_.empty()) {
+    auto& top = updatable_sessions_.top();
+
+    if (top.first > presentation_time) {
+      break;
+    }
+
+    sessions_to_update.push_back(std::move(top.second));
+    updatable_sessions_.pop();
+  }
+
+  bool needs_render = delegate_.session_updater->UpdateSessions(
+      std::move(sessions_to_update), frame_number, presentation_time,
+      presentation_interval);
+
+  return needs_render;
+}
+
+void DefaultFrameScheduler::OnFramePresented(FrameTimings* timings) {
   FXL_DCHECK(!outstanding_frames_.empty());
   // TODO(MZ-400): how should we handle this case?  It is theoretically
   // possible, but if if it happens then it means that the EventTimestamper is
@@ -267,7 +311,7 @@
   }
 }
 
-bool FrameScheduler::TooMuchBackPressure() {
+bool DefaultFrameScheduler::TooMuchBackPressure() {
   if (outstanding_frames_.size() >= kMaxOutstandingFrames) {
     back_pressure_applied_ = true;
     return true;
diff --git a/garnet/lib/ui/gfx/engine/default_frame_scheduler.h b/garnet/lib/ui/gfx/engine/default_frame_scheduler.h
new file mode 100644
index 0000000..3eb0ba0
--- /dev/null
+++ b/garnet/lib/ui/gfx/engine/default_frame_scheduler.h
@@ -0,0 +1,148 @@
+// Copyright 2019 The Fuchsia 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 GARNET_LIB_UI_GFX_ENGINE_DEFAULT_FRAME_SCHEDULER_H_
+#define GARNET_LIB_UI_GFX_ENGINE_DEFAULT_FRAME_SCHEDULER_H_
+
+#include <queue>
+
+#include <lib/async/dispatcher.h>
+#include <lib/zx/time.h>
+#include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
+#include "garnet/lib/ui/gfx/id.h"
+#include "lib/fxl/macros.h"
+#include "lib/fxl/memory/ref_ptr.h"
+#include "lib/fxl/memory/weak_ptr.h"
+
+namespace scenic_impl {
+namespace gfx {
+
+class DefaultFrameScheduler : public FrameScheduler {
+ public:
+  explicit DefaultFrameScheduler(Display* display);
+  ~DefaultFrameScheduler();
+
+  // |FrameScheduler|
+  //
+  // Helper method for ScheduleFrame().  Returns the target presentation time
+  // for the requested presentation time, and a wake-up time that is early
+  // enough to start rendering in order to hit the target presentation time.
+  std::pair<zx_time_t, zx_time_t> ComputeTargetPresentationAndWakeupTimes(
+      zx_time_t requested_presentation_time) const override;
+
+  // |FrameScheduler|
+  void SetDelegate(FrameSchedulerDelegate delegate) override {
+    delegate_ = delegate;
+  };
+
+  // |FrameScheduler|
+  //
+  // If |render_continuously|, we keep rendering frames regardless of whether
+  // they're requested using RequestFrame().
+  void SetRenderContinuously(bool render_continuously) override;
+
+  // |FrameScheduler|
+  //
+  // Tell the FrameScheduler to schedule a frame. This is also used for
+  // updates triggered by something other than a Session update i.e. an
+  // ImagePipe with a new Image to present.
+  void ScheduleUpdateForSession(uint64_t presentation_time,
+                                scenic_impl::SessionId session) override;
+
+ protected:
+  // |FrameScheduler|
+  void OnFramePresented(FrameTimings* timings) override;
+
+ private:
+  // Used to compare presentation times so that the priority_queue acts as a min
+  // heap, placing the earliest PresentationTime at the top
+  class UpdatableSessionsComparator {
+   public:
+    bool operator()(
+        std::pair<PresentationTime, scenic_impl::SessionId> updatable_session1,
+        std::pair<PresentationTime, scenic_impl::SessionId>
+            updatable_session2) {
+      return updatable_session1.first > updatable_session2.first;
+    }
+  };
+
+  // Request a frame to be scheduled at or after |presentation_time|, which
+  // may be in the past.
+  void RequestFrame(zx_time_t presentation_time);
+
+  // Update the global scene and then draw it... maybe.  There are multiple
+  // reasons why this might not happen.  For example, the swapchain might apply
+  // back-pressure if we can't hit our target frame rate.  Or, after this frame
+  // was scheduled, another frame was scheduled to be rendered at an earlier
+  // time, and not enough time has elapsed to render this frame.  Etc.
+  void MaybeRenderFrame(zx_time_t presentation_time, zx_time_t wakeup_time);
+
+  // Schedule a frame for the earliest of |requested_presentation_times_|.  The
+  // scheduled time will be the earliest achievable time, such that rendering
+  // can start early enough to hit the next Vsync.
+  void ScheduleFrame();
+
+  // Returns true to apply back-pressure when we cannot hit our target frame
+  // rate.  Otherwise, return false to indicate that it is OK to immediately
+  // render a frame.
+  // TODO(MZ-225): We need to track backpressure so that the frame scheduler
+  // doesn't get too far ahead. With that in mind, Renderer::DrawFrame should
+  // have a callback which is invoked when the frame is fully flushed through
+  // the graphics pipeline. Then Engine::RenderFrame itself should have a
+  // callback which is invoked when all renderers finish work for that frame.
+  // Then FrameScheduler should listen to the callback to count how many
+  bool TooMuchBackPressure();
+
+  // Helper method for ScheduleFrame().  Returns the target presentation time
+  // for the next frame, and a wake-up time that is early enough to start
+  // rendering in order to hit the target presentation time.
+  std::pair<zx_time_t, zx_time_t> ComputeNextPresentationAndWakeupTimes() const;
+
+  // Return the predicted amount of time required to render a frame.
+  zx_time_t PredictRequiredFrameRenderTime() const;
+
+  // Executes updates that are scheduled up to and including a given
+  // presentation time. Returns true if rendering is needed.
+  bool ApplyScheduledSessionUpdates(uint64_t frame_number,
+                                    uint64_t presentation_time,
+                                    uint64_t presentation_interval);
+
+  // Apply updates to all sessions who have updates and have acquired all
+  // fences.  Return true if there were any updates applied.
+  bool UpdateSessions(uint64_t presentation_time,
+                      uint64_t presentation_interval,
+                      uint64_t frame_number_for_tracing);
+
+  async_dispatcher_t* const dispatcher_;
+  Display* const display_;
+
+  std::priority_queue<zx_time_t, std::vector<zx_time_t>,
+                      std::greater<zx_time_t>>
+      requested_presentation_times_;
+
+  uint64_t frame_number_ = 0;
+  constexpr static size_t kMaxOutstandingFrames = 2;
+  std::vector<FrameTimingsPtr> outstanding_frames_;
+  bool back_pressure_applied_ = false;
+  bool render_continuously_ = false;
+
+  // Lists all Session that have updates to apply, sorted by the earliest
+  // requested presentation time of each update.
+  std::priority_queue<
+      std::pair<PresentationTime, scenic_impl::SessionId>,
+      std::vector<std::pair<PresentationTime, scenic_impl::SessionId>>,
+      UpdatableSessionsComparator>
+      updatable_sessions_;
+
+  FrameSchedulerDelegate delegate_;
+
+  fxl::WeakPtrFactory<DefaultFrameScheduler> weak_factory_;  // must be last
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(DefaultFrameScheduler);
+};
+
+}  // namespace gfx
+}  // namespace scenic_impl
+
+#endif  // GARNET_LIB_UI_GFX_ENGINE_DEFAULT_FRAME_SCHEDULER_H_
\ No newline at end of file
diff --git a/garnet/lib/ui/gfx/engine/engine.cc b/garnet/lib/ui/gfx/engine/engine.cc
index 2a4c14f..65e3321 100644
--- a/garnet/lib/ui/gfx/engine/engine.cc
+++ b/garnet/lib/ui/gfx/engine/engine.cc
@@ -13,7 +13,6 @@
 #include <trace/event.h>
 #include <zx/time.h>
 
-#include "garnet/lib/ui/gfx/engine/engine_renderer.h"
 #include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
 #include "garnet/lib/ui/gfx/engine/frame_timings.h"
 #include "garnet/lib/ui/gfx/engine/hardware_layer_assignment.h"
@@ -29,6 +28,19 @@
 namespace scenic_impl {
 namespace gfx {
 
+CommandContext::CommandContext(
+    std::unique_ptr<escher::BatchGpuUploader> uploader)
+    : batch_gpu_uploader_(std::move(uploader)) {}
+
+void CommandContext::Flush() {
+  if (batch_gpu_uploader_) {
+    // Submit regardless of whether or not there are updates to release the
+    // underlying CommandBuffer so the pool and sequencer don't stall out.
+    // TODO(ES-115) to remove this restriction.
+    batch_gpu_uploader_->Submit();
+  }
+}
+
 Engine::Engine(std::unique_ptr<FrameScheduler> frame_scheduler,
                DisplayManager* display_manager,
                escher::EscherWeakPtr weak_escher)
@@ -47,13 +59,11 @@
           escher()->vk_physical_device(), escher()->vk_device())),
       has_vulkan_(escher_ && escher_->vk_device()),
       weak_factory_(this) {
+  FXL_DCHECK(frame_scheduler_);
   FXL_DCHECK(display_manager_);
   FXL_DCHECK(escher_);
 
-  // TODO(SCN-1092): make |frame_scheduler_| non-nullable.  For testing, this
-  // might entail plugging in a dummy Display.  Relates to SCN-452.
-  if (frame_scheduler_)
-    frame_scheduler_->set_delegate(this);
+  InitializeFrameScheduler();
 }
 
 Engine::Engine(
@@ -73,49 +83,16 @@
                   : 0),
       has_vulkan_(escher_ && escher_->vk_device()),
       weak_factory_(this) {
+  FXL_DCHECK(frame_scheduler_);
   FXL_DCHECK(display_manager_);
 
-  // TODO(SCN-1092): make |frame_scheduler_| non-nullable.  For testing, this
-  // might entail plugging in a dummy Display.  Relates to SCN-452.
-  if (frame_scheduler_)
-    frame_scheduler_->set_delegate(this);
+  InitializeFrameScheduler();
 }
 
-Engine::~Engine() = default;
-
-void Engine::ScheduleUpdate(uint64_t presentation_time) {
-  // TODO(SCN-1092): make |frame_scheduler_| non-nullable.  This is feasible now
-  // that we can use TestLoopFixture::RunLoopFor() to cause the scheduler to
-  // render.
-  if (frame_scheduler_) {
-    frame_scheduler_->RequestFrame(presentation_time);
-  } else {
-    // Apply update immediately.  This is done for tests.
-    FXL_LOG(WARNING)
-        << "No FrameScheduler available; applying update immediately";
-    RenderFrame(FrameTimingsPtr(), presentation_time, 0, false);
-  }
-}
-
-CommandContext InitializeCommandContext(bool has_vulkan,
-                                        escher::EscherWeakPtr escher,
-                                        uint64_t frame_number_for_tracing) {
-  return CommandContext(has_vulkan ? escher::BatchGpuUploader::New(
-                                         escher, frame_number_for_tracing)
-                                   : nullptr);
-}
-
-bool Engine::UpdateSessions(uint64_t presentation_time,
-                            uint64_t presentation_interval,
-                            uint64_t frame_number_for_tracing) {
-  CommandContext command_context =
-      InitializeCommandContext(has_vulkan(), escher_, frame_number_for_tracing);
-  bool any_updates_were_applied =
-      session_manager_->ApplyScheduledSessionUpdates(
-          &command_context, presentation_time, presentation_interval);
-  command_context.Flush();
-
-  return any_updates_were_applied;
+void Engine::InitializeFrameScheduler() {
+  auto weak = weak_factory_.GetWeakPtr();
+  frame_scheduler_->SetDelegate(FrameSchedulerDelegate{
+      /* FrameRenderer */ weak, /* SessionUpdater */ weak});
 }
 
 // Helper for RenderFrame().  Generate a mapping between a Compositor's Layer
@@ -141,9 +118,53 @@
   };
 }
 
+CommandContext Engine::CreateCommandContext(uint64_t frame_number_for_tracing) {
+  return CommandContext(has_vulkan() ? escher::BatchGpuUploader::New(
+                                           escher_, frame_number_for_tracing)
+                                     : nullptr);
+}
+
+// Applies scheduled updates to a session. If the update fails, the session is
+// killed. Returns true if a new render is needed, false otherwise.
+bool Engine::UpdateSessions(std::vector<SessionId> sessions,
+                            uint64_t frame_number, uint64_t presentation_time,
+                            uint64_t presentation_interval) {
+  auto command_context = CreateCommandContext(frame_number);
+
+  bool needs_render = false;
+  for (auto session_id : sessions) {
+    auto session_handler = session_manager_->FindSessionHandler(session_id);
+    if (!session_handler) {
+      // This means the session that requested the update died after the
+      // request. Requiring the scene to be re-rendered to reflect the session's
+      // disappearance is probably desirable. ImagePipe also relies on this to
+      // be true, since it calls ScheduleUpdate() in it's destructor.
+      needs_render = true;
+      continue;
+    }
+
+    auto session = session_handler->session();
+
+    auto update_results = session->ApplyScheduledUpdates(
+        &command_context, presentation_time, presentation_interval);
+
+    // If update fails, kill the entire client session.
+    if (!update_results.success) {
+      session_manager_->KillSession(session->id());
+    }
+
+    needs_render |= update_results.needs_render;
+  }
+
+  // Flush work to the gpu
+  command_context.Flush();
+
+  return needs_render;
+}
+
 bool Engine::RenderFrame(const FrameTimingsPtr& timings,
                          uint64_t presentation_time,
-                         uint64_t presentation_interval, bool force_render) {
+                         uint64_t presentation_interval) {
   // NOTE: this name is important for benchmarking.  Do not remove or modify it
   // without also updating the "process_scenic_trace.go" script.
   TRACE_DURATION("gfx", "RenderFrame", "frame_number", timings->frame_number(),
@@ -153,13 +174,6 @@
   // timings->frame_number() below.  When this is done, uncomment the following
   // line:
   // FXL_DCHECK(timings);
-
-  // TODO(SCN-1108): consider applying updates as each fence is signalled.
-  if (!UpdateSessions(presentation_time, presentation_interval,
-                      timings ? timings->frame_number() : 0) &&
-      !force_render) {
-    return false;
-  }
   UpdateAndDeliverMetrics(presentation_time);
 
   // Some updates were applied; we interpret this to mean that the scene may
@@ -248,7 +262,7 @@
 
   // TODO(MZ-216): Traversing the whole graph just to compute this is pretty
   // inefficient.  We should optimize this.
-  ::fuchsia::ui::gfx::Metrics metrics;
+  fuchsia::ui::gfx::Metrics metrics;
   metrics.scale_x = 1.f;
   metrics.scale_y = 1.f;
   metrics.scale_z = 1.f;
@@ -263,7 +277,7 @@
   // have some kind of backpointer from a session to its handler.
   for (auto node : updated_nodes) {
     if (node->session()) {
-      auto event = ::fuchsia::ui::gfx::Event();
+      fuchsia::ui::gfx::Event event;
       event.set_metrics(::fuchsia::ui::gfx::MetricsEvent());
       event.metrics().node_id = node->id();
       event.metrics().metrics = node->reported_metrics();
@@ -273,21 +287,21 @@
 }
 
 // TODO(mikejurka): move this to appropriate util file
-bool MetricsEquals(const ::fuchsia::ui::gfx::Metrics& a,
-                   const ::fuchsia::ui::gfx::Metrics& b) {
+bool MetricsEquals(const fuchsia::ui::gfx::Metrics& a,
+                   const fuchsia::ui::gfx::Metrics& b) {
   return a.scale_x == b.scale_x && a.scale_y == b.scale_y &&
          a.scale_z == b.scale_z;
 }
 
 void Engine::UpdateMetrics(Node* node,
-                           const ::fuchsia::ui::gfx::Metrics& parent_metrics,
+                           const fuchsia::ui::gfx::Metrics& parent_metrics,
                            std::vector<Node*>* updated_nodes) {
-  ::fuchsia::ui::gfx::Metrics local_metrics;
+  fuchsia::ui::gfx::Metrics local_metrics;
   local_metrics.scale_x = parent_metrics.scale_x * node->scale().x;
   local_metrics.scale_y = parent_metrics.scale_y * node->scale().y;
   local_metrics.scale_z = parent_metrics.scale_z * node->scale().z;
 
-  if ((node->event_mask() & ::fuchsia::ui::gfx::kMetricsEventMask) &&
+  if ((node->event_mask() & fuchsia::ui::gfx::kMetricsEventMask) &&
       !MetricsEquals(node->reported_metrics(), local_metrics)) {
     node->set_reported_metrics(local_metrics);
     updated_nodes->push_back(node);
diff --git a/garnet/lib/ui/gfx/engine/engine.h b/garnet/lib/ui/gfx/engine/engine.h
index d91a314..1e10fbd 100644
--- a/garnet/lib/ui/gfx/engine/engine.h
+++ b/garnet/lib/ui/gfx/engine/engine.h
@@ -17,24 +17,26 @@
 #include "lib/escher/vk/image_factory.h"
 
 #include "garnet/lib/ui/gfx/displays/display_manager.h"
+#include "garnet/lib/ui/gfx/engine/engine_renderer.h"
 #include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
 #include "garnet/lib/ui/gfx/engine/object_linker.h"
 #include "garnet/lib/ui/gfx/engine/resource_linker.h"
 #include "garnet/lib/ui/gfx/engine/scene_graph.h"
 #include "garnet/lib/ui/gfx/engine/session_context.h"
 #include "garnet/lib/ui/gfx/engine/session_manager.h"
-#include "garnet/lib/ui/gfx/engine/update_scheduler.h"
 #include "garnet/lib/ui/gfx/id.h"
 #include "garnet/lib/ui/gfx/resources/import.h"
 #include "garnet/lib/ui/gfx/resources/nodes/scene.h"
 #include "garnet/lib/ui/gfx/util/event_timestamper.h"
 #include "garnet/lib/ui/scenic/event_reporter.h"
+#include "lib/escher/renderer/batch_gpu_uploader.h"
 
 namespace scenic_impl {
 namespace gfx {
 
 class Compositor;
-class EngineRenderer;
+class FrameTimings;
+using FrameTimingsPtr = fxl::RefPtr<FrameTimings>;
 class Session;
 class SessionHandler;
 class View;
@@ -42,16 +44,34 @@
 
 using ViewLinker = ObjectLinker<ViewHolder, View>;
 
+// Graphical context for a set of session updates.
+// The CommandContext is only valid during RenderFrame() and should not be
+// accessed outside of that.
+class CommandContext {
+ public:
+  CommandContext(std::unique_ptr<escher::BatchGpuUploader> uploader);
+
+  escher::BatchGpuUploader* batch_gpu_uploader() const {
+    return batch_gpu_uploader_.get();
+  }
+
+  // Flush any work accumulated during command processing.
+  void Flush();
+
+ private:
+  std::unique_ptr<escher::BatchGpuUploader> batch_gpu_uploader_;
+};
+
 // Owns a group of sessions which can share resources with one another
 // using the same resource linker and which coexist within the same timing
 // domain using the same frame scheduler.  It is not possible for sessions
 // which belong to different engines to communicate with one another.
-class Engine : public UpdateScheduler, private FrameSchedulerDelegate {
+class Engine : public SessionUpdater, public FrameRenderer {
  public:
   Engine(std::unique_ptr<FrameScheduler> frame_scheduler,
          DisplayManager* display_manager, escher::EscherWeakPtr escher);
 
-  ~Engine() override;
+  ~Engine() override = default;
 
   escher::Escher* escher() const { return escher_.get(); }
   escher::EscherWeakPtr GetEscherWeakPtr() const { return escher_; }
@@ -86,28 +106,34 @@
                           event_timestamper(),
                           session_manager(),
                           frame_scheduler(),
-                          static_cast<UpdateScheduler*>(this),
                           display_manager_,
                           scene_graph(),
                           resource_linker(),
                           view_linker()};
   }
 
-  // |UpdateScheduler|
-  //
-  // Tell the FrameScheduler to schedule a frame. This is also used for
-  // updates triggered by something other than a Session update i.e. an
-  // ImagePipe with a new Image to present.
-  void ScheduleUpdate(uint64_t presentation_time) override;
-
-  // Dumps the contents of all scene graphs.
-  std::string DumpScenes() const;
-
   // Invoke Escher::Cleanup().  If more work remains afterward, post a delayed
   // task to try again; this is typically because cleanup couldn't finish due
   // to unfinished GPU work.
   void CleanupEscher();
 
+  // Dumps the contents of all scene graphs.
+  std::string DumpScenes() const;
+
+  // |SessionUpdater|
+  //
+  // Applies scheduled updates to a session. If the update fails, the session is
+  // killed. Returns true if a new render is needed, false otherwise.
+  bool UpdateSessions(std::vector<SessionId> sessions, uint64_t frame_number,
+                      uint64_t presentation_time,
+                      uint64_t presentation_interval) override;
+
+  // |FrameRenderer|
+  //
+  // Renders a new frame. Returns true if successful, false otherwise.
+  bool RenderFrame(const FrameTimingsPtr& frame, uint64_t presentation_time,
+                   uint64_t presentation_interval) override;
+
  protected:
   // Only used by subclasses used in testing.
   Engine(std::unique_ptr<FrameScheduler> frame_scheduler,
@@ -117,6 +143,11 @@
          escher::EscherWeakPtr escher);
 
  private:
+  void InitializeFrameScheduler();
+
+  // Creates a command context.
+  CommandContext CreateCommandContext(uint64_t frame_number_for_tracing);
+
   // Used by GpuMemory to import VMOs from clients.
   uint32_t imported_memory_type_index() const {
     return imported_memory_type_index_;
@@ -138,18 +169,8 @@
     return release_fence_signaller_.get();
   }
 
-  // |FrameSchedulerDelegate|:
-  bool RenderFrame(const FrameTimingsPtr& frame, uint64_t presentation_time,
-                   uint64_t presentation_interval, bool force_render) override;
-
   void InitializeShaderFs();
 
-  // Apply updates to all sessions who have updates and have acquired all
-  // fences.  Return true if there were any updates applied.
-  bool UpdateSessions(uint64_t presentation_time,
-                      uint64_t presentation_interval,
-                      uint64_t frame_number_for_tracing);
-
   // Update and deliver metrics for all nodes which subscribe to metrics
   // events.
   void UpdateAndDeliverMetrics(uint64_t presentation_time);
diff --git a/garnet/lib/ui/gfx/engine/frame_scheduler.h b/garnet/lib/ui/gfx/engine/frame_scheduler.h
index 8b8a9a3..25931b2 100644
--- a/garnet/lib/ui/gfx/engine/frame_scheduler.h
+++ b/garnet/lib/ui/gfx/engine/frame_scheduler.h
@@ -5,13 +5,11 @@
 #ifndef GARNET_LIB_UI_GFX_ENGINE_FRAME_SCHEDULER_H_
 #define GARNET_LIB_UI_GFX_ENGINE_FRAME_SCHEDULER_H_
 
-#include <queue>
+#include <vector>
 
-#include <lib/async/dispatcher.h>
+#include "garnet/lib/ui/gfx/id.h"
+
 #include <lib/zx/time.h>
-
-#include "lib/fxl/macros.h"
-#include "lib/fxl/memory/ref_ptr.h"
 #include "lib/fxl/memory/weak_ptr.h"
 
 namespace scenic_impl {
@@ -20,16 +18,29 @@
 class Display;
 class FrameTimings;
 using FrameTimingsPtr = fxl::RefPtr<FrameTimings>;
+using PresentationTime = uint64_t;
 
-// Interface implemented by the engine to perform per-frame processing in
-// response to a frame being scheduled.
-class FrameSchedulerDelegate {
+// Interface for performing session updates.
+class SessionUpdater {
  public:
-  virtual ~FrameSchedulerDelegate() = default;
+  virtual ~SessionUpdater() = default;
 
-  // Called when it's time to apply changes to the scene graph and render
-  // a new frame.  The FrameTimings object is used to accumulate timing
-  // for all swapchains that are used as render targets in that frame.
+  // Applies all updates scheduled before or at |presentation_time|, for each
+  // session in |sessions|. Returns true if any updates were applied, false
+  // otherwise.
+  virtual bool UpdateSessions(std::vector<SessionId> sessions,
+                              uint64_t frame_number, uint64_t presentation_time,
+                              uint64_t presentation_interval) = 0;
+};
+
+// Interface for rendering frames.
+class FrameRenderer {
+ public:
+  virtual ~FrameRenderer() = default;
+
+  // Called when it's time to render a new frame.  The FrameTimings object is
+  // used to accumulate timing for all swapchains that are used as render
+  // targets in that frame.
   //
   // If RenderFrame() returns true, the delegate is responsible for calling
   // FrameTimings::OnFrameRendered/Presented/Dropped().  Otherwise, rendering
@@ -37,18 +48,14 @@
   // receive any timing information for that frame.
   // TODO(SCN-1089): these return value semantics are not ideal.  See comments
   // in Engine::RenderFrame() regarding this same issue.
-  //
-  // TODO(MZ-225): We need to track backpressure so that the frame scheduler
-  // doesn't get too far ahead. With that in mind, Renderer::DrawFrame should
-  // have a callback which is invoked when the frame is fully flushed through
-  // the graphics pipeline. Then Engine::RenderFrame itself should have a
-  // callback which is invoked when all renderers finish work for that frame.
-  // Then FrameScheduler should listen to the callback to count how many
-  // frames are in flight and back off.
   virtual bool RenderFrame(const FrameTimingsPtr& frame_timings,
                            uint64_t presentation_time,
-                           uint64_t presentation_interval,
-                           bool force_render) = 0;
+                           uint64_t presentation_interval) = 0;
+};
+
+struct FrameSchedulerDelegate {
+  fxl::WeakPtr<FrameRenderer> frame_renderer;
+  fxl::WeakPtr<SessionUpdater> session_updater;
 };
 
 // The FrameScheduler is responsible for scheduling frames to be drawn in
@@ -61,73 +68,33 @@
 // later Vsync.
 class FrameScheduler {
  public:
-  explicit FrameScheduler(Display* display);
-  ~FrameScheduler();
-
-  void set_delegate(FrameSchedulerDelegate* delegate) { delegate_ = delegate; }
-
-  // Request a frame to be scheduled at or after |presentation_time|, which
-  // may be in the past.
-  void RequestFrame(zx_time_t presentation_time);
-
-  // If |render_continuously|, we keep rendering frames regardless of whether
-  // they're requested using RequestFrame().
-  void SetRenderContinuously(bool render_continuously);
+  virtual ~FrameScheduler() = default;
 
   // Helper method for ScheduleFrame().  Returns the target presentation time
   // for the requested presentation time, and a wake-up time that is early
   // enough to start rendering in order to hit the target presentation time.
-  std::pair<zx_time_t, zx_time_t> ComputeTargetPresentationAndWakeupTimes(
-      zx_time_t requested_presentation_time) const;
+  virtual std::pair<zx_time_t, zx_time_t>
+  ComputeTargetPresentationAndWakeupTimes(
+      zx_time_t requested_presentation_time) const = 0;
 
- private:
-  // Update the global scene and then draw it... maybe.  There are multiple
-  // reasons why this might not happen.  For example, the swapchain might apply
-  // back-pressure if we can't hit our target frame rate.  Or, after this frame
-  // was scheduled, another frame was scheduled to be rendered at an earlier
-  // time, and not enough time has elapsed to render this frame.  Etc.
-  void MaybeRenderFrame(zx_time_t presentation_time, zx_time_t wakeup_time);
+  virtual void SetDelegate(FrameSchedulerDelegate delegate) = 0;
 
-  // Schedule a frame for the earliest of |requested_presentation_times_|.  The
-  // scheduled time will be the earliest achievable time, such that rendering
-  // can start early enough to hit the next Vsync.
-  void ScheduleFrame();
+  // If |render_continuously|, we keep scheduling new frames immediately after
+  // each presented frame, regardless of whether they're explicitly requested
+  // using RequestFrame().
+  virtual void SetRenderContinuously(bool render_continuously) = 0;
 
-  // Returns true to apply back-pressure when we cannot hit our target frame
-  // rate.  Otherwise, return false to indicate that it is OK to immediately
-  // render a frame.
-  bool TooMuchBackPressure();
+  // Tell the FrameScheduler to schedule a frame. This is also used for
+  // updates triggered by something other than a Session update i.e. an
+  // ImagePipe with a new Image to present.
+  virtual void ScheduleUpdateForSession(uint64_t presentation_time,
+                                        scenic_impl::SessionId session) = 0;
 
-  // Helper method for ScheduleFrame().  Returns the target presentation time
-  // for the next frame, and a wake-up time that is early enough to start
-  // rendering in order to hit the target presentation time.
-  std::pair<zx_time_t, zx_time_t> ComputeNextPresentationAndWakeupTimes() const;
-
-  // Return the predicted amount of time required to render a frame.
-  zx_time_t PredictRequiredFrameRenderTime() const;
-
-  // Called by the delegate when the frame drawn by RenderFrame() has been
+ protected:
+  // Called when the frame drawn by RenderFrame() has been
   // presented to the display.
   friend class FrameTimings;
-  void OnFramePresented(FrameTimings* timings);
-
-  async_dispatcher_t* const dispatcher_;
-  FrameSchedulerDelegate* delegate_;
-  Display* const display_;
-
-  std::priority_queue<zx_time_t, std::vector<zx_time_t>,
-                      std::greater<zx_time_t>>
-      requested_presentation_times_;
-
-  uint64_t frame_number_ = 0;
-  constexpr static size_t kMaxOutstandingFrames = 2;
-  std::vector<FrameTimingsPtr> outstanding_frames_;
-  bool back_pressure_applied_ = false;
-  bool render_continuously_ = false;
-
-  fxl::WeakPtrFactory<FrameScheduler> weak_factory_;  // must be last
-
-  FXL_DISALLOW_COPY_AND_ASSIGN(FrameScheduler);
+  virtual void OnFramePresented(FrameTimings* timings) = 0;
 };
 
 }  // namespace gfx
diff --git a/garnet/lib/ui/gfx/engine/gfx_command_applier.cc b/garnet/lib/ui/gfx/engine/gfx_command_applier.cc
index edba2bc..fc49e9f 100644
--- a/garnet/lib/ui/gfx/engine/gfx_command_applier.cc
+++ b/garnet/lib/ui/gfx/engine/gfx_command_applier.cc
@@ -1105,7 +1105,7 @@
     Session* session, ResourceId id, fuchsia::ui::gfx::ImagePipeArgs args) {
   auto image_pipe = fxl::MakeRefCounted<ImagePipe>(
       session, id, std::move(args.image_pipe_request),
-      session->session_context().update_scheduler);
+      session->session_context().frame_scheduler);
   return session->resources()->AddResource(id, image_pipe);
 }
 
diff --git a/garnet/lib/ui/gfx/engine/session.cc b/garnet/lib/ui/gfx/engine/session.cc
index 8423656..313ddab 100644
--- a/garnet/lib/ui/gfx/engine/session.cc
+++ b/garnet/lib/ui/gfx/engine/session.cc
@@ -39,7 +39,7 @@
   wrapped_hits.resize(hits.size());
   for (size_t i = 0; i < hits.size(); ++i) {
     const Hit& hit = hits[i];
-    ::fuchsia::ui::gfx::Hit wrapped_hit;
+    fuchsia::ui::gfx::Hit wrapped_hit;
     wrapped_hit.tag_value = hit.tag_value;
     wrapped_hit.ray_origin = Wrap(hit.ray.origin);
     wrapped_hit.ray_direction = Wrap(hit.ray.direction);
@@ -103,8 +103,8 @@
 bool Session::ScheduleUpdate(
     uint64_t requested_presentation_time,
     std::vector<::fuchsia::ui::gfx::Command> commands,
-    ::std::vector<zx::event> acquire_fences,
-    ::std::vector<zx::event> release_events,
+    std::vector<zx::event> acquire_fences,
+    std::vector<zx::event> release_events,
     fuchsia::ui::scenic::Session::PresentCallback callback) {
   uint64_t last_scheduled_presentation_time =
       last_applied_update_presentation_time_;
@@ -157,9 +157,8 @@
   acquire_fence_set->WaitReadyAsync(
       [weak = GetWeakPtr(), requested_presentation_time] {
         if (weak) {
-          weak->session_context_.session_manager->ScheduleUpdateForSession(
-              weak->session_context_.update_scheduler,
-              requested_presentation_time, std::move(weak));
+          weak->session_context_.frame_scheduler->ScheduleUpdateForSession(
+              requested_presentation_time, weak->id());
         }
       });
 
@@ -177,8 +176,8 @@
   scheduled_image_pipe_updates_.push(
       {presentation_time, std::move(image_pipe)});
 
-  session_context_.session_manager->ScheduleUpdateForSession(
-      session_context_.update_scheduler, presentation_time, GetWeakPtr());
+  session_context_.frame_scheduler->ScheduleUpdateForSession(presentation_time,
+                                                             id_);
 }
 
 Session::ApplyUpdateResult Session::ApplyScheduledUpdates(
@@ -309,8 +308,8 @@
   // consumed by the FrameScheduler.
 }
 
-void Session::HitTest(uint32_t node_id, ::fuchsia::ui::gfx::vec3 ray_origin,
-                      ::fuchsia::ui::gfx::vec3 ray_direction,
+void Session::HitTest(uint32_t node_id, fuchsia::ui::gfx::vec3 ray_origin,
+                      fuchsia::ui::gfx::vec3 ray_direction,
                       fuchsia::ui::scenic::Session::HitTestCallback callback) {
   if (auto node = resources_.FindResource<Node>(node_id)) {
     SessionHitTester hit_tester(node->session());
@@ -330,7 +329,7 @@
 }
 
 void Session::HitTestDeviceRay(
-    ::fuchsia::ui::gfx::vec3 ray_origin, ::fuchsia::ui::gfx::vec3 ray_direction,
+    fuchsia::ui::gfx::vec3 ray_origin, fuchsia::ui::gfx::vec3 ray_direction,
     fuchsia::ui::scenic::Session::HitTestCallback callback) {
   escher::ray4 ray =
       escher::ray4{{Unwrap(ray_origin), 1.f}, {Unwrap(ray_direction), 0.f}};
diff --git a/garnet/lib/ui/gfx/engine/session_context.h b/garnet/lib/ui/gfx/engine/session_context.h
index c047334..e32202d2 100644
--- a/garnet/lib/ui/gfx/engine/session_context.h
+++ b/garnet/lib/ui/gfx/engine/session_context.h
@@ -45,7 +45,6 @@
   EventTimestamper* event_timestamper;
   SessionManager* session_manager;
   FrameScheduler* frame_scheduler;
-  UpdateScheduler* update_scheduler;
   DisplayManager* display_manager;
   SceneGraphWeakPtr scene_graph;
   ResourceLinker* resource_linker;
diff --git a/garnet/lib/ui/gfx/engine/session_handler.cc b/garnet/lib/ui/gfx/engine/session_handler.cc
index e9a5953..ab38dc0 100644
--- a/garnet/lib/ui/gfx/engine/session_handler.cc
+++ b/garnet/lib/ui/gfx/engine/session_handler.cc
@@ -37,8 +37,8 @@
 }
 
 void SessionHandler::Present(
-    uint64_t presentation_time, ::std::vector<zx::event> acquire_fences,
-    ::std::vector<zx::event> release_fences,
+    uint64_t presentation_time, std::vector<zx::event> acquire_fences,
+    std::vector<zx::event> release_fences,
     fuchsia::ui::scenic::Session::PresentCallback callback) {
   if (!session_->ScheduleUpdate(
           presentation_time, std::move(buffered_commands_),
@@ -51,15 +51,15 @@
 }
 
 void SessionHandler::HitTest(
-    uint32_t node_id, ::fuchsia::ui::gfx::vec3 ray_origin,
-    ::fuchsia::ui::gfx::vec3 ray_direction,
+    uint32_t node_id, fuchsia::ui::gfx::vec3 ray_origin,
+    fuchsia::ui::gfx::vec3 ray_direction,
     fuchsia::ui::scenic::Session::HitTestCallback callback) {
   session_->HitTest(node_id, std::move(ray_origin), std::move(ray_direction),
                     std::move(callback));
 }
 
 void SessionHandler::HitTestDeviceRay(
-    ::fuchsia::ui::gfx::vec3 ray_origin, ::fuchsia::ui::gfx::vec3 ray_direction,
+    fuchsia::ui::gfx::vec3 ray_origin, fuchsia::ui::gfx::vec3 ray_direction,
     fuchsia::ui::scenic::Session::HitTestDeviceRayCallback callback) {
   session_->HitTestDeviceRay(std::move(ray_origin), std::move(ray_direction),
                              std::move(callback));
diff --git a/garnet/lib/ui/gfx/engine/session_manager.cc b/garnet/lib/ui/gfx/engine/session_manager.cc
index ccb4125..d5b7eca 100644
--- a/garnet/lib/ui/gfx/engine/session_manager.cc
+++ b/garnet/lib/ui/gfx/engine/session_manager.cc
@@ -8,27 +8,12 @@
 #include <lib/async/default.h>
 #include <trace/event.h>
 
-#include "garnet/lib/ui/gfx/engine/session.h"
 #include "garnet/lib/ui/gfx/engine/session_handler.h"
-#include "garnet/lib/ui/gfx/engine/update_scheduler.h"
 #include "garnet/lib/ui/scenic/session.h"
 
 namespace scenic_impl {
 namespace gfx {
 
-CommandContext::CommandContext(
-    std::unique_ptr<escher::BatchGpuUploader> uploader)
-    : batch_gpu_uploader_(std::move(uploader)) {}
-
-void CommandContext::Flush() {
-  if (batch_gpu_uploader_) {
-    // Submit regardless of whether or not there are updates to release the
-    // underlying CommandBuffer so the pool and sequencer don't stall out.
-    // TODO(ES-115) to remove this restriction.
-    batch_gpu_uploader_->Submit();
-  }
-}
-
 SessionHandler* SessionManager::FindSessionHandler(SessionId id) {
   auto it = session_handlers_.find(id);
   if (it != session_handlers_.end()) {
@@ -61,52 +46,6 @@
   ++session_count_;
 }
 
-void SessionManager::ScheduleUpdateForSession(
-    UpdateScheduler* update_scheduler, uint64_t presentation_time,
-    fxl::WeakPtr<scenic_impl::gfx::Session> session) {
-  FXL_DCHECK(update_scheduler);
-  if (session) {
-    updatable_sessions_.push({presentation_time, std::move(session)});
-    update_scheduler->ScheduleUpdate(presentation_time);
-  }
-}
-
-bool SessionManager::ApplyScheduledSessionUpdates(
-    CommandContext* command_context, uint64_t presentation_time,
-    uint64_t presentation_interval) {
-  // NOTE: this name is important for benchmarking.  Do not remove or modify it
-  // without also updating the "process_scenic_trace.go" script.
-  TRACE_DURATION("gfx", "ApplyScheduledSessionUpdates", "time",
-                 presentation_time, "interval", presentation_interval);
-
-  bool needs_render = false;
-  while (!updatable_sessions_.empty()) {
-    auto& top = updatable_sessions_.top();
-    if (top.first > presentation_time)
-      break;
-    auto session = std::move(top.second);
-    updatable_sessions_.pop();
-    if (session.get() != nullptr) {
-      auto update_results = session->ApplyScheduledUpdates(
-          command_context, presentation_time, presentation_interval);
-
-      needs_render |= update_results.needs_render;
-
-      // If update fails, kill the entire client session
-      if (!update_results.success) {
-        auto session_handler = FindSessionHandler(session->id());
-        FXL_DCHECK(session_handler);
-        session_handler->BeginTearDown();
-      }
-    } else {
-      // Corresponds to a call to ScheduleUpdate(), which always triggers a
-      // render.
-      needs_render = true;
-    }
-  }
-  return needs_render;
-}
-
 void SessionManager::RemoveSessionHandler(SessionId id) {
   auto it = session_handlers_.find(id);
   if (it != session_handlers_.end()) {
@@ -116,5 +55,11 @@
   }
 }
 
+void SessionManager::KillSession(SessionId session_id) {
+  auto session_handler = FindSessionHandler(session_id);
+  FXL_DCHECK(session_handler);
+  session_handler->BeginTearDown();
+}
+
 }  // namespace gfx
 }  // namespace scenic_impl
diff --git a/garnet/lib/ui/gfx/engine/session_manager.h b/garnet/lib/ui/gfx/engine/session_manager.h
index 9fa5de6..54fd8e8 100644
--- a/garnet/lib/ui/gfx/engine/session_manager.h
+++ b/garnet/lib/ui/gfx/engine/session_manager.h
@@ -5,12 +5,9 @@
 #ifndef GARNET_LIB_UI_GFX_ENGINE_SESSION_MANAGER_H_
 #define GARNET_LIB_UI_GFX_ENGINE_SESSION_MANAGER_H_
 
-#include <set>
 #include <unordered_map>
 
-#include "garnet/lib/ui/gfx/engine/session.h"
 #include "garnet/lib/ui/scenic/command_dispatcher.h"
-#include "lib/escher/renderer/batch_gpu_uploader.h"
 
 namespace scenic_impl {
 class EventReporter;
@@ -21,29 +18,9 @@
 namespace gfx {
 
 using SessionId = ::scenic_impl::SessionId;
-using PresentationTime = uint64_t;
 
 class SessionHandler;
 class Engine;
-class UpdateScheduler;
-
-// Graphical context for a set of session updates.
-// The CommandContext is only valid during RenderFrame() and should not be
-// accessed outside of that.
-class CommandContext {
- public:
-  CommandContext(std::unique_ptr<escher::BatchGpuUploader> uploader);
-
-  escher::BatchGpuUploader* batch_gpu_uploader() const {
-    return batch_gpu_uploader_.get();
-  }
-
-  // Flush any work accumulated during command processing.
-  void Flush();
-
- private:
-  std::unique_ptr<escher::BatchGpuUploader> batch_gpu_uploader_;
-};
 
 // Manages a collection of SessionHandlers.
 // Tracks future updates requested by Sessions, and executes updates for a
@@ -64,56 +41,28 @@
   std::unique_ptr<CommandDispatcher> CreateCommandDispatcher(
       CommandDispatcherContext context, Engine* engine);
 
-  // Tell the UpdateScheduler to schedule a frame, and remember the Session so
-  // that we can tell it to apply updates in ApplyScheduledSessionUpdates().
-  void ScheduleUpdateForSession(
-      UpdateScheduler* update_scheduler, uint64_t presentation_time,
-      fxl::WeakPtr<scenic_impl::gfx::Session> session);
-
-  // Executes updates that are schedule up to and including a given presentation
-  // time. Returns true if rendering is needed.
-  bool ApplyScheduledSessionUpdates(CommandContext* command_context,
-                                    uint64_t presentation_time,
-                                    uint64_t presentation_interval);
+  // Called by engine on a failed session update
+  void KillSession(SessionId session_id);
 
  protected:
   // Protected for testing subclass
   void InsertSessionHandler(SessionId session_id,
                             SessionHandler* session_handler);
 
+  virtual std::unique_ptr<SessionHandler> CreateSessionHandler(
+      CommandDispatcherContext context, Engine* engine,
+      EventReporter* event_reporter, ErrorReporter* error_reporter) const;
+
  private:
   friend class SessionHandler;
 
-  // Used to compare presentation times so that the priority_queue acts as a min
-  // heap, placing the earliest PresentationTime at the top
-  class UpdatableSessionsComparator {
-   public:
-    bool operator()(
-        std::pair<PresentationTime, fxl::WeakPtr<Session>> updatable_session1,
-        std::pair<PresentationTime, fxl::WeakPtr<Session>> updatable_session2) {
-      return updatable_session1.first > updatable_session2.first;
-    }
-  };
-
   // Removes the SessionHandler from the session_handlers_ map.  We assume that
   // the SessionHandler has already taken care of itself and its Session.
   void RemoveSessionHandler(SessionId id);
 
-  virtual std::unique_ptr<SessionHandler> CreateSessionHandler(
-      CommandDispatcherContext context, Engine* engine,
-      EventReporter* event_reporter, ErrorReporter* error_reporter) const;
-
   // Map of all the sessions.
   std::unordered_map<SessionId, SessionHandler*> session_handlers_;
   size_t session_count_ = 0;
-
-  // Lists all Session that have updates to apply, sorted by the earliest
-  // requested presentation time of each update.
-  std::priority_queue<
-      std::pair<PresentationTime, fxl::WeakPtr<Session>>,
-      std::vector<std::pair<PresentationTime, fxl::WeakPtr<Session>>>,
-      UpdatableSessionsComparator>
-      updatable_sessions_;
 };
 
 }  // namespace gfx
diff --git a/garnet/lib/ui/gfx/engine/update_scheduler.h b/garnet/lib/ui/gfx/engine/update_scheduler.h
deleted file mode 100644
index 6380ccd..0000000
--- a/garnet/lib/ui/gfx/engine/update_scheduler.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2018 The Fuchsia 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 GARNET_LIB_UI_GFX_ENGINE_UPDATE_SCHEDULER_H_
-#define GARNET_LIB_UI_GFX_ENGINE_UPDATE_SCHEDULER_H_
-
-namespace scenic_impl {
-namespace gfx {
-
-class UpdateScheduler {
- public:
-  virtual void ScheduleUpdate(uint64_t presentation_time) = 0;
-  virtual ~UpdateScheduler() = default;
-};
-
-}  // namespace gfx
-}  // namespace scenic_impl
-
-#endif  // GARNET_LIB_UI_GFX_ENGINE_UPDATE_SCHEDULER_H_
diff --git a/garnet/lib/ui/gfx/gfx_system.cc b/garnet/lib/ui/gfx/gfx_system.cc
index ecea93a..3f80f20 100644
--- a/garnet/lib/ui/gfx/gfx_system.cc
+++ b/garnet/lib/ui/gfx/gfx_system.cc
@@ -6,6 +6,7 @@
 
 #include <fs/pseudo-file.h>
 
+#include "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
 #include "garnet/lib/ui/gfx/engine/session_handler.h"
 #include "garnet/lib/ui/gfx/screenshotter.h"
 #include "garnet/lib/ui/gfx/util/vulkan_utils.h"
@@ -54,9 +55,10 @@
 }
 
 std::unique_ptr<Engine> GfxSystem::InitializeEngine() {
-  return std::make_unique<Engine>(
-      std::make_unique<FrameScheduler>(display_manager_->default_display()),
-      display_manager_.get(), escher_->GetWeakPtr());
+  return std::make_unique<Engine>(std::make_unique<DefaultFrameScheduler>(
+                                      display_manager_->default_display()),
+                                  display_manager_.get(),
+                                  escher_->GetWeakPtr());
 }
 
 std::unique_ptr<escher::Escher> GfxSystem::InitializeEscher() {
diff --git a/garnet/lib/ui/gfx/resources/image_pipe.cc b/garnet/lib/ui/gfx/resources/image_pipe.cc
index 50a8161..7883cd9 100644
--- a/garnet/lib/ui/gfx/resources/image_pipe.cc
+++ b/garnet/lib/ui/gfx/resources/image_pipe.cc
@@ -6,8 +6,8 @@
 
 #include <trace/event.h>
 
+#include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
 #include "garnet/lib/ui/gfx/engine/session.h"
-#include "garnet/lib/ui/gfx/engine/update_scheduler.h"
 #include "garnet/lib/ui/gfx/resources/memory.h"
 #include "lib/escher/flib/fence.h"
 
@@ -18,22 +18,22 @@
     ResourceType::kImagePipe | ResourceType::kImageBase, "ImagePipe"};
 
 ImagePipe::ImagePipe(Session* session, ResourceId id,
-                     UpdateScheduler* update_scheduler)
+                     FrameScheduler* frame_scheduler)
     : ImageBase(session, id, ImagePipe::kTypeInfo),
-      update_scheduler_(update_scheduler),
+      frame_scheduler_(frame_scheduler),
       weak_ptr_factory_(this) {
-  FXL_DCHECK(update_scheduler);
+  FXL_DCHECK(frame_scheduler);
 }
 
 ImagePipe::ImagePipe(
     Session* session, ResourceId id,
     ::fidl::InterfaceRequest<fuchsia::images::ImagePipe> request,
-    UpdateScheduler* update_scheduler)
+    FrameScheduler* frame_scheduler)
     : ImageBase(session, id, ImagePipe::kTypeInfo),
       handler_(std::make_unique<ImagePipeHandler>(std::move(request), this)),
-      update_scheduler_(update_scheduler),
+      frame_scheduler_(frame_scheduler),
       weak_ptr_factory_(this) {
-  FXL_DCHECK(update_scheduler);
+  FXL_DCHECK(frame_scheduler);
 }
 
 void ImagePipe::AddImage(uint32_t image_id,
@@ -87,7 +87,7 @@
   images_.clear();
 
   // Schedule a new frame.
-  update_scheduler_->ScheduleUpdate(0);
+  frame_scheduler_->ScheduleUpdateForSession(0, session()->id());
 }
 
 void ImagePipe::OnConnectionError() { CloseConnectionAndCleanUp(); }
diff --git a/garnet/lib/ui/gfx/resources/image_pipe.h b/garnet/lib/ui/gfx/resources/image_pipe.h
index 43603bd..75a2825 100644
--- a/garnet/lib/ui/gfx/resources/image_pipe.h
+++ b/garnet/lib/ui/gfx/resources/image_pipe.h
@@ -32,10 +32,10 @@
  public:
   static const ResourceTypeInfo kTypeInfo;
 
-  ImagePipe(Session* session, ResourceId id, UpdateScheduler* update_scheduler);
+  ImagePipe(Session* session, ResourceId id, FrameScheduler* frame_scheduler);
   ImagePipe(Session* session, ResourceId id,
             ::fidl::InterfaceRequest<fuchsia::images::ImagePipe> request,
-            UpdateScheduler* update_scheduler);
+            FrameScheduler* frame_scheduler);
 
   // Called by |ImagePipeHandler|, part of |ImagePipe| interface.
   void AddImage(uint32_t image_id, fuchsia::images::ImageInfo image_info,
@@ -112,7 +112,7 @@
   std::unordered_map<ResourceId, ImagePtr> images_;
   bool is_valid_ = true;
 
-  UpdateScheduler* update_scheduler_;
+  FrameScheduler* frame_scheduler_;
 
   fxl::WeakPtrFactory<ImagePipe> weak_ptr_factory_;  // must be last
 
diff --git a/garnet/lib/ui/gfx/tests/gfx_apptest.cc b/garnet/lib/ui/gfx/tests/gfx_apptest.cc
index b46e6762..9f7fed3 100644
--- a/garnet/lib/ui/gfx/tests/gfx_apptest.cc
+++ b/garnet/lib/ui/gfx/tests/gfx_apptest.cc
@@ -85,14 +85,14 @@
   // Call Present with release fences.
   session->Present(0u, std::vector<zx::event>(), std::move(release_fences),
                    [](fuchsia::images::PresentationInfo info) {});
-  RunLoopUntilIdle();
+  RunLoopFor(zx::sec(1));
   EXPECT_EQ(1u, handler->present_count());
   EXPECT_FALSE(IsFenceSignalled(release_fence1));
   EXPECT_FALSE(IsFenceSignalled(release_fence2));
   // Call Present again with no release fences.
   session->Present(0u, std::vector<zx::event>(), std::vector<zx::event>(),
                    [](fuchsia::images::PresentationInfo info) {});
-  RunLoopUntilIdle();
+  RunLoopFor(zx::sec(1));
   EXPECT_EQ(2u, handler->present_count());
   EXPECT_TRUE(IsFenceSignalled(release_fence2));
 }
@@ -129,20 +129,20 @@
   // Call Present with both the acquire and release fences.
   session->Present(0u, std::move(acquire_fences), std::move(release_fences),
                    [](fuchsia::images::PresentationInfo info) {});
-  RunLoopUntilIdle();
+  RunLoopFor(zx::sec(1));
   EXPECT_EQ(1u, handler->present_count());
   EXPECT_FALSE(IsFenceSignalled(release_fence));
   // Call Present again with no fences.
   session->Present(0u, std::vector<zx::event>(), std::vector<zx::event>(),
                    [](fuchsia::images::PresentationInfo info) {});
-  RunLoopUntilIdle();
+  RunLoopFor(zx::sec(1));
   EXPECT_EQ(2u, handler->present_count());
   EXPECT_FALSE(IsFenceSignalled(release_fence));
   // Now signal the acquire fence.
   acquire_fence.signal(0u, escher::kFenceSignalled);
   // Now expect that the first frame was presented, and its release fence was
   // signalled.
-  RunLoopUntilIdle();
+  RunLoopFor(zx::sec(1));
   EXPECT_TRUE(IsFenceSignalled(release_fence));
 }
 
diff --git a/garnet/lib/ui/gfx/tests/image_pipe_unittest.cc b/garnet/lib/ui/gfx/tests/image_pipe_unittest.cc
index c9aa858..17f54f6 100644
--- a/garnet/lib/ui/gfx/tests/image_pipe_unittest.cc
+++ b/garnet/lib/ui/gfx/tests/image_pipe_unittest.cc
@@ -4,7 +4,7 @@
 
 #include "garnet/lib/ui/gfx/resources/image_pipe.h"
 #include "garnet/lib/ui/gfx/tests/mocks.h"
-#include "garnet/lib/ui/gfx/tests/session_test.h"
+#include "garnet/lib/ui/gfx/tests/session_handler_test.h"
 #include "garnet/lib/ui/gfx/tests/util.h"
 #include "gtest/gtest.h"
 #include "lib/escher/flib/fence.h"
@@ -27,7 +27,7 @@
   uint32_t update_count_ = 0;
 
  protected:
-  bool UpdatePixels(escher::BatchGpuUploader* uploader) override {
+  bool UpdatePixels(escher::BatchGpuUploader* gpu_uploader) override {
     ++update_count_;
     // Update pixels returns the new dirty state. False will stop additional
     // calls to UpdatePixels() until the image is marked dirty.
@@ -35,31 +35,12 @@
   }
 };  // namespace test
 
-class ImagePipeTest : public SessionTest, public escher::ResourceManager {
+class ImagePipeTest : public SessionHandlerTest,
+                      public escher::ResourceManager {
  public:
   ImagePipeTest() : escher::ResourceManager(escher::EscherWeakPtr()) {}
 
-  std::unique_ptr<SessionForTest> CreateSession() override {
-    SessionContext session_context = CreateBarebonesSessionContext();
-
-    command_buffer_sequencer_ =
-        std::make_unique<escher::impl::CommandBufferSequencer>();
-
-    mock_release_fence_signaller_ =
-        std::make_unique<ReleaseFenceSignallerForTest>(
-            command_buffer_sequencer_.get());
-    session_context.release_fence_signaller =
-        mock_release_fence_signaller_.get();
-
-    return std::make_unique<SessionForTest>(1, std::move(session_context), this,
-                                            error_reporter());
-  }
-
   void OnReceiveOwnable(std::unique_ptr<escher::Resource> resource) override {}
-
-  std::unique_ptr<escher::impl::CommandBufferSequencer>
-      command_buffer_sequencer_;
-  std::unique_ptr<ReleaseFenceSignallerForTest> mock_release_fence_signaller_;
 };
 
 fxl::RefPtr<fsl::SharedVmo> CreateVmoWithBuffer(
@@ -96,11 +77,10 @@
 class ImagePipeThatCreatesDummyImages : public ImagePipe {
  public:
   ImagePipeThatCreatesDummyImages(
-      Session* session, escher::ResourceManager* dummy_resource_manager,
-      UpdateScheduler* update_scheduler)
-      : ImagePipe(session, 0u, update_scheduler),
+      Session* session, escher::ResourceManager* dummy_resource_manager)
+      : ImagePipe(session, 0u, session->session_context().frame_scheduler),
         dummy_resource_manager_(dummy_resource_manager) {
-    FXL_CHECK(update_scheduler);
+    FXL_CHECK(session->session_context().frame_scheduler);
   }
 
   std::vector<fxl::RefPtr<DummyImage>> dummy_images_;
@@ -130,8 +110,7 @@
 TEST_F(ImagePipeTest, ImagePipeImageIdMustNotBeZero) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
-
+          session_handler_->session(), this);
   uint32_t image1_id = 0;
   // Create a checkerboard image and copy it into a vmo.
   {
@@ -154,7 +133,7 @@
 TEST_F(ImagePipeTest, PresentImagesOutOfOrder) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
+          session_handler_->session(), this);
 
   uint32_t image1_id = 1;
   // Create a checkerboard image and copy it into a vmo.
@@ -187,7 +166,7 @@
 TEST_F(ImagePipeTest, PresentImagesInOrder) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
+          session_handler_->session(), this);
 
   uint32_t image1_id = 1;
   // Create a checkerboard image and copy it into a vmo.
@@ -219,7 +198,7 @@
 TEST_F(ImagePipeTest, PresentImagesWithOffset) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
+          session_handler_->session(), this);
 
   uint32_t image1_id = 1;
   // Create a checkerboard image and copy it into a vmo.
@@ -259,7 +238,7 @@
 TEST_F(ImagePipeTest, ImagePipePresentTwoFrames) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
+          session_handler_->session(), this);
 
   uint32_t image1_id = 1;
 
@@ -285,18 +264,18 @@
 
   // Current presented image should be null, since we haven't signalled
   // acquire fence yet.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopFor(zx::sec(1)));
   ASSERT_FALSE(image_pipe->GetEscherImage());
 
   // Signal on the acquire fence.
   acquire_fence1.signal(0u, escher::kFenceSignalled);
 
   // Run until image1 is presented.
-  RunLoopUntilIdle();
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
   ASSERT_TRUE(image_pipe->GetEscherImage());
-  escher::ImagePtr image1 = image_pipe->GetEscherImage();
 
   // Image should now be presented.
+  escher::ImagePtr image1 = image_pipe->GetEscherImage();
   ASSERT_TRUE(image1);
 
   uint32_t image2_id = 2;
@@ -313,7 +292,7 @@
   }
 
   // The first image should not have been released.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopFor(zx::sec(1)));
   ASSERT_FALSE(IsEventSignalled(release_fence1, escher::kFenceSignalled));
 
   // Make gradient the currently displayed image.
@@ -325,21 +304,19 @@
 
   // Verify that the currently display image hasn't changed yet, since we
   // haven't signalled the acquire fence.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopUntilIdle());
   ASSERT_EQ(image_pipe->GetEscherImage(), image1);
 
   // Signal on the acquire fence.
   acquire_fence2.signal(0u, escher::kFenceSignalled);
 
   // There should be a new image presented.
-  RunLoopUntilIdle();
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
   escher::ImagePtr image2 = image_pipe->GetEscherImage();
   ASSERT_TRUE(image2);
   ASSERT_NE(image1, image2);
 
   // The first image should have been released.
-  ASSERT_EQ(mock_release_fence_signaller_->num_calls_to_add_cpu_release_fence(),
-            1u);
   ASSERT_TRUE(IsEventSignalled(release_fence1, escher::kFenceSignalled));
   ASSERT_FALSE(IsEventSignalled(release_fence2, escher::kFenceSignalled));
 }
@@ -348,10 +325,7 @@
 // called on images that are acquired and used.
 TEST_F(ImagePipeTest, ImagePipeUpdateTwoFrames) {
   auto image_pipe = fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-      session_.get(), this, update_scheduler_.get());
-  zx::time now = Now();
-  zx::time now_ish = now + zx::sec(2);
-  zx::time later = now + zx::sec(5);
+      session_handler_->session(), this);
 
   // Image A is a 2x2 image with id=2.
   // Image B is a 4x4 image with id=4.
@@ -368,12 +342,13 @@
       imageIdB, std::move(image_info_b), CopyVmo(gradient_b->vmo()), 0,
       GetVmoSize(gradient_b->vmo()), fuchsia::images::MemoryType::HOST_MEMORY);
 
-  image_pipe->PresentImage(imageIdA, now_ish.get(), std::vector<zx::event>(),
+  image_pipe->PresentImage(imageIdA, 0, std::vector<zx::event>(),
                            std::vector<zx::event>(), nullptr);
-  image_pipe->PresentImage(imageIdB, now_ish.get(), std::vector<zx::event>(),
+  image_pipe->PresentImage(imageIdB, 0, std::vector<zx::event>(),
                            std::vector<zx::event>(), nullptr);
 
-  RunLoopUntil(later);
+  // Let all updates get scheduled and finished
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
 
   auto image_out = image_pipe->GetEscherImage();
   // We should get the second image in the queue, since both should have been
@@ -384,20 +359,18 @@
   ASSERT_EQ(image_pipe->dummy_images_[0]->update_count_, 0u);
   ASSERT_EQ(image_pipe->dummy_images_[1]->update_count_, 1u);
 
-  zx::time even_later = later + zx::sec(1);
-  zx::time much_later = even_later + zx::sec(2);
   // Do it again, to make sure that update is called a second time (since
   // released images could be edited by the client before presentation).
   //
   // In this case, we need to run to idle after presenting image A, so that
   // image B is returned by the pool, marked dirty, and is free to be acquired
   // again.
-  image_pipe->PresentImage(imageIdA, even_later.get(), std::vector<zx::event>(),
+  image_pipe->PresentImage(imageIdA, 0, std::vector<zx::event>(),
                            std::vector<zx::event>(), nullptr);
-  RunLoopUntil(even_later);
-  image_pipe->PresentImage(imageIdB, even_later.get(), std::vector<zx::event>(),
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
+  image_pipe->PresentImage(imageIdB, 0, std::vector<zx::event>(),
                            std::vector<zx::event>(), nullptr);
-  RunLoopUntil(much_later);
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
 
   image_out = image_pipe->GetEscherImage();
   ASSERT_EQ(image_pipe->dummy_images_.size(), 2u);
@@ -413,7 +386,7 @@
 TEST_F(ImagePipeTest, ImagePipeRemoveImageThatIsPendingPresent) {
   ImagePipePtr image_pipe =
       fxl::MakeRefCounted<ImagePipeThatCreatesDummyImages>(
-          session_.get(), this, update_scheduler_.get());
+          session_handler_->session(), this);
 
   uint32_t image1_id = 1;
 
@@ -439,7 +412,7 @@
 
   // Current presented image should be null, since we haven't signalled
   // acquire fence yet.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopFor(zx::sec(1)));
   ASSERT_FALSE(image_pipe->GetEscherImage());
 
   // Remove the image; by the ImagePipe semantics, the consumer will
@@ -450,7 +423,7 @@
   acquire_fence1.signal(0u, escher::kFenceSignalled);
 
   // Run until image1 is presented.
-  RunLoopUntilIdle();
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
   ASSERT_TRUE(image_pipe->GetEscherImage());
   escher::ImagePtr image1 = image_pipe->GetEscherImage();
 
@@ -471,7 +444,7 @@
   }
 
   // The first image should not have been released.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopFor(zx::sec(1)));
   ASSERT_FALSE(IsEventSignalled(release_fence1, escher::kFenceSignalled));
 
   // Make gradient the currently displayed image.
@@ -483,21 +456,19 @@
 
   // Verify that the currently display image hasn't changed yet, since we
   // haven't signalled the acquire fence.
-  RunLoopUntilIdle();
+  ASSERT_FALSE(RunLoopFor(zx::sec(1)));
   ASSERT_EQ(image_pipe->GetEscherImage(), image1);
 
   // Signal on the acquire fence.
   acquire_fence2.signal(0u, escher::kFenceSignalled);
 
   // There should be a new image presented.
-  RunLoopUntilIdle();
+  ASSERT_TRUE(RunLoopFor(zx::sec(1)));
   escher::ImagePtr image2 = image_pipe->GetEscherImage();
   ASSERT_TRUE(image2);
   ASSERT_NE(image1, image2);
 
   // The first image should have been released.
-  ASSERT_EQ(mock_release_fence_signaller_->num_calls_to_add_cpu_release_fence(),
-            1u);
   ASSERT_TRUE(IsEventSignalled(release_fence1, escher::kFenceSignalled));
   ASSERT_FALSE(IsEventSignalled(release_fence2, escher::kFenceSignalled));
   EXPECT_ERROR_COUNT(0);
diff --git a/garnet/lib/ui/gfx/tests/mocks.cc b/garnet/lib/ui/gfx/tests/mocks.cc
index ba2dcd2..4e8b6dc 100644
--- a/garnet/lib/ui/gfx/tests/mocks.cc
+++ b/garnet/lib/ui/gfx/tests/mocks.cc
@@ -4,6 +4,7 @@
 
 #include "garnet/lib/ui/gfx/tests/mocks.h"
 
+#include "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
 #include "garnet/lib/ui/gfx/tests/session_test.h"
 #include "garnet/lib/ui/scenic/command_dispatcher.h"
 
@@ -16,14 +17,24 @@
                                ErrorReporter* error_reporter)
     : Session(id, std::move(context), event_reporter, error_reporter) {}
 
-SessionHandlerForTest::SessionHandlerForTest(CommandDispatcherContext context,
-                                             SessionManager* session_manager,
+SessionHandlerForTest::SessionHandlerForTest(SessionManager* session_manager,
                                              SessionContext session_context,
+                                             SessionId session_id,
+                                             Scenic* scenic,
                                              EventReporter* event_reporter,
                                              ErrorReporter* error_reporter)
-    : SessionHandler(std::move(context), session_manager,
-                     std::move(session_context), event_reporter,
-                     error_reporter),
+    : SessionHandler(
+          CommandDispatcherContext(scenic, /* session = */ nullptr, session_id),
+          session_manager, std::move(session_context), event_reporter, error_reporter),
+      command_count_(0),
+      present_count_(0) {}
+
+SessionHandlerForTest::SessionHandlerForTest(
+    CommandDispatcherContext command_dispatcher_context,
+    SessionManager* session_manager, SessionContext session_context,
+    EventReporter* event_reporter, ErrorReporter* error_reporter)
+    : SessionHandler(std::move(command_dispatcher_context), session_manager,
+                     std::move(session_context), event_reporter, error_reporter),
       command_count_(0),
       present_count_(0) {}
 
@@ -52,8 +63,6 @@
   fence.signal(0u, escher::kFenceSignalled);
 }
 
-SessionManagerForTest::SessionManagerForTest() : SessionManager() {}
-
 void SessionManagerForTest::InsertSessionHandler(
     SessionId session_id, SessionHandler* session_handler) {
   SessionManager::InsertSessionHandler(session_id, session_handler);
@@ -65,12 +74,14 @@
   return std::make_unique<SessionHandlerForTest>(
       std::move(context), engine->session_manager(), engine->session_context(),
       event_reporter, error_reporter);
-};
+}
 
 EngineForTest::EngineForTest(DisplayManager* display_manager,
                              std::unique_ptr<escher::ReleaseFenceSignaller> r,
                              escher::EscherWeakPtr escher)
-    : Engine(std::unique_ptr<FrameScheduler>(), display_manager, std::move(r),
+    : Engine(std::make_unique<DefaultFrameScheduler>(
+                 display_manager->default_display()),
+             display_manager, std::move(r),
              std::make_unique<SessionManagerForTest>(), std::move(escher)) {}
 
 }  // namespace test
diff --git a/garnet/lib/ui/gfx/tests/mocks.h b/garnet/lib/ui/gfx/tests/mocks.h
index 3bcf9ec..b8fc06a 100644
--- a/garnet/lib/ui/gfx/tests/mocks.h
+++ b/garnet/lib/ui/gfx/tests/mocks.h
@@ -26,11 +26,16 @@
 class SessionHandlerForTest : public SessionHandler {
  public:
   SessionHandlerForTest(
-      CommandDispatcherContext context, SessionManager* session_manager,
-      SessionContext session_context,
+      SessionManager* session_manager, SessionContext session_context,
+      SessionId session_id, Scenic* scenic,
       EventReporter* event_reporter = EventReporter::Default(),
       ErrorReporter* error_reporter = ErrorReporter::Default());
 
+  SessionHandlerForTest(
+    CommandDispatcherContext command_dispatcher_context,
+    SessionManager* session_manager, SessionContext session_context,
+    EventReporter* event_reporter, ErrorReporter* error_reporter);
+
   // |scenic::CommandDispatcher|
   void DispatchCommand(fuchsia::ui::scenic::Command command) override;
 
@@ -68,12 +73,14 @@
 
 class SessionManagerForTest : public SessionManager {
  public:
-  SessionManagerForTest();
+  SessionManagerForTest() = default;
+  ~SessionManagerForTest() override = default;
 
   // Publicly accessible for tests.
   void InsertSessionHandler(SessionId session_id,
                             SessionHandler* session_handler);
 
+ protected:
   std::unique_ptr<SessionHandler> CreateSessionHandler(
       CommandDispatcherContext context, Engine* engine,
       EventReporter* event_reporter,
diff --git a/garnet/lib/ui/gfx/tests/session_handler_test.cc b/garnet/lib/ui/gfx/tests/session_handler_test.cc
index e68f7ed..6b991c6 100644
--- a/garnet/lib/ui/gfx/tests/session_handler_test.cc
+++ b/garnet/lib/ui/gfx/tests/session_handler_test.cc
@@ -8,13 +8,22 @@
 namespace gfx {
 namespace test {
 
-void SessionHandlerTest::SetUp() { SessionTest::SetUp(); }
+void SessionHandlerTest::SetUp() {
+  InitializeScenic();
+  InitializeDisplayManager();
+  InitializeEngine();
+
+  InitializeSessionHandler();
+}
 
 void SessionHandlerTest::TearDown() {
-  SessionTest::TearDown();
   session_handler_.reset();
+  engine_.reset();
+  command_buffer_sequencer_.reset();
+  display_manager_.reset();
   scenic_.reset();
   app_context_.reset();
+  events_.clear();
 }
 
 void SessionHandlerTest::InitializeScenic() {
@@ -25,16 +34,51 @@
 }
 
 void SessionHandlerTest::InitializeSessionHandler() {
-  if (!scenic_) {
-    InitializeScenic();
-  }
+  auto session_context = engine_->session_context();
+  auto session_manager = session_context.session_manager;
+  auto session_id = SessionId(1);
 
-  auto session_context = CreateBarebonesSessionContext();
   session_handler_ = std::make_unique<SessionHandlerForTest>(
-      CommandDispatcherContext(scenic_.get(), nullptr, session_->id()),
-      session_manager_.get(), std::move(session_context));
-  session_manager_->InsertSessionHandler(session_->id(),
-                                         session_handler_.get());
+      session_manager, std::move(session_context), session_id, scenic_.get(),
+      this, error_reporter());
+  static_cast<SessionManagerForTest*>(session_manager)
+      ->InsertSessionHandler(session_id, session_handler_.get());
+}
+
+void SessionHandlerTest::InitializeDisplayManager() {
+  display_manager_ = std::make_unique<DisplayManager>();
+  display_manager_->SetDefaultDisplayForTests(std::make_unique<Display>(
+      /*id*/ 0, /*px-width*/ 0, /*px-height*/ 0));
+}
+
+void SessionHandlerTest::InitializeEngine() {
+  command_buffer_sequencer_ =
+      std::make_unique<escher::impl::CommandBufferSequencer>();
+
+  auto mock_release_fence_signaller =
+      std::make_unique<ReleaseFenceSignallerForTest>(
+          command_buffer_sequencer_.get());
+
+  engine_ = std::make_unique<EngineForTest>(
+      display_manager_.get(), std::move(mock_release_fence_signaller));
+}
+
+void SessionHandlerTest::EnqueueEvent(fuchsia::ui::gfx::Event event) {
+  fuchsia::ui::scenic::Event scenic_event;
+  scenic_event.set_gfx(std::move(event));
+  events_.push_back(std::move(scenic_event));
+}
+
+void SessionHandlerTest::EnqueueEvent(fuchsia::ui::input::InputEvent event) {
+  fuchsia::ui::scenic::Event scenic_event;
+  scenic_event.set_input(std::move(event));
+  events_.push_back(std::move(scenic_event));
+}
+
+void SessionHandlerTest::EnqueueEvent(fuchsia::ui::scenic::Command unhandled) {
+  fuchsia::ui::scenic::Event scenic_event;
+  scenic_event.set_unhandled(std::move(unhandled));
+  events_.push_back(std::move(scenic_event));
 }
 
 }  // namespace test
diff --git a/garnet/lib/ui/gfx/tests/session_handler_test.h b/garnet/lib/ui/gfx/tests/session_handler_test.h
index be07096..80a7048 100644
--- a/garnet/lib/ui/gfx/tests/session_handler_test.h
+++ b/garnet/lib/ui/gfx/tests/session_handler_test.h
@@ -5,7 +5,7 @@
 #ifndef GARNET_LIB_UI_GFX_TESTS_SESSION_HANDLER_TEST_H_
 #define GARNET_LIB_UI_GFX_TESTS_SESSION_HANDLER_TEST_H_
 
-#include "garnet/lib/ui/gfx/tests/session_test.h"
+#include "garnet/lib/ui/gfx/tests/error_reporting_test.h"
 
 #include <lib/fit/function.h>
 
@@ -23,19 +23,31 @@
 
 // For testing SessionHandler without having to manually provide all the state
 // necessary for SessionHandler to run
-class SessionHandlerTest : public SessionTest {
- public:
-  void ResetSessionHandler() { session_handler_.reset(); }
-  void InitializeScenic();
-  void InitializeSessionHandler();
-
+class SessionHandlerTest : public ErrorReportingTest, public EventReporter {
+ protected:
+  // | ::testing::Test |
   void SetUp() override;
   void TearDown() override;
 
- protected:
+  // |EventReporter|
+  void EnqueueEvent(fuchsia::ui::gfx::Event event) override;
+  void EnqueueEvent(fuchsia::ui::input::InputEvent event) override;
+  void EnqueueEvent(fuchsia::ui::scenic::Command unhandled) override;
+
+  void InitializeScenic();
+  void InitializeDisplayManager();
+  void InitializeEngine();
+  void InitializeSessionHandler();
+
   std::unique_ptr<component::StartupContext> app_context_;
   std::unique_ptr<Scenic> scenic_;
+  std::unique_ptr<escher::impl::CommandBufferSequencer>
+      command_buffer_sequencer_;
+  std::unique_ptr<EngineForTest> engine_;
   std::unique_ptr<SessionHandlerForTest> session_handler_;
+  std::unique_ptr<DisplayManager> display_manager_;
+
+  std::vector<fuchsia::ui::scenic::Event> events_;
 };
 
 }  // namespace test
diff --git a/garnet/lib/ui/gfx/tests/session_handler_unittest.cc b/garnet/lib/ui/gfx/tests/session_handler_unittest.cc
index 3ba37e7..71a08c5 100644
--- a/garnet/lib/ui/gfx/tests/session_handler_unittest.cc
+++ b/garnet/lib/ui/gfx/tests/session_handler_unittest.cc
@@ -14,15 +14,18 @@
     SessionHandlerTest,
     WhenSessionHandlerDestroyed_ShouldRemoveSessionHandlerPtrFromSessionManager) {
   InitializeSessionHandler();
-  auto id = session_->id();
+  auto id = session_handler_->session()->id();
+
+  auto session_manager = engine_->session_context().session_manager;
+  ASSERT_NE(session_manager, nullptr);
 
   EXPECT_NE(session_handler_.get(), nullptr);
-  EXPECT_EQ(session_manager_->FindSessionHandler(id), session_handler_.get());
+  EXPECT_EQ(session_manager->FindSessionHandler(id), session_handler_.get());
 
-  ResetSessionHandler();
+  session_handler_.reset();
 
   EXPECT_EQ(session_handler_.get(), nullptr);
-  EXPECT_EQ(session_manager_->FindSessionHandler(id), nullptr);
+  EXPECT_EQ(session_manager->FindSessionHandler(id), nullptr);
 }
 
 }  // namespace test
diff --git a/garnet/lib/ui/gfx/tests/session_test.cc b/garnet/lib/ui/gfx/tests/session_test.cc
index 50105b2..785679a 100644
--- a/garnet/lib/ui/gfx/tests/session_test.cc
+++ b/garnet/lib/ui/gfx/tests/session_test.cc
@@ -4,6 +4,7 @@
 
 #include "garnet/lib/ui/gfx/tests/session_test.h"
 
+#include "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
 #include "garnet/lib/ui/gfx/tests/mocks.h"
 
 #include "lib/fxl/logging.h"
@@ -12,45 +13,39 @@
 namespace gfx {
 namespace test {
 
-FakeUpdateScheduler::FakeUpdateScheduler(SessionManager* session_manager)
-    : session_manager_(session_manager) {}
-
-void FakeUpdateScheduler::ScheduleUpdate(uint64_t presentation_time) {
-  CommandContext empty_command_context(nullptr);
-  session_manager_->ApplyScheduledSessionUpdates(&empty_command_context,
-                                                 presentation_time, 0);
-}
-
 void SessionTest::SetUp() { session_ = CreateSession(); }
 
 void SessionTest::TearDown() {
+  session_.reset();
   session_manager_.reset();
-  if (session_) {
-    session_.reset();
-  }
+  frame_scheduler_.reset();
+  display_manager_.reset();
   events_.clear();
 }
 
 SessionContext SessionTest::CreateBarebonesSessionContext() {
   session_manager_ = std::make_unique<SessionManagerForTest>();
-  update_scheduler_ =
-      std::make_unique<FakeUpdateScheduler>(session_manager_.get());
+
+  display_manager_ = std::make_unique<DisplayManager>();
+  display_manager_->SetDefaultDisplayForTests(std::make_unique<Display>(
+      /*id*/ 0, /*px-width*/ 0, /*px-height*/ 0));
+  frame_scheduler_ = std::make_unique<DefaultFrameScheduler>(
+      display_manager_->default_display());
   SessionContext session_context{
       vk::Device(),
-      nullptr,                  // escher::Escher*
-      0,                        // imported_memory_type_index;
-      nullptr,                  // escher::ResourceRecycler
-      nullptr,                  // escher::ImageFactory*
-      nullptr,                  // escher::RoundedRectFactory*
-      nullptr,                  // escher::ReleaseFenceSignaller*
-      nullptr,                  // EventTimestamper*
-      session_manager_.get(),   // SessionManager*
-      nullptr,                  // FrameScheduler*
-      update_scheduler_.get(),  // UpdateScheduler*
-      nullptr,                  // DisplayManager*
-      SceneGraphWeakPtr(),      // SceneGraphWeakPtr
-      nullptr,                  // ResourceLinker*
-      nullptr                   // ViewLinker*
+      nullptr,                 // escher::Escher*
+      0,                       // imported_memory_type_index;
+      nullptr,                 // escher::ResourceRecycler
+      nullptr,                 // escher::ImageFactory*
+      nullptr,                 // escher::RoundedRectFactory*
+      nullptr,                 // escher::ReleaseFenceSignaller*
+      nullptr,                 // EventTimestamper*
+      session_manager_.get(),  // SessionManager*
+      frame_scheduler_.get(),  // FrameScheduler*
+      display_manager_.get(),  // DisplayManager*
+      SceneGraphWeakPtr(),     // SceneGraphWeakPtr
+      nullptr,                 // ResourceLinker*
+      nullptr                  // ViewLinker*
   };
   return session_context;
 }
diff --git a/garnet/lib/ui/gfx/tests/session_test.h b/garnet/lib/ui/gfx/tests/session_test.h
index 3916eaf..adabf02 100644
--- a/garnet/lib/ui/gfx/tests/session_test.h
+++ b/garnet/lib/ui/gfx/tests/session_test.h
@@ -20,16 +20,6 @@
 namespace gfx {
 namespace test {
 
-class FakeUpdateScheduler : public UpdateScheduler {
- public:
-  FakeUpdateScheduler(SessionManager* session_manager);
-
-  void ScheduleUpdate(uint64_t presentation_time) override;
-
- private:
-  SessionManager* session_manager_ = nullptr;
-};
-
 class SessionTest : public ErrorReportingTest, public EventReporter {
  protected:
   // | ::testing::Test |
@@ -45,7 +35,7 @@
   virtual std::unique_ptr<SessionForTest> CreateSession();
 
   // Creates a SessionContext with only a SessionManager and a
-  // FakeUpdateScheduler.
+  // FrameScheduler.
   SessionContext CreateBarebonesSessionContext();
 
   // Apply the specified Command.  Return true if it was applied successfully,
@@ -60,7 +50,8 @@
     return session_->resources()->FindResource<ResourceT>(id);
   }
 
-  std::unique_ptr<UpdateScheduler> update_scheduler_;
+  std::unique_ptr<DisplayManager> display_manager_;
+  std::unique_ptr<FrameScheduler> frame_scheduler_;
   std::unique_ptr<SessionForTest> session_;
   std::unique_ptr<SessionManagerForTest> session_manager_;
   std::vector<fuchsia::ui::scenic::Event> events_;
diff --git a/garnet/lib/ui/scenic/tests/mocks.cc b/garnet/lib/ui/scenic/tests/mocks.cc
index 4b5f2de..a513ed4 100644
--- a/garnet/lib/ui/scenic/tests/mocks.cc
+++ b/garnet/lib/ui/scenic/tests/mocks.cc
@@ -4,6 +4,8 @@
 
 #include "garnet/lib/ui/scenic/tests/mocks.h"
 
+#include "garnet/lib/ui/gfx/engine/default_frame_scheduler.h"
+
 namespace scenic_impl {
 namespace test {
 
@@ -21,7 +23,7 @@
     gfx::DisplayManager* display_manager,
     std::unique_ptr<escher::ReleaseFenceSignaller> release_signaler,
     escher::EscherWeakPtr escher)
-    : gfx::Engine(std::make_unique<gfx::FrameScheduler>(
+    : gfx::Engine(std::make_unique<gfx::DefaultFrameScheduler>(
                       display_manager->default_display()),
                   display_manager, std::move(release_signaler),
                   std::make_unique<gfx::SessionManager>(), std::move(escher)) {}