[scenic] Collect all waiting frames into a single instance

Make sure we only have a single async wait happening at a time in DefaultFrameScheduler.
This prevents the bug (SCN-1306) where a delayed frame can cause a second frame to
start rendering at the same time. It also makes it easier to reason about scheduling.

SCN-1306 #done

Change-Id: I9bc0fb4f077db9f6c7f346669785b744fc3113d7
diff --git a/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc b/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
index aa477c8..ffbcb78 100644
--- a/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
+++ b/garnet/lib/ui/gfx/engine/default_frame_scheduler.cc
@@ -17,17 +17,7 @@
 namespace scenic_impl {
 namespace gfx {
 
-namespace {
-
-// Generates a unique ID, used for flow trace events.
-static uint64_t NextFlowId() {
-  static std::atomic<uint64_t> counter(0);
-  return ++counter;
-}
-
-}  // anonymous namespace
-
-DefaultFrameScheduler::DefaultFrameScheduler(Display* const display)
+DefaultFrameScheduler::DefaultFrameScheduler(const Display* display)
     : dispatcher_(async_get_default_dispatcher()),
       display_(display),
       weak_factory_(this) {
@@ -41,26 +31,16 @@
                 TRACE_SCOPE_PROCESS, "Timestamp",
                 timings.rendering_finished_time(), "Frame number",
                 timings.frame_number());
-}
-
-void DefaultFrameScheduler::RequestFrame(zx_time_t presentation_time) {
-  if (frame_number_ < 5) {
-    FXL_LOG(INFO) << "DefaultFrameScheduler::RequestFrame presentation_time="
-                  << presentation_time;
-  }
-  const bool should_schedule_frame =
-      requested_presentation_times_.empty() ||
-      requested_presentation_times_.top() > presentation_time;
-  requested_presentation_times_.push(presentation_time);
-  if (should_schedule_frame) {
-    ScheduleFrame();
+  currently_rendering_ = false;
+  if (render_pending_) {
+    RequestFrame();
   }
 }
 
 void DefaultFrameScheduler::SetRenderContinuously(bool render_continuously) {
   render_continuously_ = render_continuously;
   if (render_continuously_) {
-    RequestFrame(0);
+    RequestFrame();
   }
 }
 
@@ -73,14 +53,7 @@
 }
 
 std::pair<zx_time_t, zx_time_t>
-DefaultFrameScheduler::ComputeNextPresentationAndWakeupTimes() const {
-  FXL_DCHECK(!requested_presentation_times_.empty());
-  return ComputeTargetPresentationAndWakeupTimes(
-      requested_presentation_times_.top());
-}
-
-std::pair<zx_time_t, zx_time_t>
-DefaultFrameScheduler::ComputeTargetPresentationAndWakeupTimes(
+DefaultFrameScheduler::ComputePresentationAndWakeupTimesForTargetTime(
     const zx_time_t requested_presentation_time) const {
   const zx_time_t last_vsync_time = display_->GetLastVsyncTime();
   const zx_duration_t vsync_interval = display_->GetVsyncInterval();
@@ -161,64 +134,51 @@
 #endif
 }
 
-void DefaultFrameScheduler::ScheduleFrame() {
-  FXL_DCHECK(!requested_presentation_times_.empty());
+void DefaultFrameScheduler::RequestFrame() {
+  FXL_CHECK(!updatable_sessions_.empty() || render_continuously_ ||
+            render_pending_);
+
+  // Logging the first few frames to find common startup bugs.
   if (frame_number_ < 5) {
-    FXL_LOG(INFO) << "DefaultFrameScheduler::ScheduleFrame";
+    FXL_LOG(INFO) << "DefaultFrameScheduler::RequestFrame";
   }
 
-  auto times = ComputeNextPresentationAndWakeupTimes();
-  zx_time_t presentation_time = times.first;
-  zx_time_t wakeup_time = times.second;
+  auto requested_presentation_time =
+      render_continuously_ || render_pending_
+          ? 0
+          : updatable_sessions_.top().requested_presentation_time;
 
-  TRACE_DURATION("gfx", "FrameScheduler::ScheduleFrame",
-                 "next requested present time",
-                 requested_presentation_times_.top(), "presentation time",
-                 presentation_time, "wakeup_time", wakeup_time);
+  auto next_times = ComputePresentationAndWakeupTimesForTargetTime(
+      requested_presentation_time);
+  auto new_presentation_time = next_times.first;
+  auto new_wakeup_time = next_times.second;
 
-  uint64_t flow_id = NextFlowId();
-  TRACE_FLOW_BEGIN("gfx", "FrameScheduler_Request", flow_id);
+  // If there is no render waiting we should schedule a frame.
+  // Likewise, if newly predicted wake up time is earlier than the current one
+  // then we need to reschedule the next wake up.
+  if (!frame_render_task_.is_pending() || new_wakeup_time < wakeup_time_) {
+    frame_render_task_.Cancel();
 
-  async::PostTaskForTime(
-      dispatcher_,
-      [weak = weak_factory_.GetWeakPtr(), presentation_time, wakeup_time,
-       flow_id] {
-        if (weak)
-          weak->MaybeRenderFrame(presentation_time, wakeup_time, flow_id);
-      },
-      zx::time(0) + zx::nsec(wakeup_time));
+    wakeup_time_ = new_wakeup_time;
+    next_presentation_time_ = new_presentation_time;
+    frame_render_task_.PostForTime(dispatcher_, zx::time(wakeup_time_));
+  }
 }
 
-void DefaultFrameScheduler::MaybeRenderFrame(zx_time_t presentation_time,
-                                             zx_time_t wakeup_time,
-                                             uint64_t flow_id) {
+void DefaultFrameScheduler::MaybeRenderFrame(async_dispatcher_t*,
+                                             async::TaskBase*, zx_status_t) {
+  auto presentation_time = next_presentation_time_;
   TRACE_DURATION("gfx", "FrameScheduler::MaybeRenderFrame", "presentation_time",
                  presentation_time);
-  TRACE_FLOW_END("gfx", "FrameScheduler_Request", flow_id);
+
+  // Logging the first few frames to find common startup bugs.
   if (frame_number_ < 5) {
     FXL_LOG(INFO)
         << "DefaultFrameScheduler::MaybeRenderFrame presentation_time="
-        << presentation_time << " wakeup_time=" << wakeup_time
+        << presentation_time << " wakeup_time=" << wakeup_time_
         << " frame_number=" << frame_number_;
   }
 
-  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
-    // have invoked this method), and coalesced with other requests that were
-    // handled by a previous invocation of MaybeRenderFrame().
-    return;
-  }
-
-  if (TooMuchBackPressure()) {
-    // No need to request another frame; ScheduleFrame() will be called
-    // when the back-pressure is relieved.
-    FXL_VLOG(2)
-        << "DefaultFrameScheduler::MaybeRenderFrame(): dropping frame, too "
-           "much back-pressure.";
-    return;
-  }
-
   // TODO(MZ-400): Check whether there is enough time to render.  If not, drop
   // a frame and reschedule.  It would be nice to simply bump the presentation
   // time, but then there is a danger of rendering too fast, and actually
@@ -231,13 +191,6 @@
   // currently: the estimate is a hard-coded constant.  Figure out what
   // happened, and log it appropriately.
 
-  // We are about to render a frame for the next scheduled presentation time, so
-  // keep only the presentation requests for later times.
-  while (!requested_presentation_times_.empty() &&
-         presentation_time >= requested_presentation_times_.top()) {
-    requested_presentation_times_.pop();
-  }
-
   // TODO(SCN-124): We should derive an appropriate value from the rendering
   // targets, in particular giving priority to couple to the display refresh
   // (vsync).
@@ -257,40 +210,51 @@
         ApplyScheduledSessionUpdates(frame_timings->frame_number(),
                                      presentation_time, presentation_interval);
 
-    if (!any_updates_were_applied && !render_continuously_) {
+    if (!any_updates_were_applied && !render_pending_ &&
+        !render_continuously_) {
       return;
     }
 
+    if (currently_rendering_) {
+      render_pending_ = true;
+      return;
+    }
+
+    // Logging the first few frames to find common startup bugs.
     if (frame_number_ < 5) {
       FXL_LOG(INFO)
           << "DefaultFrameScheduler: calling RenderFrame presentation_time="
           << presentation_time << " frame_number=" << frame_number_;
     }
+
     // Render the frame
-    if (delegate_.frame_renderer->RenderFrame(frame_timings, presentation_time,
-                                              presentation_interval)) {
+    currently_rendering_ = delegate_.frame_renderer->RenderFrame(
+        frame_timings, presentation_time, presentation_interval);
+    if (currently_rendering_) {
       outstanding_frames_.push_back(frame_timings);
+      render_pending_ = false;
     }
   }
 
   // If necessary, schedule another frame.
-  if (!requested_presentation_times_.empty()) {
-    ScheduleFrame();
+  if (!updatable_sessions_.empty()) {
+    RequestFrame();
   }
 }
 
 void DefaultFrameScheduler::ScheduleUpdateForSession(
-    uint64_t presentation_time, scenic_impl::SessionId session_id) {
+    zx_time_t presentation_time, scenic_impl::SessionId session_id) {
   updatable_sessions_.push({.session_id = session_id,
                             .requested_presentation_time = presentation_time});
-  RequestFrame(presentation_time);
+  RequestFrame();
 }
 
 bool DefaultFrameScheduler::ApplyScheduledSessionUpdates(
-    uint64_t frame_number, uint64_t presentation_time,
-    uint64_t presentation_interval) {
+    uint64_t frame_number, zx_time_t presentation_time,
+    zx_duration_t presentation_interval) {
   FXL_DCHECK(delegate_.session_updater);
 
+  // Logging the first few frames to find common startup bugs.
   if (frame_number_ < 5) {
     FXL_LOG(INFO) << "DefaultFrameScheduler::ApplyScheduledSessionUpdates "
                      "presentation_time="
@@ -355,29 +319,10 @@
   }
   outstanding_frames_.resize(outstanding_frames_.size() - 1);
 
-  // If a frame was not scheduled due to back-pressure, try again.
-  if (back_pressure_applied_) {
-    // This will be reset if the next scheduled frame fails to render due to
-    // back-pressure.
-    back_pressure_applied_ = false;
-
-    if (!requested_presentation_times_.empty()) {
-      ScheduleFrame();
-    }
-  }
-
   if (render_continuously_) {
-    RequestFrame(0);
+    RequestFrame();
   }
 }
 
-bool DefaultFrameScheduler::TooMuchBackPressure() {
-  if (outstanding_frames_.size() >= kMaxOutstandingFrames) {
-    back_pressure_applied_ = true;
-    return true;
-  }
-  return false;
-}
-
 }  // namespace gfx
 }  // namespace scenic_impl
diff --git a/garnet/lib/ui/gfx/engine/default_frame_scheduler.h b/garnet/lib/ui/gfx/engine/default_frame_scheduler.h
index cabdebc..f4cbab4 100644
--- a/garnet/lib/ui/gfx/engine/default_frame_scheduler.h
+++ b/garnet/lib/ui/gfx/engine/default_frame_scheduler.h
@@ -7,6 +7,7 @@
 
 #include <queue>
 
+#include <lib/async/cpp/task.h>
 #include <lib/async/dispatcher.h>
 #include <lib/zx/time.h>
 #include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
@@ -23,7 +24,7 @@
 
 class DefaultFrameScheduler : public FrameScheduler {
  public:
-  explicit DefaultFrameScheduler(Display* const display);
+  explicit DefaultFrameScheduler(const Display* display);
   ~DefaultFrameScheduler();
 
   // |FrameScheduler|
@@ -42,7 +43,7 @@
   // 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,
+  void ScheduleUpdateForSession(zx_time_t presentation_time,
                                 scenic_impl::SessionId session) override;
 
  protected:
@@ -54,7 +55,7 @@
 
  private:
   // Used to compare presentation times so that the priority_queue acts as a min
-  // heap, placing the earliest PresentationTime at the top
+  // heap, placing the earliest PresentationTime at the top.
   class UpdatableSessionsComparator {
    public:
     bool operator()(SessionUpdate updatable_session1,
@@ -64,40 +65,22 @@
     }
   };
 
-  // Request a frame to be scheduled at or after |presentation_time|, which
-  // may be in the past.
-  void RequestFrame(zx_time_t presentation_time);
+  // Requests a new frame to be drawn, which schedules the next wake up time for
+  // rendering. If we've already scheduled a wake up time, it checks if it needs
+  // rescheduling and deals with it appropriately.
+  void RequestFrame();
 
   // 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,
-                        uint64_t flow_id);
+  // back-pressure if we can't hit our target frame rate. Or, the frame before
+  // this one has yet to finish rendering. Etc.
+  void MaybeRenderFrame(async_dispatcher_t*, async::TaskBase*, zx_status_t);
 
-  // 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;
-  // Computes the target presentation time for the requested presentation time.
-  std::pair<zx_time_t, zx_time_t> ComputeTargetPresentationAndWakeupTimes(
+  // Computes 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>
+  ComputePresentationAndWakeupTimesForTargetTime(
       zx_time_t requested_presentation_time) const;
 
   // Return the predicted amount of time required to render a frame.
@@ -106,30 +89,35 @@
   // 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);
+                                    zx_time_t presentation_time,
+                                    zx_duration_t presentation_interval);
 
+  // References.
   async_dispatcher_t* const dispatcher_;
   const Display* const display_;
+  FrameSchedulerDelegate delegate_;
 
-  std::priority_queue<zx_time_t, std::vector<zx_time_t>,
-                      std::greater<zx_time_t>>
-      requested_presentation_times_;
-
+  // State.
   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;
+  bool currently_rendering_ = false;
+  bool render_pending_ = false;
+  zx_time_t wakeup_time_;
+  zx_time_t next_presentation_time_;
 
-  // Lists all Session that have updates to apply, sorted by the earliest
-  // requested presentation time of each update.
+  // The async task that wakes up to start rendering.
+  async::TaskMethod<DefaultFrameScheduler,
+                    &DefaultFrameScheduler::MaybeRenderFrame>
+      frame_render_task_{this};
+
+  // Sessions that have updates to apply, sorted by requested presentation time
+  // from earliest to latest.
   std::priority_queue<SessionUpdate, std::vector<SessionUpdate>,
                       UpdatableSessionsComparator>
       updatable_sessions_;
 
-  FrameSchedulerDelegate delegate_;
-
   fxl::WeakPtrFactory<DefaultFrameScheduler> weak_factory_;  // must be last
 
   FXL_DISALLOW_COPY_AND_ASSIGN(DefaultFrameScheduler);
diff --git a/garnet/lib/ui/gfx/engine/engine.cc b/garnet/lib/ui/gfx/engine/engine.cc
index 1c58105..cd55a0e6 100644
--- a/garnet/lib/ui/gfx/engine/engine.cc
+++ b/garnet/lib/ui/gfx/engine/engine.cc
@@ -10,8 +10,8 @@
 
 #include <lib/async/cpp/task.h>
 #include <lib/async/default.h>
-#include <trace/event.h>
 #include <lib/zx/time.h>
+#include <trace/event.h>
 
 #include "garnet/lib/ui/gfx/engine/frame_scheduler.h"
 #include "garnet/lib/ui/gfx/engine/frame_timings.h"
@@ -131,8 +131,8 @@
 // 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<SessionUpdate> sessions_to_update,
-                            uint64_t frame_number, uint64_t presentation_time,
-                            uint64_t presentation_interval) {
+                            uint64_t frame_number, zx_time_t presentation_time,
+                            zx_duration_t presentation_interval) {
   auto command_context = CreateCommandContext(frame_number);
 
   bool needs_render = false;
@@ -172,8 +172,8 @@
 }
 
 bool Engine::RenderFrame(const FrameTimingsPtr& timings,
-                         uint64_t presentation_time,
-                         uint64_t presentation_interval) {
+                         zx_time_t presentation_time,
+                         zx_duration_t presentation_interval) {
   // NOTE: this name is important for benchmarking.  Do not remove or modify it
   // without also updating the "process_gfx_trace.go" script.
   TRACE_DURATION("gfx", "RenderFrame", "frame_number", timings->frame_number(),
diff --git a/garnet/lib/ui/gfx/engine/engine.h b/garnet/lib/ui/gfx/engine/engine.h
index f0c1b24..217b3f1 100644
--- a/garnet/lib/ui/gfx/engine/engine.h
+++ b/garnet/lib/ui/gfx/engine/engine.h
@@ -127,14 +127,14 @@
   // 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<SessionUpdate> sessions_to_update,
-                      uint64_t frame_number, uint64_t presentation_time,
-                      uint64_t presentation_interval) override;
+                      uint64_t frame_number, zx_time_t presentation_time,
+                      zx_duration_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;
+  bool RenderFrame(const FrameTimingsPtr& frame, zx_time_t presentation_time,
+                   zx_duration_t presentation_interval) override;
 
  protected:
   // Only used by subclasses used in testing.
diff --git a/garnet/lib/ui/gfx/engine/frame_scheduler.h b/garnet/lib/ui/gfx/engine/frame_scheduler.h
index 4d8047b..32f55de 100644
--- a/garnet/lib/ui/gfx/engine/frame_scheduler.h
+++ b/garnet/lib/ui/gfx/engine/frame_scheduler.h
@@ -17,7 +17,7 @@
 
 class FrameTimings;
 using FrameTimingsPtr = fxl::RefPtr<FrameTimings>;
-using PresentationTime = uint64_t;
+using PresentationTime = zx_time_t;
 
 // Interface for performing session updates.
 class SessionUpdater {
@@ -33,8 +33,9 @@
   // session in |sessions|. Returns true if any updates were applied, false
   // otherwise.
   virtual bool UpdateSessions(std::vector<SessionUpdate> sessions_to_update,
-                              uint64_t frame_number, uint64_t presentation_time,
-                              uint64_t presentation_interval) = 0;
+                              uint64_t frame_number,
+                              zx_time_t presentation_time,
+                              zx_duration_t presentation_interval) = 0;
 };
 
 // Interface for rendering frames.
@@ -53,8 +54,8 @@
   // TODO(SCN-1089): these return value semantics are not ideal.  See comments
   // in Engine::RenderFrame() regarding this same issue.
   virtual bool RenderFrame(const FrameTimingsPtr& frame_timings,
-                           uint64_t presentation_time,
-                           uint64_t presentation_interval) = 0;
+                           zx_time_t presentation_time,
+                           zx_duration_t presentation_interval) = 0;
 };
 
 struct FrameSchedulerDelegate {
@@ -84,7 +85,7 @@
   // 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,
+  virtual void ScheduleUpdateForSession(zx_time_t presentation_time,
                                         scenic_impl::SessionId session) = 0;
 
  protected:
diff --git a/garnet/lib/ui/gfx/tests/default_frame_scheduler_unittest.cc b/garnet/lib/ui/gfx/tests/default_frame_scheduler_unittest.cc
index 4964efa..a999678 100644
--- a/garnet/lib/ui/gfx/tests/default_frame_scheduler_unittest.cc
+++ b/garnet/lib/ui/gfx/tests/default_frame_scheduler_unittest.cc
@@ -37,7 +37,7 @@
 
   // Schedule an update for in between the next two vsyncs.
   zx_time_t time_after_vsync = fake_display_->GetLastVsyncTime() +
-                          (1.5 * fake_display_->GetVsyncInterval());
+                               (1.5 * fake_display_->GetVsyncInterval());
 
   scheduler->ScheduleUpdateForSession(/* presentation time*/ time_after_vsync,
                                       /* session id */ 1);
@@ -59,6 +59,155 @@
   EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
 }
 
