SurfaceFlinger: add explicit eEarlyWakeup start and end

The current handling of eEarlyWakeup is using 2 frames of timeout
in SurfaceFlinger to know when we are no longer in early offset config.
There are few cases where a transaction is dropped or delayed (usually
caused by the offset change itself) which results in switching back
and forth from early offset to non-early offset. This change adds two
new flags, eExplicitEarlyWakeupStart and eExplicitEarlyWakeupEnd that
will be used by WindowManager to indicate when to enter early offset
and when to exit. With these explicit flags, the timings on transaction
no longer matters and we consistently stay at early offset config for the desired
duration.

Bug: 158127834
Test: Quick switch between apps and verify that offset doesn't change
Test: adb shell /data/nativetest64/libsurfaceflinger_unittest/libsurfaceflinger_unittest
Change-Id: Ie10af30a2d8c0f4f21ac7ffed469a74e1bf8dbc1
diff --git a/libs/gui/SurfaceComposerClient.cpp b/libs/gui/SurfaceComposerClient.cpp
index 5922f3a..83bc069 100644
--- a/libs/gui/SurfaceComposerClient.cpp
+++ b/libs/gui/SurfaceComposerClient.cpp
@@ -353,6 +353,8 @@
         mTransactionNestCount(other.mTransactionNestCount),
         mAnimation(other.mAnimation),
         mEarlyWakeup(other.mEarlyWakeup),
+        mExplicitEarlyWakeupStart(other.mExplicitEarlyWakeupStart),
+        mExplicitEarlyWakeupEnd(other.mExplicitEarlyWakeupEnd),
         mContainsBuffer(other.mContainsBuffer),
         mDesiredPresentTime(other.mDesiredPresentTime) {
     mDisplayStates = other.mDisplayStates;
@@ -375,6 +377,8 @@
     const uint32_t transactionNestCount = parcel->readUint32();
     const bool animation = parcel->readBool();
     const bool earlyWakeup = parcel->readBool();
+    const bool explicitEarlyWakeupStart = parcel->readBool();
+    const bool explicitEarlyWakeupEnd = parcel->readBool();
     const bool containsBuffer = parcel->readBool();
     const int64_t desiredPresentTime = parcel->readInt64();
 
@@ -443,6 +447,8 @@
     mTransactionNestCount = transactionNestCount;
     mAnimation = animation;
     mEarlyWakeup = earlyWakeup;
+    mExplicitEarlyWakeupStart = explicitEarlyWakeupStart;
+    mExplicitEarlyWakeupEnd = explicitEarlyWakeupEnd;
     mContainsBuffer = containsBuffer;
     mDesiredPresentTime = desiredPresentTime;
     mDisplayStates = displayStates;
@@ -470,6 +476,8 @@
     parcel->writeUint32(mTransactionNestCount);
     parcel->writeBool(mAnimation);
     parcel->writeBool(mEarlyWakeup);
+    parcel->writeBool(mExplicitEarlyWakeupStart);
+    parcel->writeBool(mExplicitEarlyWakeupEnd);
     parcel->writeBool(mContainsBuffer);
     parcel->writeInt64(mDesiredPresentTime);
     parcel->writeUint32(static_cast<uint32_t>(mDisplayStates.size()));
@@ -545,6 +553,8 @@
 
     mContainsBuffer |= other.mContainsBuffer;
     mEarlyWakeup = mEarlyWakeup || other.mEarlyWakeup;
+    mExplicitEarlyWakeupStart = mExplicitEarlyWakeupStart || other.mExplicitEarlyWakeupStart;
+    mExplicitEarlyWakeupEnd = mExplicitEarlyWakeupEnd || other.mExplicitEarlyWakeupEnd;
     other.clear();
     return *this;
 }
@@ -559,6 +569,8 @@
     mTransactionNestCount = 0;
     mAnimation = false;
     mEarlyWakeup = false;
+    mExplicitEarlyWakeupStart = false;
+    mExplicitEarlyWakeupEnd = false;
     mDesiredPresentTime = -1;
 }
 
@@ -682,9 +694,20 @@
         flags |= ISurfaceComposer::eEarlyWakeup;
     }
 
