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

Commit 54fe6093 authored by ramindani's avatar ramindani Committed by Rachel Lee
Browse files

NDK attached choreographer tests

Gets an attached AChoreographer from ASurfaceControl and tests callbacks
are called at the frame rate set on the surface control.

Bug: 258235138
Test: atest AttachedChoreographerNativeTest
Change-Id: I93fce04d58436e136134fb058e7d41a1d859311d
parent 93d2d0b4
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -36,6 +36,9 @@ android_test {
        "com.google.android.material_material",
        "truth-prebuilt",
    ],
    jni_libs: [
        "libchoreographertests_jni",
    ],
    resource_dirs: ["src/main/res"],
    certificate: "platform",
    platform_apis: true,
+44 −0
Original line number Diff line number Diff line
// Copyright 2023 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.

package {
    default_applicable_licenses: ["Android-Apache-2.0"],
}

cc_test_library {
    name: "libchoreographertests_jni",
    cflags: [
        "-Werror",
        "-Wthread-safety",
    ],

    gtest: false,

    srcs: [
        "ChoreographerTestsJniOnLoad.cpp",
        "android_view_tests_ChoreographerNativeTest.cpp",
    ],

    shared_libs: [
        "libandroid",
        "libnativehelper",
        "liblog",
    ],

    header_libs: [
        "libandroid_headers_private",
    ],

    stl: "c++_static",
}
+33 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 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 <jni.h>

#define LOG_TAG "ChoreographerTestsJniOnLoad"

extern int register_android_android_view_tests_ChoreographerNativeTest(JNIEnv* env);

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) {
    JNIEnv* env = NULL;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    if (register_android_android_view_tests_ChoreographerNativeTest(env)) {
        return JNI_ERR;
    }

    return JNI_VERSION_1_6;
}
 No newline at end of file
+167 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 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 <android/choreographer.h>
#include <android/log.h>
#include <android/surface_control_jni.h>
#include <jni.h>
#include <private/surface_control_private.h>
#include <time.h>
#include <utils/Log.h>
#include <utils/Mutex.h>

#include <chrono>
#include <cmath>
#include <condition_variable>
#include <mutex>
#include <thread>

#undef LOG_TAG
#define LOG_TAG "AttachedChoreographerNativeTest"

