Merge changes I05a42795,Ibe230906

* changes:
  SF: Decouple VsyncModulator from Scheduler
  SF: Rename VsyncModulator
diff --git a/services/surfaceflinger/Android.bp b/services/surfaceflinger/Android.bp
index c55e9df..e452187 100644
--- a/services/surfaceflinger/Android.bp
+++ b/services/surfaceflinger/Android.bp
@@ -176,7 +176,7 @@
         "Scheduler/Timer.cpp",
         "Scheduler/VSyncDispatchTimerQueue.cpp",
         "Scheduler/VSyncPredictor.cpp",
-        "Scheduler/VSyncModulator.cpp",
+        "Scheduler/VsyncModulator.cpp",
         "Scheduler/VSyncReactor.cpp",
         "StartPropertySetThread.cpp",
         "SurfaceFlinger.cpp",
diff --git a/services/surfaceflinger/Scheduler/PhaseOffsets.h b/services/surfaceflinger/Scheduler/PhaseOffsets.h
index 9ec6d56..0ae9fef 100644
--- a/services/surfaceflinger/Scheduler/PhaseOffsets.h
+++ b/services/surfaceflinger/Scheduler/PhaseOffsets.h
@@ -19,7 +19,7 @@
 #include <unordered_map>
 
 #include "RefreshRateConfigs.h"
