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

Commit 34b07545 authored by Johannes Gallmann's avatar Johannes Gallmann
Browse files

Add Tests to ViewCapture Library

I have added some tests to verify the correctness of ViewCapture. Additionally, I have added a Readme with some remarks regarding performance.
In order to make the ViewCapture class better testable, I have modified it such that there are now two getInstance() functions. One of them takes three arguments to tune some parameters for testing.

Bug: 255929005
Test: atest frameworks/libs/systemui/viewcapturelib/tests/com/android/app/viewcapture
Change-Id: I36b72a2cc0aba45ef2af2684ca1487525b9595fc
parent f17a0788
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
@@ -46,3 +46,28 @@ android_library {
    ],
}

android_test {
    name: "view_capture_tests",
    manifest: "tests/AndroidManifest.xml",
    platform_apis: true,
    min_sdk_version: "26",

    static_libs: [
        "androidx.core_core",
        "view_capture",
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "testables",
        "mockito-target-extended-minus-junit4",
    ],
    srcs: [
        "**/*.java",
        "**/*.kt"
    ],
    libs: [
        "android.test.runner",
        "android.test.base",
        "android.test.mock",
    ],
    test_suites: ["device-tests"],
}
+11 −0
Original line number Diff line number Diff line
###ViewCapture Library Readme

ViewCapture.java is extremely performance sensitive. Any changes should be carried out with great caution not to hurt performance. 

The following measurements should serve as a performance baseline (as of 02.10.2022):


The onDraw() function invocation time in WindowListener within ViewCapture is measured with System.nanoTime(). The following scenario was measured:

