EGL Multifile BlobCache: Limit entry count

Limit the number of entries to 4096.

This is an empirical number based on app behavior. Some using many
small entries are taking a long time to load the cache.

Test: MultifileBlobCacheTest.CacheMaxEntrySucceeds
Bug: b/295051628
Bug: b/310535559
Change-Id: Ibc671cec25dd7c9bc10b9d1ee1fb837180eb7551
diff --git a/opengl/libs/EGL/MultifileBlobCache.cpp b/opengl/libs/EGL/MultifileBlobCache.cpp
index ed3c616..13a5e7b 100644
--- a/opengl/libs/EGL/MultifileBlobCache.cpp
+++ b/opengl/libs/EGL/MultifileBlobCache.cpp
@@ -62,12 +62,14 @@
 namespace android {
 
 MultifileBlobCache::MultifileBlobCache(size_t maxKeySize, size_t maxValueSize, size_t maxTotalSize,
-                                       const std::string& baseDir)
+                                       size_t maxTotalEntries, const std::string& baseDir)
       : mInitialized(false),
         mMaxKeySize(maxKeySize),
         mMaxValueSize(maxValueSize),
         mMaxTotalSize(maxTotalSize),
+        mMaxTotalEntries(maxTotalEntries),
         mTotalCacheSize(0),
