SF: add VSyncPredictor implementation

Adds an implementation of VSyncPredictor, an object that will
predict vsync events for multiple fixed-rate vsync systems.
The prediction is based on both the HWVsync signal and the
presentation fence timings for the VSyncDispatch object.

Bug: 140201379
Fixes: 140302888
Test: 13 new unit tests

Change-Id: I195902cc70561d028741d822e4001ad21ab391cf
diff --git a/services/surfaceflinger/Android.bp b/services/surfaceflinger/Android.bp
index 3d94918..4b71bd8 100644
--- a/services/surfaceflinger/Android.bp
+++ b/services/surfaceflinger/Android.bp
@@ -165,8 +165,9 @@
         "Scheduler/PhaseOffsets.cpp",
         "Scheduler/Scheduler.cpp",
         "Scheduler/SchedulerUtils.cpp",
-        "Scheduler/VSyncDispatchTimerQueue.cpp",
         "Scheduler/Timer.cpp",
+        "Scheduler/VSyncDispatchTimerQueue.cpp",
+        "Scheduler/VSyncPredictor.cpp",
         "Scheduler/VSyncModulator.cpp",
         "StartPropertySetThread.cpp",
         "SurfaceFlinger.cpp",
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
new file mode 100644
index 0000000..643c5d2
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
@@ -0,0 +1,208 @@
+/*
+ * 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
+//#define LOG_NDEBUG 0
+#include "VSyncPredictor.h"
+#include <android-base/logging.h>
+#include <cutils/compiler.h>
+#include <utils/Log.h>
+#include <utils/Trace.h>
+#include <algorithm>
+#include <chrono>
+#include "SchedulerUtils.h"
+
+namespace android::scheduler {
+static auto constexpr kNeedsSamplesTag = "SamplesRequested";
+static auto constexpr kMaxPercent = 100u;
+
+VSyncPredictor::~VSyncPredictor() = default;
+
+VSyncPredictor::VSyncPredictor(nsecs_t idealPeriod, size_t historySize,
+                               size_t minimumSamplesForPrediction, uint32_t outlierTolerancePercent)
+      : kHistorySize(historySize),
+        kMinimumSamplesForPrediction(minimumSamplesForPrediction),
+        kOutlierTolerancePercent(std::min(outlierTolerancePercent, kMaxPercent)),
+        mIdealPeriod(idealPeriod) {
+    mRateMap[mIdealPeriod] = {idealPeriod, 0};
+}
+
+inline size_t VSyncPredictor::next(int i) const {
+    return (i + 1) % timestamps.size();
+}
+
+bool VSyncPredictor::validate(nsecs_t timestamp) const {
+    if (lastTimestampIndex < 0 || timestamps.empty()) {
+        return true;
+    }
+
+    auto const aValidTimestamp = timestamps[lastTimestampIndex];
+    auto const percent = (timestamp - aValidTimestamp) % mIdealPeriod * kMaxPercent / mIdealPeriod;
+    return percent < kOutlierTolerancePercent || percent > (kMaxPercent - kOutlierTolerancePercent);
+}
+
+void VSyncPredictor::addVsyncTimestamp(nsecs_t timestamp) {
+    std::lock_guard<std::mutex> lk(mMutex);
+
+    if (!validate(timestamp)) {
+        ALOGW("timestamp was too far off the last known timestamp");
+        return;
+    }
+
+    if (timestamps.size() != kHistorySize) {
+        timestamps.push_back(timestamp);
+        lastTimestampIndex = next(lastTimestampIndex);
+    } else {
+        lastTimestampIndex = next(lastTimestampIndex);
+        timestamps[lastTimestampIndex] = timestamp;
+    }
+
+    if (timestamps.size() < kMinimumSamplesForPrediction) {
+        mRateMap[mIdealPeriod] = {mIdealPeriod, 0};
+        return;
+    }
+
+    // This is a 'simple linear regression' calculation of Y over X, with Y being the
+    // vsync timestamps, and X being the ordinal of vsync count.
+    // The calculated slope is the vsync period.
+    // Formula for reference:
+    // Sigma_i: means sum over all timestamps.
+    // mean(variable): statistical mean of variable.
+    // X: snapped ordinal of the timestamp
+    // Y: vsync timestamp
+    //
+    //         Sigma_i( (X_i - mean(X)) * (Y_i - mean(Y) )
+    // slope = -------------------------------------------
+    //         Sigma_i ( X_i - mean(X) ) ^ 2
+    //
+    // intercept = mean(Y) - slope * mean(X)
+    //
+    std::vector<nsecs_t> vsyncTS(timestamps.size());
+    std::vector<nsecs_t> ordinals(timestamps.size());
+
+    // normalizing to the oldest timestamp cuts down on error in calculating the intercept.
+    auto const oldest_ts = *std::min_element(timestamps.begin(), timestamps.end());
+    auto it = mRateMap.find(mIdealPeriod);
+    auto const currentPeriod = std::get<0>(it->second);
+    // TODO (b/144707443): its important that there's some precision in the mean of the ordinals
+    //                     for the intercept calculation, so scale the ordinals by 10 to continue
+    //                     fixed point calculation. Explore expanding
+    //                     scheduler::utils::calculate_mean to have a fixed point fractional part.
+    static constexpr int kScalingFactor = 10;
+
+    for (auto i = 0u; i < timestamps.size(); i++) {
+        vsyncTS[i] = timestamps[i] - oldest_ts;
+        ordinals[i] = ((vsyncTS[i] + (currentPeriod / 2)) / currentPeriod) * kScalingFactor;
+    }
+
+    auto meanTS = scheduler::calculate_mean(vsyncTS);
+    auto meanOrdinal = scheduler::calculate_mean(ordinals);
+    for (auto i = 0; i < vsyncTS.size(); i++) {
+        vsyncTS[i] -= meanTS;
+        ordinals[i] -= meanOrdinal;
+    }
+
+    auto top = 0ll;
+    auto bottom = 0ll;
+    for (auto i = 0; i < vsyncTS.size(); i++) {
+        top += vsyncTS[i] * ordinals[i];
+        bottom += ordinals[i] * ordinals[i];
+    }
+
+    if (CC_UNLIKELY(bottom == 0)) {
+        it->second = {mIdealPeriod, 0};
+        return;
+    }
+
+    nsecs_t const anticipatedPeriod = top / bottom * kScalingFactor;
+    nsecs_t const intercept = meanTS - (anticipatedPeriod * meanOrdinal / kScalingFactor);
+
+    it->second = {anticipatedPeriod, intercept};
+
+    ALOGV("model update ts: %" PRId64 " slope: %" PRId64 " intercept: %" PRId64, timestamp,
+          anticipatedPeriod, intercept);
+}
+
+nsecs_t VSyncPredictor::nextAnticipatedVSyncTimeFrom(nsecs_t timePoint) const {
+    std::lock_guard<std::mutex> lk(mMutex);
+
+    auto const [slope, intercept] = getVSyncPredictionModel(lk);
+
+    if (timestamps.empty()) {
+        auto const knownTimestamp = mKnownTimestamp ? *mKnownTimestamp : timePoint;
+        auto const numPeriodsOut = ((timePoint - knownTimestamp) / mIdealPeriod) + 1;
+        return knownTimestamp + numPeriodsOut * mIdealPeriod;
+    }
+
+    auto const oldest = *std::min_element(timestamps.begin(), timestamps.end());
+    auto const ordinalRequest = (timePoint - oldest + slope) / slope;
+    auto const prediction = (ordinalRequest * slope) + intercept + oldest;
+
+    ALOGV("prediction made from: %" PRId64 " prediction: %" PRId64 " (+%" PRId64 ") slope: %" PRId64
+          " intercept: %" PRId64,
+          timePoint, prediction, prediction - timePoint, slope, intercept);
+    return prediction;
+}
+
+std::tuple<nsecs_t, nsecs_t> VSyncPredictor::getVSyncPredictionModel() const {
+    std::lock_guard<std::mutex> lk(mMutex);
+    return VSyncPredictor::getVSyncPredictionModel(lk);
+}
+
+std::tuple<nsecs_t, nsecs_t> VSyncPredictor::getVSyncPredictionModel(
+        std::lock_guard<std::mutex> const&) const {
+    return mRateMap.find(mIdealPeriod)->second;
+}
+
+void VSyncPredictor::setPeriod(nsecs_t period) {
+    ATRACE_CALL();
+
+    std::lock_guard<std::mutex> lk(mMutex);
+    static constexpr size_t kSizeLimit = 30;
+    if (CC_UNLIKELY(mRateMap.size() == kSizeLimit)) {
+        mRateMap.erase(mRateMap.begin());
+    }
+
+    mIdealPeriod = period;
+    if (mRateMap.find(period) == mRateMap.end()) {
+        mRateMap[mIdealPeriod] = {period, 0};
+    }
+
+    if (!timestamps.empty()) {
+        mKnownTimestamp = *std::max_element(timestamps.begin(), timestamps.end());
+        timestamps.clear();
+        lastTimestampIndex = 0;
+    }
+}
+
+bool VSyncPredictor::needsMoreSamples(nsecs_t now) const {
+    using namespace std::literals::chrono_literals;
+    std::lock_guard<std::mutex> lk(mMutex);
+    bool needsMoreSamples = true;
+    if (timestamps.size() >= kMinimumSamplesForPrediction) {
+        nsecs_t constexpr aLongTime =
+                std::chrono::duration_cast<std::chrono::nanoseconds>(500ms).count();
+        if (!(lastTimestampIndex < 0 || timestamps.empty())) {
+            auto const lastTimestamp = timestamps[lastTimestampIndex];
+            needsMoreSamples = !((lastTimestamp + aLongTime) > now);
+        }
+    }
+
+    ATRACE_INT(kNeedsSamplesTag, needsMoreSamples);
+    return needsMoreSamples;
+}
+
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.h b/services/surfaceflinger/Scheduler/VSyncPredictor.h
new file mode 100644
index 0000000..1590f49
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.h
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <android-base/thread_annotations.h>
+#include <mutex>
+#include <unordered_map>
+#include <vector>
+#include "VSyncTracker.h"
+
+namespace android::scheduler {
+
+class VSyncPredictor : public VSyncTracker {
+public:
+    /*
+     * \param [in] idealPeriod  The initial ideal period to use.
+     * \param [in] historySize  The internal amount of entries to store in the model.
+     * \param [in] minimumSamplesForPrediction The minimum number of samples to collect before
+     * predicting. \param [in] outlierTolerancePercent a number 0 to 100 that will be used to filter
+     * samples that fall outlierTolerancePercent from an anticipated vsync event.
+     */
+    VSyncPredictor(nsecs_t idealPeriod, size_t historySize, size_t minimumSamplesForPrediction,
+                   uint32_t outlierTolerancePercent);
+    ~VSyncPredictor();
+
+    void addVsyncTimestamp(nsecs_t timestamp) final;
+    nsecs_t nextAnticipatedVSyncTimeFrom(nsecs_t timePoint) const final;
+
+    /*
+     * Inform the model that the period is anticipated to change to a new value.
+     * model will use the period parameter to predict vsync events until enough
+     * timestamps with the new period have been collected.
+     *
+     * \param [in] period   The new period that should be used.
+     */
+    void setPeriod(nsecs_t period);
+
+    /* Query if the model is in need of more samples to make a prediction at timePoint.
+     * \param [in] timePoint    The timePoint to inquire of.
+     * \return  True, if model would benefit from more samples, False if not.
+     */
+    bool needsMoreSamples(nsecs_t timePoint) const;
+
+    std::tuple<nsecs_t /* slope */, nsecs_t /* intercept */> getVSyncPredictionModel() const;
+
+private:
+    VSyncPredictor(VSyncPredictor const&) = delete;
+    VSyncPredictor& operator=(VSyncPredictor const&) = delete;
+
+    size_t const kHistorySize;
+    size_t const kMinimumSamplesForPrediction;
+    size_t const kOutlierTolerancePercent;
+
+    std::mutex mutable mMutex;
+    size_t next(int i) const REQUIRES(mMutex);
+    bool validate(nsecs_t timestamp) const REQUIRES(mMutex);
+    std::tuple<nsecs_t, nsecs_t> getVSyncPredictionModel(std::lock_guard<std::mutex> const&) const
+            REQUIRES(mMutex);
+
+    nsecs_t mIdealPeriod GUARDED_BY(mMutex);
+    std::optional<nsecs_t> mKnownTimestamp GUARDED_BY(mMutex);
+
+    std::unordered_map<nsecs_t, std::tuple<nsecs_t, nsecs_t>> mutable mRateMap GUARDED_BY(mMutex);
+
+    int lastTimestampIndex GUARDED_BY(mMutex) = 0;
+    std::vector<nsecs_t> timestamps GUARDED_BY(mMutex);
+};
+
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index c6c3d29..0c4a752 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -56,6 +56,7 @@
         "StrongTypingTest.cpp",
         "VSyncDispatchTimerQueueTest.cpp",
         "VSyncDispatchRealtimeTest.cpp",
