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

Commit 05a4d006 authored by Lee Shombert's avatar Lee Shombert
Browse files

Separate caches for separate UIDs

A cache instance maintains one hashmap for each UID making the binder
call.  The UID is give by Binder.getCallingUid().  This isolates the
UIDs but will likely reduce cache efficacy.  Per-UID statistics are
collected.

An individual cache may choose to keep all UID information in a single
hashmap.  This is controlled by a configuration flag to the cache
constructor.  This is also the legacy behavior.

Isolation by UID is flag-guarded.  Statistics collection is guarded by
a separate flag, as statistics turn out to be expensive.

This CL reverts an initial update to the default bypass() method,
since the check for cross-uid cache access is now part of the cache
infrastructure.

Flag: android.app.pic_isolate_cache_by_uid
Flag: android.app.pic_isolated_cache_statistics
Bug: 373752556
Test: atest
 * FrameworksCoreTests:PropertyInvalidatedCacheTests
 * FrameworksCoreTests:IpcDataCacheTest
 * CtsOsTestCases:IpcDataCacheTest
 * ServiceBluetoothTests
Change-Id: I6f3a9df55c820518739814c958a019b04897b42d
parent 912193ba
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);
        }
    }
}