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

Commit 204b1338 authored by Soonil Nagarkar's avatar Soonil Nagarkar
Browse files

Reduce LocationManager memory usage

Move GNSS listener overhead into statics so it's shared per process, and
do not instantiate by default.

Previously GNSS requests were multiplexed by LocationManager instance,
this is now removed. This could mean additional binder overhead, but
it's pretty unlikely that this feature was in active use anywhere, as in
any large application that has multiple gnss requests, they are usually
coming from different contexts (and thus LocationManager instances) and
would not have seen the benefit of multiplexing anyways.

Bug: 179261871
Test: atest CtsLocationFineTestCases
Change-Id: I8b7a41c13dfe219c326b1e519483d89d2b721ce5
parent 9ee2e0d3
Loading
Loading
Loading
Loading
+14 −36
Original line number Diff line number Diff line
@@ -16,54 +16,43 @@

package com.android.internal.listeners;


import android.annotation.NonNull;
import android.annotation.Nullable;

import com.android.internal.util.Preconditions;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A listener registration object which holds data associated with a listener, such the executor
 * the listener should run on.
 * A listener transport object which can run listener operations on an executor.
 *
 * @param <TListener> listener type
 */
public class ListenerTransport<TListener> {

    private final Executor mExecutor;
public interface ListenerTransport<TListener> {

    private volatile @Nullable TListener mListener;

    protected ListenerTransport(@NonNull Executor executor, @NonNull TListener listener) {
        Preconditions.checkArgument(executor != null, "invalid null executor");
        Preconditions.checkArgument(listener != null, "invalid null listener/callback");
        mExecutor = executor;
        mListener = listener;
    }
    /**
     * Should return a valid listener until {@link #unregister()} is invoked, and must return
     * null after that. Recommended (but not required) that this is implemented via a volatile
     * variable.
     */
    @Nullable TListener getListener();

    /**
     * Prevents any listener invocations that happen-after this call.
     * Must be implemented so that {@link #getListener()} returns null after this is invoked.
     */
    public final void unregister() {
        mListener = null;
    }
    void unregister();

