From 09593d71de4b05feb96cf910e2d39f542da406cd Mon Sep 17 00:00:00 2001 From: Aaron Klotz Date: Tue, 10 Mar 2020 15:25:03 +0000 Subject: [PATCH] Bug 1608301: Part 3 - Add an allocator for generating unique service names and instance IDs; r=geckoview-reviewers,snorp `ServiceAllocator` wraps the various `Context.bindService` APIs and manages the allocation of service names (in the case of non-isolated services) or instance names (in the case of isolated services on Android 10+). During the first allocation of a content process, we construct a policy that is used for all content process allocations. The `DefaultContentPolicy` computes the maximum number of content processes and then allocates those names using a `BitSet`. The `IsolatedContentPolicy` tracks the number of live content processes, but simply uses a monotonically-increasing counter for generating instance IDs. This patch also adds a `ServiceUtils` class that contains numerous functions relating to generating service names and retrieving information about service definitions in this package. * Content processes are now named `tab0` through `tabN`. When a single content process name is used (either for single-e10s or for the process name used by isolated services), we always use `tab0`. * I am not wedded to the names of the priorities used in the `PriorityLevel` enum -- suggestions welcome! * Some of the `ServiceUtils` functions could arguably go into `ContextUtils` instead, but I thought that this was fine since they are fairly specific to this use case. * Further modifications will need to be made to support multiple priorities. This patch is enough to get everything up and running for testing, with further prioritization work being done in bug 1620145. Differential Revision: https://phabricator.services.mozilla.com/D65636 --HG-- extra : moz-landing-system : lando --- .../gecko/process/ServiceAllocator.java | 452 ++++++++++++++++++ .../mozilla/gecko/process/ServiceUtils.java | 131 +++++ 2 files changed, 583 insertions(+) create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java create mode 100644 mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java new file mode 100644 index 000000000000..d79c36423d3a --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceAllocator.java @@ -0,0 +1,452 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.process; + +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.util.IXPCOMEventTarget; +import org.mozilla.gecko.util.XPCOMEventTarget; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ServiceInfo; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.support.annotation.NonNull; + +import java.lang.reflect.Method; +import java.util.BitSet; +import java.util.concurrent.Executor; + +/* package */ final class ServiceAllocator { + private static final int MAX_NUM_ISOLATED_CONTENT_SERVICES = 50; + + private static final Method sBindIsolatedService = resolveBindIsolatedService(); + private static final Method sBindServiceWithExecutor = resolveBindServiceWithExecutor(); + + /** + * Possible priority levels that are available to child services. Each one maps to a flag that + * is passed into Context.bindService(). + */ + public static enum PriorityLevel { + FOREGROUND (Context.BIND_IMPORTANT), + BACKGROUND (0), + IDLE (Context.BIND_WAIVE_PRIORITY); + + private final int mAndroidFlag; + + private PriorityLevel(final int androidFlag) { + mAndroidFlag = androidFlag; + } + + public int getAndroidFlag() { + return mAndroidFlag; + } + } + + /** + * Abstract class that holds the essential per-service data that is required to work with + * ServiceAllocator. ServiceAllocator clients should extend this class when implementing their + * per-service connection objects. + */ + public static abstract class InstanceInfo implements ServiceConnection { + private final ServiceAllocator mAllocator; + private final GeckoProcessType mType; + private final Integer mId; + // Priority level is not yet adjustable, so mPriority is final for now + private final PriorityLevel mPriority; + + protected InstanceInfo(@NonNull final ServiceAllocator allocator, @NonNull final GeckoProcessType type, + @NonNull final PriorityLevel priority) { + mAllocator = allocator; + mType = type; + mId = mAllocator.allocate(type); + mPriority = priority; + } + + public PriorityLevel getPriorityLevel() { + return mPriority; + } + + /** + * Only content services have unique IDs. This method throws if called for a non-content + * service type. + */ + public int getId() { + if (mId == null) { + throw new RuntimeException("This service does not have a unique id"); + } + + return mId.intValue(); + } + + /** + * This method is infallible and returns an empty string for non-content services. + */ + private String getIdAsString() { + return mId == null ? "" : mId.toString(); + } + + public boolean isContent() { + return mType == GeckoProcessType.CONTENT; + } + + public GeckoProcessType getType() { + return mType; + } + + protected boolean bindService() { + return mAllocator.bindService(this); + } + + protected void unbindService() { + mAllocator.unbindService(this); + } + + /** + * This implementation of ServiceConnection.onServiceConnected simply bounces the + * connection notification over to the launcher thread (if it is not already on it). + */ + @Override + public final void onServiceConnected(final ComponentName name, + final IBinder service) { + final IXPCOMEventTarget launcherThread = XPCOMEventTarget.launcherThread(); + if (launcherThread.isOnCurrentThread()) { + // If we were able to specify an Executor during binding then we are already on + // the launcher thread; there is no reason to bounce through its event queue. + onBinderConnected(service); + return; + } + + launcherThread.execute(() -> { + onBinderConnected(service); + }); + } + + /** + * This implementation of ServiceConnection.onServiceDisconnected simply bounces the + * disconnection notification over to the launcher thread (if it is not already on it). + */ + @Override + public final void onServiceDisconnected(final ComponentName name) { + final IXPCOMEventTarget launcherThread = XPCOMEventTarget.launcherThread(); + if (launcherThread.isOnCurrentThread()) { + // If we were able to specify an Executor during binding then we are already on + // the launcher thread; there is no reason to bounce through its event queue. + onBinderConnectionLost(); + return; + } + + launcherThread.execute(() -> { + onBinderConnectionLost(); + }); + } + + /** + * Called on the launcher thread to inform the client that the service's Binder has been + * connected. This method is named differently from its ServiceConnection counterpart for + * the sake of clarity. + */ + protected abstract void onBinderConnected(@NonNull final IBinder service); + + /** + * Called on the launcher thread to inform the client that the service's Binder has been + * lost. This method is named differently from its ServiceConnection counterpart for the + * sake of clarity. Note that this method is *not* called during a clean unbind. + */ + protected abstract void onBinderConnectionLost(); + } + + private interface ContentAllocationPolicy { + /** + * Bind a new content service. + */ + boolean bindService(Context context, InstanceInfo info); + + /** + * Allocate an unused service ID for use by the caller. + * @return The new service id. + */ + int allocate(); + + /** + * Release a previously used service ID. + * @param id The service id being released. + */ + void release(final int id); + } + + /** + * This policy is intended for Android versions < 10, as well as for content process services + * that are not defined as isolated processes. In this case, the number of possible content + * service IDs has a fixed upper bound, so we use a BitSet to manage their allocation. + */ + private static final class DefaultContentPolicy implements ContentAllocationPolicy { + private final int mMaxNumSvcs; + private final BitSet mAllocator; + + public DefaultContentPolicy() { + mMaxNumSvcs = getContentServiceCount(); + mAllocator = new BitSet(mMaxNumSvcs); + } + + /** + * This implementation of bindService uses the default implementation as provided by + * ServiceAllocator. + */ + @Override + public boolean bindService(@NonNull final Context context, @NonNull final InstanceInfo info) { + return ServiceAllocator.bindServiceDefault(context, ServiceAllocator.getSvcClassNameDefault(info), info); + } + + @Override + public int allocate() { + final int next = mAllocator.nextClearBit(0); + if (next >= mMaxNumSvcs) { + throw new RuntimeException("No more content services available"); + } + + mAllocator.set(next); + return next; + } + + @Override + public void release(final int id) { + if (!mAllocator.get(id)) { + throw new IllegalStateException("Releasing an unallocated id!"); + } + + mAllocator.clear(id); + } + + /** + * @return The number of content services defined in our manifest. + */ + private static int getContentServiceCount() { + return ServiceUtils.getServiceCount(GeckoAppShell.getApplicationContext(), GeckoProcessType.CONTENT); + } + } + + /** + * This policy is intended for Android versions >= 10 when our content process services + * are defined in our manifest as having isolated processes. Since isolated services share a + * single service definition, there is no longer an Android-induced hard limit on the number of + * content processes that may be started. We simply use a monotonically-increasing counter to + * generate unique instance IDs in this case. + */ + private static final class IsolatedContentPolicy implements ContentAllocationPolicy { + private int mNextIsolatedSvcId = 0; + private int mCurNumIsolatedSvcs = 0; + + /** + * This implementation of bindService uses the isolated bindService implementation as + * provided by ServiceAllocator. + */ + @Override + public boolean bindService(@NonNull final Context context, @NonNull final InstanceInfo info) { + return ServiceAllocator.bindServiceIsolated(context, ServiceUtils.buildIsolatedSvcName(info.getType()), info); + } + + /** + * We generate a new instance ID simply by incrementing a counter. We do track how many + * content services are currently active for the purposes of maintaining the configured + * limit on number of simulatneous content processes. + */ + @Override + public int allocate() { + if (mCurNumIsolatedSvcs >= MAX_NUM_ISOLATED_CONTENT_SERVICES) { + throw new RuntimeException("No more content services available"); + } + + ++mCurNumIsolatedSvcs; + return mNextIsolatedSvcId++; + } + + /** + * Just drop the count of active services. + */ + @Override + public void release(final int id) { + if (mCurNumIsolatedSvcs <= 0) { + throw new IllegalStateException("Releasing an unallocated id"); + } + + --mCurNumIsolatedSvcs; + } + } + + /** + * The policy used for allocating content processes. + */ + private ContentAllocationPolicy mContentAllocPolicy = null; + + /** + * Clients should call this method to bind their services. This method automagically does the + * right things depending on the state of info. + * @param info The InstanceInfo-derived object that contains essential information for setting + * up the child service. + */ + public boolean bindService(@NonNull final InstanceInfo info) { + XPCOMEventTarget.assertOnLauncherThread(); + final Context context = GeckoAppShell.getApplicationContext(); + if (!info.isContent()) { + // Non-content services just use standard binding. + return bindServiceDefault(context, getSvcClassNameDefault(info), info); + } + + // Content services defer to the alloc policy to determine how to bind. + return mContentAllocPolicy.bindService(context, info); + } + + /** + * Unbinds the service described by |info| and releases its unique ID. + */ + public void unbindService(@NonNull final InstanceInfo info) { + XPCOMEventTarget.assertOnLauncherThread(); + final Context context = GeckoAppShell.getApplicationContext(); + try { + context.unbindService(info); + } finally { + release(info); + } + } + + /** + * Allocate a service ID. + * @param type The type of service. + * @return Integer encapsulating the service ID, or null if no ID is necessary. + */ + private Integer allocate(@NonNull final GeckoProcessType type) { + XPCOMEventTarget.assertOnLauncherThread(); + if (type != GeckoProcessType.CONTENT) { + // No unique id necessary + return null; + } + + // Lazy initialization of mContentAllocPolicy to ensure that it is constructed on the + // launcher thread. + if (mContentAllocPolicy == null) { + if (canBindIsolated(GeckoProcessType.CONTENT)) { + mContentAllocPolicy = new IsolatedContentPolicy(); + } else { + mContentAllocPolicy = new DefaultContentPolicy(); + } + } + + return Integer.valueOf(mContentAllocPolicy.allocate()); + } + + /** + * Free a defunct service's ID if necessary. + * @param info The InstanceInfo-derived object that contains essential information for tearing + * down the child service. + */ + private void release(@NonNull final InstanceInfo info) { + XPCOMEventTarget.assertOnLauncherThread(); + if (!info.isContent()) { + return; + } + + mContentAllocPolicy.release(info.getId()); + } + + /** + * Find out whether the desired service type is defined in our manifest as having an isolated + * process. + * @param type Service type to query + * @return true if this service type may use isolated binding, otherwise false. + */ + private static boolean canBindIsolated(@NonNull final GeckoProcessType type) { + if (sBindIsolatedService == null) { + return false; + } + + final Context context = GeckoAppShell.getApplicationContext(); + final int svcFlags = ServiceUtils.getServiceFlags(context, type); + return (svcFlags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0; + } + + /** + * Convert PriorityLevel into the flags argument to Context.bindService() et al + */ + private static int getAndroidFlags(@NonNull final PriorityLevel priority) { + return Context.BIND_AUTO_CREATE | priority.getAndroidFlag(); + } + + /** + * Obtain the class name to use for service binding in the default (ie, non-isolated) case. + */ + private static String getSvcClassNameDefault(@NonNull final InstanceInfo info) { + return ServiceUtils.buildSvcName(info.getType(), info.getIdAsString()); + } + + /** + * Wrapper for bindService() that utilizes the Context.bindService() overload that accepts an + * Executor argument, when available. Otherwise it falls back to the legacy overload. + */ + private static boolean bindServiceDefault(@NonNull final Context context, @NonNull final String svcClassName, @NonNull final InstanceInfo info) { + final Intent intent = new Intent(); + intent.setClassName(context, svcClassName); + + if (sBindServiceWithExecutor != null) { + return bindServiceWithExecutor(context, intent, info); + } + + return context.bindService(intent, info, getAndroidFlags(info.getPriorityLevel())); + } + + /** + * Wrapper that calls the reflected Context.bindIsolatedService() method. + */ + private static boolean bindServiceIsolated(@NonNull final Context context, @NonNull final String svcClassName, @NonNull final InstanceInfo info) { + final Intent intent = new Intent(); + intent.setClassName(context, svcClassName); + + final String instanceId = info.getIdAsString(); + + try { + final Boolean result = (Boolean) sBindIsolatedService.invoke(context, intent, getAndroidFlags(info.getPriorityLevel()), + instanceId, XPCOMEventTarget.launcherThread(), info); + return result.booleanValue(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Wrapper that calls the reflected Context.bindService() overload that accepts an Executor argument. + * We always specify the launcher thread as our Executor. + */ + private static boolean bindServiceWithExecutor(@NonNull final Context context, @NonNull final Intent intent, @NonNull final InstanceInfo info) { + try { + final Boolean result = (Boolean) sBindServiceWithExecutor.invoke(context, intent, getAndroidFlags(info.getPriorityLevel()), + XPCOMEventTarget.launcherThread(), info); + return result.booleanValue(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static Method resolveBindIsolatedService() { + try { + return Context.class.getDeclaredMethod("bindIsolatedService", Intent.class, Integer.class, String.class, Executor.class, ServiceConnection.class); + } catch (NoSuchMethodException e) { + return null; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private static Method resolveBindServiceWithExecutor() { + try { + return Context.class.getDeclaredMethod("bindService", Intent.class, Integer.class, Executor.class, ServiceConnection.class); + } catch (NoSuchMethodException e) { + return null; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} + diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java new file mode 100644 index 000000000000..ca9908b6be4b --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/ServiceUtils.java @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.process; + +import org.mozilla.gecko.util.ContextUtils; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.support.annotation.NonNull; + +/* package */ final class ServiceUtils { + private static final String DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX = "0"; + + private ServiceUtils() {} + + /** + * @return StringBuilder containing the name of a service class but not qualifed with any + * unique identifiers. + */ + private static StringBuilder startSvcName(@NonNull final GeckoProcessType type) { + final StringBuilder builder = new StringBuilder(GeckoChildProcessServices.class.getName()); + builder.append("$").append(type); + return builder; + } + + /** + * Given a service's GeckoProcessType, obtain the name of its class, including any qualifiers + * that are needed to uniquely identify its manifest definition. + */ + public static String buildSvcName(@NonNull final GeckoProcessType type, final String... suffixes) { + final StringBuilder builder = startSvcName(type); + + for (final String suffix : suffixes) { + builder.append(suffix); + } + + return builder.toString(); + } + + /** + * Given a service's GeckoProcessType, obtain the name of its class to be used for the purpose + * of binding as an isolated service. + * + * Content services are defined in the manifest as "tab0" through "tabN" for some value of N. + * For the purposes of binding to an isolated content service, we simply need to repeatedly + * re-use the definition of "tab0", the "0" being stored as the + * DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX constant. + */ + public static String buildIsolatedSvcName(@NonNull final GeckoProcessType type) { + if (type == GeckoProcessType.CONTENT) { + return buildSvcName(type, DEFAULT_ISOLATED_CONTENT_SERVICE_NAME_SUFFIX); + } + + // Non-content services do not require any unique IDs + return buildSvcName(type); + } + + /** + * Given a service's GeckoProcessType, obtain the unqualified name of its class. + * @return The name of the class that hosts the implementation of the service corresponding + * to type, but without any unique identifiers that may be required to actually instantiate it. + */ + private static String buildSvcNamePrefix(@NonNull final GeckoProcessType type) { + return startSvcName(type).toString(); + } + + /** + * Extracts flags from the manifest definition of a service. + * @param context Context to use for extraction + * @param type Service type + * @return flags that are specified in the service's definition in our manifest. + * @see android.content.pm.ServiceInfo for explanation of the various flags. + */ + public static int getServiceFlags(@NonNull final Context context, @NonNull final GeckoProcessType type) { + final ComponentName component = new ComponentName(context, buildIsolatedSvcName(type)); + final PackageManager pkgMgr = context.getPackageManager(); + + try { + final ServiceInfo svcInfo = pkgMgr.getServiceInfo(component, 0); + // svcInfo is never null + return svcInfo.flags; + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Obtain the list of all services defined for |context|. + */ + private static ServiceInfo[] getServiceList(@NonNull final Context context) { + return ContextUtils.getCurrentPackageInfo(context, PackageManager.GET_SERVICES).services; + } + + /** + * Count the number of service definitions in our manifest that satisfy bindings for a + * particular service type. + * @param context Context object to use for extracting the service definitions + * @param type The type of service to count + * @return The number of available service definitions. + */ + public static int getServiceCount(@NonNull final Context context, @NonNull final GeckoProcessType type) { + final ServiceInfo[] svcList = getServiceList(context); + final String serviceNamePrefix = buildSvcNamePrefix(type); + + int result = 0; + for (final ServiceInfo svc : svcList) { + final String svcName = svc.name; + // If svcName starts with serviceNamePrefix, then both strings must either be equal + // or else the first subsequent character in svcName must be a digit. + // This guards against any future GeckoProcessType whose string representation shares + // a common prefix with another GeckoProcessType value. + if (svcName.startsWith(serviceNamePrefix) && + (svcName.length() == serviceNamePrefix.length() || + Character.isDigit(svcName.codePointAt(serviceNamePrefix.length())))) { + ++result; + } + } + + if (result <= 0) { + throw new RuntimeException("Could not count " + serviceNamePrefix + " services in manifest"); + } + + return result; + } + +} +