[flatland] Handle fence overflow in flatland_connection.cc (#53366)

flatland_connection.cc used to allow an arbitrary number of acquire and release fences to be scheduled for each frame.

Sadly, Fuchsia has a limitation of (1) the number of total handles that can be sent per a FIDL call, but also (2) the Flatland protocol only supports sending up to 16 fences per each fence type.

Now, normally there should be very few scheduled fences per frame. But if frames get skipped, we could amass many fences which would then crash our attempts to send all of them to the Flatland `Present` endpoint.

This change introduces two fence multiplexer, which allow us to signal more than 16 fences per type, at a performance penalty. We expect to be able *not* to crash the FIDL subsystem using this approach, and may even be able to hobble along for a bit, until the fences issue is hopefully self-resolved.

That said, this issue seems to indicate there are frame scheduling problems elsewhere. But this is a fairly straightforward change to make without affecting the rest of the flatland code or integration, so we opt to do that first.

Issues: [#150136](https://github.com/flutter/engine/pull/53366)

- [] I updated/added relevant documentation (doc comments with `///`).
diff --git a/shell/platform/fuchsia/flutter/flatland_connection.cc b/shell/platform/fuchsia/flutter/flatland_connection.cc
index 6a45b8b..6a0b6dd 100644
--- a/shell/platform/fuchsia/flutter/flatland_connection.cc
+++ b/shell/platform/fuchsia/flutter/flatland_connection.cc
@@ -4,10 +4,16 @@
 
 #include "flatland_connection.h"
 
+#include <lib/async/cpp/task.h>
+#include <lib/async/default.h>
+
+#include <zircon/rights.h>
 #include <zircon/status.h>
+#include <zircon/types.h>
+
+#include <utility>
 
 #include "flutter/fml/logging.h"
-#include "flutter/fml/trace_event.h"
 
 namespace flutter_runner {
 
@@ -22,12 +28,14 @@
 }  // namespace
 
 FlatlandConnection::FlatlandConnection(
-    std::string debug_label,
+    const std::string& debug_label,
     fuchsia::ui::composition::FlatlandHandle flatland,
     fml::closure error_callback,
-    on_frame_presented_event on_frame_presented_callback)
-    : flatland_(flatland.Bind()),
-      error_callback_(error_callback),
+    on_frame_presented_event on_frame_presented_callback,
+    async_dispatcher_t* dispatcher)
+    : dispatcher_(dispatcher),
+      flatland_(flatland.Bind()),
+      error_callback_(std::move(error_callback)),
       on_frame_presented_callback_(std::move(on_frame_presented_callback)) {
   flatland_.set_error_handler([callback = error_callback_](zx_status_t status) {
     FML_LOG(ERROR) << "Flatland disconnected: " << zx_status_get_string(status);
@@ -71,6 +79,42 @@
   fuchsia::ui::composition::PresentArgs present_args;
   present_args.set_requested_presentation_time(0);
   present_args.set_acquire_fences(std::move(acquire_fences_));
+
+  // Schedule acquire fence overflow signaling if there is one.
+  if (acquire_overflow_ != nullptr) {
+    FML_CHECK(acquire_overflow_->event_.is_valid());
+    async::PostTask(dispatcher_, [dispatcher = dispatcher_,
+                                  overflow = acquire_overflow_]() {
+      const size_t fences_size = overflow->fences_.size();
+      std::shared_ptr<size_t> fences_completed = std::make_shared<size_t>(0);
+      std::shared_ptr<std::vector<async::WaitOnce>> closures;
+
+      for (auto i = 0u; i < fences_size; i++) {
+        auto wait = std::make_unique<async::WaitOnce>(
+            overflow->fences_[i].get(), ZX_EVENT_SIGNALED, 0u);
+        auto wait_ptr = wait.get();
+        wait_ptr->Begin(
+            dispatcher,
+            [wait = std::move(wait), overflow, fences_size, fences_completed,
+             closures](async_dispatcher_t*, async::WaitOnce*,
+                       zx_status_t status, const zx_packet_signal_t*) {
+              (*fences_completed)++;
+              FML_CHECK(status == ZX_OK)
+                  << "status: " << zx_status_get_string(status);
+              if (*fences_completed == fences_size) {
+                // Signal the acquire fence passed on to Flatland.
+                const zx_status_t status =
+                    overflow->event_.signal(0, ZX_EVENT_SIGNALED);
+                FML_CHECK(status == ZX_OK)
+                    << "status: " << zx_status_get_string(status);
+              }
+            });
+      }
+    });
+    acquire_overflow_.reset();
+  }
+  FML_CHECK(acquire_overflow_ == nullptr);
+
   present_args.set_release_fences(std::move(previous_present_release_fences_));
   // Frame rate over latency.
   present_args.set_unsquashable(true);
@@ -81,6 +125,44 @@
   // the correct ones for VulkanSurface's interpretation.
   previous_present_release_fences_.clear();
   previous_present_release_fences_.swap(current_present_release_fences_);
+  previous_release_overflow_ = current_release_overflow_;
+  current_release_overflow_ = nullptr;
+
+  // Similar to the treatment of acquire_fences_overflow_ above. Except in
+  // the other direction.
+  if (previous_release_overflow_ != nullptr) {
+    FML_CHECK(previous_release_overflow_->event_.is_valid());
+
+    std::shared_ptr<Overflow> fences = previous_release_overflow_;
+
+    async::PostTask(dispatcher_, [dispatcher = dispatcher_,
+                                  fences = previous_release_overflow_]() {
+      FML_CHECK(fences != nullptr);
+      FML_CHECK(fences->event_.is_valid());
+
+      auto wait = std::make_unique<async::WaitOnce>(fences->event_.get(),
+                                                    ZX_EVENT_SIGNALED, 0u);
+      auto wait_ptr = wait.get();
+
+      wait_ptr->Begin(
+          dispatcher, [_wait = std::move(wait), fences](
+                          async_dispatcher_t*, async::WaitOnce*,
+                          zx_status_t status, const zx_packet_signal_t*) {
+            FML_CHECK(status == ZX_OK)
+                << "status: " << zx_status_get_string(status);
+
+            // Multiplex signaling all events.
+            for (auto& event : fences->fences_) {
+              const zx_status_t status = event.signal(0, ZX_EVENT_SIGNALED);
+              FML_CHECK(status == ZX_OK)
+                  << "status: " << zx_status_get_string(status);
+            }
+          });
+    });
+    previous_release_overflow_ = nullptr;
+  }
+  FML_CHECK(previous_release_overflow_ == nullptr);  // Moved.
+
   acquire_fences_.clear();
 }
 
@@ -93,12 +175,13 @@
   const auto now = fml::TimePoint::Now();
 
   // Initial case.
-  if (MaybeRunInitialVsyncCallback(now, callback))
+  if (MaybeRunInitialVsyncCallback(now, callback)) {
     return;
+  }
 
   // Throttle case.
   if (threadsafe_state_.present_credits_ == 0) {
-    threadsafe_state_.pending_fire_callback_ = callback;
+    threadsafe_state_.pending_fire_callback_ = std::move(callback);
     return;
   }
 
@@ -116,8 +199,9 @@
   const auto now = fml::TimePoint::Now();
 
   // Initial case.
-  if (MaybeRunInitialVsyncCallback(now, callback))
+  if (MaybeRunInitialVsyncCallback(now, callback)) {
     return;
+  }
 
   // Regular case.
   RunVsyncCallback(now, callback);
@@ -260,14 +344,75 @@
   callback(frame_start, frame_end);
 }
 
+// Enqueue a single fence into either the "base" vector of fences, or a
+// "special" overflow multiplexer.
+//
+// Args:
+//   - fence: the fence to add
+//   - fences: the "regular" fences vector to add to.
+//   - overflow: the overflow fences vector. Fences added here if there are
+//     more than can fit in `fences`.
+static void Enqueue(zx::event fence,
+                    std::vector<zx::event>* fences,
+                    std::shared_ptr<Overflow>* overflow) {
+  constexpr size_t kMaxFences =
+      fuchsia::ui::composition::MAX_ACQUIRE_RELEASE_FENCE_COUNT;
+
+  // Number of all previously added fences, plus this one.
+  const auto num_all_fences =
+      fences->size() + 1 +
+      ((*overflow == nullptr) ? 0 : (*overflow)->fences_.size());
+
+  // If more than max number of fences come in, schedule any further fences into
+  // an overflow. The overflow fences are scheduled for processing here, but are
+  // processed in DoPresent().
+  if (num_all_fences <= kMaxFences) {
+    fences->push_back(std::move(fence));
+  } else if (num_all_fences == kMaxFences + 1) {
+    // The ownership of the overflow will be handed over to the signaling
+    // closure on DoPresent call. So we always expect that we enter here with
+    // overflow not set.
+    FML_CHECK((*overflow) == nullptr) << "overflow is still active";
+    *overflow = std::make_shared<Overflow>();
+
+    // Set up the overflow fences. Creates an overflow handle, places it
+    // into `fences` instead of the previous fence, and puts the prior fence
+    // and this one into overflow.
+    zx::event overflow_handle = std::move(fences->back());
+    fences->pop_back();
+
+    zx::event overflow_fence;
+    zx_status_t status = zx::event::create(0, &overflow_fence);
+    FML_CHECK(status == ZX_OK) << "status: " << zx_status_get_string(status);
+
+    // Every DoPresent should invalidate this handle.  Holler if not.
+    FML_CHECK(!(*overflow)->event_.is_valid()) << "overflow valid";
+    status =
+        overflow_fence.duplicate(ZX_RIGHT_SAME_RIGHTS, &(*overflow)->event_);
+    FML_CHECK(status == ZX_OK) << "status: " << zx_status_get_string(status);
+    fences->push_back(std::move(overflow_fence));
+
+    // Prepare for wait_many call.
+    (*overflow)->fences_.push_back(std::move(overflow_handle));
+    (*overflow)->fences_.push_back(std::move(fence));
+
+    FML_LOG(INFO) << "Enqueue using fence overflow, expect a performance hit.";
+  } else {
+    FML_CHECK((*overflow) != nullptr);
+    // Just add to the overflow fences.
+    (*overflow)->fences_.push_back(std::move(fence));
+  }
+}
+
 // This method is called from the raster thread.
 void FlatlandConnection::EnqueueAcquireFence(zx::event fence) {
-  acquire_fences_.push_back(std::move(fence));
+  Enqueue(std::move(fence), &acquire_fences_, &acquire_overflow_);
 }
 
 // This method is called from the raster thread.
 void FlatlandConnection::EnqueueReleaseFence(zx::event fence) {
-  current_present_release_fences_.push_back(std::move(fence));
+  Enqueue(std::move(fence), &current_present_release_fences_,
+          &current_release_overflow_);
 }
 
 }  // namespace flutter_runner
diff --git a/shell/platform/fuchsia/flutter/flatland_connection.h b/shell/platform/fuchsia/flutter/flatland_connection.h
index 59c67e1..37cb47b 100644
--- a/shell/platform/fuchsia/flutter/flatland_connection.h
+++ b/shell/platform/fuchsia/flutter/flatland_connection.h
@@ -2,10 +2,14 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+// Maintains a connection to Fuchsia's Flatland protocol used for rendering
+// 2D graphics scenes.
+
 #ifndef FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_FLATLAND_CONNECTION_H_
 #define FLUTTER_SHELL_PLATFORM_FUCHSIA_FLUTTER_FLATLAND_CONNECTION_H_
 
 #include <fuchsia/ui/composition/cpp/fidl.h>
+#include <lib/async/default.h>
 
 #include "flutter/fml/closure.h"
 #include "flutter/fml/macros.h"
@@ -21,6 +25,19 @@
 
 namespace flutter_runner {
 
+// The maximum number of fences that can be signaled at a time.
+static constexpr size_t kMaxFences =
+    fuchsia::ui::composition::MAX_ACQUIRE_RELEASE_FENCE_COUNT;
+
+// A helper to ferry around multiplexed events for signaling. Helps move
+// non-copyable events into closures.
+class Overflow {
+ public:
+  std::vector<zx::event> fences_;
+  zx::event event_;
+  Overflow() { fences_.reserve(kMaxFences); };
+};
+
 using on_frame_presented_event =
     std::function<void(fuchsia::scenic::scheduling::FramePresentedInfo)>;
 
@@ -33,10 +50,12 @@
 // maintaining the Flatland instance connection and presenting updates.
 class FlatlandConnection final {
  public:
-  FlatlandConnection(std::string debug_label,
-                     fuchsia::ui::composition::FlatlandHandle flatland,
-                     fml::closure error_callback,
-                     on_frame_presented_event on_frame_presented_callback);
+  FlatlandConnection(
+      const std::string& debug_label,
+      fuchsia::ui::composition::FlatlandHandle flatland,
+      fml::closure error_callback,
+      on_frame_presented_event on_frame_presented_callback,
+      async_dispatcher_t* dispatcher = async_get_default_dispatcher());
 
   ~FlatlandConnection();
 
@@ -58,7 +77,24 @@
     return {++next_content_id_};
   }
 
+  // Adds a new acquire fence to be sent out to the next Present() call.
+  //
+  // Acquire fences must all be signaled by the user.
+  //
+  // PERFORMANCE NOTES:
+  //
+  // Enqueuing more than 16 fences per frame incurs a performance penalty, so
+  // use them sparingly.
+  //
+  // Skipped frames may cause the number of fences to increase, leading to
+  // more performance issues. Ideally, the flow of the frames should be smooth.
   void EnqueueAcquireFence(zx::event fence);
+
+  // Adds a new release fence to be sent out to the next Present() call.
+  //
+  // Release fences are all signaled by Scenic (Flatland server).
+  //
+  // See the performance notes on EnqueueAcquireFence for performance details.
   void EnqueueReleaseFence(zx::event fence);
 
  private:
@@ -75,6 +111,8 @@
   void RunVsyncCallback(const fml::TimePoint& now,
                         FireCallbackCallback& callback);
 
+  async_dispatcher_t* dispatcher_;
+
   fuchsia::ui::composition::FlatlandPtr flatland_;
 
   fml::closure error_callback_;
@@ -102,9 +140,21 @@
     bool first_feedback_received_ = false;
   } threadsafe_state_;
 
+  // Acquire fences sent to Flatland.
   std::vector<zx::event> acquire_fences_;
+
+  // Multiplexed acquire fences over the critical number of ~16.
+  std::shared_ptr<Overflow> acquire_overflow_;
+
+  // Release fences sent to Flatland. Similar to acquire fences above.
   std::vector<zx::event> current_present_release_fences_;
+  std::shared_ptr<Overflow> current_release_overflow_;
+
+  // Release fences from the prior call to DoPresent().
+  // Similar to acquire fences above.
   std::vector<zx::event> previous_present_release_fences_;
+  std::shared_ptr<Overflow> previous_release_overflow_;
+
   std::string debug_label_;
 
   FML_DISALLOW_COPY_AND_ASSIGN(FlatlandConnection);
diff --git a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
index 8322275..db8dc67 100644
--- a/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
+++ b/shell/platform/fuchsia/flutter/tests/fakes/scenic/fake_flatland.cc
@@ -41,7 +41,7 @@
 }
 
 void FakeFlatland::Disconnect(fuchsia::ui::composition::FlatlandError error) {
-  flatland_binding_.events().OnError(std::move(error));
+  flatland_binding_.events().OnError(error);
   flatland_binding_.Unbind();
   allocator_binding_
       .Unbind();  // TODO(fxb/85619): Does the real Scenic unbind this when
diff --git a/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc b/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc
index f70afc9..affbf45 100644
--- a/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc
+++ b/shell/platform/fuchsia/flutter/tests/flatland_connection_unittests.cc
@@ -8,6 +8,8 @@
 #include <fuchsia/ui/composition/cpp/fidl.h>
 #include <lib/async-testing/test_loop.h>
 #include <lib/async/cpp/task.h>
+#include <zircon/rights.h>
+#include <zircon/types.h>
 
 #include <string>
 #include <vector>
@@ -31,9 +33,8 @@
                        bool& condition_variable,
                        fml::TimeDelta expected_frame_delta) {
   flatland_connection.AwaitVsync(
-      [&condition_variable,
-       expected_frame_delta = std::move(expected_frame_delta)](
-          fml::TimePoint frame_start, fml::TimePoint frame_end) {
+      [&condition_variable, expected_frame_delta](fml::TimePoint frame_start,
+                                                  fml::TimePoint frame_end) {
         EXPECT_EQ(frame_end.ToEpochDelta() - frame_start.ToEpochDelta(),
                   expected_frame_delta);
         condition_variable = true;
@@ -44,8 +45,8 @@
                        bool& condition_variable,
                        fml::TimePoint expected_frame_end) {
   flatland_connection.AwaitVsync(
-      [&condition_variable, expected_frame_end = std::move(expected_frame_end)](
-          fml::TimePoint frame_start, fml::TimePoint frame_end) {
+      [&condition_variable, expected_frame_end](fml::TimePoint frame_start,
+                                                fml::TimePoint frame_end) {
         EXPECT_EQ(frame_end, expected_frame_end);
         condition_variable = true;
       });
@@ -78,6 +79,10 @@
 
   async::TestLoop& loop() { return loop_; }
 
+  async_dispatcher_t* subloop_dispatcher() {
+    return session_subloop_->dispatcher();
+  }
+
   FakeFlatland& fake_flatland() { return fake_flatland_; }
 
   fidl::InterfaceHandle<fuchsia::ui::composition::Flatland>
@@ -122,7 +127,7 @@
   const std::string debug_name = GetCurrentTestName();
   flutter_runner::FlatlandConnection flatland_connection(
       debug_name, TakeFlatlandHandle(), []() { FAIL(); },
-      [](auto...) { FAIL(); });
+      [](auto...) { FAIL(); }, loop().dispatcher());
   EXPECT_EQ(fake_flatland().debug_name(), "");
 
   // Simulate an AwaitVsync that returns immediately.
@@ -145,7 +150,7 @@
   // completed yet.
   flutter_runner::FlatlandConnection flatland_connection(
       GetCurrentTestName(), TakeFlatlandHandle(), std::move(on_session_error),
-      [](auto...) { FAIL(); });
+      [](auto...) { FAIL(); }, loop().dispatcher());
   EXPECT_FALSE(error_fired);
 
   // Simulate a flatland disconnection, then Pump the loop.  The error callback
@@ -179,7 +184,7 @@
   // completed yet.
   flutter_runner::FlatlandConnection flatland_connection(
       GetCurrentTestName(), TakeFlatlandHandle(), []() { FAIL(); },
-      std::move(on_frame_presented));
+      std::move(on_frame_presented), loop().dispatcher());
   EXPECT_EQ(presents_called, 0u);
   EXPECT_EQ(vsyncs_handled, 0u);
 
@@ -198,7 +203,7 @@
   // release fence should be queued.
   await_vsync_fired = false;
   zx::event first_release_fence;
-  zx::event::create(0, &first_release_fence);
+  zx::event::create(0u, &first_release_fence);
   const zx_handle_t first_release_fence_handle = first_release_fence.get();
   flatland_connection.EnqueueReleaseFence(std::move(first_release_fence));
   flatland_connection.Present();
@@ -252,7 +257,7 @@
   // completed yet.
   flutter_runner::FlatlandConnection flatland_connection(
       GetCurrentTestName(), TakeFlatlandHandle(), []() { FAIL(); },
-      [](auto...) {});
+      [](auto...) {}, loop().dispatcher());
   EXPECT_EQ(presents_called, 0u);
 
   // Pump the loop. Nothing is called.
@@ -292,7 +297,7 @@
   // completed yet.
   flutter_runner::FlatlandConnection flatland_connection(
       GetCurrentTestName(), TakeFlatlandHandle(), []() { FAIL(); },
-      std::move(on_frame_presented));
+      std::move(on_frame_presented), loop().dispatcher());
   EXPECT_EQ(presents_called, 0u);
   EXPECT_EQ(vsyncs_handled, 0u);
 