1. Capturing the notification shade window root view on a freshly rebooted bluejay device (2 notifications present) -> avg. time = 204237ns (0.2ms)
+15 −0
Original line number Diff line number Diff line
{
  "presubmit": [
    {
      "name": "view_capture_tests",
      "options": [
        {
          "exclude-annotation": "org.junit.Ignore"
        },
        {
          "exclude-annotation": "androidx.test.filters.FlakyTest"
        }
      ]
    }
  ]
}
+9 −1
Original line number Diff line number Diff line
plugins {
    id 'com.android.library'
    id 'sysuigradleproject.android-library-conventions'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.protobuf'
}
@@ -13,6 +13,7 @@ android {
    defaultConfig {
        minSdkVersion TARGET_SDK.toInteger()
        targetSdkVersion TARGET_SDK.toInteger()
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    sourceSets {
@@ -21,6 +22,10 @@ android {
            manifest.srcFile 'AndroidManifest.xml'
            proto.srcDirs = ["${PROTOS_DIR}"]
        }
        androidTest {
            java.srcDirs = ["tests"]
            manifest.srcFile "tests/AndroidManifest.xml"
        }
    }

    lintOptions {
@@ -31,6 +36,9 @@ android {
dependencies {
    implementation "androidx.core:core:1.9.0"
    implementation PROTOBUF_DEPENDENCY
    androidTestImplementation project(':SharedTestLib')
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation "androidx.test:rules:1.4.0"
}

protobuf {
+28 −16
Original line number Diff line number Diff line
@@ -71,21 +71,28 @@ public class ViewCapture {
    private static final int PFLAG_DIRTY_MASK = 0x00200000;

    // Number of frames to keep in memory
    private static final int MEMORY_SIZE = 2000;
    private final int mMemorySize;
    private static final int DEFAULT_MEMORY_SIZE = 2000;
    // Initial size of the reference pool. This is at least be 5 * total number of views in
    // Launcher. This allows the first free frames avoid object allocation during view capture.
    private static final int INIT_POOL_SIZE = 300;
    private static final int DEFAULT_INIT_POOL_SIZE = 300;

    private static ViewCapture INSTANCE;
    public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());

    public static ViewCapture getInstance() {
        return getInstance(true, DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE);
    }

    public static ViewCapture getInstance(boolean offloadToBackgroundThread, int memorySize,
            int initPoolSize) {
        if (INSTANCE == null) {
            if (Looper.myLooper() == Looper.getMainLooper()) {
                INSTANCE = new ViewCapture();
                INSTANCE = new ViewCapture(offloadToBackgroundThread, memorySize, initPoolSize);
            } else {
                try {
                    return MAIN_EXECUTOR.submit(ViewCapture::getInstance).get();
                    return MAIN_EXECUTOR.submit(() ->
                            getInstance(offloadToBackgroundThread, memorySize, initPoolSize)).get();
                } catch (InterruptedException | ExecutionException e) {
                    throw new RuntimeException(e);
                }
@@ -101,10 +108,15 @@ public class ViewCapture {
    // Pool used for capturing view tree on the UI thread.
    private ViewRef mPool = new ViewRef();

    private ViewCapture() {
    private ViewCapture(boolean offloadToBackgroundThread, int memorySize, int initPoolSize) {
        mMemorySize = memorySize;
        if (offloadToBackgroundThread) {
            mExecutor = createAndStartNewLooperExecutor("ViewCapture",
                    Process.THREAD_PRIORITY_FOREGROUND);
        mExecutor.execute(this::initPool);
        } else {
            mExecutor = MAIN_EXECUTOR;
        }
        mExecutor.execute(() -> initPool(initPoolSize));
    }

    private static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
@@ -120,11 +132,11 @@ public class ViewCapture {
    }

    @WorkerThread
    private void initPool() {
    private void initPool(int initPoolSize) {
        ViewRef start = new ViewRef();
        ViewRef current = start;

        for (int i = 0; i < INIT_POOL_SIZE; i++) {
        for (int i = 0; i < initPoolSize; i++) {
            current.next = new ViewRef();
            current = current.next;
        }
@@ -216,8 +228,8 @@ public class ViewCapture {

        private int mFrameIndexBg = -1;
        private boolean mIsFirstFrame = true;
        private final long[] mFrameTimesNanosBg = new long[MEMORY_SIZE];
        private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[MEMORY_SIZE];
        private final long[] mFrameTimesNanosBg = new long[mMemorySize];
        private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];

        private boolean mDestroyed = false;
        private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
@@ -255,7 +267,7 @@ public class ViewCapture {
        private void captureViewPropertiesBg(ViewRef viewRefStart) {
            long choreographerTimeNanos = viewRefStart.choreographerTimeNanos;
            mFrameIndexBg++;
            if (mFrameIndexBg >= MEMORY_SIZE) {
            if (mFrameIndexBg >= mMemorySize) {
                mFrameIndexBg = 0;
            }
            mFrameTimesNanosBg[mFrameIndexBg] = choreographerTimeNanos;
@@ -324,7 +336,7 @@ public class ViewCapture {
        }

        private ViewPropertyRef findInLastFrame(int hashCode) {
            int lastFrameIndex = (mFrameIndexBg == 0) ? MEMORY_SIZE - 1 : mFrameIndexBg - 1;
            int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1;
            ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex];
            while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) {
                viewPropertyRef = viewPropertyRef.next;
@@ -359,13 +371,13 @@ public class ViewCapture {

        @WorkerThread
        private ExportedData dumpToProto(ViewIdProvider idProvider) {
            int size = (mNodesBg[MEMORY_SIZE - 1] == null) ? mFrameIndexBg + 1 : MEMORY_SIZE;
            int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
            ExportedData exportedData = new ExportedData();
            exportedData.frameData = new FrameData[size];
            ArrayList<Class> classList = new ArrayList<>();

            for (int i = size - 1; i >= 0; i--) {
                int index = (MEMORY_SIZE + mFrameIndexBg - i) % MEMORY_SIZE;
                int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
                ViewNode node = new ViewNode();
                mNodesBg[index].toProto(idProvider, classList, node);
                FrameData frameData = new FrameData();
Loading