// Copied from cts/tests/tests/view/jni/jniAssert.h, to be removed when integrated in CTS.
#define ASSERT(condition, format, args...) \
    if (!(condition)) {                    \
        fail(env, format, ##args);         \
        return;                            \
    }

using namespace std::chrono_literals;

static constexpr std::chrono::nanoseconds kMaxRuntime{1s};
static constexpr float kFpsTolerance = 5.0f;

static constexpr int kNumOfFrames = 20;

struct {
    struct {
        jclass clazz;
        jmethodID endTest;
    } attachedChoreographerNativeTest;
} gJni;

struct CallbackData {
    std::mutex mutex;

    // Condition to signal callbacks are done running and test can be verified.
    std::condition_variable_any condition;

    // Flag to ensure not to lock on the condition if notify is called before wait_for.
    bool callbacksComplete = false;

    AChoreographer* choreographer = nullptr;
    int count GUARDED_BY(mutex){0};
    std::chrono::nanoseconds frameTime GUARDED_BY(mutex){0};
    std::chrono::nanoseconds startTime;
    std::chrono::nanoseconds endTime GUARDED_BY(mutex){0};
};

static std::chrono::nanoseconds now() {
    return std::chrono::steady_clock::now().time_since_epoch();
}

static void vsyncCallback(const AChoreographerFrameCallbackData* callbackData, void* data) {
    ALOGI("%s: Vsync callback running", __func__);
    long frameTimeNanos = AChoreographerFrameCallbackData_getFrameTimeNanos(callbackData);

    auto* cb = static_cast<CallbackData*>(data);
    {
        std::lock_guard<std::mutex> _l(cb->mutex);
        cb->count++;
        cb->endTime = now();
        cb->frameTime = std::chrono::nanoseconds{frameTimeNanos};

        ALOGI("%s: ran callback now %ld, frameTimeNanos %ld, new count %d", __func__,
              static_cast<long>(cb->endTime.count()), frameTimeNanos, cb->count);
        if (cb->endTime - cb->startTime > kMaxRuntime) {
            cb->callbacksComplete = true;
            cb->condition.notify_all();
            return;
        }
    }

    ALOGI("%s: Posting next callback", __func__);
    AChoreographer_postVsyncCallback(cb->choreographer, vsyncCallback, data);
}

static void fail(JNIEnv* env, const char* format, ...) {
    va_list args;

    va_start(args, format);
    char* msg;
    int rc = vasprintf(&msg, format, args);
    va_end(args);

    jclass exClass;
    const char* className = "java/lang/AssertionError";
    exClass = env->FindClass(className);
    env->ThrowNew(exClass, msg);
    free(msg);
}

jlong SurfaceControl_getChoreographer(JNIEnv* env, jclass, jobject surfaceControlObj) {
    return reinterpret_cast<jlong>(
            ASurfaceControl_getChoreographer(ASurfaceControl_fromJava(env, surfaceControlObj)));
}

static bool frameRateEquals(float fr1, float fr2) {
    return std::abs(fr1 - fr2) <= kFpsTolerance;
}

static void endTest(JNIEnv* env, jobject clazz) {
    env->CallVoidMethod(clazz, gJni.attachedChoreographerNativeTest.endTest);
}

static void android_view_ChoreographerNativeTest_testPostVsyncCallbackAtFrameRate(
        JNIEnv* env, jobject clazz, jlong choreographerPtr, jfloat expectedFrameRate) {
    AChoreographer* choreographer = reinterpret_cast<AChoreographer*>(choreographerPtr);
    CallbackData cb;
    cb.choreographer = choreographer;
    cb.startTime = now();
    ALOGI("%s: Post first callback at %ld", __func__, static_cast<long>(cb.startTime.count()));
    AChoreographer_postVsyncCallback(choreographer, vsyncCallback, &cb);

    std::scoped_lock<std::mutex> conditionLock(cb.mutex);
    ASSERT(cb.condition.wait_for(cb.mutex, 2 * kMaxRuntime, [&cb] { return cb.callbacksComplete; }),
           "Never received callbacks!");

    float actualFrameRate = static_cast<float>(cb.count) /
            (static_cast<double>((cb.endTime - cb.startTime).count()) / 1'000'000'000.0);
    ALOGI("%s: callback called %d times with final start time %ld, end time %ld, effective "
          "frame rate %f",
          __func__, cb.count, static_cast<long>(cb.startTime.count()),
          static_cast<long>(cb.endTime.count()), actualFrameRate);
    ASSERT(frameRateEquals(actualFrameRate, expectedFrameRate),
           "Effective frame rate is %f but expected to be %f", actualFrameRate, expectedFrameRate);

    endTest(env, clazz);
}

static JNINativeMethod gMethods[] = {
        {"nativeSurfaceControl_getChoreographer", "(Landroid/view/SurfaceControl;)J",
         (void*)SurfaceControl_getChoreographer},
        {"nativeTestPostVsyncCallbackAtFrameRate", "(JF)V",
         (void*)android_view_ChoreographerNativeTest_testPostVsyncCallbackAtFrameRate},
};

int register_android_android_view_tests_ChoreographerNativeTest(JNIEnv* env) {
    jclass clazz =
            env->FindClass("android/view/choreographertests/AttachedChoreographerNativeTest");
    gJni.attachedChoreographerNativeTest.clazz = static_cast<jclass>(env->NewGlobalRef(clazz));
    gJni.attachedChoreographerNativeTest.endTest = env->GetMethodID(clazz, "endTest", "()V");
    return env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));
}
+179 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.
 */

package android.view.choreographertests;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.Manifest;
import android.hardware.display.DisplayManager;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
public class AttachedChoreographerNativeTest {
    private static final String TAG = "AttachedChoreographerNativeTest";

    static {
        System.loadLibrary("choreographertests_jni");
    }

    private final CountDownLatch mSurfaceCreationCountDown = new CountDownLatch(1);
    private CountDownLatch mTestCompleteSignal;
    private long mChoreographerPtr;
    private SurfaceView mSurfaceView;
    private SurfaceHolder mSurfaceHolder;
    private ActivityScenario<GraphicsActivity> mScenario;
    private int mInitialMatchContentFrameRate;
    private DisplayManager mDisplayManager;

    private static native long nativeSurfaceControl_getChoreographer(SurfaceControl surfaceControl);
    private native void nativeTestPostVsyncCallbackAtFrameRate(
            long choreographerPtr, float expectedFrameRate);

