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

Commit 46055df9 authored by Lee Shombert's avatar Lee Shombert Committed by Android (Google) Code Review
Browse files

Merge "Separate caches for separate UIDs" into main

parents 70b4265e 05a4d006
Loading
Loading
Loading
Loading
+236 −48
Original line number Diff line number Diff line
@@ -32,7 +32,10 @@ import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseBooleanArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -92,9 +95,6 @@ public class PropertyInvalidatedCache<Query, Result> {
         * caching on behalf of other processes.
         */
        public boolean shouldBypassCache(@NonNull Q query) {
            if(android.multiuser.Flags.propertyInvalidatedCacheBypassMismatchedUids()) {
                return Binder.getCallingUid() != Process.myUid();
            }
            return false;
        }
    };
@@ -392,8 +392,213 @@ public class PropertyInvalidatedCache<Query, Result> {
        }
    }

    /**
     * An array of hash maps, indexed by calling UID.  The class behaves a bit like a hash map
     * except that it uses the calling UID internally.
     */
    private class CacheMap<Query, Result> {

        // Create a new map for a UID, using the parent's configuration for max size.
        private LinkedHashMap<Query, Result> createMap() {
            return new LinkedHashMap<Query, Result>(
                2 /* start small */,
                0.75f /* default load factor */,
                true /* LRU access order */) {
                @GuardedBy("mLock")
                @Override
                protected boolean removeEldestEntry(Map.Entry eldest) {
                    final int size = size();
                    if (size > mHighWaterMark) {
                        mHighWaterMark = size;
                    }
                    if (size > mMaxEntries) {
                        mMissOverflow++;
                        return true;
                    }
                    return false;
                }
            };
        }

        // An array of maps, indexed by UID.
        private final SparseArray<LinkedHashMap<Query, Result>> mCache = new SparseArray<>();

        // If true, isolate the hash entries by calling UID.  If this is false, allow the cache
        // entries to be combined in a single hash map.
        private final boolean mIsolated;

        // Collect statistics.
        private final boolean mStatistics;

        // An array of booleans to indicate if a UID has been involved in a map access.  A value
        // exists for every UID that was ever involved during cache access. This is updated only
        // if statistics are being collected.
        private final SparseBooleanArray mUidSeen;

        // A hash map that ignores the UID.  This is used in look-aside fashion just for hit/miss
        // statistics.  This is updated only if statistics are being collected.
        private final ArraySet<Query> mShadowCache;

        // Shadow statistics.  Only hits and misses need to be recorded.  These are updated only
        // if statistics are being collected.  The "SelfHits" records hits when the UID is the
        // process uid.
        private int mShadowHits;
        private int mShadowMisses;
        private int mShadowSelfHits;

        // The process UID.
        private final int mSelfUid;

        // True in test mode.  In test mode, the cache uses Binder.getWorkSource() as the UID.
        private final boolean mTestMode;

        /**
         * Create a CacheMap.  UID isolation is enabled if the input parameter is true and if the
         * isolation feature is enabled.
         */
        CacheMap(boolean isolate, boolean testMode) {
            mIsolated = Flags.picIsolateCacheByUid() && isolate;
            mStatistics = Flags.picIsolatedCacheStatistics() && mIsolated;
            if (mStatistics) {
                mUidSeen = new SparseBooleanArray();
                mShadowCache = new ArraySet<>();
            } else {
                mUidSeen = null;
                mShadowCache = null;
            }
            mSelfUid = Process.myUid();
            mTestMode = testMode;
        }

        // Return the UID for this cache invocation.  If uid isolation is disabled, the value of 0
        // is returned, which effectively places all entries in a single hash map.
        private int callerUid() {
            if (!mIsolated) {
                return 0;
            } else if (mTestMode) {
                return Binder.getCallingWorkSourceUid();
            } else {
                return Binder.getCallingUid();
            }
        }

        /**
         * Lookup an entry in the cache.
         */
        Result get(Query query) {
            final int uid = callerUid();

            // Shadow statistics
            if (mStatistics) {
                if (mShadowCache.contains(query)) {
                    mShadowHits++;
                    if (uid == mSelfUid) {
                        mShadowSelfHits++;
                    }
                } else {
                    mShadowMisses++;
                }
            }

            var map = mCache.get(uid);
            if (map != null) {
                return map.get(query);
            } else {
                return null;
            }
        }

        /**
         * Remove an entry from the cache.
         */
        void remove(Query query) {
            final int uid = callerUid();
            if (mStatistics) {
                mShadowCache.remove(query);
            }

            var map = mCache.get(uid);
            if (map != null) {
                map.remove(query);
            }
        }

        /**
         * Record an entry in the cache.
         */
        void put(Query query, Result result) {
            final int uid = callerUid();
            if (mStatistics) {
                mShadowCache.add(query);
                mUidSeen.put(uid, true);
            }

            var map = mCache.get(uid);
            if (map == null) {
                map = createMap();
                mCache.put(uid, map);
            }
            map.put(query, result);
        }

        /**
         * Return the number of entries in the cache.
         */
        int size() {
            int total = 0;
            for (int i = 0; i < mCache.size(); i++) {
                var map = mCache.valueAt(i);
                total += map.size();
            }
            return total;
        }

        /**
         * Clear the entries in the cache.  Update the shadow statistics.
         */
        void clear() {
            if (mStatistics) {
                mShadowCache.clear();
            }

            mCache.clear();
        }

        // Dump basic statistics, if any are collected.  Do nothing if statistics are not enabled.
        void dump(PrintWriter pw) {
            if (mStatistics) {
                pw.println(formatSimple("    ShadowHits: %d, ShadowMisses: %d, ShadowSize: %d",
                                mShadowHits, mShadowMisses, mShadowCache.size()));
                pw.println(formatSimple("    ShadowUids: %d, SelfUid: %d",
                                mUidSeen.size(), mShadowSelfHits));
            }
        }

        // Dump detailed statistics
        void dumpDetailed(PrintWriter pw) {
            for (int i = 0; i < mCache.size(); i++) {
                int uid = mCache.keyAt(i);
                var map = mCache.valueAt(i);

                Set<Map.Entry<Query, Result>> cacheEntries = map.entrySet();
                if (cacheEntries.size() == 0) {
                    break;
                }

                pw.println("    Contents:");
                pw.println(formatSimple("      Uid: %d\n", uid));
                for (Map.Entry<Query, Result> entry : cacheEntries) {
                    String key = Objects.toString(entry.getKey());
                    String value = Objects.toString(entry.getValue());

                    pw.println(formatSimple("      Key: %s\n      Value: %s\n", key, value));
                }
            }
        }
    }

    @GuardedBy("mLock")
    private final LinkedHashMap<Query, Result> mCache;
    private final CacheMap<Query, Result> mCache;

    /**
     * The nonce handler for this cache.
@@ -895,7 +1100,8 @@ public class PropertyInvalidatedCache<Query, Result> {
     * is allowed to be null in the record constructor to facility reuse of Args instances.
     * @hide
     */
    public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries) {
    public static record Args(@NonNull String mModule, @Nullable String mApi,
            int mMaxEntries, boolean mIsolateUids, boolean mTestMode) {

        // Validation: the module must be one of the known module strings and the maxEntries must
        // be positive.
@@ -909,15 +1115,28 @@ public class PropertyInvalidatedCache<Query, Result> {
        // which is not legal, but there is no reasonable default.  Clients must call the api
        // method to set the field properly.
        public Args(@NonNull String module) {
            this(module, /* api */ null, /* maxEntries */ 32);
            this(module,
                    null,       // api
                    32,         // maxEntries
                    true,       // isolateUids
                    false       // testMode
                 );
        }

        public Args api(@NonNull String api) {
            return new Args(mModule, api, mMaxEntries);
            return new Args(mModule, api, mMaxEntries, mIsolateUids, mTestMode);
        }

        public Args maxEntries(int val) {
            return new Args(mModule, mApi, val);
            return new Args(mModule, mApi, val, mIsolateUids, mTestMode);
        }

        public Args isolateUids(boolean val) {
            return new Args(mModule, mApi, mMaxEntries, val, mTestMode);
        }

        public Args testMode(boolean val) {
            return new Args(mModule, mApi, mMaxEntries, mIsolateUids, val);
        }
    }

@@ -936,7 +1155,7 @@ public class PropertyInvalidatedCache<Query, Result> {
        mCacheName = cacheName;
        mNonce = getNonceHandler(mPropertyName);
        mMaxEntries = args.mMaxEntries;
        mCache = createMap();
        mCache = new CacheMap<>(args.mIsolateUids, args.mTestMode);
        mComputer = (computer != null) ? computer : new DefaultComputer<>(this);
        registerCache();
    }
@@ -1006,28 +1225,6 @@ public class PropertyInvalidatedCache<Query, Result> {
        this(new Args(module).maxEntries(maxEntries).api(api), cacheName, computer);
    }

    // Create a map.  This should be called only from the constructor.
    private LinkedHashMap<Query, Result> createMap() {
        return new LinkedHashMap<Query, Result>(
            2 /* start small */,
            0.75f /* default load factor */,
            true /* LRU access order */) {
                @GuardedBy("mLock")
                @Override
                protected boolean removeEldestEntry(Map.Entry eldest) {
                    final int size = size();
                    if (size > mHighWaterMark) {
                        mHighWaterMark = size;
                    }
                    if (size > mMaxEntries) {
                        mMissOverflow++;
                        return true;
                    }
                    return false;
                }
        };
    }

    /**
     * Register the map in the global list.  If the cache is disabled globally, disable it
     * now.  This method is only ever called from the constructor, which means no other thread has
@@ -1778,8 +1975,8 @@ public class PropertyInvalidatedCache<Query, Result> {
            pw.println(formatSimple("  Cache Name: %s", cacheName()));
            pw.println(formatSimple("    Property: %s", mPropertyName));
            pw.println(formatSimple(
                "    Hits: %d, Misses: %d, Skips: %d, Clears: %d",
                mHits, mMisses, getSkipsLocked(), mClears));
                "    Hits: %d, Misses: %d, Skips: %d, Clears: %d, Uids: %d",
                mHits, mMisses, getSkipsLocked(), mClears, mCache.size()));

            // Print all the skip reasons.
            pw.format("    Skip-%s: %d", sNonceName[0], mSkips[0]);
@@ -1794,25 +1991,16 @@ public class PropertyInvalidatedCache<Query, Result> {
            pw.println(formatSimple(
                "    Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d",
                mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow));
            mCache.dump(pw);
            pw.println(formatSimple("    Enabled: %s", mDisabled ? "false" : "true"));

            // No specific cache was requested.  This is the default, and no details
            // should be dumped.
            if (!detailed) {
                return;
            }
            Set<Map.Entry<Query, Result>> cacheEntries = mCache.entrySet();
            if (cacheEntries.size() == 0) {
                return;
            // Dump the contents of the cache.
            if (detailed) {
                mCache.dumpDetailed(pw);
            }

            pw.println("    Contents:");
            for (Map.Entry<Query, Result> entry : cacheEntries) {
                String key = Objects.toString(entry.getKey());
                String value = Objects.toString(entry.getValue());

                pw.println(formatSimple("      Key: %s\n      Value: %s\n", key, value));
            }
            // Separator between caches.
            pw.println("");
        }
    }

+17 −0
Original line number Diff line number Diff line
@@ -18,3 +18,20 @@ flag {
     description: "Enforce PropertyInvalidatedCache.setTestMode() protocol"
     bug: "360897450"
}

flag {
     namespace: "system_performance"
     name: "pic_isolate_cache_by_uid"
     is_fixed_read_only: true
     description: "Ensure that different UIDs use different caches"
     bug: "373752556"
}

flag {
     namespace: "system_performance"
     name: "pic_isolated_cache_statistics"
     is_fixed_read_only: true
     description: "Collects statistics for cache UID isolation strategies"
     bug: "373752556"
}
+76 −3
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.app;

import static android.app.Flags.FLAG_PIC_ISOLATE_CACHE_BY_UID;
import static android.app.PropertyInvalidatedCache.NONCE_UNSET;
import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH;
import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
@@ -30,8 +31,9 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.app.PropertyInvalidatedCache.Args;
import android.annotation.SuppressLint;
import android.app.PropertyInvalidatedCache.Args;
import android.os.Binder;
import com.android.internal.os.ApplicationSharedMemory;

import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -58,6 +60,7 @@ import org.junit.Test;
 */
@SmallTest
public class PropertyInvalidatedCacheTests {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule =
            DeviceFlagsValueProvider.createCheckFlagsRule();

@@ -455,8 +458,9 @@ public class PropertyInvalidatedCacheTests {
    // Test the Args-style constructor.
    @Test
    public void testArgsConstructor() {
        // Create a cache with a maximum of four entries.
        TestCache cache = new TestCache(new Args(MODULE_TEST).api("init1").maxEntries(4),
        // Create a cache with a maximum of four entries and non-isolated UIDs.
        TestCache cache = new TestCache(new Args(MODULE_TEST)
                .maxEntries(4).isolateUids(false).api("init1"),
                new TestQuery());

        cache.invalidateCache();
@@ -570,4 +574,73 @@ public class PropertyInvalidatedCacheTests {
            // Expected exception.
        }
    }

    // Verify that a cache created with isolatedUids(true) separates out the results.
    @RequiresFlagsEnabled(FLAG_PIC_ISOLATE_CACHE_BY_UID)
    @Test
    public void testIsolatedUids() {
        TestCache cache = new TestCache(new Args(MODULE_TEST)
                .maxEntries(4).isolateUids(true).api("testIsolatedUids").testMode(true),
                new TestQuery());
        cache.invalidateCache();
        final int uid1 = 1;
        final int uid2 = 2;

        long token = Binder.setCallingWorkSourceUid(uid1);
        try {
            // Populate the cache for user 1
            assertEquals("foo5", cache.query(5));
            assertEquals(1, cache.getRecomputeCount());
            assertEquals("foo5", cache.query(5));
            assertEquals(1, cache.getRecomputeCount());
            assertEquals("foo6", cache.query(6));
            assertEquals(2, cache.getRecomputeCount());

            // Populate the cache for user 2.  User 1 values are not reused.
            Binder.setCallingWorkSourceUid(uid2);
            assertEquals("foo5", cache.query(5));
            assertEquals(3, cache.getRecomputeCount());
            assertEquals("foo5", cache.query(5));
            assertEquals(3, cache.getRecomputeCount());

            // Verify that the cache for user 1 is still populated.
            Binder.setCallingWorkSourceUid(uid1);
            assertEquals("foo5", cache.query(5));
            assertEquals(3, cache.getRecomputeCount());

        } finally {
            Binder.restoreCallingWorkSource(token);
        }

        // Repeat the test with a non-isolated cache.
        cache = new TestCache(new Args(MODULE_TEST)
                .maxEntries(4).isolateUids(false).api("testIsolatedUids2").testMode(true),
                new TestQuery());
        cache.invalidateCache();
        token = Binder.setCallingWorkSourceUid(uid1);
        try {
            // Populate the cache for user 1
            assertEquals("foo5", cache.query(5));
            assertEquals(1, cache.getRecomputeCount());
            assertEquals("foo5", cache.query(5));
            assertEquals(1, cache.getRecomputeCount());
            assertEquals("foo6", cache.query(6));
            assertEquals(2, cache.getRecomputeCount());

            // Populate the cache for user 2.  User 1 values are reused.
            Binder.setCallingWorkSourceUid(uid2);
            assertEquals("foo5", cache.query(5));
            assertEquals(2, cache.getRecomputeCount());
            assertEquals("foo5", cache.query(5));
            assertEquals(2, cache.getRecomputeCount());

            // Verify that the cache for user 1 is still populated.
            Binder.setCallingWorkSourceUid(uid1);
            assertEquals("foo5", cache.query(5));
            assertEquals(2, cache.getRecomputeCount());

        } finally {
            Binder.restoreCallingWorkSource(token);
        }
    }
}