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

Commit c8673a88 authored by Chris Wren's avatar Chris Wren
Browse files

rate limit notification enqueues

This is distinct from and in addition to the limit on the number of
distinct notifications. This includes many updates to a single
notification.

Bug: 28693065
Change-Id: I5ec0911716cace91d3ad85435a5c6123b290b3a2
parent 5a68af38
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -93,6 +93,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -176,6 +177,7 @@ public class NotificationManagerService extends SystemService {
            = SystemProperties.getBoolean("debug.child_notifs", true);

    static final int MAX_PACKAGE_NOTIFICATIONS = 50;
    static final float MAX_PACKAGE_ENQUEUE_RATE = 50f;

    // message codes
    static final int MESSAGE_TIMEOUT = 2;
@@ -218,6 +220,7 @@ public class NotificationManagerService extends SystemService {

    /** notification_enqueue status value for an ignored notification. */
    private static final int EVENTLOG_ENQUEUE_STATUS_IGNORED = 2;
    private static final long MIN_PACKAGE_OVERRATE_LOG_INTERVAL = 5000; // milliseconds
    private String mRankerServicePackageName;

    private IActivityManager mAm;
@@ -297,6 +300,7 @@ public class NotificationManagerService extends SystemService {
    private static final int MY_UID = Process.myUid();
    private static final int MY_PID = Process.myPid();
    private RankingHandler mRankingHandler;
    private long mLastOverRateLogTime;

    private static class Archive {
        final int mBufferSize;
@@ -2496,6 +2500,18 @@ public class NotificationManagerService extends SystemService {
        // package or a registered listener can enqueue.  Prevents DOS attacks and deals with leaks.
        if (!isSystemNotification && !isNotificationFromListener) {
            synchronized (mNotificationList) {
                final float appEnqueueRate = mUsageStats.getAppEnqueueRate(pkg);
                if (appEnqueueRate > MAX_PACKAGE_ENQUEUE_RATE) {
                    mUsageStats.registerOverRateQuota(pkg);
                    final long now = SystemClock.elapsedRealtime();
                    if ((now - mLastOverRateLogTime) > MIN_PACKAGE_OVERRATE_LOG_INTERVAL) {
                        Slog.e(TAG, "Package enqueue rate is " + appEnqueueRate
                                + ". Shedding events. package=" + pkg);
                        mLastOverRateLogTime = now;
                    }
                    return;
                }

                int count = 0;
                final int N = mNotificationList.size();
                for (int i=0; i<N; i++) {
@@ -2506,6 +2522,7 @@ public class NotificationManagerService extends SystemService {
                        }
                        count++;
                        if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                            mUsageStats.registerOverCountQuota(pkg);
                            Slog.e(TAG, "Package has already posted " + count
                                    + " notifications.  Not showing more.  package=" + pkg);
                            return;
+84 −5
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.logging.MetricsLogger;
@@ -74,6 +75,7 @@ public class NotificationUsageStats {
    // Guarded by synchronized(this).
    private final Map<String, AggregatedStats> mStats = new HashMap<>();
    private final ArrayDeque<AggregatedStats[]> mStatsArrays = new ArrayDeque<>();
    private ArraySet<String> mStatExpiredkeys = new ArraySet<>();
    private final SQLiteLog mSQLiteLog;
    private final Context mContext;
    private final Handler mHandler;
@@ -99,15 +101,29 @@ public class NotificationUsageStats {
        mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
    }

    /**
     * Called when a notification has been posted.
     */
    public synchronized float getAppEnqueueRate(String packageName) {
        AggregatedStats stats = getOrCreateAggregatedStatsLocked(packageName);
        if (stats != null) {
            return stats.getEnqueueRate(SystemClock.elapsedRealtime());
        } else {
            return 0f;
        }
    }

    /**
     * Called when a notification has been posted.
     */
    public synchronized void registerPostedByApp(NotificationRecord notification) {
        notification.stats.posttimeElapsedMs = SystemClock.elapsedRealtime();
        final long now = SystemClock.elapsedRealtime();
        notification.stats.posttimeElapsedMs = now;

        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
        for (AggregatedStats stats : aggregatedStatsArray) {
            stats.numPostedByApp++;
            stats.updateInterarrivalEstimate(now);
            stats.countApiUse(notification);
        }
        releaseAggregatedStatsLocked(aggregatedStatsArray);
@@ -124,6 +140,7 @@ public class NotificationUsageStats {
        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(notification);
        for (AggregatedStats stats : aggregatedStatsArray) {
            stats.numUpdatedByApp++;
            stats.updateInterarrivalEstimate(SystemClock.elapsedRealtime());
            stats.countApiUse(notification);
        }
        releaseAggregatedStatsLocked(aggregatedStatsArray);
@@ -206,18 +223,37 @@ public class NotificationUsageStats {
        releaseAggregatedStatsLocked(aggregatedStatsArray);
    }

    public synchronized void registerOverRateQuota(String packageName) {
        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
        for (AggregatedStats stats : aggregatedStatsArray) {
            stats.numRateViolations++;
        }
    }

    public synchronized void registerOverCountQuota(String packageName) {
        AggregatedStats[] aggregatedStatsArray = getAggregatedStatsLocked(packageName);
        for (AggregatedStats stats : aggregatedStatsArray) {
            stats.numQuotaViolations++;
        }
    }

    // Locked by this.
    private AggregatedStats[] getAggregatedStatsLocked(NotificationRecord record) {
        return getAggregatedStatsLocked(record.sbn.getPackageName());
    }

    // Locked by this.
    private AggregatedStats[] getAggregatedStatsLocked(String packageName) {
        if (!ENABLE_AGGREGATED_IN_MEMORY_STATS) {
            return EMPTY_AGGREGATED_STATS;
        }

        // TODO: expand to package-level counts in the future.
        AggregatedStats[] array = mStatsArrays.poll();
        if (array == null) {
            array = new AggregatedStats[1];
            array = new AggregatedStats[2];
        }
        array[0] = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
        array[1] = getOrCreateAggregatedStatsLocked(packageName);
        return array;
    }

@@ -236,6 +272,7 @@ public class NotificationUsageStats {
            result = new AggregatedStats(mContext, key);
            mStats.put(key, result);
        }
        result.mLastAccessTime = SystemClock.elapsedRealtime();
        return result;
    }

@@ -272,6 +309,7 @@ public class NotificationUsageStats {
                as.dump(pw, indent);
            }
            pw.println(indent + "mStatsArrays.size(): " + mStatsArrays.size());
            pw.println(indent + "mStats.size(): " + mStats.size());
        }
        if (ENABLE_SQLITE_LOG) {
            mSQLiteLog.dump(pw, indent, filter);
@@ -279,12 +317,20 @@ public class NotificationUsageStats {
    }

    public synchronized void emit() {
        // TODO: expand to package-level counts in the future.
        AggregatedStats stats = getOrCreateAggregatedStatsLocked(DEVICE_GLOBAL_STATS);
        stats.emit();
        mLastEmitTime = SystemClock.elapsedRealtime();
        mHandler.removeMessages(MSG_EMIT);
        mHandler.sendEmptyMessageDelayed(MSG_EMIT, EMIT_PERIOD);
        for(String key: mStats.keySet()) {
            if (mStats.get(key).mLastAccessTime < mLastEmitTime) {
                mStatExpiredkeys.add(key);
            }
        }
        for(String key: mStatExpiredkeys) {
            mStats.remove(key);
        }
        mStatExpiredkeys.clear();
        mLastEmitTime = SystemClock.elapsedRealtime();
    }

    /**
@@ -326,6 +372,10 @@ public class NotificationUsageStats {
        public ImportanceHistogram noisyImportance;
        public ImportanceHistogram quietImportance;
        public ImportanceHistogram finalImportance;
        public RateEstimator enqueueRate;
        public int numRateViolations;
        public int numQuotaViolations;
        public long mLastAccessTime;

        public AggregatedStats(Context context, String key) {
            this.key = key;
@@ -334,6 +384,7 @@ public class NotificationUsageStats {
            noisyImportance = new ImportanceHistogram(context, "note_imp_noisy_");
            quietImportance = new ImportanceHistogram(context, "note_imp_quiet_");
            finalImportance = new ImportanceHistogram(context, "note_importance_");
            enqueueRate = new RateEstimator(mCreated);
        }

        public AggregatedStats getPrevious() {
@@ -444,6 +495,8 @@ public class NotificationUsageStats {
            maybeCount("note_text", (numWithText - previous.numWithText));
            maybeCount("note_sub_text", (numWithSubText - previous.numWithSubText));
            maybeCount("note_info_text", (numWithInfoText - previous.numWithInfoText));
            maybeCount("note_over_rate", (numRateViolations - previous.numRateViolations));
            maybeCount("note_over_quota", (numQuotaViolations - previous.numQuotaViolations));
            noisyImportance.maybeCount(previous.noisyImportance);
            quietImportance.maybeCount(previous.quietImportance);
            finalImportance.maybeCount(previous.finalImportance);
@@ -473,6 +526,8 @@ public class NotificationUsageStats {
            previous.numWithText = numWithText;
            previous.numWithSubText = numWithSubText;
            previous.numWithInfoText = numWithInfoText;
            previous.numRateViolations = numRateViolations;
            previous.numQuotaViolations = numQuotaViolations;
            noisyImportance.update(previous.noisyImportance);
            quietImportance.update(previous.quietImportance);
            finalImportance.update(previous.finalImportance);
@@ -493,6 +548,19 @@ public class NotificationUsageStats {
            return toStringWithIndent("");
        }

        /** @return the enqueue rate if there were a new enqueue event right now. */
        public float getEnqueueRate() {
            return getEnqueueRate(SystemClock.elapsedRealtime());
        }

        public float getEnqueueRate(long now) {
            return enqueueRate.getRate(now);
        }

        public void updateInterarrivalEstimate(long now) {
            enqueueRate.update(now);
        }

        private String toStringWithIndent(String indent) {
            StringBuilder output = new StringBuilder();
            output.append(indent).append("AggregatedStats{\n");
@@ -549,6 +617,8 @@ public class NotificationUsageStats {
            output.append("numWithSubText=").append(numWithSubText).append("\n");
            output.append(indentPlusTwo);
            output.append("numWithInfoText=").append(numWithInfoText).append("\n");
            output.append("numRateViolations=").append(numRateViolations).append("\n");
            output.append("numQuotaViolations=").append(numQuotaViolations).append("\n");
            output.append(indentPlusTwo).append(noisyImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(quietImportance.toString()).append("\n");
            output.append(indentPlusTwo).append(finalImportance.toString()).append("\n");
@@ -586,6 +656,9 @@ public class NotificationUsageStats {
            maybePut(dump, "numWithText", numWithText);
            maybePut(dump, "numWithSubText", numWithSubText);
            maybePut(dump, "numWithInfoText", numWithInfoText);
            maybePut(dump, "numRateViolations", numRateViolations);
            maybePut(dump, "numQuotaLViolations", numQuotaViolations);
            maybePut(dump, "notificationEnqueueRate", getEnqueueRate());
            noisyImportance.maybePut(dump, previous.noisyImportance);
            quietImportance.maybePut(dump, previous.quietImportance);
            finalImportance.maybePut(dump, previous.finalImportance);
@@ -598,6 +671,12 @@ public class NotificationUsageStats {
                dump.put(name, value);
            }
        }

        private void maybePut(JSONObject dump, String name, float value) throws JSONException {
            if (value > 0.0) {
                dump.put(name, value);
            }
        }
    }

    private static class ImportanceHistogram {
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.notification;


/**
 * Exponentially weighted moving average estimator for event rate.
 *
 * {@hide}
 */
public class RateEstimator {
    private static final double RATE_ALPHA = 0.8;
    private static final double MINIMUM_DT = 0.0005;
    private long mLastEventTime;
    private float mInterarrivalTime;

    public RateEstimator(long now) {
        mLastEventTime = now;
    }

    /** Update the estimate to account for an event that jsut happened. */
    public float update(long now) {
        mInterarrivalTime = (float) getInterarrivalEstimate(now);
        mLastEventTime = now;
        return (float) (1.0 / mInterarrivalTime);
    }

    /** @return the estimated rate if there were a new event right now. */
    public float getRate(long now) {
        return (float) (1.0 / getInterarrivalEstimate(now));
    }

    /** @return the average inter-arrival time if there were a new event right now. */
    private double getInterarrivalEstimate(long now) {
        // a*iat_old + (1-a)*(t_now-t_last)
        double dt = ((double) (now - mLastEventTime)) / 1000.0;
        dt = Math.max(dt, MINIMUM_DT);
        return (RATE_ALPHA * mInterarrivalTime + (1.0 - RATE_ALPHA) * dt);
    }
}
+118 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.notification;


import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;

public class RateEstimatorTest extends AndroidTestCase {
    private long mTestStartTime;
    private RateEstimator mEstimator;

    @Override
    public void setUp() {
        mTestStartTime = 1225731600000L;
        mEstimator = new RateEstimator(mTestStartTime);
    }

    @SmallTest
    public void testRunningTimeBackwardDoesntExplodeUpdate() throws Exception {
        final float rate = mEstimator.update(mTestStartTime - 1000L);
        assertFalse(Float.isInfinite(rate));
        assertFalse(Float.isNaN(rate));
    }

    @SmallTest
    public void testRunningTimeBackwardDoesntExplodeGet() throws Exception {
        final float rate = mEstimator.getRate(mTestStartTime - 1000L);
        assertFalse(Float.isInfinite(rate));
        assertFalse(Float.isNaN(rate));
    }

    @SmallTest
    public void testInstantaneousEventsDontExplodeUpdate() throws Exception {
        final float rate = mEstimator.update(mTestStartTime);
        assertFalse(Float.isInfinite(rate));
        assertFalse(Float.isNaN(rate));
    }

    @SmallTest
    public void testInstantaneousEventsDontExplodeGet() throws Exception {
        final float rate = mEstimator.getRate(mTestStartTime);
        assertFalse(Float.isInfinite(rate));
        assertFalse(Float.isNaN(rate));
    }

    @SmallTest
    public void testCompactBurstIsEstimatedUnderTwoPercent() throws Exception {
        long eventStart = mTestStartTime + 1000; // start event a long time after initialization
        long nextEventTime = postEvents(eventStart, 1, 5); // five events at 1000Hz
        final float rate = mEstimator.getRate(nextEventTime);
        assertLessThan("Rate", rate, 20f);
    }

    @SmallTest
    public void testSustained1000HzBurstIsEstimatedOverNinetyPercent() throws Exception {
        long eventStart = mTestStartTime + 1000; // start event a long time after initialization
        long nextEventTime = postEvents(eventStart, 1, 100); // one hundred events at 1000Hz
        final float rate = mEstimator.getRate(nextEventTime);
        assertGreaterThan("Rate", rate, 900f);
    }

    @SmallTest
    public void testSustained100HzBurstIsEstimatedOverNinetyPercent() throws Exception {
        long eventStart = mTestStartTime + 1000; // start event a long time after initialization
        long nextEventTime = postEvents(eventStart, 10, 100); // one hundred events at 100Hz
        final float rate = mEstimator.getRate(nextEventTime);

        assertGreaterThan("Rate", rate, 90f);
    }

    @SmallTest
    public void testRecoverQuicklyAfterSustainedBurst() throws Exception {
        long eventStart = mTestStartTime + 1000; // start event a long time after initialization
        long nextEventTime = postEvents(eventStart, 10, 1000); // one hundred events at 100Hz
        final float rate = mEstimator.getRate(nextEventTime + 5000L); // two seconds later
        assertLessThan("Rate", rate, 2f);
    }

    @SmallTest
    public void testEstimateShouldNotOvershoot() throws Exception {
        long eventStart = mTestStartTime + 1000; // start event a long time after initialization
        long nextEventTime = postEvents(eventStart, 1, 1000); // one thousand events at 1000Hz
        final float rate = mEstimator.getRate(nextEventTime);
        assertLessThan("Rate", rate, 1000f);
    }

    private void assertLessThan(String label, float a, float b)  {
        assertTrue(String.format("%s was %f, but should be less than %f", label, a, b), a < b);
    }

    private void assertGreaterThan(String label, float a, float b)  {
        assertTrue(String.format("%s was %f, but should be more than %f", label, a, b), a > b);
    }

    /** @returns the next event time. */
    private long postEvents(long start, long dt, int num) {
        long time = start;
        for (int i = 0; i < num; i++) {
            mEstimator.update(time);
            time += dt;
        }
        return time;
    }
}