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

Commit 1d837f7f authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Limit the UserPackage cache size." into udc-dev

parents 42570f58 0a08c674
Loading
Loading
Loading
Loading
+55 −9
Original line number Diff line number Diff line
@@ -18,14 +18,16 @@ package android.content.pm;

import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.os.Process;
import android.os.UserHandle;
import android.util.SparseArrayMap;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;

import libcore.util.EmptyArray;

import java.util.Objects;
import java.util.Random;

/**
 * POJO to represent a package for a specific user ID.
@@ -34,6 +36,16 @@ import java.util.Objects;
 */
public final class UserPackage {
    private static final boolean ENABLE_CACHING = true;
    /**
     * The maximum number of entries to keep in the cache per user ID.
     * The value should ideally be high enough to cover all packages on an end-user device,
     * but low enough that stale or invalid packages would eventually (probably) get removed.
     * This should benefit components that loop through all packages on a device and use this class,
     * since being able to cache the objects for all packages on the device
     * means we don't have to keep recreating the objects.
     */
    @VisibleForTesting
    static final int MAX_NUM_CACHED_ENTRIES_PER_USER = 1000;

    @UserIdInt
    public final int userId;
@@ -43,11 +55,13 @@ public final class UserPackage {
    @GuardedBy("sCacheLock")
    private static final SparseArrayMap<String, UserPackage> sCache = new SparseArrayMap<>();

    private static final class NoPreloadHolder {
        /** Set of userIDs to cache objects for. */
    /**
     * Set of userIDs to cache objects for. We start off with an empty set, so there's no caching
     * by default. The system will override with a valid set of userIDs in its process so that
     * caching becomes active in the system process.
     */
    @GuardedBy("sCacheLock")
        private static int[] sUserIds = new int[]{UserHandle.getUserId(Process.myUid())};
    }
    private static int[] sUserIds = EmptyArray.INT;

    private UserPackage(int userId, String packageName) {
        this.userId = userId;
@@ -87,13 +101,14 @@ public final class UserPackage {
        }

        synchronized (sCacheLock) {
            if (!ArrayUtils.contains(NoPreloadHolder.sUserIds, userId)) {
            if (!ArrayUtils.contains(sUserIds, userId)) {
                // Don't cache objects for invalid userIds.
                return new UserPackage(userId, packageName);
            }

            UserPackage up = sCache.get(userId, packageName);
            if (up == null) {
                maybePurgeRandomEntriesLocked(userId);
                packageName = packageName.intern();
                up = new UserPackage(userId, packageName);
                sCache.add(userId, packageName, up);
@@ -121,7 +136,7 @@ public final class UserPackage {

        userIds = userIds.clone();
        synchronized (sCacheLock) {
            NoPreloadHolder.sUserIds = userIds;
            sUserIds = userIds;

            for (int u = sCache.numMaps() - 1; u >= 0; --u) {
                final int userId = sCache.keyAt(u);
@@ -131,4 +146,35 @@ public final class UserPackage {
            }
        }
    }

    @VisibleForTesting
    public static int numEntriesForUser(int userId) {
        synchronized (sCacheLock) {
            return sCache.numElementsForKey(userId);
        }
    }

    /** Purge a random set of entries if the cache size is too large. */
    @GuardedBy("sCacheLock")
    private static void maybePurgeRandomEntriesLocked(int userId) {
        final int uIdx = sCache.indexOfKey(userId);
        if (uIdx < 0) {
            return;
        }
        int numCached = sCache.numElementsForKeyAt(uIdx);
        if (numCached < MAX_NUM_CACHED_ENTRIES_PER_USER) {
            return;
        }
        // Purge a random set of 1% of cached elements for the userId. We don't want to use a
        // deterministic system of purging because that may cause us to repeatedly remove elements
        // that are frequently added and queried more than others. Choosing a random set
        // means we will probably eventually remove less useful elements.
        // An LRU cache is too expensive for this commonly used utility class.
        final Random rand = new Random();
        final int numToPurge = Math.max(1, MAX_NUM_CACHED_ENTRIES_PER_USER / 100);
        for (int i = 0; i < numToPurge && numCached > 0; ++i) {
            final int removeIdx = rand.nextInt(numCached--);
            sCache.deleteAt(uIdx, removeIdx);
        }
    }
}
+8 −0
Original line number Diff line number Diff line
@@ -89,6 +89,14 @@ public class SparseArrayMap<K, V> {
        return null;
    }

    /**
     * Removes the data for the keyIndex and mapIndex, if there was any.
     * @hide
     */
    public void deleteAt(int keyIndex, int mapIndex) {
        mData.valueAt(keyIndex).removeAt(mapIndex);
    }

    /**
     * Get the value associated with the int-K pair.
     */
+39 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.content.pm;

import android.platform.test.annotations.Presubmit;

import junit.framework.TestCase;

@Presubmit
public class UserPackageTest extends TestCase {
    public void testCacheLimit() {
        UserPackage.setValidUserIds(new int[]{0});
        for (int i = 0; i < UserPackage.MAX_NUM_CACHED_ENTRIES_PER_USER; ++i) {
            UserPackage.of(0, "app" + i);
            assertEquals(i + 1, UserPackage.numEntriesForUser(0));
        }

        for (int i = 0; i < UserPackage.MAX_NUM_CACHED_ENTRIES_PER_USER; ++i) {
            UserPackage.of(0, "appOverLimit" + i);
            final int numCached = UserPackage.numEntriesForUser(0);
            assertTrue(numCached >= 1);
            assertTrue(numCached <= UserPackage.MAX_NUM_CACHED_ENTRIES_PER_USER);
        }
    }
}