Commit 9723628c authored by Bo Liu's avatar Bo Liu Committed by Commit Bot

weblayer: Add test to verify acitivty does not leak

Fix ContentViewRenderView's usage of postOnAnimation
which can lead to leak after destroy as well.

Bug: 1018148
Change-Id: I8d6509eed77355359a285e2ddb67bb80ac1e79de
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1903688
Commit-Queue: Bo <boliu@chromium.org>
Reviewed-by: default avatarTobias Sargeant <tobiasjs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713923}
parent c733d82d
...@@ -74,6 +74,32 @@ public class ContentViewRenderView extends FrameLayout { ...@@ -74,6 +74,32 @@ public class ContentViewRenderView extends FrameLayout {
void surfaceDestroyed(boolean cacheBackBuffer); void surfaceDestroyed(boolean cacheBackBuffer);
} }
private final ArrayList<TrackedRunnable> mPendingRunnables = new ArrayList<>();
// Runnables posted via View.postOnAnimation may not run after the view is detached,
// if nothing else causes animation. However a pending runnable may held by a GC root
// from the thread itself, and thus can cause leaks. This class here is so ensure that
// on destroy, all pending tasks are run immediately so they do not lead to leaks.
private abstract class TrackedRunnable implements Runnable {
private boolean mHasRun;
public TrackedRunnable() {
mPendingRunnables.add(this);
}
@Override
public final void run() {
// View.removeCallbacks is not always reliable, and may run the callback even
// after it has been removed.
if (mHasRun) return;
assert mPendingRunnables.contains(this);
mPendingRunnables.remove(this);
mHasRun = true;
doRun();
}
protected abstract void doRun();
}
// Non-static implementation of SurfaceEventListener that forward calls to native Compositor. // Non-static implementation of SurfaceEventListener that forward calls to native Compositor.
// It is also responsible for updating |mRequested| and |mCurrent|. // It is also responsible for updating |mRequested| and |mCurrent|.
private class SurfaceEventListenerImpl implements SurfaceEventListener { private class SurfaceEventListenerImpl implements SurfaceEventListener {
...@@ -122,7 +148,7 @@ public class ContentViewRenderView extends FrameLayout { ...@@ -122,7 +148,7 @@ public class ContentViewRenderView extends FrameLayout {
// Abstract differences between SurfaceView and TextureView behind this class. // Abstract differences between SurfaceView and TextureView behind this class.
// Also responsible for holding and calling callbacks. // Also responsible for holding and calling callbacks.
private static class SurfaceData implements SurfaceEventListener { private class SurfaceData implements SurfaceEventListener {
private class TextureViewWithInvalidate extends TextureView { private class TextureViewWithInvalidate extends TextureView {
public TextureViewWithInvalidate(Context context) { public TextureViewWithInvalidate(Context context) {
super(context); super(context);
...@@ -213,16 +239,19 @@ public class ContentViewRenderView extends FrameLayout { ...@@ -213,16 +239,19 @@ public class ContentViewRenderView extends FrameLayout {
} }
// This postOnAnimation is to avoid manipulating the view tree inside layout or draw. // This postOnAnimation is to avoid manipulating the view tree inside layout or draw.
parent.postOnAnimation(() -> { parent.postOnAnimation(new TrackedRunnable() {
if (mMarkedForDestroy) return; @Override
View view = (mMode == MODE_SURFACE_VIEW) ? mSurfaceView : mTextureView; protected void doRun() {
assert view != null; if (mMarkedForDestroy) return;
// Always insert view for new surface below the existing view to avoid artifacts View view = (mMode == MODE_SURFACE_VIEW) ? mSurfaceView : mTextureView;
// during surface swaps. Index 0 is the lowest child. assert view != null;
mParent.addView(view, 0, // Always insert view for new surface below the existing view to avoid artifacts
new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, // during surface swaps. Index 0 is the lowest child.
FrameLayout.LayoutParams.MATCH_PARENT)); mParent.addView(view, 0,
mParent.invalidate(); new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT));
mParent.invalidate();
}
}); });
} }
...@@ -272,34 +301,47 @@ public class ContentViewRenderView extends FrameLayout { ...@@ -272,34 +301,47 @@ public class ContentViewRenderView extends FrameLayout {
assert mMarkedForDestroy; assert mMarkedForDestroy;
runCallbacks(); runCallbacks();
// This postOnAnimation is to avoid manipulating the view tree inside layout or draw. // This postOnAnimation is to avoid manipulating the view tree inside layout or draw.
mParent.postOnAnimation(() -> { mParent.postOnAnimation(new TrackedRunnable() {
if (mMode == MODE_SURFACE_VIEW) { @Override
// Detaching a SurfaceView causes a flicker because the SurfaceView tears down protected void doRun() {
// the Surface in SurfaceFlinger before removing its hole in the view tree. if (mMode == MODE_SURFACE_VIEW) {
// This is a complicated heuristics to avoid this. It first moves the // Detaching a SurfaceView causes a flicker because the SurfaceView tears
// SurfaceView behind the new View. Then wait two frames before detaching // down the Surface in SurfaceFlinger before removing its hole in the view
// the SurfaceView. Waiting for a single frame still causes flickers on // tree. This is a complicated heuristics to avoid this. It first moves the
// high end devices like Pixel 3. // SurfaceView behind the new View. Then wait two frames before detaching
moveChildToBackWithoutDetach(mParent, mSurfaceView); // the SurfaceView. Waiting for a single frame still causes flickers on
mParent.postOnAnimation(() -> mParent.postOnAnimation(() -> { // high end devices like Pixel 3.
mParent.removeView(mSurfaceView); moveChildToBackWithoutDetach(mParent, mSurfaceView);
mParent.invalidate(); TrackedRunnable inner = new TrackedRunnable() {
if (mCachedSurfaceNeedsEviction) { @Override
mEvict.run(); public void doRun() {
mCachedSurfaceNeedsEviction = false; mParent.removeView(mSurfaceView);
} mParent.invalidate();
if (mCachedSurfaceNeedsEviction) {
mEvict.run();
mCachedSurfaceNeedsEviction = false;
}
runCallbackOnNextSurfaceData();
}
};
TrackedRunnable outer = new TrackedRunnable() {
@Override
public void doRun() {
mParent.postOnAnimation(inner);
}
};
mParent.postOnAnimation(outer);
} else if (mMode == MODE_TEXTURE_VIEW) {
mParent.removeView(mTextureView);
runCallbackOnNextSurfaceData(); runCallbackOnNextSurfaceData();
})); } else {
} else if (mMode == MODE_TEXTURE_VIEW) { assert false;
mParent.removeView(mTextureView); }
runCallbackOnNextSurfaceData();
} else {
assert false;
} }
}); });
} }
private static void moveChildToBackWithoutDetach(ViewGroup parent, View child) { private void moveChildToBackWithoutDetach(ViewGroup parent, View child) {
final int numberOfChildren = parent.getChildCount(); final int numberOfChildren = parent.getChildCount();
final int childIndex = parent.indexOfChild(child); final int childIndex = parent.indexOfChild(child);
if (childIndex <= 0) return; if (childIndex <= 0) return;
...@@ -593,6 +635,13 @@ public class ContentViewRenderView extends FrameLayout { ...@@ -593,6 +635,13 @@ public class ContentViewRenderView extends FrameLayout {
mCurrent = null; mCurrent = null;
mWindowAndroid = null; mWindowAndroid = null;
while (!mPendingRunnables.isEmpty()) {
TrackedRunnable runnable = mPendingRunnables.get(0);
removeCallbacks(runnable);
runnable.run();
assert !mPendingRunnables.contains(runnable);
}
ContentViewRenderViewJni.get().destroy(mNativeContentViewRenderView); ContentViewRenderViewJni.get().destroy(mNativeContentViewRenderView);
mNativeContentViewRenderView = 0; mNativeContentViewRenderView = 0;
} }
......
...@@ -12,9 +12,14 @@ import org.junit.Test; ...@@ -12,9 +12,14 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner; import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.shell.InstrumentationActivity; import org.chromium.weblayer.shell.InstrumentationActivity;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@RunWith(BaseJUnit4ClassRunner.class) @RunWith(BaseJUnit4ClassRunner.class)
...@@ -48,4 +53,34 @@ public class SmokeTest { ...@@ -48,4 +53,34 @@ public class SmokeTest {
} }
mActivityTestRule.navigateAndWait(url); mActivityTestRule.navigateAndWait(url);
} }
@Test
@SmallTest
public void testActivityShouldNotLeak() {
ReferenceQueue<InstrumentationActivity> referenceQueue = new ReferenceQueue<>();
PhantomReference<InstrumentationActivity> reference;
{
InstrumentationActivity activity = mActivityTestRule.launchShellWithUrl("about:blank");
mActivityTestRule.recreateActivity();
boolean destroyed =
TestThreadUtils.runOnUiThreadBlockingNoException(() -> activity.isDestroyed());
Assert.assertTrue(destroyed);
reference = new PhantomReference<>(activity, referenceQueue);
}
Runtime.getRuntime().gc();
CriteriaHelper.pollInstrumentationThread(new Criteria() {
@Override
public boolean isSatisfied() {
Reference enqueuedReference = referenceQueue.poll();
if (enqueuedReference == null) {
Runtime.getRuntime().gc();
return false;
}
Assert.assertEquals(reference, enqueuedReference);
return true;
}
});
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment