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

Commit 0a34ef5c authored by Matías Hernández's avatar Matías Hernández
Browse files

Rate-limit (presumably) unnecessary NotificationManager.cancel() calls

Also fixed the enqueue rate calcualtion to clarify that we're only interested in the update rate, and added some logging to Slog.

Bug: 384467103
Test: atest NotificationManagerTest + CTS
Flag: android.app.nm_binder_perf_throttle_notify
Change-Id: I2780229b95108e9b802b33a3d7f606017ea27b62
parent d3057f0d
Loading
Loading
Loading
Loading
+64 −24
Original line number Original line Diff line number Diff line
@@ -68,6 +68,7 @@ import android.service.notification.ZenModeConfig;
import android.service.notification.ZenPolicy;
import android.service.notification.ZenPolicy;
import android.util.Log;
import android.util.Log;
import android.util.LruCache;
import android.util.LruCache;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.util.proto.ProtoOutputStream;


import java.lang.annotation.Retention;
import java.lang.annotation.Retention;
@@ -646,16 +647,21 @@ public class NotificationManager {
     */
     */
    public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500;
    public static int MAX_SERVICE_COMPONENT_NAME_LENGTH = 500;


    private static final float MAX_NOTIFICATION_ENQUEUE_RATE = 5f;
    private static final float MAX_NOTIFICATION_UPDATE_RATE = 5f;
    private static final float MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE = 5f;
    private static final int KNOWN_STATUS_ENQUEUED = 1;
    private static final int KNOWN_STATUS_CANCELLED = 2;


    private final Context mContext;
    private final Context mContext;
    private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub>
    private final Map<CallNotificationEventListener, CallNotificationEventCallbackStub>
            mCallNotificationEventCallbacks = new HashMap<>();
            mCallNotificationEventCallbacks = new HashMap<>();


    private final InstantSource mClock;
    private final InstantSource mClock;
    private final RateEstimator mEnqueueRateEstimator = new RateEstimator();
    private final RateEstimator mUpdateRateEstimator = new RateEstimator();
    private final LruCache<String, Boolean> mEnqueuedNotificationKeys = new LruCache<>(10);
    private final RateEstimator mUnnecessaryCancelRateEstimator = new RateEstimator();
    private final Object mEnqueueThrottleLock = new Object();
    // Value is KNOWN_STATUS_ENQUEUED/_CANCELLED
    private final LruCache<NotificationKey, Integer> mKnownNotifications = new LruCache<>(100);
    private final Object mThrottleLock = new Object();


    @UnsupportedAppUsage
    @UnsupportedAppUsage
    private static INotificationManager sService;
    private static INotificationManager sService;
@@ -780,7 +786,7 @@ public class NotificationManager {
    {
    {
        INotificationManager service = service();
        INotificationManager service = service();
        String pkg = mContext.getPackageName();
        String pkg = mContext.getPackageName();
        if (discardNotify(tag, id, notification)) {
        if (discardNotify(user, pkg, tag, id, notification)) {
            return;
            return;
        }
        }


@@ -797,32 +803,39 @@ public class NotificationManager {
     * Determines whether a {@link #notify} call should be skipped. If the notification is not
     * Determines whether a {@link #notify} call should be skipped. If the notification is not
     * skipped, updates tracking metadata to use in future decisions.
     * skipped, updates tracking metadata to use in future decisions.
     */
     */
    private boolean discardNotify(@Nullable String tag, int id, Notification notification) {
    private boolean discardNotify(UserHandle user, String pkg, @Nullable String tag, int id,
            Notification notification) {
        if (notificationClassification()
        if (notificationClassification()
                && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) {
                && NotificationChannel.SYSTEM_RESERVED_IDS.contains(notification.getChannelId())) {
            return true;
            return true;
        }
        }


        if (Flags.nmBinderPerfThrottleNotify()) {
        if (Flags.nmBinderPerfThrottleNotify()) {
            String key = toEnqueuedNotificationKey(tag, id);
            NotificationKey key = new NotificationKey(user, pkg, tag, id);
            long now = mClock.millis();
            long now = mClock.millis();
            synchronized (mEnqueueThrottleLock) {
            synchronized (mThrottleLock) {
                if (mEnqueuedNotificationKeys.get(key) != null
                Integer status = mKnownNotifications.get(key);
                        && !notification.hasCompletedProgress()
                if (status != null && status == KNOWN_STATUS_ENQUEUED
                        && mEnqueueRateEstimator.getRate(now) > MAX_NOTIFICATION_ENQUEUE_RATE) {
                        && !notification.hasCompletedProgress()) {
                    float updateRate = mUpdateRateEstimator.getRate(now);
                    if (updateRate > MAX_NOTIFICATION_UPDATE_RATE) {
                        Slog.w(TAG, "Shedding update of " + key
                                + ", notification update maximum rate exceeded (" + updateRate
                                + ")");
                        return true;
                        return true;
                    }
                    }
                    mUpdateRateEstimator.update(now);
                }


                mEnqueueRateEstimator.update(now);
                mKnownNotifications.put(key, KNOWN_STATUS_ENQUEUED);
                mEnqueuedNotificationKeys.put(key, Boolean.TRUE);
            }
            }
        }
        }


        return false;
        return false;
    }
    }
    private static String toEnqueuedNotificationKey(@Nullable String tag, int id) {

        return tag + "," + id;
    private record NotificationKey(@NonNull UserHandle user, @NonNull String pkg,
    }
                                   @Nullable String tag, int id) { }


    private Notification fixNotification(Notification notification) {
    private Notification fixNotification(Notification notification) {
        String pkg = mContext.getPackageName();
        String pkg = mContext.getPackageName();
@@ -920,14 +933,12 @@ public class NotificationManager {
    @UnsupportedAppUsage
    @UnsupportedAppUsage
    public void cancelAsUser(@Nullable String tag, int id, UserHandle user)
    public void cancelAsUser(@Nullable String tag, int id, UserHandle user)
    {
    {
        if (Flags.nmBinderPerfThrottleNotify()) {
        String pkg = mContext.getPackageName();
            synchronized (mEnqueueThrottleLock) {
        if (discardCancel(user, pkg, tag, id)) {
                mEnqueuedNotificationKeys.remove(toEnqueuedNotificationKey(tag, id));
            return;
            }
        }
        }


        INotificationManager service = service();
        INotificationManager service = service();
        String pkg = mContext.getPackageName();
        if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
        if (localLOGV) Log.v(TAG, pkg + ": cancel(" + id + ")");
        try {
        try {
            service.cancelNotificationWithTag(
            service.cancelNotificationWithTag(
@@ -937,6 +948,33 @@ public class NotificationManager {
        }
        }
    }
    }


    /**
     * Determines whether a {@link #cancel} call should be skipped. If not skipped, updates tracking
     * metadata to use in future decisions.
     */
    private boolean discardCancel(UserHandle user, String pkg, @Nullable String tag, int id) {
        if (Flags.nmBinderPerfThrottleNotify()) {
            NotificationKey key = new NotificationKey(user, pkg, tag, id);
            long now = mClock.millis();
            synchronized (mThrottleLock) {
                Integer status = mKnownNotifications.get(key);
                if (status != null && status == KNOWN_STATUS_CANCELLED) {
                    float cancelRate = mUnnecessaryCancelRateEstimator.getRate(now);
                    if (cancelRate > MAX_NOTIFICATION_UNNECESSARY_CANCEL_RATE) {
                        Slog.w(TAG, "Shedding cancel of " + key
                                + ", presumably unnecessary and maximum rate exceeded ("
                                + cancelRate + ")");
                        return true;
                    }
                    mUnnecessaryCancelRateEstimator.update(now);
                }
                mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED);
            }
        }

        return false;
    }

    /**
    /**
     * Cancel all previously shown notifications. See {@link #cancel} for the
     * Cancel all previously shown notifications. See {@link #cancel} for the
     * detailed behavior.
     * detailed behavior.
@@ -944,8 +982,10 @@ public class NotificationManager {
    public void cancelAll()
    public void cancelAll()
    {
    {
        if (Flags.nmBinderPerfThrottleNotify()) {
        if (Flags.nmBinderPerfThrottleNotify()) {
            synchronized (mEnqueueThrottleLock) {
            synchronized (mThrottleLock) {
                mEnqueuedNotificationKeys.evictAll();
                for (NotificationKey key : mKnownNotifications.snapshot().keySet()) {
                    mKnownNotifications.put(key, KNOWN_STATUS_CANCELLED);
                }
            }
            }
        }
        }


+52 −1
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package android.app;


import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.atMost;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.times;
@@ -67,6 +68,8 @@ public class NotificationManagerTest {
            mClock.advanceByMillis(5);
            mClock.advanceByMillis(5);
        }
        }


        verify(mNotificationManager.mBackendService, atLeast(20)).enqueueNotificationWithTag(any(),
                any(), any(), anyInt(), any(), anyInt());
        verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(),
        verify(mNotificationManager.mBackendService, atMost(30)).enqueueNotificationWithTag(any(),
                any(), any(), anyInt(), any(), anyInt());
                any(), any(), anyInt(), any(), anyInt());
    }
    }
@@ -101,7 +104,38 @@ public class NotificationManagerTest {


    @Test
    @Test
    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY)
    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY)
    public void notify_rapidAddAndCancel_isNotThrottled() throws Exception {
    public void cancel_unnecessaryAndRapid_isThrottled() throws Exception {

        for (int i = 0; i < 100; i++) {
            mNotificationManager.cancel(1);
            mClock.advanceByMillis(5);
        }

        verify(mNotificationManager.mBackendService, atLeast(20)).cancelNotificationWithTag(any(),
                any(), any(), anyInt(), anyInt());
        verify(mNotificationManager.mBackendService, atMost(30)).cancelNotificationWithTag(any(),
                any(), any(), anyInt(), anyInt());
    }

    @Test
    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY)
    public void cancel_unnecessaryButReasonable_isNotThrottled() throws Exception {
        // Scenario: the app tries to repeatedly cancel a single notification, but at a reasonable
        // rate. Strange, but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY.
        for (int i = 0; i < 100; i++) {
            mNotificationManager.cancel(1);
            mClock.advanceByMillis(500);
        }

        verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(),
                any(), any(), anyInt(), anyInt());
    }

    @Test
    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY)
    public void cancel_necessaryAndRapid_isNotThrottled() throws Exception {
        // Scenario: the app posts and immediately cancels a bunch of notifications. Strange,
        // but not what we're trying to block with NM_BINDER_PERF_THROTTLE_NOTIFY.
        Notification n = exampleNotification();
        Notification n = exampleNotification();


        for (int i = 0; i < 100; i++) {
        for (int i = 0; i < 100; i++) {
@@ -112,6 +146,23 @@ public class NotificationManagerTest {


        verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(),
        verify(mNotificationManager.mBackendService, times(100)).enqueueNotificationWithTag(any(),
                any(), any(), anyInt(), any(), anyInt());
                any(), any(), anyInt(), any(), anyInt());
        verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(),
                any(), any(), anyInt(), anyInt());
    }

    @Test
    @EnableFlags(Flags.FLAG_NM_BINDER_PERF_THROTTLE_NOTIFY)
    public void cancel_maybeNecessaryAndRapid_isNotThrottled() throws Exception {
        // Scenario: the app posted a lot of notifications, is killed, then restarts (so NM client
        // doesn't know about them), then cancels them one by one. We don't want to throttle this
        // case.
        for (int i = 0; i < 100; i++) {
            mNotificationManager.cancel(i);
            mClock.advanceByMillis(1);
        }

        verify(mNotificationManager.mBackendService, times(100)).cancelNotificationWithTag(any(),
                any(), any(), anyInt(), anyInt());
    }
    }


    private Notification exampleNotification() {
    private Notification exampleNotification() {