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

Commit ace585b4 authored by Lee Shombert's avatar Lee Shombert
Browse files

Modern queue: enqueue multiple broadcast receivers

Bug: 253906553

Dispatch multiple broadcast receivers to the same process in a single
binder call.  This change only applies to the modern queue.  The
strategy is to collect a list of receiver instructions in the
ReceiverBatch object, and then process those instructions.

This change never dispatches more than one receiver in a single call,
which means the behavior of the queue has not been changed.  A later
commit will update the maximum number of receivers that can be sent in
a single call.  That change will modify the maximum batch size in
BroadcastConstants.

The two "testDead" tests in the BroadcastQueueTest have been modified
to ensure the first broadcast is processed before the next broadcast
is enqueued.  This avoids the situation in which the modern queue
tries to send a batch of receivers in a single message, failing both
broadcasts in the test method at the same time.

Test: atest
 * FrameworksMockingServicesTests:BroadcastQueueTest

Change-Id: I4f07df2a2edc16a85b9cfb06451429950abdf1fc
parent c7886e98
Loading
Loading
Loading
Loading
+191 −67
Original line number Diff line number Diff line
@@ -87,6 +87,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
@@ -140,6 +141,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        // We configure runnable size only once at boot; it'd be too complex to
        // try resizing dynamically at runtime
        mRunning = new BroadcastProcessQueue[mConstants.getMaxRunningQueues()];

        // Set up the statistics for batched broadcasts.
        final int batchSize = mConstants.MAX_BROADCAST_BATCH_SIZE;
        mReceiverBatch = new BroadcastReceiverBatch(batchSize);
        Slog.i(TAG, "maximum broadcast batch size " + batchSize);
    }

    /**
@@ -202,12 +208,13 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
    private final BroadcastConstants mBgConstants;

    /**
     * This single object allows the queue to dispatch receivers using scheduleReceiverList
     * without constantly allocating new ReceiverInfo objects or ArrayLists.  This queue
     * implementation is known to have a maximum size of one entry.
     * The sole instance of BroadcastReceiverBatch that is used by scheduleReceiverWarmLocked().
     * The class is not a true singleton but only one instance is needed for the broadcast queue.
     * Although this is guarded by mService, it should never be accessed by any other function.
     */
    @VisibleForTesting
    final BroadcastReceiverBatch mReceiverBatch = new BroadcastReceiverBatch(1);
    @GuardedBy("mService")
    final BroadcastReceiverBatch mReceiverBatch;

    /**
     * Timestamp when last {@link #testAllProcessQueues} failure was observed;
@@ -576,7 +583,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        if (queue != null) {
            // If queue was running a broadcast, fail it
            if (queue.isActive()) {
                finishReceiverLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
                finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_FAILURE,
                        "onApplicationCleanupLocked");
            }

@@ -749,22 +756,25 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
     * case where a broadcast is handled by a remote app, and the case where the
     * broadcast was finished locally without the remote app being involved.
     */
    @GuardedBy("mService")
    private void scheduleReceiverWarmLocked(@NonNull BroadcastProcessQueue queue) {
        checkState(queue.isActive(), "isActive");
        BroadcastReceiverBatch batch = mReceiverBatch;
        batch.reset();

        final BroadcastRecord r = queue.getActive();
        final int index = queue.getActiveIndex();

        if (r.terminalCount == 0) {
            r.dispatchTime = SystemClock.uptimeMillis();
            r.dispatchRealTime = SystemClock.elapsedRealtime();
            r.dispatchClockTime = System.currentTimeMillis();
        while (collectReceiverList(queue, batch)) {
            if (batch.isFull()) {
                break;
            }

        if (maybeSkipReceiver(queue, r, index)) {
            return;
            if (!shouldContinueScheduling(queue)) {
                break;
             }
        dispatchReceivers(queue, r, index);
            if (queue.isEmpty()) {
                break;
            }
            queue.makeActiveNextPending();
        }
        processReceiverList(queue, batch);
    }

    /**
@@ -772,36 +782,36 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
     * skipped (and therefore no more work is required).
     */
    private boolean maybeSkipReceiver(@NonNull BroadcastProcessQueue queue,
            @NonNull BroadcastRecord r, int index) {
            @NonNull BroadcastReceiverBatch batch, @NonNull BroadcastRecord r, int index) {
        final int oldDeliveryState = getDeliveryState(r, index);
        final ProcessRecord app = queue.app;
        final Object receiver = r.receivers.get(index);

        // If someone already finished this broadcast, finish immediately
        if (isDeliveryStateTerminal(oldDeliveryState)) {
            enqueueFinishReceiver(queue, oldDeliveryState, "already terminal state");
            batch.finish(r, index, oldDeliveryState, "already terminal state");
            return true;
        }

        // Consider additional cases where we'd want to finish immediately
        if (app.isInFullBackup()) {
            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
            batch.finish(r, index, BroadcastRecord.DELIVERY_SKIPPED, "isInFullBackup");
            return true;
        }
        if (mSkipPolicy.shouldSkip(r, receiver)) {
            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
            batch.finish(r, index, BroadcastRecord.DELIVERY_SKIPPED, "mSkipPolicy");
            return true;
        }
        final Intent receiverIntent = r.getReceiverIntent(receiver);
        if (receiverIntent == null) {
            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED, "getReceiverIntent");
            batch.finish(r, index, BroadcastRecord.DELIVERY_SKIPPED, "getReceiverIntent");
            return true;
        }

        // Ignore registered receivers from a previous PID
        if ((receiver instanceof BroadcastFilter)
                && ((BroadcastFilter) receiver).receiverList.pid != app.getPid()) {
            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_SKIPPED,
            batch.finish(r, index, BroadcastRecord.DELIVERY_SKIPPED,
                    "BroadcastFilter for mismatched PID");
            return true;
        }
@@ -809,17 +819,144 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        return false;
    }

    /**
     * Collect receivers into a list, to be dispatched in a single receiver list call.  Return
     * true if remaining receivers in the queue should be examined, and false if the current list
     * is complete.
     */
    private boolean collectReceiverList(@NonNull BroadcastProcessQueue queue,
            @NonNull BroadcastReceiverBatch batch) {
        final ProcessRecord app = queue.app;
        final BroadcastRecord r = queue.getActive();
        final int index = queue.getActiveIndex();
        final Object receiver = r.receivers.get(index);
        final Intent receiverIntent = r.getReceiverIntent(receiver);

        if (r.terminalCount == 0) {
            r.dispatchTime = SystemClock.uptimeMillis();
            r.dispatchRealTime = SystemClock.elapsedRealtime();
            r.dispatchClockTime = System.currentTimeMillis();
        }
        if (maybeSkipReceiver(queue, batch, r, index)) {
            return true;
        }

        final IApplicationThread thread = app.getOnewayThread();
        if (thread == null) {
            batch.finish(r, index, BroadcastRecord.DELIVERY_FAILURE, "missing IApplicationThread");
            return true;
        }

        if (receiver instanceof BroadcastFilter) {
            batch.schedule(((BroadcastFilter) receiver).receiverList.receiver,
                    receiverIntent, r.resultCode, r.resultData, r.resultExtras,
                    r.ordered, r.initialSticky, r.userId,
                    app.mState.getReportedProcState(), r, index);
            // TODO: consider making registered receivers of unordered
            // broadcasts report results to detect ANRs
            if (!r.ordered) {
                batch.success(r, index, BroadcastRecord.DELIVERY_DELIVERED, "assuming delivered");
                return true;
            }
        } else {
            batch.schedule(receiverIntent, ((ResolveInfo) receiver).activityInfo,
                    null, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
                    app.mState.getReportedProcState(), r, index);
        }

        return false;
    }

    /**
     * Process the information in a BroadcastReceiverBatch.  Elements in the finish and success
     * lists are sent to enqueueFinishReceiver().  Elements in the receivers list are transmitted
     * to the target in a single binder call.
     */
    private void processReceiverList(@NonNull BroadcastProcessQueue queue,
            @NonNull BroadcastReceiverBatch batch) {
        // Transmit the receiver list.
        final ProcessRecord app = queue.app;
        final IApplicationThread thread = app.getOnewayThread();

        batch.recordBatch(thread instanceof SameProcessApplicationThread);

        // Mark all the receivers that were discarded.  None of these have actually been scheduled.
        for (int i = 0; i < batch.finished().size(); i++) {
            final var finish = batch.finished().get(i);
            enqueueFinishReceiver(queue, finish.r, finish.index, finish.deliveryState,
                    finish.reason);
        }
        // Prepare for delivery of all receivers that are about to be scheduled.
        for (int i = 0; i < batch.cookies().size(); i++) {
            final var cookie = batch.cookies().get(i);
            prepareToDispatch(queue, cookie.r, cookie.index);
        }

        // Notify on dispatch.  Note that receiver/cookies are recorded only if the thread is
        // non-null and the list will therefore be sent.
        for (int i = 0; i < batch.cookies().size(); i++) {
            // Cookies and receivers are 1:1
            final var cookie = batch.cookies().get(i);
            final BroadcastRecord r = cookie.r;
            final int index = cookie.index;
            final Object receiver = r.receivers.get(index);
            if (receiver instanceof BroadcastFilter) {
                notifyScheduleRegisteredReceiver(queue.app, r, (BroadcastFilter) receiver);
            } else {
                notifyScheduleReceiver(queue.app, r, (ResolveInfo) receiver);
            }
        }

        // Transmit the enqueued receivers.  The thread cannot be null because the lock has been
        // held since collectReceiverList(), which will not add any receivers if the thread is null.
        boolean remoteFailed = false;
        if (batch.receivers().size()  > 0) {
            try {
                thread.scheduleReceiverList(batch.receivers());
            } catch (RemoteException e) {
                // Log the failure of the first receiver in the list.  Note that there must be at
                // least one receiver/cookie to reach this point in the code, which means
                // cookie[0] is a valid element.
                final var info = batch.cookies().get(0);
                final BroadcastRecord r = info.r;
                final int index = info.index;
                final Object receiver = r.receivers.get(index);
                final String msg = "Failed to schedule " + r + " to " + receiver
                                   + " via " + app + ": " + e;
                logw(msg);
                app.killLocked("Can't deliver broadcast", ApplicationExitInfo.REASON_OTHER, true);
                remoteFailed = true;
            }
        }

        if (!remoteFailed) {
            // If transmission succeed, report all receivers that are assumed to be delivered.
            for (int i = 0; i < batch.success().size(); i++) {
                final var finish = batch.success().get(i);
                enqueueFinishReceiver(queue, finish.r, finish.index, finish.deliveryState,
                        finish.reason);
            }
        } else {
            // If transmission failed, fail all receivers in the list.
            for (int i = 0; i < batch.cookies().size(); i++) {
                final var cookie = batch.cookies().get(i);
                enqueueFinishReceiver(queue, cookie.r, cookie.index,
                        BroadcastRecord.DELIVERY_FAILURE, "remote app");
            }
        }
    }

    /**
     * Return true if this receiver should be assumed to have been delivered.
     */
    private boolean isAssumedDelivered(@NonNull BroadcastRecord r, int index) {
    private boolean isAssumedDelivered(BroadcastRecord r, int index) {
        return (r.receivers.get(index) instanceof BroadcastFilter) && !r.ordered;
    }

    /**
     * A receiver is about to be dispatched.  Start ANR timers, if necessary.
     */
    private void dispatchReceivers(@NonNull BroadcastProcessQueue queue,
    private void prepareToDispatch(@NonNull BroadcastProcessQueue queue,
            @NonNull BroadcastRecord r, int index) {
        final ProcessRecord app = queue.app;
        final Object receiver = r.receivers.get(index);
@@ -858,43 +995,6 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        if (DEBUG_BROADCAST) logv("Scheduling " + r + " to warm " + app);
        setDeliveryState(queue, app, r, index, receiver, BroadcastRecord.DELIVERY_SCHEDULED,
                "scheduleReceiverWarmLocked");

        final IApplicationThread thread = app.getOnewayThread();
        if (thread != null) {
            try {
                final Intent receiverIntent = r.getReceiverIntent(receiver);
                if (receiver instanceof BroadcastFilter) {
                    notifyScheduleRegisteredReceiver(app, r, (BroadcastFilter) receiver);
                    thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(
                            ((BroadcastFilter) receiver).receiverList.receiver, receiverIntent,
                            r.resultCode, r.resultData, r.resultExtras, r.ordered, r.initialSticky,
                            r.userId, app.mState.getReportedProcState()));

                    // TODO: consider making registered receivers of unordered
                    // broadcasts report results to detect ANRs
                    if (assumeDelivered) {
                        enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_DELIVERED,
                                "assuming delivered");
                    }
                } else {
                    notifyScheduleReceiver(app, r, (ResolveInfo) receiver);
                    thread.scheduleReceiverList(mReceiverBatch.manifestReceiver(
                            receiverIntent, ((ResolveInfo) receiver).activityInfo,
                            null, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
                            app.mState.getReportedProcState()));
                }
            } catch (RemoteException e) {
                final String msg = "Failed to schedule " + r + " to " + receiver
                        + " via " + app + ": " + e;
                logw(msg);
                app.killLocked("Can't deliver broadcast", ApplicationExitInfo.REASON_OTHER,
                        ApplicationExitInfo.SUBREASON_UNDELIVERED_BROADCAST, true);
                enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE, "remote app");
            }
        } else {
            enqueueFinishReceiver(queue, BroadcastRecord.DELIVERY_FAILURE,
                    "missing IApplicationThread");
        }
    }

    /**
@@ -909,7 +1009,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
            mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(
                    app, OOM_ADJ_REASON_FINISH_RECEIVER);
            try {
                thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(r.resultTo, r.intent,
                thread.scheduleReceiverList(mReceiverBatch.registeredReceiver(
                        r.resultTo, r.intent,
                        r.resultCode, r.resultData, r.resultExtras, false, r.initialSticky,
                        r.userId, app.mState.getReportedProcState()));
            } catch (RemoteException e) {
@@ -940,7 +1041,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
    }

    private void deliveryTimeoutHardLocked(@NonNull BroadcastProcessQueue queue) {
        finishReceiverLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT,
        finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_TIMEOUT,
                "deliveryTimeoutHardLocked");
    }

@@ -973,7 +1074,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
            }
        }

        return finishReceiverLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
        return finishReceiverActiveLocked(queue, BroadcastRecord.DELIVERY_DELIVERED, "remote app");
    }

    /**
@@ -989,7 +1090,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        return queue.isRunnable() && queue.isProcessWarm() && !shouldRetire;
    }

    private boolean finishReceiverLocked(@NonNull BroadcastProcessQueue queue,
    /**
     * Terminate all active broadcasts on the queue.
     */
    private boolean finishReceiverActiveLocked(@NonNull BroadcastProcessQueue queue,
            @DeliveryState int deliveryState, @NonNull String reason) {
        if (!queue.isActive()) {
            logw("Ignoring finish; no active broadcast for " + queue);
@@ -1015,16 +1119,25 @@ class BroadcastQueueModernImpl extends BroadcastQueue {

        setDeliveryState(queue, app, r, index, receiver, deliveryState, reason);

        final boolean early = r != queue.getActive() || index != queue.getActiveIndex();

        if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) {
            r.anrCount++;
            if (app != null && !app.isDebugging()) {
                mService.appNotResponding(queue.app, TimeoutRecord.forBroadcastReceiver(r.intent));
            }
        } else {
        } else if (!early) {
            mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_SOFT, queue);
            mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_HARD, queue);
        }

        if (early) {
            // This is an early receiver that was transmitted as part of a group.  The delivery
            // state has been updated but don't make any further decisions.
            traceEnd(cookie);
            return false;
        }

        final boolean res = shouldContinueScheduling(queue);
        if (res) {
            // We're on a roll; move onto the next broadcast for this process
@@ -1705,6 +1818,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue {
        ipw.decreaseIndent();
        ipw.println();

        ipw.println("Batch statistics:");
        ipw.increaseIndent();
        {
            final var stats = mReceiverBatch.getStatistics();
            ipw.println("Finished         " + Arrays.toString(stats.finish));
            ipw.println("DispatchedLocal  " + Arrays.toString(stats.local));
            ipw.println("DispatchedRemote " + Arrays.toString(stats.remote));
        }
        ipw.decreaseIndent();
        ipw.println();

        if (dumpConstants) {
            mConstants.dump(ipw);
        }