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
This commit is contained in:
@@ -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
tests/ChoreographerTests/jni/Android.bp
Normal file
44
tests/ChoreographerTests/jni/Android.bp
Normal file
@@ -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",
|
||||
}
|
||||
33
tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
Normal file
33
tests/ChoreographerTests/jni/ChoreographerTestsJniOnLoad.cpp
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user