+TEST_F(FrameSchedulerTest, SinglePresent_ShouldGetSingleRenderCall) {
+  auto scheduler = CreateDefaultFrameScheduler();
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  scheduler->ScheduleUpdateForSession(Now().get(), /* session id */ 1);
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  // Wait for one vsync period.
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+  mock_renderer_->EndFrame();
+
+  // Present should have been scheduled and handled.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+
+  // Wait for a very long time.
+  RunLoopFor(zx::sec(10));
+  mock_renderer_->EndFrame();
+
+  // No further render calls should have been made.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+}
+
+TEST_F(FrameSchedulerTest, PresentsForTheSameFrame_ShouldGetSingleRenderCall) {
+  auto scheduler = CreateDefaultFrameScheduler();
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  SessionId session_id1 = 1;
+  SessionId session_id2 = 2;
+
+  // Schedule two updates for now.
+  zx_time_t now = Now().get();
+  scheduler->ScheduleUpdateForSession(now, session_id1);
+  scheduler->ScheduleUpdateForSession(now, session_id2);
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  // Wait for one vsync period.
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+  mock_renderer_->EndFrame();
+
+  // Both Presents should have been scheduled and handled.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+  ASSERT_EQ(mock_updater_->last_requested_updates().size(), 2u);
+  EXPECT_EQ(
+      mock_updater_->last_requested_updates()[0].requested_presentation_time,
+      now);
+
+  // Wait for a very long time.
+  RunLoopFor(zx::sec(10));
+  mock_renderer_->EndFrame();
+
+  // No further render calls should have been made.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+}
+
+TEST_F(FrameSchedulerTest,
+       PresentsForDifferentFrames_ShouldGetSeparateRenderCalls) {
+  auto scheduler = CreateDefaultFrameScheduler();
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  SessionId session_id = 1;
+
+  // Schedule an update for now.
+  zx_time_t now = Now().get();
+  scheduler->ScheduleUpdateForSession(now, session_id);
+
+  // Schedule an update for in between the next two vsyncs.
+  zx_time_t time_after_vsync = fake_display_->GetLastVsyncTime() +
+                               (1.5 * fake_display_->GetVsyncInterval());
+  scheduler->ScheduleUpdateForSession(time_after_vsync, session_id);
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  // Wait for one vsync period.
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+  mock_renderer_->EndFrame();
+
+  // First Present should have been scheduled and handled.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+  ASSERT_EQ(mock_updater_->last_requested_updates().size(), 1u);
+  EXPECT_EQ(
+      mock_updater_->last_requested_updates()[0].requested_presentation_time,
+      now);
+
+  // Wait for one more vsync period.
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+  mock_renderer_->EndFrame();
+
+  // Second Present should have been scheduled and handled.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 2u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 2u);
+  ASSERT_EQ(mock_updater_->last_requested_updates().size(), 1u);
+  EXPECT_EQ(
+      mock_updater_->last_requested_updates()[0].requested_presentation_time,
+      time_after_vsync);
+}
+
+TEST_F(FrameSchedulerTest,
+       SecondPresentDuringRender_ShouldApplyUpdatesAndReschedule) {
+  auto scheduler = CreateDefaultFrameScheduler();
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 0u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 0u);
+
+  SessionId session_id = 1;
+
+  // Schedule an update for now.
+  zx_time_t now = Now().get();
+  scheduler->ScheduleUpdateForSession(now, session_id);
+
+  // Wait for one vsync period.
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 1u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+
+  // Schedule another update for now.
+  scheduler->ScheduleUpdateForSession(now, session_id);
+
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+
+  // Updates should be applied, but not rendered.
+  EXPECT_EQ(mock_updater_->update_sessions_call_count(), 2u);
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 1u);
+
+  // End previous frame.
+  mock_renderer_->EndFrame();
+
+  RunLoopFor(zx::duration(fake_display_->GetVsyncInterval()));
+
+  // Second render should have occured.
+  EXPECT_EQ(mock_renderer_->render_frame_call_count(), 2u);
+}
+
 }  // namespace test
 }  // namespace gfx
 }  // namespace scenic_impl
