Loading core/java/android/content/pm/UserPackage.java +55 −9 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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); } } } core/java/android/util/SparseArrayMap.java +8 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading core/tests/coretests/src/android/content/pm/UserPackageTest.java 0 → 100644 +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); } } } Loading
core/java/android/content/pm/UserPackage.java +55 −9 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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; Loading @@ -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; Loading Loading @@ -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); Loading Loading @@ -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); Loading @@ -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); } } }
core/java/android/util/SparseArrayMap.java +8 −0 Original line number Diff line number Diff line Loading @@ -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. */ Loading
core/tests/coretests/src/android/content/pm/UserPackageTest.java 0 → 100644 +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); } } }