-#include "VSyncModulator.h"
+#include "VsyncModulator.h"
 
 namespace android::scheduler {
 
@@ -31,7 +31,7 @@
  */
 class PhaseConfiguration {
 public:
-    using Offsets = VSyncModulator::OffsetsConfig;
+    using Offsets = VsyncModulator::OffsetsConfig;
 
     virtual ~PhaseConfiguration();
 
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index 6a5082a..4e7a9a1 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -60,28 +60,13 @@
     ~ISchedulerCallback() = default;
 };
 
-struct IPhaseOffsetControl {
-    virtual void setPhaseOffset(scheduler::ConnectionHandle, nsecs_t phaseOffset) = 0;
-
-protected:
-    ~IPhaseOffsetControl() = default;
-};
-
-class Scheduler : public IPhaseOffsetControl {
+class Scheduler {
 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,      // 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(const scheduler::RefreshRateConfigs&, ISchedulerCallback&);
-    virtual ~Scheduler();
+    ~Scheduler();
 
     DispSync& getPrimaryDispSync();
 
@@ -104,7 +89,7 @@
     void onScreenReleased(ConnectionHandle);
 
     // Modifies phase offset in the event thread.
-    void setPhaseOffset(ConnectionHandle, nsecs_t phaseOffset) override;
+    void setPhaseOffset(ConnectionHandle, nsecs_t phaseOffset);
 
     void getDisplayStatInfo(DisplayStatInfo* stats);
 
diff --git a/services/surfaceflinger/Scheduler/VSyncModulator.cpp b/services/surfaceflinger/Scheduler/VSyncModulator.cpp
deleted file mode 100644
index 2567c04..0000000
--- a/services/surfaceflinger/Scheduler/VSyncModulator.cpp
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright 2019 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.
- */
-
-// TODO(b/129481165): remove the #pragma below and fix conversion issues
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wconversion"
-
-#define ATRACE_TAG ATRACE_TAG_GRAPHICS
-
-#include "VSyncModulator.h"
-
-#include <cutils/properties.h>
-#include <utils/Trace.h>
-
-#include <chrono>
-#include <cinttypes>
-#include <mutex>
-
-namespace android::scheduler {
-
-VSyncModulator::VSyncModulator(IPhaseOffsetControl& phaseOffsetControl,
-                               Scheduler::ConnectionHandle appConnectionHandle,
-                               Scheduler::ConnectionHandle sfConnectionHandle,
-                               const OffsetsConfig& config)
-      : mPhaseOffsetControl(phaseOffsetControl),
-        mAppConnectionHandle(appConnectionHandle),
-        mSfConnectionHandle(sfConnectionHandle),
-        mOffsetsConfig(config) {
-    char value[PROPERTY_VALUE_MAX];
-    property_get("debug.sf.vsync_trace_detailed_info", value, "0");
-    mTraceDetailedInfo = atoi(value);
-}
-
-void VSyncModulator::setPhaseOffsets(const OffsetsConfig& config) {
-    std::lock_guard<std::mutex> lock(mMutex);
-    mOffsetsConfig = config;
-    updateOffsetsLocked();
-}
-
-void VSyncModulator::setTransactionStart(Scheduler::TransactionStart transactionStart) {
-    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::EarlyEnd) {
-        return;
-    }
-    mTransactionStart = transactionStart;
-    updateOffsets();
-}
-
-void VSyncModulator::onTransactionHandled() {
-    mTxnAppliedTime = std::chrono::steady_clock::now();
-    if (mTransactionStart == Scheduler::TransactionStart::Normal) return;
-    mTransactionStart = Scheduler::TransactionStart::Normal;
-    updateOffsets();
-}
-
-void VSyncModulator::onRefreshRateChangeInitiated() {
-    if (mRefreshRateChangePending) {
-        return;
-    }
-    mRefreshRateChangePending = true;
-    updateOffsets();
-}
-
-void VSyncModulator::onRefreshRateChangeCompleted() {
-    if (!mRefreshRateChangePending) {
-        return;
-    }
-    mRefreshRateChangePending = false;
-    updateOffsets();
-}
-
-void VSyncModulator::onRefreshed(bool usedRenderEngine) {
-    bool updateOffsetsNeeded = false;
-
-    // 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.
-    if ((mEarlyTxnStartTime.load() + MARGIN_FOR_TX_APPLY) < mTxnAppliedTime.load()) {
-        if (mRemainingEarlyFrameCount > 0) {
-            mRemainingEarlyFrameCount--;
-            updateOffsetsNeeded = true;
-        }
-    }
-    if (usedRenderEngine) {
-        mRemainingRenderEngineUsageCount = MIN_EARLY_GL_FRAME_COUNT_TRANSACTION;
-        updateOffsetsNeeded = true;
-    } else if (mRemainingRenderEngineUsageCount > 0) {
-        mRemainingRenderEngineUsageCount--;
-        updateOffsetsNeeded = true;
-    }
-    if (updateOffsetsNeeded) {
-        updateOffsets();
-    }
-}
-
-VSyncModulator::Offsets VSyncModulator::getOffsets() const {
-    std::lock_guard<std::mutex> lock(mMutex);
-    return mOffsets;
-}
-
-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 (mExplicitEarlyWakeup || mTransactionStart == Scheduler::TransactionStart::EarlyEnd ||
-        mRemainingEarlyFrameCount > 0 || mRefreshRateChangePending) {
-        return mOffsetsConfig.early;
-    } else if (mRemainingRenderEngineUsageCount > 0) {
-        return mOffsetsConfig.earlyGl;
-    } else {
-        return mOffsetsConfig.late;
-    }
-}
-
-void VSyncModulator::updateOffsets() {
-    std::lock_guard<std::mutex> lock(mMutex);
-    updateOffsetsLocked();
-}
-
-void VSyncModulator::updateOffsetsLocked() {
-    const Offsets& offsets = getNextOffsets();
-
-    mPhaseOffsetControl.setPhaseOffset(mSfConnectionHandle, offsets.sf);
-    mPhaseOffsetControl.setPhaseOffset(mAppConnectionHandle, offsets.app);
-
-    mOffsets = offsets;
-
-    if (!mTraceDetailedInfo) {
-        return;
-    }
-
-    const bool isEarly = &offsets == &mOffsetsConfig.early;
-    const bool isEarlyGl = &offsets == &mOffsetsConfig.earlyGl;
-    const bool isLate = &offsets == &mOffsetsConfig.late;
-
-    ATRACE_INT("Vsync-EarlyOffsetsOn", isEarly);
-    ATRACE_INT("Vsync-EarlyGLOffsetsOn", isEarlyGl);
-    ATRACE_INT("Vsync-LateOffsetsOn", isLate);
-}
-
-} // namespace android::scheduler
-
-// TODO(b/129481165): remove the #pragma below and fix conversion issues
-#pragma clang diagnostic pop // ignored "-Wconversion"
diff --git a/services/surfaceflinger/Scheduler/VSyncModulator.h b/services/surfaceflinger/Scheduler/VSyncModulator.h
deleted file mode 100644
index ab678c9..0000000
--- a/services/surfaceflinger/Scheduler/VSyncModulator.h
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2018 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.
- */
-
-#pragma once
-
-#include <chrono>
-#include <mutex>
-
-#include "Scheduler.h"
-
-namespace android::scheduler {
-
-/*
- * Modulates the vsync-offsets depending on current SurfaceFlinger state.
- */
-class VSyncModulator {
-private:
-    // Number of frames we'll keep the early phase offsets once they are activated for a
-    // transaction. This acts as a low-pass filter in case the client isn't quick enough in
-    // sending new transactions.
-    static constexpr int MIN_EARLY_FRAME_COUNT_TRANSACTION = 2;
-
-    // Number of frames we'll keep the early gl phase offsets once they are activated.
-    // This acts as a low-pass filter to avoid scenarios where we rapidly
-    // 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.
-    struct Offsets {
-        nsecs_t sf;
-        nsecs_t app;
-
-        bool operator==(const Offsets& other) const { return sf == other.sf && app == other.app; }
-
-        bool operator!=(const Offsets& other) const { return !(*this == other); }
-    };
-
-    struct OffsetsConfig {
-        Offsets early;   // For transactions with the eEarlyWakeup flag.
-        Offsets earlyGl; // As above but while compositing with GL.
-        Offsets late;    // Default.
-
-        bool operator==(const OffsetsConfig& other) const {
-            return early == other.early && earlyGl == other.earlyGl && late == other.late;
-        }
-
-        bool operator!=(const OffsetsConfig& other) const { return !(*this == other); }
-    };
-
-    VSyncModulator(IPhaseOffsetControl&, ConnectionHandle appConnectionHandle,
-                   ConnectionHandle sfConnectionHandle, const OffsetsConfig&);
-
-    void setPhaseOffsets(const OffsetsConfig&) EXCLUDES(mMutex);
-
-    // Signals that a transaction has started, and changes offsets accordingly.
-    void setTransactionStart(Scheduler::TransactionStart transactionStart);
-
-    // Signals that a transaction has been completed, so that we can finish
-    // special handling for a transaction.
-    void onTransactionHandled();
-
-    // Called when we send a refresh rate change to hardware composer, so that
-    // we can move into early offsets.
-    void onRefreshRateChangeInitiated();
-
-    // Called when we detect from vsync signals that the refresh rate changed.
-    // This way we can move out of early offsets if no longer necessary.
-    void onRefreshRateChangeCompleted();
-
-    // Called when the display is presenting a new frame. usedRenderEngine
-    // should be set to true if RenderEngine was involved with composing the new
-    // frame.
-    void onRefreshed(bool usedRenderEngine);
-
-    // Returns the offsets that we are currently using
-    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);
-
-    IPhaseOffsetControl& mPhaseOffsetControl;
-    const ConnectionHandle mAppConnectionHandle;
-    const ConnectionHandle mSfConnectionHandle;
-
-    mutable std::mutex mMutex;
-    OffsetsConfig mOffsetsConfig GUARDED_BY(mMutex);
-
-    Offsets mOffsets GUARDED_BY(mMutex){mOffsetsConfig.late};
-
-    std::atomic<Scheduler::TransactionStart> mTransactionStart =
-            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 = {};
-    std::atomic<std::chrono::steady_clock::time_point> mTxnAppliedTime = {};
-
-    bool mTraceDetailedInfo = false;
-};
-
-} // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/VsyncModulator.cpp b/services/surfaceflinger/Scheduler/VsyncModulator.cpp
new file mode 100644
index 0000000..7a1b7e4
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/VsyncModulator.cpp
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2019 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.
+ */
+
+#define ATRACE_TAG ATRACE_TAG_GRAPHICS
+
+#undef LOG_TAG
+#define LOG_TAG "VsyncModulator"
+
+#include "VsyncModulator.h"
+
+#include <android-base/properties.h>
+#include <log/log.h>
+#include <utils/Trace.h>
+
+#include <chrono>
+#include <cinttypes>
+#include <mutex>
+
+using namespace std::chrono_literals;
+
+namespace android::scheduler {
+
+const std::chrono::nanoseconds VsyncModulator::MIN_EARLY_TRANSACTION_TIME = 1ms;
+
+VsyncModulator::VsyncModulator(const OffsetsConfig& config, Now now)
+      : mOffsetsConfig(config),
+        mNow(now),
+        mTraceDetailedInfo(base::GetBoolProperty("debug.sf.vsync_trace_detailed_info", false)) {}
+
+VsyncModulator::Offsets VsyncModulator::setPhaseOffsets(const OffsetsConfig& config) {
+    std::lock_guard<std::mutex> lock(mMutex);
+    mOffsetsConfig = config;
+    return updateOffsetsLocked();
+}
+
+VsyncModulator::OffsetsOpt VsyncModulator::setTransactionSchedule(TransactionSchedule schedule) {
+    switch (schedule) {
+        case Schedule::EarlyStart:
+            ALOGW_IF(mExplicitEarlyWakeup, "%s: Duplicate EarlyStart", __FUNCTION__);
+            mExplicitEarlyWakeup = true;
+            break;
+        case Schedule::EarlyEnd:
+            ALOGW_IF(!mExplicitEarlyWakeup, "%s: Unexpected EarlyEnd", __FUNCTION__);
+            mExplicitEarlyWakeup = false;
+            break;
+        case Schedule::Early:
+        case Schedule::Late:
+            // No change to mExplicitEarlyWakeup for non-explicit states.
+            break;
+    }
+
+    if (mTraceDetailedInfo) {
+        ATRACE_INT("mExplicitEarlyWakeup", mExplicitEarlyWakeup);
+    }
+
+    if (!mExplicitEarlyWakeup && (schedule == Schedule::Early || schedule == Schedule::EarlyEnd)) {
+        mEarlyTransactionFrames = MIN_EARLY_TRANSACTION_FRAMES;
+        mEarlyTransactionStartTime = mNow();
+    }
+
+    // An early transaction stays an early transaction.
+    if (schedule == mTransactionSchedule || mTransactionSchedule == Schedule::EarlyEnd) {
+        return std::nullopt;
+    }
+    mTransactionSchedule = schedule;
+    return updateOffsets();
+}
+
+VsyncModulator::OffsetsOpt VsyncModulator::onTransactionCommit() {
+    mLastTransactionCommitTime = mNow();
+    if (mTransactionSchedule == Schedule::Late) return std::nullopt;
+    mTransactionSchedule = Schedule::Late;
+    return updateOffsets();
+}
+
+VsyncModulator::OffsetsOpt VsyncModulator::onRefreshRateChangeInitiated() {
+    if (mRefreshRateChangePending) return std::nullopt;
+    mRefreshRateChangePending = true;
+    return updateOffsets();
+}
+
+VsyncModulator::OffsetsOpt VsyncModulator::onRefreshRateChangeCompleted() {
+    if (!mRefreshRateChangePending) return std::nullopt;
+    mRefreshRateChangePending = false;
+    return updateOffsets();
+}
+
+VsyncModulator::OffsetsOpt VsyncModulator::onDisplayRefresh(bool usedGpuComposition) {
+    bool updateOffsetsNeeded = false;
+
+    if (mEarlyTransactionStartTime.load() + MIN_EARLY_TRANSACTION_TIME <=
+        mLastTransactionCommitTime.load()) {
+        if (mEarlyTransactionFrames > 0) {
+            mEarlyTransactionFrames--;
+            updateOffsetsNeeded = true;
+        }
+    }
+    if (usedGpuComposition) {
+        mEarlyGpuFrames = MIN_EARLY_GPU_FRAMES;
+        updateOffsetsNeeded = true;
+    } else if (mEarlyGpuFrames > 0) {
+        mEarlyGpuFrames--;
+        updateOffsetsNeeded = true;
+    }
+
+    if (!updateOffsetsNeeded) return std::nullopt;
+    return updateOffsets();
+}
+
+VsyncModulator::Offsets VsyncModulator::getOffsets() const {
+    std::lock_guard<std::mutex> lock(mMutex);
+    return mOffsets;
+}
+
+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 (mExplicitEarlyWakeup || mTransactionSchedule == Schedule::EarlyEnd ||
+        mEarlyTransactionFrames > 0 || mRefreshRateChangePending) {
+        return mOffsetsConfig.early;
+    } else if (mEarlyGpuFrames > 0) {
+        return mOffsetsConfig.earlyGpu;
+    } else {
+        return mOffsetsConfig.late;
+    }
+}
+
+VsyncModulator::Offsets VsyncModulator::updateOffsets() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    return updateOffsetsLocked();
+}
+
+VsyncModulator::Offsets VsyncModulator::updateOffsetsLocked() {
+    const Offsets& offsets = getNextOffsets();
+    mOffsets = offsets;
+
+    if (mTraceDetailedInfo) {
+        const bool isEarly = &offsets == &mOffsetsConfig.early;
+        const bool isEarlyGpu = &offsets == &mOffsetsConfig.earlyGpu;
+        const bool isLate = &offsets == &mOffsetsConfig.late;
+
+        ATRACE_INT("Vsync-EarlyOffsetsOn", isEarly);
+        ATRACE_INT("Vsync-EarlyGpuOffsetsOn", isEarlyGpu);
+        ATRACE_INT("Vsync-LateOffsetsOn", isLate);
+    }
+
+    return offsets;
+}
+
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/VsyncModulator.h b/services/surfaceflinger/Scheduler/VsyncModulator.h
new file mode 100644
index 0000000..f920bd2
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/VsyncModulator.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#pragma once
+
+#include <chrono>
+#include <mutex>
+#include <optional>
+
+#include <android-base/thread_annotations.h>
+#include <utils/Timers.h>
+
+namespace android::scheduler {
+
+// State machine controlled by transaction flags. VsyncModulator switches to early phase offsets
+// when a transaction is flagged EarlyStart or Early, lasting until an EarlyEnd transaction or a
+// fixed number of frames, respectively.
+enum class TransactionSchedule {
+    Late,  // Default.
+    Early, // Deprecated.
+    EarlyStart,
+    EarlyEnd
+};
+
+// Modulates VSYNC phase depending on transaction schedule and refresh rate changes.
+class VsyncModulator {
+public:
+    // Number of frames to keep early offsets after an early transaction or GPU composition.
+    // This acts as a low-pass filter in case subsequent transactions are delayed, or if the
+    // composition strategy alternates on subsequent frames.
+    static constexpr int MIN_EARLY_TRANSACTION_FRAMES = 2;
+    static constexpr int MIN_EARLY_GPU_FRAMES = 2;
+
+    // Duration to delay the MIN_EARLY_TRANSACTION_FRAMES countdown after an early transaction.
+    // This may keep early offsets for an extra frame, but avoids a race with transaction commit.
+    static const std::chrono::nanoseconds MIN_EARLY_TRANSACTION_TIME;
+
+    // Phase offsets for SF and app deadlines from VSYNC.
+    struct Offsets {
+        nsecs_t sf;
+        nsecs_t app;
+
+        bool operator==(const Offsets& other) const { return sf == other.sf && app == other.app; }
+        bool operator!=(const Offsets& other) const { return !(*this == other); }
+    };
+
+    using OffsetsOpt = std::optional<Offsets>;
+
+    struct OffsetsConfig {
+        Offsets early;    // Used for early transactions, and during refresh rate change.
+        Offsets earlyGpu; // Used during GPU composition.
+        Offsets late;     // Default.
+
+        bool operator==(const OffsetsConfig& other) const {
+            return early == other.early && earlyGpu == other.earlyGpu && late == other.late;
+        }
+
+        bool operator!=(const OffsetsConfig& other) const { return !(*this == other); }
+    };
+
+    using Clock = std::chrono::steady_clock;
+    using TimePoint = Clock::time_point;
+    using Now = TimePoint (*)();
+
+    explicit VsyncModulator(const OffsetsConfig&, Now = Clock::now);
+
+    Offsets getOffsets() const EXCLUDES(mMutex);
+
+    [[nodiscard]] Offsets setPhaseOffsets(const OffsetsConfig&) EXCLUDES(mMutex);
+
+    // Changes offsets in response to transaction flags or commit.
+    [[nodiscard]] OffsetsOpt setTransactionSchedule(TransactionSchedule);
+    [[nodiscard]] OffsetsOpt onTransactionCommit();
+
+    // Called when we send a refresh rate change to hardware composer, so that
+    // we can move into early offsets.
+    [[nodiscard]] OffsetsOpt onRefreshRateChangeInitiated();
+
+    // Called when we detect from VSYNC signals that the refresh rate changed.
+    // This way we can move out of early offsets if no longer necessary.
+    [[nodiscard]] OffsetsOpt onRefreshRateChangeCompleted();
+
+    [[nodiscard]] OffsetsOpt onDisplayRefresh(bool usedGpuComposition);
+
+private:
+    const Offsets& getNextOffsets() const REQUIRES(mMutex);
+    [[nodiscard]] Offsets updateOffsets() EXCLUDES(mMutex);
+    [[nodiscard]] Offsets updateOffsetsLocked() REQUIRES(mMutex);
+
+    mutable std::mutex mMutex;
+    OffsetsConfig mOffsetsConfig GUARDED_BY(mMutex);
+
+    Offsets mOffsets GUARDED_BY(mMutex){mOffsetsConfig.late};
+
+    using Schedule = TransactionSchedule;
+    std::atomic<Schedule> mTransactionSchedule = Schedule::Late;
+    std::atomic<bool> mExplicitEarlyWakeup = false;
+
+    std::atomic<bool> mRefreshRateChangePending = false;
+
+    std::atomic<int> mEarlyTransactionFrames = 0;
+    std::atomic<int> mEarlyGpuFrames = 0;
+    std::atomic<TimePoint> mEarlyTransactionStartTime = TimePoint();
+    std::atomic<TimePoint> mLastTransactionCommitTime = TimePoint();
+
+    const Now mNow;
+    const bool mTraceDetailedInfo;
+};
+
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index b74d375..e9970cf 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -1036,10 +1036,9 @@
         mScheduler->resyncToHardwareVsync(true, refreshRate.getVsyncPeriod());
         // As we called to set period, we will call to onRefreshRateChangeCompleted once
         // DispSync model is locked.
-        mVSyncModulator->onRefreshRateChangeInitiated();
+        modulateVsync(&VsyncModulator::onRefreshRateChangeInitiated);
 
-        mPhaseConfiguration->setRefreshRateFps(refreshRate.getFps());
-        mVSyncModulator->setPhaseOffsets(mPhaseConfiguration->getCurrentOffsets());
+        updatePhaseConfiguration(refreshRate);
         mScheduler->setConfigChangePending(true);
     }
 