diff --git a/garnet/lib/ui/gfx/tests/frame_scheduler_mocks.h b/garnet/lib/ui/gfx/tests/frame_scheduler_mocks.h
index 1b42641..98fecf2 100644
--- a/garnet/lib/ui/gfx/tests/frame_scheduler_mocks.h
+++ b/garnet/lib/ui/gfx/tests/frame_scheduler_mocks.h
@@ -35,9 +35,10 @@
   MockSessionUpdater() : weak_factory_(this) {}
 
   bool UpdateSessions(std::vector<SessionUpdate> sessions_to_update,
-                      uint64_t frame_number, uint64_t presentation_time,
-                      uint64_t presentation_interval) override {
+                      uint64_t frame_number, zx_time_t presentation_time,
+                      zx_duration_t presentation_interval) override {
     ++update_sessions_call_count_;
+    last_requested_updates_ = std::move(sessions_to_update);
     return update_sessions_return_value_;
   };
 
@@ -46,6 +47,9 @@
     update_sessions_return_value_ = new_value;
   }
   uint32_t update_sessions_call_count() { return update_sessions_call_count_; }
+  const std::vector<SessionUpdate>& last_requested_updates() {
+    return last_requested_updates_;
+  }
 
   fxl::WeakPtr<MockSessionUpdater> GetWeakPtr() {
     return weak_factory_.GetWeakPtr();
@@ -54,6 +58,7 @@
  private:
   bool update_sessions_return_value_ = true;
   uint32_t update_sessions_call_count_ = 0;
+  std::vector<SessionUpdate> last_requested_updates_;
 
   fxl::WeakPtrFactory<MockSessionUpdater> weak_factory_;  // must be last
 };
@@ -63,8 +68,8 @@
   MockFrameRenderer() : weak_factory_(this) {}
 
   bool RenderFrame(const FrameTimingsPtr& frame_timings,
-                   uint64_t presentation_time,
-                   uint64_t presentation_interval) override {
+                   zx_time_t presentation_time,
+                   zx_duration_t presentation_interval) override {
     ++render_frame_call_count_;
     last_frame_timings_ = frame_timings.get();
     return render_frame_return_value_;
@@ -74,7 +79,7 @@
   // FrameScheduler, but is not valid to do until after RenderFrame has returned
   // to FrameScheduler. Hence this separate method.
   void EndFrame() {
-    if(last_frame_timings_) {
+    if (last_frame_timings_) {
       last_frame_timings_->AddSwapchain(nullptr);
       last_frame_timings_->OnFrameRendered(/*swapchain index*/ 0, /*time*/ 1);
       last_frame_timings_->OnFramePresented(/*swapchain index*/ 0, /*time*/ 1);