+        mTotalCacheEntries(0),
         mHotCacheLimit(0),
         mHotCacheSize(0),
         mWorkerThreadIdle(true) {
@@ -270,7 +272,7 @@
     size_t fileSize = sizeof(MultifileHeader) + keySize + valueSize;
 
     // If we're going to be over the cache limit, kick off a trim to clear space
-    if (getTotalSize() + fileSize > mMaxTotalSize) {
+    if (getTotalSize() + fileSize > mMaxTotalSize || getTotalEntries() + 1 > mMaxTotalEntries) {
         ALOGV("SET: Cache is full, calling trimCache to clear space");
         trimCache();
     }
@@ -485,10 +487,12 @@
 
 void MultifileBlobCache::increaseTotalCacheSize(size_t fileSize) {
     mTotalCacheSize += fileSize;
+    mTotalCacheEntries++;
 }
 
 void MultifileBlobCache::decreaseTotalCacheSize(size_t fileSize) {
     mTotalCacheSize -= fileSize;
+    mTotalCacheEntries--;
 }
 
 bool MultifileBlobCache::addToHotCache(uint32_t newEntryHash, int newFd, uint8_t* newEntryBuffer,
@@ -557,7 +561,7 @@
     return false;
 }
 
-bool MultifileBlobCache::applyLRU(size_t cacheLimit) {
+bool MultifileBlobCache::applyLRU(size_t cacheSizeLimit, size_t cacheEntryLimit) {
     // Walk through our map of sorted last access times and remove files until under the limit
     for (auto cacheEntryIter = mEntryStats.begin(); cacheEntryIter != mEntryStats.end();) {
         uint32_t entryHash = cacheEntryIter->first;
@@ -590,9 +594,10 @@
 
         // See if it has been reduced enough
         size_t totalCacheSize = getTotalSize();
-        if (totalCacheSize <= cacheLimit) {
+        size_t totalCacheEntries = getTotalEntries();
+        if (totalCacheSize <= cacheSizeLimit && totalCacheEntries <= cacheEntryLimit) {
             // Success
-            ALOGV("LRU: Reduced cache to %zu", totalCacheSize);
+            ALOGV("LRU: Reduced cache to size %zu entries %zu", totalCacheSize, totalCacheEntries);
             return true;
         }
     }
@@ -603,6 +608,7 @@
 
 // When removing files, what fraction of the overall limit should be reached when removing files
 // A divisor of two will decrease the cache to 50%, four to 25% and so on
+// We use the same limit to manage size and entry count
 constexpr uint32_t kCacheLimitDivisor = 2;
 
 // Calculate the cache size and remove old entries until under the limit
@@ -611,8 +617,9 @@
     ALOGV("TRIM: Waiting for work to complete.");
     waitForWorkComplete();
 
-    ALOGV("TRIM: Reducing multifile cache size to %zu", mMaxTotalSize / kCacheLimitDivisor);
-    if (!applyLRU(mMaxTotalSize / kCacheLimitDivisor)) {
+    ALOGV("TRIM: Reducing multifile cache size to %zu, entries %zu",
+          mMaxTotalSize / kCacheLimitDivisor, mMaxTotalEntries / kCacheLimitDivisor);
+    if (!applyLRU(mMaxTotalSize / kCacheLimitDivisor, mMaxTotalEntries / kCacheLimitDivisor)) {
         ALOGE("Error when clearing multifile shader cache");
         return;
     }
diff --git a/opengl/libs/EGL/MultifileBlobCache.h b/opengl/libs/EGL/MultifileBlobCache.h
index 5e527dc..9a396f0 100644
--- a/opengl/libs/EGL/MultifileBlobCache.h
+++ b/opengl/libs/EGL/MultifileBlobCache.h
@@ -92,7 +92,7 @@
 class MultifileBlobCache {
 public:
     MultifileBlobCache(size_t maxKeySize, size_t maxValueSize, size_t maxTotalSize,
-                       const std::string& baseDir);
+                       size_t maxTotalEntries, const std::string& baseDir);
     ~MultifileBlobCache();
 
     void set(const void* key, EGLsizeiANDROID keySize, const void* value,
@@ -103,6 +103,7 @@
     void finish();
 
     size_t getTotalSize() const { return mTotalCacheSize; }
+    size_t getTotalEntries() const { return mTotalCacheEntries; }
 
 private:
     void trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
@@ -121,7 +122,7 @@
     bool removeFromHotCache(uint32_t entryHash);
 
     void trimCache();
-    bool applyLRU(size_t cacheLimit);
+    bool applyLRU(size_t cacheSizeLimit, size_t cacheEntryLimit);
 
     bool mInitialized;
     std::string mMultifileDirName;
@@ -133,7 +134,9 @@
     size_t mMaxKeySize;
     size_t mMaxValueSize;
     size_t mMaxTotalSize;
+    size_t mMaxTotalEntries;
     size_t mTotalCacheSize;
+    size_t mTotalCacheEntries;
     size_t mHotCacheLimit;
     size_t mHotCacheEntryLimit;
     size_t mHotCacheSize;
diff --git a/opengl/libs/EGL/MultifileBlobCache_test.cpp b/opengl/libs/EGL/MultifileBlobCache_test.cpp
index 1639be6..8e27f5a 100644
--- a/opengl/libs/EGL/MultifileBlobCache_test.cpp
+++ b/opengl/libs/EGL/MultifileBlobCache_test.cpp
@@ -31,13 +31,14 @@
 constexpr size_t kMaxKeySize = 2 * 1024;
 constexpr size_t kMaxValueSize = 6 * 1024;
 constexpr size_t kMaxTotalSize = 32 * 1024;
+constexpr size_t kMaxTotalEntries = 64;
 
 class MultifileBlobCacheTest : public ::testing::Test {
 protected:
     virtual void SetUp() {
         mTempFile.reset(new TemporaryFile());
         mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize,
-                                          &mTempFile->path[0]));
+                                          kMaxTotalEntries, &mTempFile->path[0]));
     }
 
     virtual void TearDown() { mMBC.reset(); }
@@ -211,6 +212,23 @@
     }
 }
 
+TEST_F(MultifileBlobCacheTest, CacheMaxEntrySucceeds) {
+    // Fill the cache with max entries
+    int i = 0;
+    for (i = 0; i < kMaxTotalEntries; i++) {
+        mMBC->set(std::to_string(i).c_str(), sizeof(i), std::to_string(i).c_str(), sizeof(i));
+    }
+
+    // Ensure it is full
+    ASSERT_EQ(mMBC->getTotalEntries(), kMaxTotalEntries);
+
+    // Add another entry
+    mMBC->set(std::to_string(i).c_str(), sizeof(i), std::to_string(i).c_str(), sizeof(i));
+
+    // Ensure total entries is cut in half + 1
+    ASSERT_EQ(mMBC->getTotalEntries(), kMaxTotalEntries / 2 + 1);
+}
+
 TEST_F(MultifileBlobCacheTest, CacheMinKeyAndValueSizeSucceeds) {
     unsigned char buf[1] = {0xee};
     mMBC->set("x", 1, "y", 1);
@@ -234,8 +252,7 @@
 
 TEST_F(MultifileBlobCacheTest, EnsureFileDescriptorsClosed) {
     // Populate the cache with a bunch of entries
-    size_t kLargeNumberOfEntries = 1024;
-    for (int i = 0; i < kLargeNumberOfEntries; i++) {
+    for (int i = 0; i < kMaxTotalEntries; i++) {
         // printf("Caching: %i", i);
 
         // Use the index as the key and value
@@ -247,27 +264,27 @@
     }
 
     // Ensure we don't have a bunch of open fds
-    ASSERT_LT(getFileDescriptorCount(), kLargeNumberOfEntries / 2);
+    ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
 
     // Close the cache so everything writes out
     mMBC->finish();
     mMBC.reset();
 
     // Now open it again and ensure we still don't have a bunch of open fds
-    mMBC.reset(
-            new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, &mTempFile->path[0]));
+    mMBC.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
+                                      &mTempFile->path[0]));
 
     // Check after initialization
-    ASSERT_LT(getFileDescriptorCount(), kLargeNumberOfEntries / 2);
+    ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
 
-    for (int i = 0; i < kLargeNumberOfEntries; i++) {
+    for (int i = 0; i < kMaxTotalEntries; i++) {
         int result = 0;
         ASSERT_EQ(sizeof(i), mMBC->get(&i, sizeof(i), &result, sizeof(result)));
         ASSERT_EQ(i, result);
     }
 
     // And again after we've actually used it
-    ASSERT_LT(getFileDescriptorCount(), kLargeNumberOfEntries / 2);
+    ASSERT_LT(getFileDescriptorCount(), kMaxTotalEntries / 2);
 }
 
 } // namespace android
diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp
index 1b68344..98ff1d1 100644
--- a/opengl/libs/EGL/egl_cache.cpp
+++ b/opengl/libs/EGL/egl_cache.cpp
@@ -41,6 +41,7 @@
 constexpr uint32_t kMaxMultifileKeySize = 1 * 1024 * 1024;
 constexpr uint32_t kMaxMultifileValueSize = 8 * 1024 * 1024;
 constexpr uint32_t kMaxMultifileTotalSize = 32 * 1024 * 1024;
+constexpr uint32_t kMaxMultifileTotalEntries = 4 * 1024;
 
 namespace android {
 
@@ -277,7 +278,7 @@
     if (mMultifileBlobCache == nullptr) {
         mMultifileBlobCache.reset(new MultifileBlobCache(kMaxMultifileKeySize,
                                                          kMaxMultifileValueSize, mCacheByteLimit,
-                                                         mFilename));
+                                                         kMaxMultifileTotalEntries, mFilename));
     }
     return mMultifileBlobCache.get();
 }