    /**
     * Executes the given operation for the listener.
     */
    public final void execute(@NonNull Consumer<TListener> operation) {
    default void execute(Executor executor, Consumer<TListener> operation) {
        Objects.requireNonNull(operation);

        if (mListener == null) {
        if (getListener() == null) {
            return;
        }

        mExecutor.execute(() -> {
            TListener listener = mListener;
        executor.execute(() -> {
            TListener listener = getListener();
            if (listener == null) {
                return;
            }
@@ -71,15 +60,4 @@ public class ListenerTransport<TListener> {
            operation.accept(listener);
        });
    }

    @Override
    public final boolean equals(Object obj) {
        // intentionally bound to reference equality so removal works as expected
        return this == obj;
    }

    @Override
    public final int hashCode() {
        return super.hashCode();
    }
}
+97 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.internal.listeners;

import android.os.RemoteException;

import com.android.internal.annotations.GuardedBy;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * A listener transport manager which handles mappings between the client facing listener and system
 * server facing transport. Supports transports which may be removed either from the client side or
 * from the system server side without leaking memory.
 *
 * @param <TTransport>> transport type
 */
public abstract class ListenerTransportManager<TTransport extends ListenerTransport<?>> {

    @GuardedBy("mRegistrations")
    private final Map<Object, WeakReference<TTransport>> mRegistrations;

    protected ListenerTransportManager() {
        // using weakhashmap means that the transport may be GCed if the server drops its reference,
        // and thus the listener may be GCed as well if the client drops that reference. if the
        // server will never drop a reference without warning (ie, transport removal may only be
        // initiated from the client side), then arraymap or similar may be used without fear of
        // memory leaks.
        mRegistrations = new WeakHashMap<>();
    }

    /**
     * Adds a new transport with the given listener key.
     */
    public final void addListener(Object key, TTransport transport) {
        try {
            synchronized (mRegistrations) {
                // ordering of operations is important so that if an error occurs at any point we
                // are left in a reasonable state
                registerTransport(transport);
                WeakReference<TTransport> oldTransportRef = mRegistrations.put(key,
                        new WeakReference<>(transport));
                if (oldTransportRef != null) {
                    TTransport oldTransport = oldTransportRef.get();
                    if (oldTransport != null) {
                        oldTransport.unregister();
                        unregisterTransport(oldTransport);
                    }
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Removes the transport with the given listener key.
     */
    public final void removeListener(Object key) {
        try {
            synchronized (mRegistrations) {
                // ordering of operations is important so that if an error occurs at any point we
                // are left in a reasonable state
                WeakReference<TTransport> transportRef = mRegistrations.remove(key);
                if (transportRef != null) {
                    TTransport transport = transportRef.get();
                    if (transport != null) {
                        transport.unregister();
                        unregisterTransport(transport);
                    }
                }
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    protected abstract void registerTransport(TTransport transport) throws RemoteException;

    protected abstract void unregisterTransport(TTransport transport) throws RemoteException;
}
+0 −258
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.internal.listeners;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.os.RemoteException;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import java.io.FileDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * A listener multiplexer designed for use by client-side code. This class ensures that listeners
 * are never invoked while a lock is held. This class is only useful for multiplexing listeners -
 * if all client listeners can be combined into a single server request, and all server results will
 * be delivered to all clients.
 *
 * By default, the multiplexer will replace requests on the server simply by registering the new
 * request and trusting the server to know this is replacing the old request. If the server needs to
 * have the old request unregistered first, subclasses should override
 * {@link #reregisterWithServer(Object, Object)}.
 *
 * @param <TRequest>  listener request type, may be Void
 * @param <TListener> listener type
 */
public abstract class ListenerTransportMultiplexer<TRequest, TListener> {

    private final Object mLock = new Object();

    @GuardedBy("mLock")
    private ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> mRegistrations =
            new ArrayMap<>();

    @GuardedBy("mLock")
    private boolean mServiceRegistered = false;

    @GuardedBy("mLock")
    private TRequest mCurrentRequest;

    /**
     * Should be implemented to register the given merged request with the server.
     *
     * @see #reregisterWithServer(Object, Object)
     */
    protected abstract void registerWithServer(TRequest mergedRequest) throws RemoteException;

    /**
     * Invoked when the server already has a request registered, and it is being replaced with a new
     * request. The default implementation simply registers the new request, trusting the server to
     * overwrite the old request.
     */
    protected void reregisterWithServer(TRequest oldMergedRequest, TRequest mergedRequest)
            throws RemoteException {
        registerWithServer(mergedRequest);
    }

    /**
     * Should be implemented to unregister from the server.
     */
    protected abstract void unregisterWithServer() throws RemoteException;

    /**
     * Called in order to generate a merged request from the given requests. The list of requests
     * will never be empty.
     */
    protected @Nullable TRequest mergeRequests(Collection<TRequest> requests) {
        if (Build.IS_DEBUGGABLE) {
            for (TRequest request : requests) {
                // if using non-null requests then implementations must override this method
                Preconditions.checkState(request == null);
            }
        }

        return null;
    }

    /**
     * Adds a new listener with no request, using the listener as the key.
     */
    public void addListener(@NonNull TListener listener, @NonNull Executor executor) {
        addListener(listener, null, listener, executor);
    }

    /**
     * Adds a new listener with the given request, using the listener as the key.
     */
    public void addListener(@Nullable TRequest request, @NonNull TListener listener,
            @NonNull Executor executor) {
        addListener(listener, request, listener, executor);
    }

    /**
     * Adds a new listener with the given request using a custom key.
     */
    public void addListener(@NonNull Object key, @Nullable TRequest request,
            @NonNull TListener listener, @NonNull Executor executor) {
        Objects.requireNonNull(key);
        RequestListenerTransport<TRequest, TListener> registration =
                new RequestListenerTransport<>(request, executor, listener);

        synchronized (mLock) {
            ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> newRegistrations =
                    new ArrayMap<>(mRegistrations.size() + 1);
            newRegistrations.putAll(mRegistrations);
            RequestListenerTransport<TRequest, TListener> old = newRegistrations.put(key,
                    registration);
            mRegistrations = newRegistrations;

            if (old != null) {
                old.unregister();
            }

            updateService();
        }
    }

    /**
     * Removes the listener with the given key.
     */
    public void removeListener(@NonNull Object key) {
        Objects.requireNonNull(key);

        synchronized (mLock) {
            if (!mRegistrations.containsKey(key)) {
                return;
            }

            ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> newRegistrations =
                    new ArrayMap<>(mRegistrations);
            RequestListenerTransport<TRequest, TListener> old = newRegistrations.remove(key);
            mRegistrations = newRegistrations;

            if (old != null) {
                old.unregister();
                updateService();
            }
        }
    }

    private void updateService() {
        synchronized (mLock) {
            if (mRegistrations.isEmpty()) {
                mCurrentRequest = null;
                if (mServiceRegistered) {
                    try {
                        mServiceRegistered = false;
                        unregisterWithServer();
                    } catch (RemoteException e) {
                        throw e.rethrowFromSystemServer();
                    }
                }
                return;
            }

            ArrayList<TRequest> requests = new ArrayList<>(mRegistrations.size());
            for (int i = 0; i < mRegistrations.size(); i++) {
                requests.add(mRegistrations.valueAt(i).getRequest());
            }

            TRequest merged = mergeRequests(requests);
            if (!mServiceRegistered || !Objects.equals(merged, mCurrentRequest)) {
                TRequest old = mCurrentRequest;
                mCurrentRequest = null;
                try {
                    if (mServiceRegistered) {
                        // if a remote exception is thrown the service should not be registered
                        mServiceRegistered = false;
                        reregisterWithServer(old, merged);
                    } else {
                        registerWithServer(merged);
                    }
                    mCurrentRequest = merged;
                    mServiceRegistered = true;
                } catch (RemoteException e) {
                    throw e.rethrowFromSystemServer();
                }
            }
        }
    }

    protected final void deliverToListeners(Consumer<TListener> operation) {
        ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> registrations;
        synchronized (mLock) {
            registrations = mRegistrations;
        }

        try {
            for (int i = 0; i < registrations.size(); i++) {
                registrations.valueAt(i).execute(operation);
            }
        } finally {
            onOperationFinished(operation);
        }
    }

    /**
     * Invoked when an operation is finished. This method will always be called once for every call
     * to {@link #deliverToListeners(Consumer)}, regardless of whether the operation encountered any
     * error or failed to execute in any way for any listeners.
     */
    protected void onOperationFinished(@NonNull Consumer<TListener> operation) {}

    /**
     * Dumps debug information.
     */
    public void dump(FileDescriptor fd, IndentingPrintWriter ipw, String[] args) {
        ArrayMap<Object, RequestListenerTransport<TRequest, TListener>> registrations;
        synchronized (mLock) {
            registrations = mRegistrations;

            ipw.print("service: ");
            if (mServiceRegistered) {
                if (mCurrentRequest == null) {
                    ipw.print("request registered");
                } else {
                    ipw.print("request registered - " + mCurrentRequest);
                }
            } else {
                ipw.print("unregistered");
            }
            ipw.println();
        }

        if (!registrations.isEmpty()) {
            ipw.println("listeners:");

            ipw.increaseIndent();
            for (int i = 0; i < registrations.size(); i++) {
                ipw.print(registrations.valueAt(i));
            }
            ipw.decreaseIndent();
        }
    }
}
+0 −45
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.internal.listeners;

import android.annotation.Nullable;

import java.util.concurrent.Executor;

/**
 * A listener transport with an associated request.
 *
 * @param <TRequest>  request type
 * @param <TListener> listener type
 */
public class RequestListenerTransport<TRequest, TListener> extends ListenerTransport<TListener> {

    private final @Nullable TRequest mRequest;

    protected RequestListenerTransport(@Nullable TRequest request, Executor executor,
            TListener listener) {
        super(executor, listener);
        mRequest = request;
    }

    /**
     * Returns the request associated with this transport.
     */
    public final @Nullable TRequest getRequest() {
        return mRequest;
    }
}
+0 −194
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.internal.listeners;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.os.Handler;
import android.os.Looper;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import junit.framework.TestCase;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Collection;
import java.util.Comparator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class ListenerTransportMultiplexerTest extends TestCase {

    TestMultiplexer mMultiplexer;

    @Before
    public void setUp() {
        mMultiplexer = new TestMultiplexer();
    }

    @Test
    public void testAdd() {
        Runnable runnable = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable, Runnable::run);
        assertThat(mMultiplexer.mRegistered).isTrue();
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);

        mMultiplexer.notifyListeners();
        verify(runnable, times(1)).run();
    }

    @Test
    public void testAdd_Multiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(0, runnable2, Runnable::run);

        mMultiplexer.notifyListeners();
        verify(runnable1).run();
        verify(runnable2).run();
    }

    @Test
    public void testRemove() {
        Runnable runnable = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable, Runnable::run);
        mMultiplexer.removeListener(runnable);
        assertThat(mMultiplexer.mRegistered).isFalse();

        mMultiplexer.notifyListeners();
        verify(runnable, never()).run();
    }

    @Test
    public void testRemove_Multiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(1, runnable2, Runnable::run);
        mMultiplexer.removeListener(runnable1);

        mMultiplexer.notifyListeners();
        verify(runnable1, never()).run();
        verify(runnable2).run();
    }

    @Test
    public void testMergeMultiple() {
        Runnable runnable1 = mock(Runnable.class);
        Runnable runnable2 = mock(Runnable.class);
        Runnable runnable3 = mock(Runnable.class);

        mMultiplexer.addListener(0, runnable1, Runnable::run);
        mMultiplexer.addListener(1, runnable2, Runnable::run);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(1)).run();
        verify(runnable2, times(1)).run();
        verify(runnable3, times(0)).run();

        mMultiplexer.addListener(0, runnable3, Runnable::run);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(1);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(2)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(1)).run();

        mMultiplexer.removeListener(runnable2);
        assertThat(mMultiplexer.mMergedRequest).isEqualTo(0);

        mMultiplexer.notifyListeners();
        verify(runnable1, times(3)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(2)).run();

        mMultiplexer.removeListener(runnable1);
        mMultiplexer.removeListener(runnable3);
        mMultiplexer.notifyListeners();
        verify(runnable1, times(3)).run();
        verify(runnable2, times(2)).run();
        verify(runnable3, times(2)).run();
    }

    @Test(timeout = 5000)
    public void testReentrancy() {
        AtomicReference<Runnable> runnable = new AtomicReference<>();
        runnable.set(() -> mMultiplexer.removeListener(runnable.get()));

        mMultiplexer.addListener(0, runnable.get(), command -> {
            CountDownLatch latch = new CountDownLatch(1);
            new Handler(Looper.getMainLooper()).post(() -> {
                command.run();
                latch.countDown();
            });
            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new AssertionError(e);
            }
        });

        mMultiplexer.notifyListeners();
        assertThat(mMultiplexer.mRegistered).isFalse();
    }

    private static class TestMultiplexer extends ListenerTransportMultiplexer<Integer, Runnable> {

        boolean mRegistered;
        int mMergedRequest;

        TestMultiplexer() {
        }

        public void notifyListeners() {
            deliverToListeners(Runnable::run);
        }

        @Override
        protected void registerWithServer(Integer mergedRequest) {
            mRegistered = true;
            mMergedRequest = mergedRequest;
        }

        @Override
        protected void unregisterWithServer() {
            mRegistered = false;
        }

        @Override
        protected Integer mergeRequests(Collection<Integer> requests) {
            return requests.stream().max(Comparator.naturalOrder()).get();
        }
    }
}
Loading