summaryrefslogtreecommitdiffstats
path: root/android_webview/javatests/src/org/chromium/android_webview/test/AwContentsGarbageCollectionTest.java
blob: b9f4fabc6a327389ddfa64860889260d80bde3ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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.base.test.util.ScalableTimeout.scaleTimeout;

import android.content.Context;
import android.content.ContextWrapper;
import android.os.ResultReceiver;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;

import org.chromium.android_webview.AwContents;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;

import java.util.concurrent.Callable;

/**
 * AwContents garbage collection tests. Most apps relies on WebView being
 * garbage collected to release memory. These tests ensure that nothing
 * accidentally prevents AwContents from garbage collected, leading to leaks.
 * See crbug.com/544098 for why @DisableHardwareAccelerationForTest is needed.
 */
public class AwContentsGarbageCollectionTest extends AwTestBase {
    // The system retains a strong ref to the last focused view (in InputMethodManager)
    // so allow for 1 'leaked' instance.
    private static final int MAX_IDLE_INSTANCES = 1;

    private TestDependencyFactory mOverridenFactory;

    @Override
    public void tearDown() throws Exception {
        mOverridenFactory = null;
        super.tearDown();
    }

    @Override
    protected TestDependencyFactory createTestDependencyFactory() {
        if (mOverridenFactory == null) {
            return new TestDependencyFactory();
        } else {
            return mOverridenFactory;
        }
    }

    @SuppressFBWarnings("URF_UNREAD_FIELD")
    private static class StrongRefTestContext extends ContextWrapper {
        private AwContents mAwContents;
        public void setAwContentsStrongRef(AwContents awContents) {
            mAwContents = awContents;
        }

        public StrongRefTestContext(Context context) {
            super(context);
        }
    }

    private static class GcTestDependencyFactory extends TestDependencyFactory {
        private final StrongRefTestContext mContext;

        public GcTestDependencyFactory(StrongRefTestContext context) {
            mContext = context;
        }

        @Override
        public AwTestContainerView createAwTestContainerView(
                AwTestRunnerActivity activity, boolean allowHardwareAcceleration) {
            if (activity != mContext.getBaseContext()) fail();
            return new AwTestContainerView(mContext, allowHardwareAcceleration);
        }
    }

    @SuppressFBWarnings("URF_UNREAD_FIELD")
    private static class StrongRefTestAwContentsClient extends TestAwContentsClient {
        private AwContents mAwContentsStrongRef;
        public void setAwContentsStrongRef(AwContents awContents) {
            mAwContentsStrongRef = awContents;
        }
    }

    @DisableHardwareAccelerationForTest
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testCreateAndGcOneTime() throws Throwable {
        gcAndCheckAllAwContentsDestroyed();

        TestAwContentsClient client = new TestAwContentsClient();
        AwTestContainerView containerViews[] = new AwTestContainerView[MAX_IDLE_INSTANCES + 1];
        for (int i = 0; i < containerViews.length; i++) {
            containerViews[i] = createAwTestContainerViewOnMainSync(client);
            loadUrlAsync(containerViews[i].getAwContents(), "about:blank");
        }

        for (int i = 0; i < containerViews.length; i++) {
            containerViews[i] = null;
        }
        containerViews = null;
        removeAllViews();
        gcAndCheckAllAwContentsDestroyed();
    }

    @DisableHardwareAccelerationForTest
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testHoldKeyboardResultReceiver() throws Throwable {
        gcAndCheckAllAwContentsDestroyed();

        TestAwContentsClient client = new TestAwContentsClient();
        AwTestContainerView containerViews[] = new AwTestContainerView[MAX_IDLE_INSTANCES + 1];
        ResultReceiver resultReceivers[] = new ResultReceiver[MAX_IDLE_INSTANCES + 1];
        for (int i = 0; i < containerViews.length; i++) {
            final AwTestContainerView containerView = createAwTestContainerViewOnMainSync(client);
            containerViews[i] = containerView;
            loadUrlAsync(containerView.getAwContents(), "about:blank");
            // When we call showSoftInput(), we pass a ResultReceiver object as a parameter.
            // Android framework will hold the object reference until the matching
            // ResultReceiver in InputMethodService (IME app) gets garbage-collected.
            // WebView object wouldn't get gc'ed once OSK shows up because of this.
            // It is difficult to show keyboard and wait until input method window shows up.
            // Instead, we simply emulate Android's behavior by keeping strong references.
            // See crbug.com/595613 for details.
            resultReceivers[i] = runTestOnUiThreadAndGetResult(new Callable<ResultReceiver>() {
                @Override
                public ResultReceiver call() throws Exception {
                    return containerView.getContentViewCore().getNewShowKeyboardReceiver();
                }
            });
        }

        for (int i = 0; i < containerViews.length; i++) {
            containerViews[i] = null;
        }
        containerViews = null;
        removeAllViews();
        gcAndCheckAllAwContentsDestroyed();
    }

    @DisableHardwareAccelerationForTest
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testReferenceFromClient() throws Throwable {
        gcAndCheckAllAwContentsDestroyed();

        AwTestContainerView containerViews[] = new AwTestContainerView[MAX_IDLE_INSTANCES + 1];
        for (int i = 0; i < containerViews.length; i++) {
            StrongRefTestAwContentsClient client = new StrongRefTestAwContentsClient();
            containerViews[i] = createAwTestContainerViewOnMainSync(client);
            loadUrlAsync(containerViews[i].getAwContents(), "about:blank");
        }

        for (int i = 0; i < containerViews.length; i++) {
            containerViews[i] = null;
        }
        containerViews = null;
        removeAllViews();
        gcAndCheckAllAwContentsDestroyed();
    }

    @DisableHardwareAccelerationForTest
    @SmallTest
    @Feature({"AndroidWebView"})
    public void testReferenceFromContext() throws Throwable {
        gcAndCheckAllAwContentsDestroyed();

        TestAwContentsClient client = new TestAwContentsClient();
        AwTestContainerView containerViews[] = new AwTestContainerView[MAX_IDLE_INSTANCES + 1];
        for (int i = 0; i < containerViews.length; i++) {
            StrongRefTestContext context = new StrongRefTestContext(getActivity());
            mOverridenFactory = new GcTestDependencyFactory(context);
            containerViews[i] = createAwTestContainerViewOnMainSync(client);
            mOverridenFactory = null;
            loadUrlAsync(containerViews[i].getAwContents(), "about:blank");
        }

        for (int i = 0; i < containerViews.length; i++) {
            containerViews[i] = null;
        }
        containerViews = null;
        removeAllViews();
        gcAndCheckAllAwContentsDestroyed();
    }

    @DisableHardwareAccelerationForTest
    @LargeTest
    @Feature({"AndroidWebView"})
    public void testCreateAndGcManyTimes() throws Throwable {
        gcAndCheckAllAwContentsDestroyed();

        final int concurrentInstances = 4;
        final int repetitions = 16;

        for (int i = 0; i < repetitions; ++i) {
            for (int j = 0; j < concurrentInstances; ++j) {
                StrongRefTestAwContentsClient client = new StrongRefTestAwContentsClient();
                StrongRefTestContext context = new StrongRefTestContext(getActivity());
                mOverridenFactory = new GcTestDependencyFactory(context);
                AwTestContainerView view = createAwTestContainerViewOnMainSync(client);
                mOverridenFactory = null;
                // Embedding app can hold onto a strong ref to the WebView from either
                // WebViewClient or WebChromeClient. That should not prevent WebView from
                // gc-ed. We simulate that behavior by making the equivalent change here,
                // have AwContentsClient hold a strong ref to the AwContents object.
                client.setAwContentsStrongRef(view.getAwContents());
                context.setAwContentsStrongRef(view.getAwContents());
                loadUrlAsync(view.getAwContents(), "about:blank");
            }
            assertTrue(AwContents.getNativeInstanceCount() >= concurrentInstances);
            assertTrue(AwContents.getNativeInstanceCount() <= (i + 1) * concurrentInstances);
            removeAllViews();
        }

        gcAndCheckAllAwContentsDestroyed();
    }

    private void removeAllViews() throws Throwable {
        runTestOnUiThread(new Runnable() {
            @Override
            public void run() {
                getActivity().removeAllViews();
            }
        });
    }

    private void gcAndCheckAllAwContentsDestroyed() throws InterruptedException {
        Runtime.getRuntime().gc();

        Criteria criteria = new Criteria() {
            @Override
            public boolean isSatisfied() {
                try {
                    return runTestOnUiThreadAndGetResult(new Callable<Boolean>() {
                        @Override
                        public Boolean call() {
                            int count = AwContents.getNativeInstanceCount();
                            return count <= MAX_IDLE_INSTANCES;
                        }
                    });
                } catch (Exception e) {
                    return false;
                }
            }
        };

        // Depending on a single gc call can make this test flaky. It's possible
        // that the WebView still has transient references during load so it does not get
        // gc-ed in the one gc-call above. Instead call gc again if exit criteria fails to
        // catch this case.
        final long timeoutBetweenGcMs = scaleTimeout(1000);
        for (int i = 0; i < 15; ++i) {
            try {
                CriteriaHelper.pollInstrumentationThread(
                        criteria, timeoutBetweenGcMs, CHECK_INTERVAL);
                break;
            } catch (AssertionError e) {
                Runtime.getRuntime().gc();
            }
        }

        assertTrue(criteria.isSatisfied());
    }
}