diff --git a/opengl/libs/EGL/fuzzer/MultifileBlobCache_fuzzer.cpp b/opengl/libs/EGL/fuzzer/MultifileBlobCache_fuzzer.cpp
index 633cc9c..2d9ee3a 100644
--- a/opengl/libs/EGL/fuzzer/MultifileBlobCache_fuzzer.cpp
+++ b/opengl/libs/EGL/fuzzer/MultifileBlobCache_fuzzer.cpp
@@ -28,6 +28,7 @@
 constexpr size_t kMaxKeySize = 2 * 1024;
 constexpr size_t kMaxValueSize = 6 * 1024;
 constexpr size_t kMaxTotalSize = 32 * 1024;
+constexpr size_t kMaxTotalEntries = 64;
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
     // To fuzz this, we're going to create a key/value pair from data
@@ -79,8 +80,8 @@
     std::unique_ptr<MultifileBlobCache> mbc;
 
     tempFile.reset(new TemporaryFile());
-    mbc.reset(
-            new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, &tempFile->path[0]));
+    mbc.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
+                                     &tempFile->path[0]));
     // With remaining data, select different paths below
     int loopCount = 1;
     uint8_t bumpCount = 0;
@@ -131,8 +132,8 @@
     // Place the maxKey/maxValue twice
     // The first will fit, the second will trigger hot cache trimming
     tempFile.reset(new TemporaryFile());
-    mbc.reset(
-            new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, &tempFile->path[0]));
+    mbc.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, kMaxTotalSize, kMaxTotalEntries,
+                                     &tempFile->path[0]));
     uint8_t* buffer = new uint8_t[kMaxValueSize];
     mbc->set(maxKey1.data(), kMaxKeySize, maxValue1.data(), kMaxValueSize);
     mbc->set(maxKey2.data(), kMaxKeySize, maxValue2.data(), kMaxValueSize);
@@ -145,7 +146,7 @@
     // overflow
     tempFile.reset(new TemporaryFile());
     mbc.reset(new MultifileBlobCache(kMaxKeySize, kMaxValueSize, 2 * (kMaxKeySize + kMaxValueSize),
-                                     &tempFile->path[0]));
+                                     kMaxTotalEntries, &tempFile->path[0]));
     mbc->set(maxKey1.data(), kMaxKeySize, maxValue1.data(), kMaxValueSize);
     mbc->set(maxKey2.data(), kMaxKeySize, maxValue2.data(), kMaxValueSize);
     mbc->get(maxKey1.data(), kMaxKeySize, buffer, kMaxValueSize);