@@ -1098,8 +1097,7 @@
     if (refreshRate.getVsyncPeriod() != oldRefreshRate.getVsyncPeriod()) {
         mTimeStats->incrementRefreshRateSwitches();
     }
-    mPhaseConfiguration->setRefreshRateFps(refreshRate.getFps());
-    mVSyncModulator->setPhaseOffsets(mPhaseConfiguration->getCurrentOffsets());
+    updatePhaseConfiguration(refreshRate);
     ATRACE_INT("ActiveConfigFPS", refreshRate.getFps());
 
     if (mUpcomingActiveConfig.event != Scheduler::ConfigEvent::None) {
@@ -1119,9 +1117,9 @@
 
     const auto& refreshRate =
             mRefreshRateConfigs->getRefreshRateFromConfigId(mDesiredActiveConfig.configId);
+
     mScheduler->resyncToHardwareVsync(true, refreshRate.getVsyncPeriod());
-    mPhaseConfiguration->setRefreshRateFps(refreshRate.getFps());
-    mVSyncModulator->setPhaseOffsets(mPhaseConfiguration->getCurrentOffsets());
+    updatePhaseConfiguration(refreshRate);
     mScheduler->setConfigChangePending(false);
 }
 
@@ -1600,7 +1598,7 @@
     bool periodFlushed = false;
     mScheduler->addResyncSample(timestamp, vsyncPeriod, &periodFlushed);
     if (periodFlushed) {
-        mVSyncModulator->onRefreshRateChangeCompleted();
+        modulateVsync(&VsyncModulator::onRefreshRateChangeCompleted);
     }
 }
 
@@ -1790,7 +1788,7 @@
     // We are storing the last 2 present fences. If sf's phase offset is to be
     // woken up before the actual vsync but targeting the next vsync, we need to check
     // fence N-2
-    return mVSyncModulator->getOffsets().sf > 0 ? mPreviousPresentFences[0]
+    return mVsyncModulator->getOffsets().sf > 0 ? mPreviousPresentFences[0]
                                                 : mPreviousPresentFences[1];
 }
 
@@ -1823,7 +1821,7 @@
     mScheduler->getDisplayStatInfo(&stats);
     const nsecs_t presentTime = mScheduler->getDispSyncExpectedPresentTime(now);
     // Inflate the expected present time if we're targetting the next vsync.
-    return mVSyncModulator->getOffsets().sf > 0 ? presentTime : presentTime + stats.vsyncPeriod;
+    return mVsyncModulator->getOffsets().sf > 0 ? presentTime : presentTime + stats.vsyncPeriod;
 }
 
 void SurfaceFlinger::onMessageReceived(int32_t what, nsecs_t expectedVSyncTime) {
@@ -2120,7 +2118,8 @@
     }
 
     // TODO: b/160583065 Enable skip validation when SF caches all client composition layers
-    mVSyncModulator->onRefreshed(mHadClientComposition || mReusedClientComposition);
+    const bool usedGpuComposition = mHadClientComposition || mReusedClientComposition;
+    modulateVsync(&VsyncModulator::onDisplayRefresh, usedGpuComposition);
 
     mLayersWithQueuedFrames.clear();
     if (mVisibleRegionsDirty) {
@@ -2416,7 +2415,7 @@
     // with mStateLock held to guarantee that mCurrentState won't change
     // until the transaction is committed.
 
-    mVSyncModulator->onTransactionHandled();
+    modulateVsync(&VsyncModulator::onTransactionCommit);
     transactionFlags = getTransactionFlags(eTransactionMask);
     handleTransactionLocked(transactionFlags);
 
@@ -2977,6 +2976,7 @@
     mRefreshRateStats->setConfigMode(currentConfig);
 
     mPhaseConfiguration = getFactory().createPhaseConfiguration(*mRefreshRateConfigs);
+    mVsyncModulator.emplace(mPhaseConfiguration->getCurrentOffsets());
 
     // start the EventThread
     mScheduler = getFactory().createScheduler(*mRefreshRateConfigs, *this);
@@ -2990,8 +2990,6 @@
                                          });
 
     mEventQueue->setEventConnection(mScheduler->getEventConnection(mSfConnectionHandle));
