Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4b884cf1 authored by Cody Heiner's avatar Cody Heiner Committed by Android (Google) Code Review
Browse files

Merge "Fix possible crash in scale-invariant error calculation" into main

parents eaa9fde9 50582ba7
Loading
Loading
Loading
Loading
+38 −31
Original line number Diff line number Diff line
@@ -113,7 +113,12 @@ void MotionPredictorMetricsManager::onRecord(const MotionEvent& inputEvent) {
// Adds new predictions to mRecentPredictions and maintains the invariant that elements are
// sorted in ascending order of targetTimestamp.
void MotionPredictorMetricsManager::onPredict(const MotionEvent& predictionEvent) {
    for (size_t i = 0; i < predictionEvent.getHistorySize() + 1; ++i) {
    const size_t numPredictions = predictionEvent.getHistorySize() + 1;
    if (numPredictions > mMaxNumPredictions) {
        LOG(WARNING) << "numPredictions (" << numPredictions << ") > mMaxNumPredictions ("
                     << mMaxNumPredictions << "). Ignoring extra predictions in metrics.";
    }
    for (size_t i = 0; (i < numPredictions) && (i < mMaxNumPredictions); ++i) {
        // Convert MotionEvent to PredictionPoint.
        const PointerCoords* coords =
                predictionEvent.getHistoricalRawPointerCoords(/*pointerIndex=*/0, i);
@@ -325,18 +330,19 @@ void MotionPredictorMetricsManager::computeAtomFields() {
            mAtomFields[i].highVelocityOffTrajectoryRmse =
                    static_cast<int>(offTrajectoryRmse * 1000);
        }
    }

        // Scale-invariant errors: reported only for the last time bucket, where the values
        // represent an average across all time buckets.
        if (i + 1 == mMaxNumPredictions) {
    // Scale-invariant errors: the average scale-invariant error across all time buckets
    // is reported in the last time bucket.
    {
        // Compute error averages.
        float alongTrajectoryRmseSum = 0;
        float offTrajectoryRmseSum = 0;
        int bucket_count = 0;
        for (size_t j = 0; j < mAggregatedMetrics.size(); ++j) {
                // If we have general errors (checked above), we should always also have
                // scale-invariant errors.
                LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantErrorsCount == 0,
                                    "mAggregatedMetrics[%zu].scaleInvariantErrorsCount is 0", j);
            if (mAggregatedMetrics[j].scaleInvariantErrorsCount == 0) {
                continue;
            }

            LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse < 0,
                                "mAggregatedMetrics[%zu].scaleInvariantAlongTrajectorySse = %f "
@@ -350,17 +356,18 @@ void MotionPredictorMetricsManager::computeAtomFields() {
                                "mAggregatedMetrics[%zu].scaleInvariantOffTrajectorySse = %f "
                                "should not be negative",
                                j, mAggregatedMetrics[j].scaleInvariantOffTrajectorySse);
                offTrajectoryRmseSum +=
                        std::sqrt(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse /
            offTrajectoryRmseSum += std::sqrt(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse /
                                              mAggregatedMetrics[j].scaleInvariantErrorsCount);

            ++bucket_count;
        }

            const float averageAlongTrajectoryRmse =
                    alongTrajectoryRmseSum / mAggregatedMetrics.size();
        if (bucket_count > 0) {
            const float averageAlongTrajectoryRmse = alongTrajectoryRmseSum / bucket_count;
            mAtomFields.back().scaleInvariantAlongTrajectoryRmse =
                    static_cast<int>(averageAlongTrajectoryRmse * 1000);

            const float averageOffTrajectoryRmse = offTrajectoryRmseSum / mAggregatedMetrics.size();
            const float averageOffTrajectoryRmse = offTrajectoryRmseSum / bucket_count;
            mAtomFields.back().scaleInvariantOffTrajectoryRmse =
                    static_cast<int>(averageOffTrajectoryRmse * 1000);
        }
+54 −27
Original line number Diff line number Diff line
@@ -238,14 +238,17 @@ TEST(MakeMotionEventTest, MakeLiftMotionEvent) {

// --- Ground-truth-generation helper functions. ---

// Generates numPoints ground truth points with values equal to those of the given
// GroundTruthPoint, and with consecutive timestamps separated by the given inputInterval.
std::vector<GroundTruthPoint> generateConstantGroundTruthPoints(
        const GroundTruthPoint& groundTruthPoint, size_t numPoints) {
        const GroundTruthPoint& groundTruthPoint, size_t numPoints,
        nsecs_t inputInterval = TEST_PREDICTION_INTERVAL_NANOS) {
    std::vector<GroundTruthPoint> groundTruthPoints;
    nsecs_t timestamp = groundTruthPoint.timestamp;
    for (size_t i = 0; i < numPoints; ++i) {
        groundTruthPoints.emplace_back(groundTruthPoint);
        groundTruthPoints.back().timestamp = timestamp;
        timestamp += TEST_PREDICTION_INTERVAL_NANOS;
        timestamp += inputInterval;
    }
    return groundTruthPoints;
}
@@ -280,7 +283,8 @@ TEST(GenerateConstantGroundTruthPointsTest, BasicTest) {
    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
                                            .timestamp = TEST_INITIAL_TIMESTAMP};
    const std::vector<GroundTruthPoint> groundTruthPoints =
            generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/3);
            generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/3,
                                              /*inputInterval=*/10);

    ASSERT_EQ(3u, groundTruthPoints.size());
    // First point.
@@ -290,11 +294,11 @@ TEST(GenerateConstantGroundTruthPointsTest, BasicTest) {
    // Second point.
    EXPECT_EQ(groundTruthPoints[1].position, groundTruthPoint.position);
    EXPECT_EQ(groundTruthPoints[1].pressure, groundTruthPoint.pressure);
    EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
    EXPECT_EQ(groundTruthPoints[1].timestamp, groundTruthPoint.timestamp + 10);
    // Third point.
    EXPECT_EQ(groundTruthPoints[2].position, groundTruthPoint.position);
    EXPECT_EQ(groundTruthPoints[2].pressure, groundTruthPoint.pressure);
    EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
    EXPECT_EQ(groundTruthPoints[2].timestamp, groundTruthPoint.timestamp + 20);
}

TEST(GenerateCircularArcGroundTruthTest, StraightLineUpwards) {
@@ -333,16 +337,19 @@ TEST(GenerateCircularArcGroundTruthTest, CounterclockwiseSquare) {

// --- Prediction-generation helper functions. ---

// Creates a sequence of predictions with values equal to those of the given GroundTruthPoint.
std::vector<PredictionPoint> generateConstantPredictions(const GroundTruthPoint& groundTruthPoint) {
// Generates TEST_MAX_NUM_PREDICTIONS predictions with values equal to those of the given
// GroundTruthPoint, and with consecutive timestamps separated by the given predictionInterval.
std::vector<PredictionPoint> generateConstantPredictions(
        const GroundTruthPoint& groundTruthPoint,
        nsecs_t predictionInterval = TEST_PREDICTION_INTERVAL_NANOS) {
    std::vector<PredictionPoint> predictions;
    nsecs_t predictionTimestamp = groundTruthPoint.timestamp + TEST_PREDICTION_INTERVAL_NANOS;
    nsecs_t predictionTimestamp = groundTruthPoint.timestamp + predictionInterval;
    for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
        predictions.push_back(PredictionPoint{{.position = groundTruthPoint.position,
                                               .pressure = groundTruthPoint.pressure},
                                              .originTimestamp = groundTruthPoint.timestamp,
                                              .targetTimestamp = predictionTimestamp});
        predictionTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
        predictionTimestamp += predictionInterval;
    }
    return predictions;
}
@@ -375,8 +382,9 @@ std::vector<PredictionPoint> generatePredictionsByLinearExtrapolation(
TEST(GeneratePredictionsTest, GenerateConstantPredictions) {
    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
                                            .timestamp = TEST_INITIAL_TIMESTAMP};
    const nsecs_t predictionInterval = 10;
    const std::vector<PredictionPoint> predictionPoints =
            generateConstantPredictions(groundTruthPoint);
            generateConstantPredictions(groundTruthPoint, predictionInterval);

    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
    for (size_t i = 0; i < predictionPoints.size(); ++i) {
@@ -385,8 +393,7 @@ TEST(GeneratePredictionsTest, GenerateConstantPredictions) {
        EXPECT_THAT(predictionPoints[i].pressure, FloatNear(groundTruthPoint.pressure, 1e-6));
        EXPECT_EQ(predictionPoints[i].originTimestamp, groundTruthPoint.timestamp);
        EXPECT_EQ(predictionPoints[i].targetTimestamp,
                  groundTruthPoint.timestamp +
                          static_cast<nsecs_t>(i + 1) * TEST_PREDICTION_INTERVAL_NANOS);
                  TEST_INITIAL_TIMESTAMP + static_cast<nsecs_t>(i + 1) * predictionInterval);
    }
}

@@ -678,12 +685,9 @@ ReportAtomFunction createMockReportAtomFunction(std::vector<AtomFields>& reporte
//  • groundTruthPoints: chronologically-ordered ground truth points, with at least 2 elements.
//  • predictionPoints: the first index points to a vector of predictions corresponding to the
//    source ground truth point with the same index.
//     - The first element should be empty, because there are not expected to be predictions until
//       we have received 2 ground truth points.
//     - The last element may be empty, because there will be no future ground truth points to
//       associate with those predictions (if not empty, it will be ignored).
//     - For empty prediction vectors, MetricsManager::onPredict will not be called.
//     - To test all prediction buckets, there should be at least TEST_MAX_NUM_PREDICTIONS non-empty
//       prediction sets (that is, excluding the first and last). Thus, groundTruthPoints and
//       prediction vectors (that is, excluding the first and last). Thus, groundTruthPoints and
//       predictionPoints should have size at least TEST_MAX_NUM_PREDICTIONS + 2.
//
// When the function returns, outReportedAtomFields will contain the reported AtomFields.
@@ -697,19 +701,12 @@ void runMetricsManager(const std::vector<GroundTruthPoint>& groundTruthPoints,
                                                 createMockReportAtomFunction(
                                                         outReportedAtomFields));

    // Validate structure of groundTruthPoints and predictionPoints.
    ASSERT_EQ(predictionPoints.size(), groundTruthPoints.size());
    ASSERT_GE(groundTruthPoints.size(), 2u);
    ASSERT_EQ(predictionPoints[0].size(), 0u);
    for (size_t i = 1; i + 1 < predictionPoints.size(); ++i) {
        SCOPED_TRACE(testing::Message() << "i = " << i);
        ASSERT_EQ(predictionPoints[i].size(), TEST_MAX_NUM_PREDICTIONS);
    }
    ASSERT_EQ(predictionPoints.size(), groundTruthPoints.size());

    // Pass ground truth points and predictions (for all except first and last ground truth).
    for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
        metricsManager.onRecord(makeMotionEvent(groundTruthPoints[i]));
        if ((i > 0) && (i + 1 < predictionPoints.size())) {
        if (!predictionPoints[i].empty()) {
            metricsManager.onPredict(makeMotionEvent(predictionPoints[i]));
        }
    }
@@ -738,7 +735,7 @@ TEST(MotionPredictorMetricsManagerTest, NoPredictions) {
// Perfect predictions test:
//  • Input: constant input events, perfect predictions matching the input events.
//  • Expectation: all error metrics should be zero, or NO_DATA_SENTINEL for "unreported" metrics.
//    (For example, scale-invariant errors are only reported for the final time bucket.)
//    (For example, scale-invariant errors are only reported for the last time bucket.)
TEST(MotionPredictorMetricsManagerTest, ConstantGroundTruthPerfectPredictions) {
    GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
                                      .timestamp = TEST_INITIAL_TIMESTAMP};
@@ -977,5 +974,35 @@ TEST(MotionPredictorMetricsManagerTest, CounterclockwiseOctagonGroundTruthLinear
    }
}

// Robustness test:
//  • Input: input events separated by a significantly greater time interval than the interval
//    between predictions.
//  • Expectation: the MetricsManager should not crash in this case. (No assertions are made about
//    the resulting metrics.)
//
// In practice, this scenario could arise either if the input and prediction intervals are
// mismatched, or if input events are missing (dropped or skipped for some reason).
TEST(MotionPredictorMetricsManagerTest, MismatchedInputAndPredictionInterval) {
    // Create two ground truth points separated by MAX_NUM_PREDICTIONS * PREDICTION_INTERVAL,
    // so that the second ground truth point corresponds to the last prediction bucket. This
    // ensures that the scale-invariant error codepath will be run, giving full code coverage.
    GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(0.0f, 0.0f), .pressure = 0.5f},
                                      .timestamp = TEST_INITIAL_TIMESTAMP};
    const nsecs_t inputInterval = TEST_MAX_NUM_PREDICTIONS * TEST_PREDICTION_INTERVAL_NANOS;
    const std::vector<GroundTruthPoint> groundTruthPoints =
            generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/2, inputInterval);

    // Create predictions separated by the prediction interval.
    std::vector<std::vector<PredictionPoint>> predictionPoints;
    for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
        predictionPoints.push_back(
                generateConstantPredictions(groundTruthPoints[i], TEST_PREDICTION_INTERVAL_NANOS));
    }

    // Test that we can run the MetricsManager without crashing.
    std::vector<AtomFields> reportedAtomFields;
    runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
}

} // namespace
} // namespace android