chromium/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsClientOnRendererUnresponsiveTest.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview.test;

import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.MULTI_PROCESS;

import android.os.SystemClock;
import android.view.KeyEvent;
import android.webkit.JavascriptInterface;

import androidx.test.filters.LargeTest;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;

import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.AwRenderProcess;
import org.chromium.android_webview.AwRenderProcessGoneDetail;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.common.ContentUrlConstants;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/** Tests for AwContentsClient.onRenderProcessGone callback. */
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
public class AwContentsClientOnRendererUnresponsiveTest extends AwParameterizedTest {
    @Rule public AwActivityTestRule mActivityTestRule;

    private static final String TAG = "AwRendererUnresponsive";

    private static class JSBlocker {
        // The Blink thread waits on this in block(), until the test thread calls releaseBlock().
        private CountDownLatch mBlockingLatch;
        // The test thread waits on this in waitUntilBlocked(),
        // until the Blink thread calls block().
        private CountDownLatch mThreadWasBlockedLatch;

        JSBlocker() {
            mBlockingLatch = new CountDownLatch(1);
            mThreadWasBlockedLatch = new CountDownLatch(1);
        }

        public void releaseBlock() {
            mBlockingLatch.countDown();
        }

        @JavascriptInterface
        public void block() throws Exception {
            mThreadWasBlockedLatch.countDown();
            mBlockingLatch.await(AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        }

        public void waitUntilBlocked() throws Exception {
            mThreadWasBlockedLatch.await(
                    AwActivityTestRule.SCALED_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        }
    }

    private static class RendererTransientlyUnresponsiveTestAwContentsClient
            extends TestAwContentsClient {
        private CallbackHelper mUnresponsiveCallbackHelper;
        private CallbackHelper mResponsiveCallbackHelper;
        private JSBlocker mBlocker;

        public RendererTransientlyUnresponsiveTestAwContentsClient() {
            mUnresponsiveCallbackHelper = new CallbackHelper();
            mResponsiveCallbackHelper = new CallbackHelper();
            mBlocker = new JSBlocker();
        }

        void transientlyBlockBlinkThread(final AwContents awContents) throws Exception {
            PostTask.runOrPostTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        awContents.evaluateJavaScript("blocker.block();", null);
                    });
            mBlocker.waitUntilBlocked();
        }

        void awaitRecovery() throws Exception {
            mUnresponsiveCallbackHelper.waitForCallback(
                    0, 1, AwActivityTestRule.WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            Assert.assertEquals(1, mUnresponsiveCallbackHelper.getCallCount());
            mResponsiveCallbackHelper.waitForCallback(
                    0, 1, AwActivityTestRule.WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            Assert.assertEquals(1, mResponsiveCallbackHelper.getCallCount());
        }

        JSBlocker getBlocker() {
            return mBlocker;
        }

        @Override
        public void onRendererResponsive(AwRenderProcess process) {
            mResponsiveCallbackHelper.notifyCalled();
        }

        @Override
        public void onRendererUnresponsive(AwRenderProcess process) {
            // onRendererResponsive should not have been called yet.
            Assert.assertEquals(0, mResponsiveCallbackHelper.getCallCount());
            mUnresponsiveCallbackHelper.notifyCalled();
            mBlocker.releaseBlock();
        }
    }

    private static class RendererUnresponsiveTestAwContentsClient extends TestAwContentsClient {
        // The renderer unresponsive callback should be called repeatedly. We will wait for two
        // callbacks.
        static final int UNRESPONSIVE_CALLBACK_COUNT = 2;

        private CallbackHelper mUnresponsiveCallbackHelper;
        private CallbackHelper mTerminatedCallbackHelper;
        private JSBlocker mBlocker;

        public RendererUnresponsiveTestAwContentsClient() {
            mUnresponsiveCallbackHelper = new CallbackHelper();
            mTerminatedCallbackHelper = new CallbackHelper();
            mBlocker = new JSBlocker();
        }

        void permanentlyBlockBlinkThread(final AwContents awContents) throws Exception {
            PostTask.runOrPostTask(
                    TaskTraits.UI_DEFAULT,
                    () -> {
                        awContents.evaluateJavaScript("blocker.block();", null);
                    });
            mBlocker.waitUntilBlocked();
        }

        void awaitRendererTermination() throws Exception {
            mUnresponsiveCallbackHelper.waitForCallback(
                    0,
                    UNRESPONSIVE_CALLBACK_COUNT,
                    AwActivityTestRule.WAIT_TIMEOUT_MS,
                    TimeUnit.MILLISECONDS);
            Assert.assertEquals(
                    UNRESPONSIVE_CALLBACK_COUNT, mUnresponsiveCallbackHelper.getCallCount());

            mTerminatedCallbackHelper.waitForCallback(
                    0, 1, AwActivityTestRule.WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
            Assert.assertEquals(1, mTerminatedCallbackHelper.getCallCount());
        }

        JSBlocker getBlocker() {
            return mBlocker;
        }

        @Override
        public boolean onRenderProcessGone(AwRenderProcessGoneDetail detail) {
            mTerminatedCallbackHelper.notifyCalled();
            return true;
        }

        @Override
        public void onRendererUnresponsive(AwRenderProcess process) {
            mUnresponsiveCallbackHelper.notifyCalled();
            if (mUnresponsiveCallbackHelper.getCallCount() == UNRESPONSIVE_CALLBACK_COUNT) {
                process.terminate();
            }
        }
    }

    public AwContentsClientOnRendererUnresponsiveTest(AwSettingsMutation param) {
        this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
    }

    private void sendInputEvent(final AwContents awContents) {
        PostTask.runOrPostTask(
                TaskTraits.UI_DEFAULT,
                () -> {
                    long eventTime = SystemClock.uptimeMillis();
                    awContents.dispatchKeyEvent(
                            new KeyEvent(
                                    eventTime,
                                    eventTime,
                                    KeyEvent.ACTION_DOWN,
                                    KeyEvent.KEYCODE_ENTER,
                                    0));
                });
    }

    private void addJsBlockerInterface(final AwContents awContents, final JSBlocker blocker)
            throws Exception {
        AwActivityTestRule.addJavascriptInterfaceOnUiThread(awContents, blocker, "blocker");
    }

    // This test requires the ability to terminate the renderer in order to recover from a
    // permanently stuck blink main thread, so it can only run in multiprocess.
    @Test
    @Feature({"AndroidWebView"})
    @LargeTest
    @OnlyRunIn(MULTI_PROCESS)
    public void testOnRendererUnresponsive() throws Throwable {
        RendererUnresponsiveTestAwContentsClient contentsClient =
                new RendererUnresponsiveTestAwContentsClient();
        AwTestContainerView testView =
                mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient);
        final AwContents awContents = testView.getAwContents();

        AwActivityTestRule.enableJavaScriptOnUiThread(awContents);
        addJsBlockerInterface(awContents, contentsClient.getBlocker());
        mActivityTestRule.loadUrlSync(
                awContents,
                contentsClient.getOnPageFinishedHelper(),
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);

        contentsClient.permanentlyBlockBlinkThread(awContents);
        // Sending a key event while the renderer is unresponsive will cause onRendererUnresponsive
        // to be called.
        sendInputEvent(awContents);
        contentsClient.awaitRendererTermination();
    }

    @Test
    @Feature({"AndroidWebView"})
    @LargeTest
    public void testTransientUnresponsiveness() throws Throwable {
        RendererTransientlyUnresponsiveTestAwContentsClient contentsClient =
                new RendererTransientlyUnresponsiveTestAwContentsClient();
        AwTestContainerView testView =
                mActivityTestRule.createAwTestContainerViewOnMainSync(contentsClient);
        final AwContents awContents = testView.getAwContents();

        AwActivityTestRule.enableJavaScriptOnUiThread(awContents);
        addJsBlockerInterface(awContents, contentsClient.getBlocker());
        mActivityTestRule.loadUrlSync(
                awContents,
                contentsClient.getOnPageFinishedHelper(),
                ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL);
        contentsClient.transientlyBlockBlinkThread(awContents);
        sendInputEvent(awContents);
        contentsClient.awaitRecovery();
    }
}