-    mVSyncModulator.emplace(*mScheduler, mAppConnectionHandle, mSfConnectionHandle,
-                            mPhaseConfiguration->getCurrentOffsets());
 
     mRegionSamplingThread =
             new RegionSamplingThread(*this, *mScheduler,
@@ -3009,8 +3007,17 @@
                                               vsyncPeriod);
 }
 
-void SurfaceFlinger::commitTransaction()
-{
+void SurfaceFlinger::updatePhaseConfiguration(const RefreshRate& refreshRate) {
+    mPhaseConfiguration->setRefreshRateFps(refreshRate.getFps());
+    setPhaseOffsets(mVsyncModulator->setPhaseOffsets(mPhaseConfiguration->getCurrentOffsets()));
+}
+
+void SurfaceFlinger::setPhaseOffsets(const VsyncModulator::Offsets& offsets) {
+    mScheduler->setPhaseOffset(mAppConnectionHandle, offsets.app);
+    mScheduler->setPhaseOffset(mSfConnectionHandle, offsets.sf);
+}
+
+void SurfaceFlinger::commitTransaction() {
     commitTransactionLocked();
     mTransactionPending = false;
     mAnimTransactionPending = false;
@@ -3254,16 +3261,13 @@
 }
 
 uint32_t SurfaceFlinger::setTransactionFlags(uint32_t flags) {
-    return setTransactionFlags(flags, Scheduler::TransactionStart::Normal);
+    return setTransactionFlags(flags, TransactionSchedule::Late);
 }
 
-uint32_t SurfaceFlinger::setTransactionFlags(uint32_t flags,
-                                             Scheduler::TransactionStart transactionStart) {
+uint32_t SurfaceFlinger::setTransactionFlags(uint32_t flags, TransactionSchedule schedule) {
     uint32_t old = mTransactionFlags.fetch_or(flags);
-    mVSyncModulator->setTransactionStart(transactionStart);
-    if ((old & flags)==0) { // wake the server up
-        signalTransaction();
-    }
+    modulateVsync(&VsyncModulator::setTransactionSchedule, schedule);
+    if ((old & flags) == 0) signalTransaction();
     return old;
 }
 
@@ -3477,17 +3481,11 @@
         mForceTraversal = 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;
+    const auto schedule = [](uint32_t flags) {
+        if (flags & eEarlyWakeup) return TransactionSchedule::Early;
+        if (flags & eExplicitEarlyWakeupEnd) return TransactionSchedule::EarlyEnd;
+        if (flags & eExplicitEarlyWakeupStart) return TransactionSchedule::EarlyStart;
+        return TransactionSchedule::Late;
     }(flags);
 
     if (transactionFlags) {
@@ -3507,7 +3505,7 @@
         }
 
         // this triggers the transaction
-        setTransactionFlags(transactionFlags, transactionStart);
+        setTransactionFlags(transactionFlags, schedule);
 
         if (flags & eAnimation) {
             mAnimTransactionPending = true;
@@ -3545,11 +3543,10 @@
             }
         }
     } 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);
