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

Commit 26835f0b authored by Garfield Tan's avatar Garfield Tan
Browse files

Track and reset mock to avoid mem leaks.

When a test calls a mock with another mock, it creates a chain of
reference inside MockHandlerImpl because invocation is saved in
invocationContainer for stubbing record. If InlineDexmakerMockMaker, the
MockHandlerImpl is then stored in a mock map to provide stubbing
behavior for future use, and will be cleaned up from the mock map when
the mock object is reclaimed by GC.

Since InlineDexmakerMockMaker only cleans up the map at most once every
16s, we need 16s plus a GC pass to shorten the chain of ref by one node
when the chain doesn't form a ring.

This CL resets all created mocks after running each test suite.
Consequently it uses a brand new MockHandlerImpl to replace the original
one and break down the reference chain.

One thing that's worth noting is this is not a complete solution to this
issue, as the MockCreationListener is held in a ThreadLocal, which means
we can only get notified for mocks created in the same thread, but this
is enough for now because it catches many large retainers like ATMS and
WMS.

Bug: 123984854
Test: atest WmTests can run to the end w/o hanging or crashing. Note
this only fixes one of two memory issues we found in the bug.

Change-Id: Iab08c4a324f4de593323d4914256970d7bc1a150
parent 0528cd9d
Loading
Loading
Loading
Loading
+17 −7
Original line number Diff line number Diff line
@@ -72,8 +72,10 @@ import com.android.server.am.PendingIntentController;
import com.android.server.appop.AppOpsService;
import com.android.server.firewall.IntentFirewall;
import com.android.server.uri.UriGrantsManagerInternal;
import com.android.server.wm.utils.MockTracker;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -88,6 +90,8 @@ import java.util.List;
class ActivityTestsBase {
    private static int sNextDisplayId = DEFAULT_DISPLAY + 1;

    private static MockTracker sMockTracker;

    @Rule
    public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
            new DexmakerShareClassLoaderRule();
@@ -107,9 +111,17 @@ class ActivityTestsBase {

    @BeforeClass
    public static void setUpOnceBase() {
        sMockTracker = new MockTracker();

        AttributeCache.init(getInstrumentation().getTargetContext());
    }

    @AfterClass
    public static void tearDownOnceBase() {
        sMockTracker.close();
        sMockTracker = null;
    }

    @Before
    public void setUpBase() {
        mTestInjector.setUp();
@@ -657,12 +669,11 @@ class ActivityTestsBase {
    private static WindowManagerService sMockWindowManagerService;

    private static WindowManagerService prepareMockWindowManager() {
        if (sMockWindowManagerService != null) {
            return sMockWindowManagerService;
        if (sMockWindowManagerService == null) {
            sMockWindowManagerService = mock(WindowManagerService.class);
        }

        final WindowManagerService service = mock(WindowManagerService.class);
        service.mRoot = mock(RootWindowContainer.class);
        sMockWindowManagerService.mRoot = mock(RootWindowContainer.class);

        doAnswer((InvocationOnMock invocationOnMock) -> {
            final Runnable runnable = invocationOnMock.<Runnable>getArgument(0);
@@ -670,10 +681,9 @@ class ActivityTestsBase {
                runnable.run();
            }
            return null;
        }).when(service).inSurfaceTransaction(any());
        }).when(sMockWindowManagerService).inSurfaceTransaction(any());

        sMockWindowManagerService = service;
        return service;
        return sMockWindowManagerService;
    }

    /**
+3 −1
Original line number Diff line number Diff line
@@ -67,6 +67,7 @@ import org.junit.Before;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
@@ -142,7 +143,8 @@ public class RootActivityContainerTests extends ActivityTestsBase {

    private static void ensureStackPlacement(ActivityStack stack, TaskRecord... tasks) {
        final ArrayList<TaskRecord> stackTasks = stack.getAllTasks();
        assertEquals(stackTasks.size(), tasks != null ? tasks.length : 0);
        assertEquals("Expecting " + Arrays.deepToString(tasks) + " got " + stackTasks,
                stackTasks.size(), tasks != null ? tasks.length : 0);

        if (tasks == null) {
            return;
+1 −0
Original line number Diff line number Diff line
@@ -88,6 +88,7 @@ class TestSystemServices {
        sPolicy = null;

        sMockitoSession.finishMocking();
        sMockitoSession = null;
    }

    private static void setUpTestWindowService() {
+11 −1
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import android.view.IWindow;
import android.view.WindowManager;

import com.android.server.AttributeCache;
import com.android.server.wm.utils.MockTracker;

import org.junit.After;
import org.junit.AfterClass;
@@ -77,6 +78,8 @@ class WindowTestsBase {
    private static int sNextDisplayId = DEFAULT_DISPLAY + 1;
    static int sNextStackId = 1000;

    private static MockTracker sMockTracker;

    /** Non-default display. */
    DisplayContent mDisplayContent;
    DisplayInfo mDisplayInfo = new DisplayInfo();
@@ -109,11 +112,18 @@ class WindowTestsBase {

        TestSystemServices.setUpWindowManagerService();

        // MockTracker needs to be initialized after TestSystemServices because we don't want to
        // track static mocks.
        sMockTracker = new MockTracker();

        sPowerManagerWrapper = mock(WindowState.PowerManagerWrapper.class);
    }

    @AfterClass
    public static void tearDonwOnceBase() {
    public static void tearDownOnceBase() {
        sMockTracker.close();
        sMockTracker = null;

        TestSystemServices.tearDownWindowManagerService();
    }

+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wm.utils;

import android.util.Log;

import org.mockito.Mockito;
import org.mockito.MockitoFramework;
import org.mockito.listeners.MockCreationListener;
import org.mockito.mock.MockCreationSettings;

import java.util.IdentityHashMap;

/**
 * An util class used to track mock creation, and reset them when closing. Note only one instance is
 * allowed at anytime, as Mockito framework throws exception if there is already a listener of the
 * same type registered.
 */
public class MockTracker implements MockCreationListener, AutoCloseable {
    private static final String TAG = "MockTracker";

    private final MockitoFramework mMockitoFramework = Mockito.framework();

    private final IdentityHashMap<Object, Void> mMocks = new IdentityHashMap<>();

    public MockTracker() {
        mMockitoFramework.addListener(this);
    }

    @Override
    public void onMockCreated(Object mock, MockCreationSettings settings) {
        mMocks.put(mock, null);
    }

    @Override
    public void close() {
        mMockitoFramework.removeListener(this);

        for (final Object mock : mMocks.keySet()) {
            try {
                Mockito.reset(mock);
            } catch (Exception e) {
                Log.e(TAG, "Failed to reset " + mock, e);
            }
        }
        mMocks.clear();
    }
}