Merge "Limit the UserPackage cache size." into udc-dev am: 1d837f7f6e am: 22fbf4b747
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21611149 Change-Id: I72862e8a3123d463bf7f5f0f9832e21d885f02df Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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. */
|
||||
@GuardedBy("sCacheLock")
|
||||
private static int[] sUserIds = new int[]{UserHandle.getUserId(Process.myUid())};
|
||||
}
|
||||
/**
|
||||
* 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 = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user