@@ -365,7 +370,7 @@
   on_frame_presented_event on_frame_presented = [](auto...) {};
   flutter_runner::FlatlandConnection flatland_connection(
       GetCurrentTestName(), TakeFlatlandHandle(), []() { FAIL(); },
-      std::move(on_frame_presented));
+      std::move(on_frame_presented), loop().dispatcher());
   EXPECT_EQ(num_presents_called, 0u);
 
   // Pump the loop. Nothing is called.
@@ -385,9 +390,9 @@
                            fml::TimePoint frame_end) {
     async::PostTask(dispatcher, [&flatland_connection]() {
       zx::event acquire_fence;
-      zx::event::create(0, &acquire_fence);
+      zx::event::create(0u, &acquire_fence);
       zx::event release_fence;
-      zx::event::create(0, &release_fence);
+      zx::event::create(0u, &release_fence);
       flatland_connection.EnqueueAcquireFence(std::move(acquire_fence));
       flatland_connection.EnqueueReleaseFence(std::move(release_fence));
       flatland_connection.Present();
@@ -483,4 +488,162 @@
   EXPECT_EQ(num_release_fences, num_onfb);
 }
 
+typedef struct {
+  std::shared_ptr<std::vector<zx::event>> fences;
+  std::shared_ptr<std::vector<zx::event>> fences_dup;
+} FencesPair;
+
+// Create two vectors of paired fences.
+FencesPair GetFencesPair(size_t num_fences) {
+  auto fences = std::make_shared<std::vector<zx::event>>();
+  auto fences_dup = std::make_shared<std::vector<zx::event>>();
+  for (size_t i = 0; i < num_fences; i++) {
+    zx::event fence;
+    auto status = zx::event::create(0u, &fence);
+    EXPECT_EQ(status, ZX_OK);
+
+    zx::event fence_dup;
+    status = fence.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence_dup);
+    EXPECT_EQ(status, ZX_OK);
+
+    fences->push_back(std::move(fence));
+    fences_dup->push_back(std::move(fence_dup));
+  }
+  return FencesPair{
+      .fences = fences,
+      .fences_dup = fences_dup,
+  };
+}
+
+void SignalAll(std::vector<zx::event>* fences) {
+  for (auto& fence : *fences) {
+    const auto status = fence.signal(0, ZX_EVENT_SIGNALED);
+    ASSERT_EQ(status, ZX_OK);
+  }
+}
+
+void WaitAll(std::vector<zx::event>* fences) {
+  for (auto& fence : *fences) {
+    zx_signals_t ignored;
+    // Maybe the timeout here should be finite.
+    const auto status =
+        fence.wait_one(ZX_EVENT_SIGNALED, zx::time::infinite(), &ignored);
+    ASSERT_EQ(status, ZX_OK);
+  }
+}
+
+TEST_F(FlatlandConnectionTest, FenceStuffing) {
+  // Set up callbacks which allow sensing of how many presents were handled.
+  size_t num_presents_called = 0u;
+  size_t num_release_fences = 0u;
+  size_t num_acquire_fences = 0u;
+
+  auto reset_test_counters = [&num_presents_called, &num_acquire_fences,
+                              &num_release_fences]() {
+    num_presents_called = 0u;
+    num_release_fences = 0u;
+    num_acquire_fences = 0u;
+  };
+
+  fuchsia::ui::composition::PresentArgs last_present_args;
+  fake_flatland().SetPresentHandler(
+      [&num_presents_called, &num_acquire_fences, &num_release_fences,
+       &last_present_args](fuchsia::ui::composition::PresentArgs present_args) {
+        num_presents_called++;
+        num_acquire_fences = present_args.acquire_fences().size();
+        num_release_fences = present_args.release_fences().size();
+
+        last_present_args = std::move(present_args);
+      });
+
+  // Create the FlatlandConnection but don't pump the loop.  No FIDL calls are
+  // completed yet.
+  on_frame_presented_event on_frame_presented = [](auto...) {};
+  flutter_runner::FlatlandConnection flatland_connection(
+      GetCurrentTestName(), TakeFlatlandHandle(), []() { FAIL(); },
+      std::move(on_frame_presented), subloop_dispatcher());
+  EXPECT_EQ(num_presents_called, 0u);
+
+  // Pump the loop. Nothing is called.
+  loop().RunUntilIdle();
+  EXPECT_EQ(num_presents_called, 0u);
+
+  // Simulate an AwaitVsync that comes before the first Present.
+  flatland_connection.AwaitVsync([](fml::TimePoint, fml::TimePoint) {});
+  loop().RunUntilIdle();
+  EXPECT_EQ(num_presents_called, 0u);
+
+  constexpr size_t kMaxFences = 16;
+
+  // We must signal these.
+  FencesPair acquire = GetFencesPair(kMaxFences + 1);
+  // Flatland will signal these.
+  FencesPair release = GetFencesPair(kMaxFences + 1);
+
+  auto fire_callback = [dispatcher = loop().dispatcher(), &flatland_connection,
+                        rfd = release.fences_dup, afd = acquire.fences_dup](
+                           fml::TimePoint frame_start,
+                           fml::TimePoint frame_end) mutable {
+    async::PostTask(dispatcher, [&flatland_connection, rf = rfd, af = afd]() {
+      for (auto& fence : *rf) {
+        zx::event fence_dup;
+        const auto status = fence.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence_dup);
+        ASSERT_EQ(status, ZX_OK);
+        flatland_connection.EnqueueReleaseFence(std::move(fence_dup));
+      }
+      for (auto& fence : *af) {
+        zx::event fence_dup;
+        const auto status = fence.duplicate(ZX_RIGHT_SAME_RIGHTS, &fence_dup);
+        ASSERT_EQ(status, ZX_OK);
+        flatland_connection.EnqueueAcquireFence(std::move(fence_dup));
+      }
+      flatland_connection.Present();
+    });
+  };
+
+  SignalAll(acquire.fences.get());
+
+  // Call Await Vsync with a callback that triggers Present and consumes the one
+  // and only present credit we start with.
+  reset_test_counters();
+  flatland_connection.AwaitVsync(fire_callback);
+
+  loop().RunUntilIdle();
+
+  EXPECT_EQ(num_presents_called, 1u);
+  EXPECT_EQ(num_acquire_fences, 16u);
+  EXPECT_EQ(num_release_fences, 0u);
+
+  // Move on to next present call. Reset all the expectations and callbacks.
+  reset_test_counters();
+  OnNextFrameBegin(1);
+  // Replenish present credits.
+  loop().RunUntilIdle();
+
+  flatland_connection.AwaitVsync([dispatcher = subloop_dispatcher(),
+                                  &flatland_connection](fml::TimePoint,
+                                                        fml::TimePoint) {
+    async::PostTask(dispatcher,
+                    [&flatland_connection] { flatland_connection.Present(); });
+  });
+  loop().RunUntilIdle();
+
+  // Simulate Flatland signaling all release fences. Note that the set of
+  // release fences here is only the first ~15 of the fences, the rest are
+  // released indirectly via the overflow mechanism.
+  SignalAll(last_present_args.mutable_release_fences());
+
+  loop().RunUntilIdle();
+
+  // At this point all release fences from prior frame should have been released
+  // by Flatland.
+  EXPECT_EQ(num_presents_called, 1u);
+  EXPECT_EQ(num_acquire_fences, 0u);
+  EXPECT_EQ(num_release_fences, 16u);
+
+  // Prove that all release fences have been signaled. If not, this will block
+  // forever.
+  WaitAll(release.fences.get());
+}
+
 }  // namespace flutter_runner::testing
diff --git a/tools/fuchsia/devshell/run_unit_tests.sh b/tools/fuchsia/devshell/run_unit_tests.sh
index da63fbd..573ac47 100755
--- a/tools/fuchsia/devshell/run_unit_tests.sh
+++ b/tools/fuchsia/devshell/run_unit_tests.sh
@@ -83,7 +83,13 @@
 for test_package in $test_packages
 do
   engine-info "... publishing ${test_package} ..."
-  ${FUCHSIA_DIR}/.jiri_root/bin/ffx repository publish $FUCHSIA_DIR/$(cat $FUCHSIA_DIR/.fx-build-dir)/amber-files --package-archive "${test_package}"
+  (
+    # ffx can not be called outside of FUCHSIA_DIR.
+    cd ${FUCHSIA_DIR}
+    ${FUCHSIA_DIR}/.jiri_root/bin/ffx repository publish \
+        $FUCHSIA_DIR/$(cat $FUCHSIA_DIR/.fx-build-dir)/amber-files \
+        --package-archive "${test_package}"
+  )
   test_names+=("$(basename ${test_package} | sed -e "s/-0.far//")")
 done