+        "VSyncPredictorTest.cpp",
         "mock/DisplayHardware/MockComposer.cpp",
         "mock/DisplayHardware/MockDisplay.cpp",
         "mock/DisplayHardware/MockPowerAdvisor.cpp",
diff --git a/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
new file mode 100644
index 0000000..d0c8090
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
@@ -0,0 +1,322 @@
+/*
+ * 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.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "LibSurfaceFlingerUnittests"
+#define LOG_NDEBUG 0
+
+#include "Scheduler/VSyncPredictor.h"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <algorithm>
+#include <chrono>
+#include <utility>
+
+using namespace testing;
+using namespace std::literals;
+
+namespace android::scheduler {
+
+MATCHER_P2(IsCloseTo, value, tolerance, "is within tolerance") {
+    return arg <= value + tolerance && value >= value - tolerance;
+}
+
+std::vector<nsecs_t> generateVsyncTimestamps(size_t count, nsecs_t period, nsecs_t bias) {
+    std::vector<nsecs_t> vsyncs(count);
+    std::generate(vsyncs.begin(), vsyncs.end(),
+                  [&, n = 0]() mutable { return n++ * period + bias; });
+    return vsyncs;
+}
+
+struct VSyncPredictorTest : testing::Test {
+    nsecs_t mNow = 0;
+    nsecs_t mPeriod = 1000;
+    static constexpr size_t kHistorySize = 10;
+    static constexpr size_t kMinimumSamplesForPrediction = 6;
+    static constexpr size_t kOutlierTolerancePercent = 25;
+    static constexpr nsecs_t mMaxRoundingError = 100;
+
+    VSyncPredictor tracker{mPeriod, kHistorySize, kMinimumSamplesForPrediction,
+                           kOutlierTolerancePercent};
+};
+
+TEST_F(VSyncPredictorTest, reportsAnticipatedPeriod) {
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+
+    EXPECT_THAT(slope, Eq(mPeriod));
+    EXPECT_THAT(intercept, Eq(0));
+
+    auto const changedPeriod = 2000;
+    tracker.setPeriod(changedPeriod);
+    std::tie(slope, intercept) = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, Eq(changedPeriod));
+    EXPECT_THAT(intercept, Eq(0));
+}
+
+TEST_F(VSyncPredictorTest, reportsSamplesNeededWhenHasNoDataPoints) {
+    for (auto i = 0u; i < kMinimumSamplesForPrediction; i++) {
+        EXPECT_TRUE(tracker.needsMoreSamples(mNow += mPeriod));
+        tracker.addVsyncTimestamp(mNow);
+    }
+    EXPECT_FALSE(tracker.needsMoreSamples(mNow));
+}
+
+TEST_F(VSyncPredictorTest, reportsSamplesNeededAfterExplicitRateChange) {
+    for (auto i = 0u; i < kMinimumSamplesForPrediction; i++) {
+        tracker.addVsyncTimestamp(mNow += mPeriod);
+    }
+    EXPECT_FALSE(tracker.needsMoreSamples(mNow));
+
+    auto const changedPeriod = mPeriod * 2;
+    tracker.setPeriod(changedPeriod);
+    EXPECT_TRUE(tracker.needsMoreSamples(mNow));
+
+    for (auto i = 0u; i < kMinimumSamplesForPrediction; i++) {
+        EXPECT_TRUE(tracker.needsMoreSamples(mNow += changedPeriod));
+        tracker.addVsyncTimestamp(mNow);
+    }
+    EXPECT_FALSE(tracker.needsMoreSamples(mNow));
+}
+
+TEST_F(VSyncPredictorTest, transitionsToModelledPointsAfterSynthetic) {
+    auto last = mNow;
+    auto const bias = 10;
+    for (auto i = 0u; i < kMinimumSamplesForPrediction; i++) {
+        EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow), Eq(last + mPeriod));
+        mNow += mPeriod - bias;
+        last = mNow;
+        tracker.addVsyncTimestamp(mNow);
+        mNow += bias;
+    }
+
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow), Eq(mNow + mPeriod - bias));
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow + 100), Eq(mNow + mPeriod - bias));
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow + 990), Eq(mNow + 2 * mPeriod - bias));
+}
+
+TEST_F(VSyncPredictorTest, uponNotifiedOfInaccuracyUsesSynthetic) {
+    auto const slightlyLessPeriod = mPeriod - 10;
+    auto const changedPeriod = mPeriod - 1;
+    for (auto i = 0u; i < kMinimumSamplesForPrediction; i++) {
+        tracker.addVsyncTimestamp(mNow += slightlyLessPeriod);
+    }
+
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow), Eq(mNow + slightlyLessPeriod));
+    tracker.setPeriod(changedPeriod);
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow), Eq(mNow + changedPeriod));
+}
+
+TEST_F(VSyncPredictorTest, adaptsToFenceTimelines_60hzHighVariance) {
+    // these are precomputed simulated 16.6s vsyncs with uniform distribution +/- 1.6ms error
+    std::vector<nsecs_t> const simulatedVsyncs{
+            15492949,  32325658,  49534984,  67496129,  84652891,
+            100332564, 117737004, 132125931, 149291099, 165199602,
+    };
+    auto constexpr idealPeriod = 16600000;
+    auto constexpr expectedPeriod = 16639242;
+    auto constexpr expectedIntercept = 1049341;
+
+    tracker.setPeriod(idealPeriod);
+    for (auto const& timestamp : simulatedVsyncs) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(expectedPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(expectedIntercept, mMaxRoundingError));
+}
+
+TEST_F(VSyncPredictorTest, adaptsToFenceTimelines_90hzLowVariance) {
+    // these are precomputed simulated 11.1 vsyncs with uniform distribution +/- 1ms error
+    std::vector<nsecs_t> const simulatedVsyncs{
+            11167047, 22603464, 32538479, 44938134, 56321268,
+            66730346, 78062637, 88171429, 99707843, 111397621,
+    };
+    auto idealPeriod = 11110000;
+    auto expectedPeriod = 11089413;
+    auto expectedIntercept = 94421;
+
+    tracker.setPeriod(idealPeriod);
+    for (auto const& timestamp : simulatedVsyncs) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(expectedPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(expectedIntercept, mMaxRoundingError));
+}
+
+TEST_F(VSyncPredictorTest, adaptsToFenceTimelinesDiscontinuous_22hzLowVariance) {
+    // these are 11.1s vsyncs with low variance, randomly computed, between -1 and 1ms
+    std::vector<nsecs_t> const simulatedVsyncs{
+            45259463,   // 0
+            91511026,   // 1
+            136307650,  // 2
+            1864501714, // 40
+            1908641034, // 41
+            1955278544, // 42
+            4590180096, // 100
+            4681594994, // 102
+            5499224734, // 120
+            5591378272, // 122
+    };
+    auto idealPeriod = 45454545;
+    auto expectedPeriod = 45450152;
+    auto expectedIntercept = 469647;
+
+    tracker.setPeriod(idealPeriod);
+    for (auto const& timestamp : simulatedVsyncs) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(expectedPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(expectedIntercept, mMaxRoundingError));
+}
+
+TEST_F(VSyncPredictorTest, againstOutliersDiscontinuous_500hzLowVariance) {
+    std::vector<nsecs_t> const simulatedVsyncs{
+            1992548,    // 0
+            4078038,    // 1
+            6165794,    // 2
+            7958171,    // 3
+            10193537,   // 4
+            2401840200, // 1200
+            2403000000, // an outlier that should be excluded (1201 and a half)
+            2405803629, // 1202
+            2408028599, // 1203
+            2410121051, // 1204
+    };
+    auto idealPeriod = 2000000;
+    auto expectedPeriod = 1999892;
+    auto expectedIntercept = 175409;
+
+    tracker.setPeriod(idealPeriod);
+    for (auto const& timestamp : simulatedVsyncs) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(expectedPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(expectedIntercept, mMaxRoundingError));
+}
+
+TEST_F(VSyncPredictorTest, handlesVsyncChange) {
+    auto const fastPeriod = 100;
+    auto const fastTimeBase = 100;
+    auto const slowPeriod = 400;
+    auto const slowTimeBase = 800;
+    auto const simulatedVsyncsFast =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction, fastPeriod, fastTimeBase);
+    auto const simulatedVsyncsSlow =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction, slowPeriod, slowTimeBase);
+
+    tracker.setPeriod(fastPeriod);
+    for (auto const& timestamp : simulatedVsyncsFast) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+
+    auto const mMaxRoundingError = 100;
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(fastPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(0, mMaxRoundingError));
+
+    tracker.setPeriod(slowPeriod);
+    for (auto const& timestamp : simulatedVsyncsSlow) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    std::tie(slope, intercept) = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, IsCloseTo(slowPeriod, mMaxRoundingError));
+    EXPECT_THAT(intercept, IsCloseTo(0, mMaxRoundingError));
+}
+
+TEST_F(VSyncPredictorTest, willBeAccurateUsingPriorResultsForRate) {
+    auto const fastPeriod = 101000;
+    auto const fastTimeBase = fastPeriod - 500;
+    auto const fastPeriod2 = 99000;
+
+    auto const slowPeriod = 400000;
+    auto const slowTimeBase = 800000 - 201;
+    auto const simulatedVsyncsFast =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction, fastPeriod, fastTimeBase);
+    auto const simulatedVsyncsSlow =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction, slowPeriod, slowTimeBase);
+    auto const simulatedVsyncsFast2 =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction, fastPeriod2, fastTimeBase);
+
+    auto idealPeriod = 100000;
+    tracker.setPeriod(idealPeriod);
+    for (auto const& timestamp : simulatedVsyncsFast) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    auto [slope, intercept] = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, Eq(fastPeriod));
+    EXPECT_THAT(intercept, Eq(0));
+
+    tracker.setPeriod(slowPeriod);
+    for (auto const& timestamp : simulatedVsyncsSlow) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+
+    // we had a model for 100ns mPeriod before, use that until the new samples are
+    // sufficiently built up
+    tracker.setPeriod(idealPeriod);
+    std::tie(slope, intercept) = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, Eq(fastPeriod));
+    EXPECT_THAT(intercept, Eq(0));
+
+    for (auto const& timestamp : simulatedVsyncsFast2) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    std::tie(slope, intercept) = tracker.getVSyncPredictionModel();
+    EXPECT_THAT(slope, Eq(fastPeriod2));
+    EXPECT_THAT(intercept, Eq(0));
+}
+
+TEST_F(VSyncPredictorTest, willBecomeInaccurateAfterA_longTimeWithNoSamples) {
+    auto const simulatedVsyncs = generateVsyncTimestamps(kMinimumSamplesForPrediction, mPeriod, 0);
+
+    for (auto const& timestamp : simulatedVsyncs) {
+        tracker.addVsyncTimestamp(timestamp);
+    }
+    auto const mNow = *simulatedVsyncs.rbegin();
+    EXPECT_FALSE(tracker.needsMoreSamples(mNow));
+
+    // TODO: would be better to decay this as a result of the variance of the samples
+    static auto constexpr aLongTimeOut = 1000000000;
+    EXPECT_TRUE(tracker.needsMoreSamples(mNow + aLongTimeOut));
+}
+
+TEST_F(VSyncPredictorTest, idealModelPredictionsBeforeRegressionModelIsBuilt) {
+    auto const simulatedVsyncs =
+            generateVsyncTimestamps(kMinimumSamplesForPrediction + 1, mPeriod, 0);
+    nsecs_t const mNow = 0;
+    EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow), Eq(mPeriod));
+
+    nsecs_t const aBitOfTime = 422;
+
+    for (auto i = 0; i < kMinimumSamplesForPrediction; i++) {
+        tracker.addVsyncTimestamp(simulatedVsyncs[i]);
+        EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(simulatedVsyncs[i] + aBitOfTime),
+                    Eq(mPeriod + simulatedVsyncs[i]));
+    }
+
+    for (auto i = kMinimumSamplesForPrediction; i < simulatedVsyncs.size(); i++) {
+        tracker.addVsyncTimestamp(simulatedVsyncs[i]);
+        EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(simulatedVsyncs[i] + aBitOfTime),
+                    Eq(mPeriod + simulatedVsyncs[i]));
+    }
+}
+
+} // namespace android::scheduler