+        // Update VsyncModulator state machine even if transaction is not needed.
+        if (schedule == TransactionSchedule::EarlyStart ||
+            schedule == TransactionSchedule::EarlyEnd) {
+            modulateVsync(&VsyncModulator::setTransactionSchedule, schedule);
         }
     }
 }
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index 0c5997c..3c24158 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -58,7 +58,7 @@
 #include "Scheduler/RefreshRateConfigs.h"
 #include "Scheduler/RefreshRateStats.h"
 #include "Scheduler/Scheduler.h"
-#include "Scheduler/VSyncModulator.h"
+#include "Scheduler/VsyncModulator.h"
 #include "SurfaceFlingerFactory.h"
 #include "SurfaceTracing.h"
 #include "TracedOrdinal.h"
@@ -549,8 +549,6 @@
     void signalLayerUpdate();
     void signalRefresh();
 
-    using RefreshRate = scheduler::RefreshRateConfigs::RefreshRate;
-
     struct ActiveConfigInfo {
         HwcConfigIndexType configId;
         Scheduler::ConfigEvent event = Scheduler::ConfigEvent::None;
@@ -604,7 +602,13 @@
     void updateInputWindowInfo();
     void commitInputWindowCommands() REQUIRES(mStateLock);
     void updateCursorAsync();
+
+    using RefreshRate = scheduler::RefreshRateConfigs::RefreshRate;
+    using VsyncModulator = scheduler::VsyncModulator;
+
     void initScheduler(PhysicalDisplayId primaryDisplayId);
+    void updatePhaseConfiguration(const RefreshRate&);
+    void setPhaseOffsets(const VsyncModulator::Offsets&);
 
     /* handlePageFlip - latch a new buffer if available and compute the dirty
      * region. Returns whether a new buffer has been latched, i.e., whether it
@@ -615,6 +619,8 @@
     /* ------------------------------------------------------------------------
      * Transactions
      */
+    using TransactionSchedule = scheduler::TransactionSchedule;
+
     void applyTransactionState(const Vector<ComposerState>& state,
                                const Vector<DisplayState>& displays, uint32_t flags,
                                const InputWindowCommands& inputWindowCommands,
@@ -638,7 +644,7 @@
     // but there is no need to try and wake up immediately to do it. Rather we rely on
     // onFrameAvailable or another layer update to wake us up.
     void setTraversalNeeded();
-    uint32_t setTransactionFlags(uint32_t flags, Scheduler::TransactionStart transactionStart);
+    uint32_t setTransactionFlags(uint32_t flags, TransactionSchedule);
     void commitTransaction() REQUIRES(mStateLock);
     void commitOffscreenLayers();
     bool transactionIsReadyToBeApplied(int64_t desiredPresentTime,
@@ -843,14 +849,13 @@
     void dispatchDisplayHotplugEvent(PhysicalDisplayId displayId, bool connected);
 
     /* ------------------------------------------------------------------------
-     * VSync
+     * VSYNC
      */
     nsecs_t getVsyncPeriodFromHWC() const REQUIRES(mStateLock);
 
     // Sets the refresh rate by switching active configs, if they are available for
     // the desired refresh rate.
-    void changeRefreshRateLocked(const RefreshRate&, Scheduler::ConfigEvent event)
-            REQUIRES(mStateLock);
+    void changeRefreshRateLocked(const RefreshRate&, Scheduler::ConfigEvent) REQUIRES(mStateLock);
 
     bool isDisplayConfigAllowed(HwcConfigIndexType configId) const REQUIRES(mStateLock);
 
@@ -1211,8 +1216,8 @@
     // Stores phase offsets configured per refresh rate.
     std::unique_ptr<scheduler::PhaseConfiguration> mPhaseConfiguration;
 
-    // Optional to defer construction until scheduler connections are created.
-    std::optional<scheduler::VSyncModulator> mVSyncModulator;
+    // Optional to defer construction until PhaseConfiguration is created.
+    std::optional<scheduler::VsyncModulator> mVsyncModulator;
 
     std::unique_ptr<scheduler::RefreshRateConfigs> mRefreshRateConfigs;
     std::unique_ptr<scheduler::RefreshRateStats> mRefreshRateStats;
@@ -1220,6 +1225,14 @@
     std::atomic<nsecs_t> mExpectedPresentTime = 0;
     hal::Vsync mHWCVsyncPendingState = hal::Vsync::DISABLE;
 
+    template <typename... Args,
+              typename Handler = VsyncModulator::OffsetsOpt (VsyncModulator::*)(Args...)>
+    void modulateVsync(Handler handler, Args... args) {
+        if (const auto offsets = (*mVsyncModulator.*handler)(args...)) {
+            setPhaseOffsets(*offsets);
+        }
+    }
+
     /* ------------------------------------------------------------------------
      * Generic Layer Metadata
      */
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index c46c914..4a59c19 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -61,7 +61,7 @@
         "StrongTypingTest.cpp",
         "VSyncDispatchTimerQueueTest.cpp",
         "VSyncDispatchRealtimeTest.cpp",
-        "VSyncModulatorTest.cpp",
+        "VsyncModulatorTest.cpp",
         "VSyncPredictorTest.cpp",
         "VSyncReactorTest.cpp",
         "mock/DisplayHardware/MockComposer.cpp",
diff --git a/services/surfaceflinger/tests/unittests/PhaseOffsetsTest.cpp b/services/surfaceflinger/tests/unittests/PhaseOffsetsTest.cpp
index 0b74682..96bb74b 100644
--- a/services/surfaceflinger/tests/unittests/PhaseOffsetsTest.cpp
+++ b/services/surfaceflinger/tests/unittests/PhaseOffsetsTest.cpp
@@ -29,8 +29,7 @@
 
 using namespace testing;
 
-namespace android {
-namespace scheduler {
+namespace android::scheduler {
 
 class TestablePhaseOffsetsAsDurations : public impl::PhaseDurations {
 public:
@@ -53,7 +52,6 @@
     TestablePhaseOffsetsAsDurations mPhaseDurations;
 };
 
-namespace {
 /* ------------------------------------------------------------------------
  * Test cases
  */
@@ -71,9 +69,9 @@
 
     EXPECT_EQ(offsets.early.app, 833'334);
 
-    EXPECT_EQ(offsets.earlyGl.sf, 3'166'667);
+    EXPECT_EQ(offsets.earlyGpu.sf, 3'166'667);
 
-    EXPECT_EQ(offsets.earlyGl.app, 15'500'001);
+    EXPECT_EQ(offsets.earlyGpu.app, 15'500'001);
 }
 
 TEST_F(PhaseDurationTest, getOffsetsForRefreshRate_90Hz) {
@@ -90,9 +88,9 @@
 
     EXPECT_EQ(offsets.early.app, 833'333);
 
-    EXPECT_EQ(offsets.earlyGl.sf, -2'388'889);
+    EXPECT_EQ(offsets.earlyGpu.sf, -2'388'889);
 
-    EXPECT_EQ(offsets.earlyGl.app, 9'944'444);
+    EXPECT_EQ(offsets.earlyGpu.app, 9'944'444);
 }
 
 TEST_F(PhaseDurationTest, getOffsetsForRefreshRate_DefaultOffsets) {
@@ -107,9 +105,9 @@
 
         EXPECT_EQ(offsets.early.app, 1'000'000);
 
-        EXPECT_EQ(offsets.earlyGl.sf, 1'000'000);
+        EXPECT_EQ(offsets.earlyGpu.sf, 1'000'000);
 
-        EXPECT_EQ(offsets.earlyGl.app, 1'000'000);
+        EXPECT_EQ(offsets.earlyGpu.app, 1'000'000);
     };
 
     phaseOffsetsWithDefaultValues.setRefreshRateFps(90.0f);
@@ -136,31 +134,20 @@
 
     EXPECT_EQ(offsets.early.app, 35'527'208);
 
-    EXPECT_EQ(offsets.earlyGl.sf, 54'527'208);
+    EXPECT_EQ(offsets.earlyGpu.sf, 54'527'208);
 
-    EXPECT_EQ(offsets.earlyGl.app, 33'527'208);
+    EXPECT_EQ(offsets.earlyGpu.app, 33'527'208);
 }
 
-} // namespace
+TEST(PhaseOffsetsTest, getOffsetsForRefreshRate_unknownRefreshRate) {
+    struct PhaseOffsets : impl::PhaseOffsets {
+        using impl::PhaseOffsets::PhaseOffsets;
+        static PhaseOffsets get() {
+            return {{60.0f, 90.0f}, 60.0f, 1'000'000, 1'000'000, {}, {}, {}, {}, 10'000'000};
+        }
+    };
 
-class TestablePhaseOffsets : public impl::PhaseOffsets {
-public:
-    TestablePhaseOffsets()
-          : impl::PhaseOffsets({60.0f, 90.0f}, 60.0f, 1'000'000, 1'000'000, {}, {}, {}, {},
-                               10'000'000) {}
-};
-
-class PhaseOffsetsTest : public testing::Test {
-protected:
-    PhaseOffsetsTest() = default;
-    ~PhaseOffsetsTest() = default;
-
-    TestablePhaseOffsets mPhaseOffsets;
-};
-
-namespace {
-TEST_F(PhaseOffsetsTest, getOffsetsForRefreshRate_unknownRefreshRate) {
-    auto offsets = mPhaseOffsets.getOffsetsForRefreshRate(14.7f);
+    auto offsets = PhaseOffsets::get().getOffsetsForRefreshRate(14.7f);
 
     EXPECT_EQ(offsets.late.sf, 1'000'000);
 
@@ -170,14 +157,12 @@
 
     EXPECT_EQ(offsets.early.app, 1'000'000);
 
-    EXPECT_EQ(offsets.earlyGl.sf, 1'000'000);
+    EXPECT_EQ(offsets.earlyGpu.sf, 1'000'000);
 
-    EXPECT_EQ(offsets.earlyGl.app, 1'000'000);
+    EXPECT_EQ(offsets.earlyGpu.app, 1'000'000);
 }
 
-} // namespace
-} // namespace scheduler
-} // namespace android
+} // namespace android::scheduler
 
 // TODO(b/129481165): remove the #pragma below and fix conversion issues
-#pragma clang diagnostic pop // ignored "-Wconversion"
\ No newline at end of file
+#pragma clang diagnostic pop // ignored "-Wconversion"
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index b349144..df15516 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -218,6 +218,7 @@
                                              /*powerMode=*/hal::PowerMode::OFF);
         mFlinger->mPhaseConfiguration =
                 mFactory.createPhaseConfiguration(*mFlinger->mRefreshRateConfigs);
+        mFlinger->mVsyncModulator.emplace(mFlinger->mPhaseConfiguration->getCurrentOffsets());
 
         constexpr bool kUseContentDetectionV2 = false;
         mScheduler =
@@ -227,10 +228,6 @@
         mFlinger->mAppConnectionHandle = mScheduler->createConnection(std::move(appEventThread));
         mFlinger->mSfConnectionHandle = mScheduler->createConnection(std::move(sfEventThread));
         resetScheduler(mScheduler);
-
-        mFlinger->mVSyncModulator.emplace(*mScheduler, mFlinger->mAppConnectionHandle,
-                                          mFlinger->mSfConnectionHandle,
-                                          mFlinger->mPhaseConfiguration->getCurrentOffsets());
     }
 
     void resetScheduler(Scheduler* scheduler) { mFlinger->mScheduler.reset(scheduler); }
diff --git a/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp b/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp
deleted file mode 100644
index af1e84b..0000000
--- a/services/surfaceflinger/tests/unittests/VSyncModulatorTest.cpp
+++ /dev/null
@@ -1,301 +0,0 @@
-/*
- * 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 final : public IPhaseOffsetControl {
-public:
-    nsecs_t getOffset(ConnectionHandle handle) { return mPhaseOffset[handle]; }
-
-private:
-    void setPhaseOffset(ConnectionHandle handle, nsecs_t phaseOffset) override {
-        mPhaseOffset[handle] = phaseOffset;
-    }
-
-    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
diff --git a/services/surfaceflinger/tests/unittests/VsyncModulatorTest.cpp b/services/surfaceflinger/tests/unittests/VsyncModulatorTest.cpp
new file mode 100644
index 0000000..d7093c6
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/VsyncModulatorTest.cpp
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "Scheduler/VsyncModulator.h"
+
+namespace android::scheduler {
+
+class VsyncModulatorTest : public testing::Test {
+    enum Offsets {
+        SF_LATE,
+        APP_LATE,
+        SF_EARLY,
+        APP_EARLY,
+        SF_EARLY_GPU,
+        APP_EARLY_GPU,
+    };
+
+    static VsyncModulator::TimePoint Now() {
+        static VsyncModulator::TimePoint now;
+        return now += VsyncModulator::MIN_EARLY_TRANSACTION_TIME;
+    }
+
+protected:
+    static constexpr auto MIN_EARLY_TRANSACTION_FRAMES =
+            VsyncModulator::MIN_EARLY_TRANSACTION_FRAMES;
+
+    using Schedule = scheduler::TransactionSchedule;
+
+    const VsyncModulator::Offsets kEarly{SF_EARLY, APP_EARLY};
+    const VsyncModulator::Offsets kEarlyGpu{SF_EARLY_GPU, APP_EARLY_GPU};
+    const VsyncModulator::Offsets kLate{SF_LATE, APP_LATE};
+
+    const VsyncModulator::OffsetsConfig mOffsets = {kEarly, kEarlyGpu, kLate};
+    VsyncModulator mVsyncModulator{mOffsets, Now};
+
+    void SetUp() override { EXPECT_EQ(kLate, mVsyncModulator.setPhaseOffsets(mOffsets)); }
+};
+
+#define CHECK_COMMIT(result, offsets)                         \
+    EXPECT_EQ(result, mVsyncModulator.onTransactionCommit()); \
+    EXPECT_EQ(offsets, mVsyncModulator.getOffsets());
+
+#define CHECK_REFRESH(N, result, offsets)                           \
+    for (int i = 0; i < N; i++) {                                   \
+        EXPECT_EQ(result, mVsyncModulator.onDisplayRefresh(false)); \
+        EXPECT_EQ(offsets, mVsyncModulator.getOffsets());           \
+    }
+
+TEST_F(VsyncModulatorTest, Late) {
+    EXPECT_FALSE(mVsyncModulator.setTransactionSchedule(Schedule::Late));
+
+    CHECK_COMMIT(std::nullopt, kLate);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES, std::nullopt, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyEnd) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyStart) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyStart));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(5 * MIN_EARLY_TRANSACTION_FRAMES, std::nullopt, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyStartWithEarly) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyStart));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(5 * MIN_EARLY_TRANSACTION_FRAMES, std::nullopt, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::Early));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(5 * MIN_EARLY_TRANSACTION_FRAMES, std::nullopt, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyStartWithMoreTransactions) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyStart));
+
+    CHECK_COMMIT(kEarly, kEarly);
+
+    for (int i = 0; i < 5 * MIN_EARLY_TRANSACTION_FRAMES; i++) {
+        EXPECT_FALSE(mVsyncModulator.setTransactionSchedule(Schedule::Late));
+        CHECK_REFRESH(1, std::nullopt, kEarly);
+    }
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyStartAfterEarlyEnd) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyStart));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(1, kEarly, kEarly);
+    CHECK_REFRESH(5 * MIN_EARLY_TRANSACTION_FRAMES, std::nullopt, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+TEST_F(VsyncModulatorTest, EarlyStartAfterEarlyEndWithMoreTransactions) {
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyStart));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(1, kEarly, kEarly);
+
+    for (int i = 0; i < 5 * MIN_EARLY_TRANSACTION_FRAMES; i++) {
+        EXPECT_FALSE(mVsyncModulator.setTransactionSchedule(Schedule::Late));
+        CHECK_REFRESH(1, std::nullopt, kEarly);
+    }
+
+    EXPECT_EQ(kEarly, mVsyncModulator.setTransactionSchedule(Schedule::EarlyEnd));
+
+    CHECK_COMMIT(kEarly, kEarly);
+    CHECK_REFRESH(MIN_EARLY_TRANSACTION_FRAMES - 1, kEarly, kEarly);
+    CHECK_REFRESH(1, kLate, kLate);
+}
+
+} // namespace android::scheduler