    @Before
    public void setup() throws Exception {
        mScenario = ActivityScenario.launch(GraphicsActivity.class);
        mScenario.moveToState(Lifecycle.State.CREATED);
        mScenario.onActivity(activity -> {
            mSurfaceView = activity.findViewById(R.id.surface);
            mSurfaceHolder = mSurfaceView.getHolder();
            mSurfaceHolder.addCallback(new SurfaceHolder.Callback() {
                @Override
                public void surfaceChanged(
                        SurfaceHolder holder, int format, int width, int height) {}

                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    mSurfaceCreationCountDown.countDown();
                }

                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {}
            });
        });

        mScenario.moveToState(Lifecycle.State.RESUMED);
        UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        uiDevice.wakeUp();
        uiDevice.executeShellCommand("wm dismiss-keyguard");

        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
                android.Manifest.permission.LOG_COMPAT_CHANGE,
                android.Manifest.permission.READ_COMPAT_CHANGE_CONFIG,
                android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE,
                android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
                Manifest.permission.MANAGE_GAME_MODE);
        mScenario.onActivity(activity -> {
            mDisplayManager = activity.getSystemService(DisplayManager.class);
            mInitialMatchContentFrameRate =
                    toSwitchingType(mDisplayManager.getMatchContentFrameRateUserPreference());
            mDisplayManager.setRefreshRateSwitchingType(
                    DisplayManager.SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY);
            mDisplayManager.setShouldAlwaysRespectAppRequestedMode(true);
        });
    }

    @After
    public void tearDown() {
        mDisplayManager.setRefreshRateSwitchingType(mInitialMatchContentFrameRate);
        mDisplayManager.setShouldAlwaysRespectAppRequestedMode(false);
        InstrumentationRegistry.getInstrumentation()
                .getUiAutomation()
                .dropShellPermissionIdentity();
    }

    @Test
    public void test_choreographer_callbacksForVariousFrameRates() {
        for (int divisor : new int[] {2, 3}) {
            mTestCompleteSignal = new CountDownLatch(1);
            mScenario.onActivity(activity -> {
                if (waitForCountDown(mSurfaceCreationCountDown, /* timeoutInSeconds= */ 1L)) {
                    fail("Unable to create surface within 1 Second");
                }

                SurfaceControl surfaceControl = mSurfaceView.getSurfaceControl();
                mChoreographerPtr = nativeSurfaceControl_getChoreographer(surfaceControl);
                Log.i(TAG, "mChoreographerPtr value " + mChoreographerPtr);

                float displayRefreshRate = activity.getDisplay().getMode().getRefreshRate();
                float expectedFrameRate = displayRefreshRate / divisor;

                SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
                transaction
                        .setFrameRate(surfaceControl, expectedFrameRate,
                                Surface.FRAME_RATE_COMPATIBILITY_DEFAULT)
                        .addTransactionCommittedListener(Runnable::run,
                                () -> {
                                    assertTrue(mChoreographerPtr != 0L);
                                    Log.i(TAG, "Testing frame rate of " + expectedFrameRate);
                                    nativeTestPostVsyncCallbackAtFrameRate(
                                            mChoreographerPtr, expectedFrameRate);
                                })
                        .apply();
            });
            // wait for the previous callbacks to finish before moving to the next divisor
            if (waitForCountDown(mTestCompleteSignal, /* timeoutInSeconds= */ 5L)) {
                fail("Test for divisor " + divisor + " not finished in 5 seconds");
            }
        }
    }

    /** Call from native to trigger test completion. */
    private void endTest() {
        Log.i(TAG, "Signal test completion!");
        mTestCompleteSignal.countDown();
    }

    private boolean waitForCountDown(CountDownLatch countDownLatch, long timeoutInSeconds) {
        try {
            return !countDownLatch.await(timeoutInSeconds, TimeUnit.SECONDS);
        } catch (InterruptedException ex) {
            throw new AssertionError("Test interrupted", ex);
        }
    }

    private int toSwitchingType(int matchContentFrameRateUserPreference) {
        switch (matchContentFrameRateUserPreference) {
            case DisplayManager.MATCH_CONTENT_FRAMERATE_NEVER:
                return DisplayManager.SWITCHING_TYPE_NONE;
            case DisplayManager.MATCH_CONTENT_FRAMERATE_SEAMLESSS_ONLY:
                return DisplayManager.SWITCHING_TYPE_WITHIN_GROUPS;
            case DisplayManager.MATCH_CONTENT_FRAMERATE_ALWAYS:
                return DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
            default:
                return -1;
        }
    }
}