summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author ramindani <ramindani@google.com> 2023-01-04 18:37:26 -0800
committer Rachel Lee <rnlee@google.com> 2023-02-09 14:13:13 -0800
commit54fe60932a6848c68f7d64f11be548137a09e1e9 (patch)
treeaa78538a04cae4917f6629f15f509b051cf1cc0c
parent93d2d0b43ec3e07cfe6b8c32a900f8d433b5f6d5 (diff)
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
-rw-r--r--tests/ChoreographerTests/Android.bp3
-rw-r--r--tests/ChoreographerTests/jni/Android.bp44
-rw-r--r--tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp33
-rw-r--r--tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp167
-rw-r--r--tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java179
5 files changed, 426 insertions, 0 deletions
diff --git a/tests/ChoreographerTests/Android.bp b/tests/ChoreographerTests/Android.bp
index ff252f717591..ca3026705c63 100644
--- a/tests/ChoreographerTests/Android.bp
+++ b/tests/ChoreographerTests/Android.bp
@@ -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,
diff --git a/tests/ChoreographerTests/jni/Android.bp b/tests/ChoreographerTests/jni/Android.bp
new file mode 100644
index 000000000000..7198c511489b
--- /dev/null
+++ b/tests/ChoreographerTests/jni/Android.bp
@@ -0,0 +1,44 @@
+// 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",
+}
diff --git a/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp b/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
new file mode 100644
index 000000000000..447376fca78f
--- /dev/null
+++ b/tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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
diff --git a/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp b/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp
new file mode 100644
index 000000000000..27f4bae9e65a
--- /dev/null
+++ b/tests/ChoreographerTests/jni/android_view_tests_ChoreographerNativeTest.cpp
@@ -0,0 +1,167 @@
+/*
+ * 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));
+}
diff --git a/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java
new file mode 100644
index 000000000000..1118eb3bfc6e
--- /dev/null
+++ b/tests/ChoreographerTests/src/main/java/android/view/choreographertests/AttachedChoreographerNativeTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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;
+ }
+ }
+}