1 /*
2  * Copyright 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.view.cts;
17 
18 import static org.junit.Assert.assertTrue;
19 
20 import android.app.Activity;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.os.Bundle;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.util.Log;
27 import android.view.Choreographer;
28 import android.view.PointerIcon;
29 import android.view.Surface;
30 import android.view.SurfaceHolder;
31 import android.view.SurfaceView;
32 import android.view.View;
33 import android.view.WindowManager;
34 import android.widget.FrameLayout;
35 import android.widget.LinearLayout;
36 
37 import androidx.annotation.NonNull;
38 
39 import java.util.Objects;
40 import java.util.concurrent.CountDownLatch;
41 import java.util.concurrent.TimeUnit;
42 
43 /**
44  * Test {@link Activity} that repeatedly creates tons of {@link Surface Surfaces} to put memory
45  * pressure on the GC.
46  */
47 public class SurfaceOOMTestActivity extends Activity implements Choreographer.FrameCallback,
48         Choreographer.VsyncCallback {
49     private static final String TAG = SurfaceOOMTestActivity.class.getSimpleName();
50 
51     private Choreographer mChoreographer;
52 
53     private FrameLayout mParent;
54     private SurfaceView mSurfaceView;
55     private Surface mSurface;
56     private Paint mPaint;
57 
58     private CountDownLatch mReadyToStart = new CountDownLatch(1);
59     private Handler mHandler;
60 
61     // Test State:
62     private boolean mHasRendered;
63     private CountDownLatch mFence;
64     private int mCurrentRunWidth;
65     private int mCurrentRunHeight;
66     private long mNumSurfacesToCreate;
67     private long mRemainingSurfacesToCreate;
68 
69     @Override
onEnterAnimationComplete()70     public void onEnterAnimationComplete() {
71         mReadyToStart.countDown();
72     }
73 
74     @Override
onCreate(Bundle savedInstanceState)75     public void onCreate(Bundle savedInstanceState) {
76         super.onCreate(savedInstanceState);
77 
78         final View decorView = getWindow().getDecorView();
79         decorView.setSystemUiVisibility(
80                 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
81         decorView.setPointerIcon(
82                 PointerIcon.getSystemIcon(this, PointerIcon.TYPE_NULL));
83         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
84 
85         mPaint = new Paint();
86         mPaint.setColor(0xFF00FF);
87 
88         mParent = findViewById(android.R.id.content);
89         mChoreographer = Choreographer.getInstance();
90         mHandler = new Handler(Objects.requireNonNull(Looper.myLooper()));
91     }
92 
93     /**
94      * Schedules a loop of surface creation / destruction on the activity's main thread.
95      *
96      * A fence is created and waited on in the calling thread, and control will return once that
97      * fence is triggered.
98      */
verifyCreatingManySurfaces(int width, int height, long numSurfacesToCreate)99     public void verifyCreatingManySurfaces(int width, int height, long numSurfacesToCreate) {
100         awaitReadyState();
101 
102         assertTrue("numSurfacesToCreate must be greater than 0", numSurfacesToCreate > 0);
103 
104         Log.d(TAG, "Creating and rendering to " + numSurfacesToCreate + " surfaces.");
105 
106         mFence = new CountDownLatch(1);
107         mCurrentRunWidth = width;
108         mCurrentRunHeight = height;
109         mNumSurfacesToCreate = numSurfacesToCreate;
110         mRemainingSurfacesToCreate = numSurfacesToCreate;
111 
112         mHandler.post(() -> {
113             setupNextFrame();
114             mChoreographer.postFrameCallback(this);
115         });
116 
117         try {
118             assertTrue(
119                     "Unable to finish creating many surfaces with " + mRemainingSurfacesToCreate
120                             + "/" + numSurfacesToCreate + " remaining",
121                     mFence.await(5, TimeUnit.MINUTES));
122             mFence = null;
123         } catch (InterruptedException e) {
124             throw new RuntimeException(e);
125         }
126     }
127 
128     @Override
doFrame(long frameTimeNanos)129     public void doFrame(long frameTimeNanos) {
130         mChoreographer.postVsyncCallback(this);
131 
132         if (mSurface == null) {
133             // We don't have a surface yet, wait for the next frame.
134             return;
135         }
136 
137         Canvas c = mSurface.lockCanvas(null);
138         c.drawRect(0, 0, c.getWidth(), c.getHeight(), mPaint);
139         mSurface.unlockCanvasAndPost(c);
140 
141         mHasRendered = true;
142     }
143 
144     @Override
onVsync(@onNull Choreographer.FrameData data)145     public void onVsync(@NonNull Choreographer.FrameData data) {
146         if (!mHasRendered) {
147             mChoreographer.postFrameCallback(this);
148             return;
149         }
150 
151         mRemainingSurfacesToCreate--;
152         if (mRemainingSurfacesToCreate == 0) {
153             mFence.countDown();
154             Log.d(TAG, "Finished processing " + mNumSurfacesToCreate + " frames.");
155             return;
156         }
157 
158         setupNextFrame();
159         mChoreographer.postFrameCallback(this);
160     }
161 
awaitReadyState()162     private void awaitReadyState() {
163         try {
164             assertTrue(mReadyToStart.await(5, TimeUnit.SECONDS));
165         } catch (InterruptedException e) {
166             throw new RuntimeException(e);
167         }
168     }
169 
setupNextFrame()170     private void setupNextFrame() {
171         Log.d(TAG, "Setting frame " + mRemainingSurfacesToCreate + "/"
172                 + mNumSurfacesToCreate + " left");
173 
174         mHasRendered = false;
175         mSurface = null;
176         if (mSurfaceView != null) {
177             mParent.removeView(mSurfaceView);
178         }
179         mSurfaceView = new SurfaceView(this);
180         mSurfaceView.setLayoutParams(
181                 new LinearLayout.LayoutParams(mCurrentRunWidth, mCurrentRunHeight));
182         mParent.addView(mSurfaceView);
183 
184         mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
185             @Override
186             public void surfaceCreated(@NonNull SurfaceHolder holder) {
187                 mSurface = holder.getSurface();
188             }
189 
190             @Override
191             public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width,
192                     int height) {
193             }
194 
195             @Override
196             public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
197             }
198         });
199     }
200 }
201