+    // If both mExplicitEarlyWakeupStart and mExplicitEarlyWakeupEnd are set
+    // it is equivalent for none
+    if (mExplicitEarlyWakeupStart && !mExplicitEarlyWakeupEnd) {
+        flags |= ISurfaceComposer::eExplicitEarlyWakeupStart;
+    }
+    if (mExplicitEarlyWakeupEnd && !mExplicitEarlyWakeupStart) {
+        flags |= ISurfaceComposer::eExplicitEarlyWakeupEnd;
+    }
+
     mForceSynchronous = false;
     mAnimation = false;
     mEarlyWakeup = false;
+    mExplicitEarlyWakeupStart = false;
+    mExplicitEarlyWakeupEnd = false;
 
     sp<IBinder> applyToken = IInterface::asBinder(TransactionCompletedListener::getIInstance());
     sf->setTransactionState(composerStates, displayStates, flags, applyToken, mInputWindowCommands,
@@ -731,6 +754,14 @@
     mEarlyWakeup = true;
 }
 
+void SurfaceComposerClient::Transaction::setExplicitEarlyWakeupStart() {
+    mExplicitEarlyWakeupStart = true;
+}
+
+void SurfaceComposerClient::Transaction::setExplicitEarlyWakeupEnd() {
+    mExplicitEarlyWakeupEnd = true;
+}
+
 layer_state_t* SurfaceComposerClient::Transaction::getLayerState(const sp<IBinder>& handle) {
     if (mComposerStates.count(handle) == 0) {
         // we don't have it, add an initialized layer_state to our list
diff --git a/libs/gui/include/gui/ISurfaceComposer.h b/libs/gui/include/gui/ISurfaceComposer.h
index b49fa1b..8d3160a 100644
--- a/libs/gui/include/gui/ISurfaceComposer.h
+++ b/libs/gui/include/gui/ISurfaceComposer.h
@@ -81,12 +81,20 @@
     // flags for setTransactionState()
     enum {
         eSynchronous = 0x01,
-        eAnimation   = 0x02,
+        eAnimation = 0x02,
 
-        // Indicates that this transaction will likely result in a lot of layers being composed, and
-        // thus, SurfaceFlinger should wake-up earlier to avoid missing frame deadlines. In this
-        // case SurfaceFlinger will wake up at (sf vsync offset - debug.sf.early_phase_offset_ns)
-        eEarlyWakeup = 0x04
+        // DEPRECATED - use eExplicitEarlyWakeup[Start|End]
+        eEarlyWakeup = 0x04,
+
+        // Explicit indication that this transaction and others to follow will likely result in a
+        // lot of layers being composed, and thus, SurfaceFlinger should wake-up earlier to avoid
+        // missing frame deadlines. In this case SurfaceFlinger will wake up at
+        // (sf vsync offset - debug.sf.early_phase_offset_ns). SurfaceFlinger will continue to be
+        // in the early configuration until it receives eExplicitEarlyWakeupEnd. These flags are
+        // expected to be used by WindowManager only and are guarded by
+        // android.permission.ACCESS_SURFACE_FLINGER
+        eExplicitEarlyWakeupStart = 0x08,
+        eExplicitEarlyWakeupEnd = 0x10,
     };
 
     enum VsyncSource {
diff --git a/libs/gui/include/gui/SurfaceComposerClient.h b/libs/gui/include/gui/SurfaceComposerClient.h
index e981a39..adcb898 100644
--- a/libs/gui/include/gui/SurfaceComposerClient.h
+++ b/libs/gui/include/gui/SurfaceComposerClient.h
@@ -345,10 +345,12 @@
         std::unordered_map<sp<ITransactionCompletedListener>, CallbackInfo, TCLHash>
                 mListenerCallbacks;
 
-        uint32_t                    mForceSynchronous = 0;
-        uint32_t                    mTransactionNestCount = 0;
-        bool                        mAnimation = false;
-        bool                        mEarlyWakeup = false;
+        uint32_t mForceSynchronous = 0;
+        uint32_t mTransactionNestCount = 0;
+        bool mAnimation = false;
+        bool mEarlyWakeup = false;
+        bool mExplicitEarlyWakeupStart = false;
+        bool mExplicitEarlyWakeupEnd = false;
 
         // Indicates that the Transaction contains a buffer that should be cached
         bool mContainsBuffer = false;
@@ -547,6 +549,8 @@
         void setDisplaySize(const sp<IBinder>& token, uint32_t width, uint32_t height);
         void setAnimationTransaction();
         void setEarlyWakeup();
+        void setExplicitEarlyWakeupStart();
+        void setExplicitEarlyWakeupEnd();
     };
 
     status_t clearLayerFrameStats(const sp<IBinder>& token) const;
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index 9e24f909..730ea8f 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -55,13 +55,24 @@
     virtual void kernelTimerChanged(bool expired) = 0;
 };
 
-class Scheduler {
+class IPhaseOffsetControl {
+public:
+    virtual ~IPhaseOffsetControl() = default;
+    virtual void setPhaseOffset(scheduler::ConnectionHandle, nsecs_t phaseOffset) = 0;
+};
+
+class Scheduler : public IPhaseOffsetControl {
 public:
     using RefreshRate = scheduler::RefreshRateConfigs::RefreshRate;
     using ConfigEvent = scheduler::RefreshRateConfigEvent;
 
     // Indicates whether to start the transaction early, or at vsync time.
-    enum class TransactionStart { EARLY, NORMAL };
+    enum class TransactionStart {
+        Early,      // DEPRECATED. Start the transaction early. Times out on its own
+        EarlyStart, // Start the transaction early and keep this config until EarlyEnd
+        EarlyEnd,   // End the early config started at EarlyStart
+        Normal      // Start the transaction at the normal time
+    };
 
     Scheduler(impl::EventControlThread::SetVSyncEnabledFunction,
               const scheduler::RefreshRateConfigs&, ISchedulerCallback& schedulerCallback,
@@ -90,7 +101,7 @@
     void onScreenReleased(ConnectionHandle);
 
     // Modifies phase offset in the event thread.
-    void setPhaseOffset(ConnectionHandle, nsecs_t phaseOffset);
+    void setPhaseOffset(ConnectionHandle, nsecs_t phaseOffset) override;
 
     void getDisplayStatInfo(DisplayStatInfo* stats);
 
diff --git a/services/surfaceflinger/Scheduler/VSyncModulator.cpp b/services/surfaceflinger/Scheduler/VSyncModulator.cpp
index 510dc2d..2567c04 100644
--- a/services/surfaceflinger/Scheduler/VSyncModulator.cpp
+++ b/services/surfaceflinger/Scheduler/VSyncModulator.cpp
@@ -31,11 +31,11 @@
 
 namespace android::scheduler {
 
-VSyncModulator::VSyncModulator(Scheduler& scheduler,
+VSyncModulator::VSyncModulator(IPhaseOffsetControl& phaseOffsetControl,
                                Scheduler::ConnectionHandle appConnectionHandle,
                                Scheduler::ConnectionHandle sfConnectionHandle,
                                const OffsetsConfig& config)
-      : mScheduler(scheduler),
+      : mPhaseOffsetControl(phaseOffsetControl),
         mAppConnectionHandle(appConnectionHandle),
         mSfConnectionHandle(sfConnectionHandle),
         mOffsetsConfig(config) {
@@ -51,14 +51,35 @@
 }
 
 void VSyncModulator::setTransactionStart(Scheduler::TransactionStart transactionStart) {
-    if (transactionStart == Scheduler::TransactionStart::EARLY) {
+    switch (transactionStart) {
+        case Scheduler::TransactionStart::EarlyStart:
+            ALOGW_IF(mExplicitEarlyWakeup, "Already in TransactionStart::EarlyStart");
+            mExplicitEarlyWakeup = true;
+            break;
+        case Scheduler::TransactionStart::EarlyEnd:
+            ALOGW_IF(!mExplicitEarlyWakeup, "Not in TransactionStart::EarlyStart");
+            mExplicitEarlyWakeup = false;
+            break;
+        case Scheduler::TransactionStart::Normal:
+        case Scheduler::TransactionStart::Early:
+            // Non explicit don't change the explicit early wakeup state
+            break;
+    }
+
+    if (mTraceDetailedInfo) {
+        ATRACE_INT("mExplicitEarlyWakeup", mExplicitEarlyWakeup);
+    }
+
+    if (!mExplicitEarlyWakeup &&
+        (transactionStart == Scheduler::TransactionStart::Early ||
+         transactionStart == Scheduler::TransactionStart::EarlyEnd)) {
         mRemainingEarlyFrameCount = MIN_EARLY_FRAME_COUNT_TRANSACTION;
         mEarlyTxnStartTime = std::chrono::steady_clock::now();
     }
 
     // An early transaction stays an early transaction.
     if (transactionStart == mTransactionStart ||
-        mTransactionStart == Scheduler::TransactionStart::EARLY) {
+        mTransactionStart == Scheduler::TransactionStart::EarlyEnd) {
         return;
     }
     mTransactionStart = transactionStart;
@@ -67,8 +88,8 @@
 
 void VSyncModulator::onTransactionHandled() {
     mTxnAppliedTime = std::chrono::steady_clock::now();
-    if (mTransactionStart == Scheduler::TransactionStart::NORMAL) return;
-    mTransactionStart = Scheduler::TransactionStart::NORMAL;
+    if (mTransactionStart == Scheduler::TransactionStart::Normal) return;
+    mTransactionStart = Scheduler::TransactionStart::Normal;
     updateOffsets();
 }
 
@@ -91,11 +112,10 @@
 void VSyncModulator::onRefreshed(bool usedRenderEngine) {
     bool updateOffsetsNeeded = false;
 
-    // Apply a 1ms margin to account for potential data races
+    // Apply a margin to account for potential data races
     // This might make us stay in early offsets for one
     // additional frame but it's better to be conservative here.
-    static const constexpr std::chrono::nanoseconds kMargin = 1ms;
-    if ((mEarlyTxnStartTime.load() + kMargin) < mTxnAppliedTime.load()) {
+    if ((mEarlyTxnStartTime.load() + MARGIN_FOR_TX_APPLY) < mTxnAppliedTime.load()) {
         if (mRemainingEarlyFrameCount > 0) {
             mRemainingEarlyFrameCount--;
             updateOffsetsNeeded = true;
@@ -121,8 +141,8 @@
 const VSyncModulator::Offsets& VSyncModulator::getNextOffsets() const {
     // Early offsets are used if we're in the middle of a refresh rate
     // change, or if we recently begin a transaction.
-    if (mTransactionStart == Scheduler::TransactionStart::EARLY || mRemainingEarlyFrameCount > 0 ||
-        mRefreshRateChangePending) {
+    if (mExplicitEarlyWakeup || mTransactionStart == Scheduler::TransactionStart::EarlyEnd ||
+        mRemainingEarlyFrameCount > 0 || mRefreshRateChangePending) {
         return mOffsetsConfig.early;
     } else if (mRemainingRenderEngineUsageCount > 0) {
         return mOffsetsConfig.earlyGl;
@@ -139,8 +159,8 @@
 void VSyncModulator::updateOffsetsLocked() {
     const Offsets& offsets = getNextOffsets();
 
-    mScheduler.setPhaseOffset(mSfConnectionHandle, offsets.sf);
-    mScheduler.setPhaseOffset(mAppConnectionHandle, offsets.app);
+    mPhaseOffsetControl.setPhaseOffset(mSfConnectionHandle, offsets.sf);
+    mPhaseOffsetControl.setPhaseOffset(mAppConnectionHandle, offsets.app);
 
     mOffsets = offsets;
 
diff --git a/services/surfaceflinger/Scheduler/VSyncModulator.h b/services/surfaceflinger/Scheduler/VSyncModulator.h
index d777ef9..ab678c9 100644
--- a/services/surfaceflinger/Scheduler/VSyncModulator.h
+++ b/services/surfaceflinger/Scheduler/VSyncModulator.h
@@ -38,6 +38,9 @@
     // switch in and out of gl composition.
     static constexpr int MIN_EARLY_GL_FRAME_COUNT_TRANSACTION = 2;
 
+    // Margin used to account for potential data races
+    static const constexpr std::chrono::nanoseconds MARGIN_FOR_TX_APPLY = 1ms;
+
 public:
     // Wrapper for a collection of surfaceflinger/app offsets for a particular
     // configuration.
@@ -62,7 +65,7 @@
         bool operator!=(const OffsetsConfig& other) const { return !(*this == other); }
     };
 
-    VSyncModulator(Scheduler&, ConnectionHandle appConnectionHandle,
+    VSyncModulator(IPhaseOffsetControl&, ConnectionHandle appConnectionHandle,
                    ConnectionHandle sfConnectionHandle, const OffsetsConfig&);
 
     void setPhaseOffsets(const OffsetsConfig&) EXCLUDES(mMutex);
@@ -91,13 +94,14 @@
     Offsets getOffsets() const EXCLUDES(mMutex);
 
 private:
+    friend class VSyncModulatorTest;
     // Returns the next offsets that we should be using
     const Offsets& getNextOffsets() const REQUIRES(mMutex);
     // Updates offsets and persists them into the scheduler framework.
     void updateOffsets() EXCLUDES(mMutex);
     void updateOffsetsLocked() REQUIRES(mMutex);
 
-    Scheduler& mScheduler;
+    IPhaseOffsetControl& mPhaseOffsetControl;
     const ConnectionHandle mAppConnectionHandle;
     const ConnectionHandle mSfConnectionHandle;
 
@@ -107,8 +111,9 @@
     Offsets mOffsets GUARDED_BY(mMutex){mOffsetsConfig.late};
 
     std::atomic<Scheduler::TransactionStart> mTransactionStart =
-            Scheduler::TransactionStart::NORMAL;
+            Scheduler::TransactionStart::Normal;
     std::atomic<bool> mRefreshRateChangePending = false;
+    std::atomic<bool> mExplicitEarlyWakeup = false;
     std::atomic<int> mRemainingEarlyFrameCount = 0;
     std::atomic<int> mRemainingRenderEngineUsageCount = 0;
     std::atomic<std::chrono::steady_clock::time_point> mEarlyTxnStartTime = {};
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index fbfe448..82d8f08 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -3235,7 +3235,7 @@
 }
 
 uint32_t SurfaceFlinger::setTransactionFlags(uint32_t flags) {
-    return setTransactionFlags(flags, Scheduler::TransactionStart::NORMAL);
+    return setTransactionFlags(flags, Scheduler::TransactionStart::Normal);
 }
 
 uint32_t SurfaceFlinger::setTransactionFlags(uint32_t flags,
@@ -3452,15 +3452,36 @@
         mTraversalNeededMainThread = true;
     }
 
+    const auto transactionStart = [](uint32_t flags) {
+        if (flags & eEarlyWakeup) {
+            return Scheduler::TransactionStart::Early;
+        }
+        if (flags & eExplicitEarlyWakeupEnd) {
+            return Scheduler::TransactionStart::EarlyEnd;
+        }
+        if (flags & eExplicitEarlyWakeupStart) {
+            return Scheduler::TransactionStart::EarlyStart;
+        }
+        return Scheduler::TransactionStart::Normal;
+    }(flags);
+
     if (transactionFlags) {
         if (mInterceptor->isEnabled()) {
             mInterceptor->saveTransaction(states, mCurrentState.displays, displays, flags);
         }
 
+        // TODO(b/159125966): Remove eEarlyWakeup completly as no client should use this flag
+        if (flags & eEarlyWakeup) {
+            ALOGW("eEarlyWakeup is deprecated. Use eExplicitEarlyWakeup[Start|End]");
+        }
+
+        if (!privileged && (flags & (eExplicitEarlyWakeupStart | eExplicitEarlyWakeupEnd))) {
+            ALOGE("Only WindowManager is allowed to use eExplicitEarlyWakeup[Start|End] flags");
+            flags &= ~(eExplicitEarlyWakeupStart | eExplicitEarlyWakeupEnd);
+        }
+
         // this triggers the transaction
-        const auto start = (flags & eEarlyWakeup) ? Scheduler::TransactionStart::EARLY
-                                                  : Scheduler::TransactionStart::NORMAL;
-        setTransactionFlags(transactionFlags, start);
+        setTransactionFlags(transactionFlags, transactionStart);
 
         if (flags & eAnimation) {
             mAnimTransactionPending = true;
@@ -3497,6 +3518,13 @@
                 break;
             }
         }
+    } else {
+        // even if a transaction is not needed, we need to update VsyncModulator
+        // about explicit early indications
+        if (transactionStart == Scheduler::TransactionStart::EarlyStart ||
+            transactionStart == Scheduler::TransactionStart::EarlyEnd) {
+            mVSyncModulator->setTransactionStart(transactionStart);
+        }
     }
 }
 
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index 7574ff1..3c4a791 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -62,6 +62,7 @@
         "StrongTypingTest.cpp",
         "VSyncDispatchTimerQueueTest.cpp",
         "VSyncDispatchRealtimeTest.cpp",
+        "VSyncModulatorTest.cpp",
         "VSyncPredictorTest.cpp",
         "VSyncReactorTest.cpp",
         "mock/DisplayHardware/MockComposer.cpp",
diff --git a/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp b/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp
new file mode 100644
index 0000000..9c1ec07
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "LibSurfaceFlingerUnittests"
+#define LOG_NDEBUG 0
+
+#include "Scheduler/VSyncModulator.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace testing;
+
+namespace android::scheduler {
+
+class MockScheduler : public IPhaseOffsetControl {
+public:
+    void setPhaseOffset(ConnectionHandle handle, nsecs_t phaseOffset) {
+        mPhaseOffset[handle] = phaseOffset;
+    }
+
+    nsecs_t getOffset(ConnectionHandle handle) { return mPhaseOffset[handle]; }
+
+private:
+    std::unordered_map<ConnectionHandle, nsecs_t> mPhaseOffset;
+};
+
+class VSyncModulatorTest : public testing::Test {
+protected:
+    static constexpr auto MIN_EARLY_FRAME_COUNT_TRANSACTION =
+            VSyncModulator::MIN_EARLY_FRAME_COUNT_TRANSACTION;
+    // Add a 1ms slack to avoid strange timer race conditions.
+    static constexpr auto MARGIN_FOR_TX_APPLY = VSyncModulator::MARGIN_FOR_TX_APPLY + 1ms;
+
+    // Used to enumerate the different offsets we have
+    enum {
+        SF_LATE,
+        APP_LATE,
+        SF_EARLY,
+        APP_EARLY,
+        SF_EARLY_GL,
+        APP_EARLY_GL,
+    };
+
+    std::unique_ptr<VSyncModulator> mVSyncModulator;
+    MockScheduler mMockScheduler;
+    ConnectionHandle mAppConnection{1};
+    ConnectionHandle mSfConnection{2};
+    VSyncModulator::OffsetsConfig mOffsets = {{SF_EARLY, APP_EARLY},
+                                              {SF_EARLY_GL, APP_EARLY_GL},
+                                              {SF_LATE, APP_LATE}};
+
+    void SetUp() override {
+        mVSyncModulator = std::make_unique<VSyncModulator>(mMockScheduler, mAppConnection,
+                                                           mSfConnection, mOffsets);
+        mVSyncModulator->setPhaseOffsets(mOffsets);
+
+        EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+    };
+
+    void TearDown() override { mVSyncModulator.reset(); }
+};
+
+TEST_F(VSyncModulatorTest, Normal) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::Normal);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+    }
+}
+
+TEST_F(VSyncModulatorTest, EarlyEnd) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+TEST_F(VSyncModulatorTest, EarlyStart) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyStart);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+TEST_F(VSyncModulatorTest, EarlyStartWithEarly) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyStart);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::Early);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+TEST_F(VSyncModulatorTest, EarlyStartWithMoreTransactions) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyStart);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::Normal);
+        std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+TEST_F(VSyncModulatorTest, EarlyStartAfterEarlyEnd) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyStart);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+TEST_F(VSyncModulatorTest, EarlyStartAfterEarlyEndWithMoreTransactions) {
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyStart);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < 5 * MIN_EARLY_FRAME_COUNT_TRANSACTION; i++) {
+        mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::Normal);
+        std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->setTransactionStart(Scheduler::TransactionStart::EarlyEnd);
+    std::this_thread::sleep_for(MARGIN_FOR_TX_APPLY);
+    mVSyncModulator->onTransactionHandled();
+    EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+
+    for (int i = 0; i < MIN_EARLY_FRAME_COUNT_TRANSACTION - 1; i++) {
+        mVSyncModulator->onRefreshed(false);
+        EXPECT_EQ(APP_EARLY, mMockScheduler.getOffset(mAppConnection));
+        EXPECT_EQ(SF_EARLY, mMockScheduler.getOffset(mSfConnection));
+    }
+
+    mVSyncModulator->onRefreshed(false);
+    EXPECT_EQ(APP_LATE, mMockScheduler.getOffset(mAppConnection));
+    EXPECT_EQ(SF_LATE, mMockScheduler.getOffset(mSfConnection));
+}
+
+} // namespace android::scheduler