Reland "EGL BlobCache: Support multifile cache and flexible size limit"

This reverts commit 82a9353d96954edd78d5e2f342a669dd8ddce5d1 and
incorporates a change to only use the fixed upper cache limit instead
of reading one from GraphicsEnvironment.

Test: Multiple apps and ANGLE traces
Test: atest CtsSdkSandboxInprocessTests
Test: /data/nativetest64/EGL_test/EGL_test
Bug: b/246966894
Change-Id: Iae44e06377de48fe2101bf547b02d3aaf37443d9
diff --git a/opengl/libs/Android.bp b/opengl/libs/Android.bp
index 62cf255..750338b 100644
--- a/opengl/libs/Android.bp
+++ b/opengl/libs/Android.bp
@@ -160,6 +160,7 @@
     srcs: [
         "EGL/egl_tls.cpp",
         "EGL/egl_cache.cpp",
+        "EGL/egl_cache_multifile.cpp",
         "EGL/egl_display.cpp",
         "EGL/egl_object.cpp",
         "EGL/egl_layers.cpp",
diff --git a/opengl/libs/EGL/FileBlobCache.cpp b/opengl/libs/EGL/FileBlobCache.cpp
index 751f3be..3f7ae7e 100644
--- a/opengl/libs/EGL/FileBlobCache.cpp
+++ b/opengl/libs/EGL/FileBlobCache.cpp
@@ -185,4 +185,10 @@
     }
 }
 
+size_t FileBlobCache::getSize() {
+    if (mFilename.length() > 0) {
+        return getFlattenedSize() + cacheFileHeaderSize;
+    }
+    return 0;
+}
 }
diff --git a/opengl/libs/EGL/FileBlobCache.h b/opengl/libs/EGL/FileBlobCache.h
index 393703f..8220723 100644
--- a/opengl/libs/EGL/FileBlobCache.h
+++ b/opengl/libs/EGL/FileBlobCache.h
@@ -33,6 +33,9 @@
     // disk.
     void writeToFile();
 
+    // Return the total size of the cache
+    size_t getSize();
+
 private:
     // mFilename is the name of the file for storing cache contents.
     std::string mFilename;
diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp
index 8348d6c..6225c26 100644
--- a/opengl/libs/EGL/egl_cache.cpp
+++ b/opengl/libs/EGL/egl_cache.cpp
@@ -16,6 +16,8 @@
 
 #include "egl_cache.h"
 
+#include <android-base/properties.h>
+#include <inttypes.h>
 #include <log/log.h>
 #include <private/EGL/cache.h>
 #include <unistd.h>
@@ -23,16 +25,23 @@
 #include <thread>
 
 #include "../egl_impl.h"
+#include "egl_cache_multifile.h"
 #include "egl_display.h"
 
-// Cache size limits.
+// Monolithic cache size limits.
 static const size_t maxKeySize = 12 * 1024;
 static const size_t maxValueSize = 64 * 1024;
 static const size_t maxTotalSize = 32 * 1024 * 1024;
 
-// The time in seconds to wait before saving newly inserted cache entries.
+// The time in seconds to wait before saving newly inserted monolithic cache entries.
 static const unsigned int deferredSaveDelay = 4;
 
+// Multifile cache size limit
+constexpr size_t kMultifileCacheByteLimit = 64 * 1024 * 1024;
+
+// Delay before cleaning up multifile cache entries
+static const unsigned int deferredMultifileCleanupDelaySeconds = 1;
+
 namespace android {
 
 #define BC_EXT_STR "EGL_ANDROID_blob_cache"
@@ -58,7 +67,8 @@
 //
 // egl_cache_t definition
 //
-egl_cache_t::egl_cache_t() : mInitialized(false) {}
+egl_cache_t::egl_cache_t()
+      : mInitialized(false), mMultifileMode(true), mCacheByteLimit(maxTotalSize) {}
 
 egl_cache_t::~egl_cache_t() {}
 
@@ -101,6 +111,18 @@
         }
     }
 
+    mMultifileMode = true;
+
+    // Allow forcing monolithic cache for debug purposes
+    if (base::GetProperty("debug.egl.blobcache.multifilemode", "") == "false") {
+        ALOGD("Forcing monolithic cache due to debug.egl.blobcache.multifilemode == \"false\"");
+        mMultifileMode = false;
+    }
+
+    if (mMultifileMode) {
+        mCacheByteLimit = kMultifileCacheByteLimit;
+    }
+
     mInitialized = true;
 }
 
@@ -110,6 +132,11 @@
         mBlobCache->writeToFile();
     }
     mBlobCache = nullptr;
+    if (mMultifileMode) {
+        checkMultifileCacheSize(mCacheByteLimit);
+    }
+    mMultifileMode = false;
+    mInitialized = false;
 }
 
 void egl_cache_t::setBlob(const void* key, EGLsizeiANDROID keySize, const void* value,
@@ -122,20 +149,37 @@
     }
 
     if (mInitialized) {
-        BlobCache* bc = getBlobCacheLocked();
-        bc->set(key, keySize, value, valueSize);
+        if (mMultifileMode) {
+            setBlobMultifile(key, keySize, value, valueSize, mFilename);
 
-        if (!mSavePending) {
-            mSavePending = true;
-            std::thread deferredSaveThread([this]() {
-                sleep(deferredSaveDelay);
-                std::lock_guard<std::mutex> lock(mMutex);
-                if (mInitialized && mBlobCache) {
-                    mBlobCache->writeToFile();
-                }
-                mSavePending = false;
-            });
-            deferredSaveThread.detach();
+            if (!mMultifileCleanupPending) {
+                mMultifileCleanupPending = true;
+                // Kick off a thread to cull cache files below limit
+                std::thread deferredMultifileCleanupThread([this]() {
+                    sleep(deferredMultifileCleanupDelaySeconds);
+                    std::lock_guard<std::mutex> lock(mMutex);
+                    // Check the size of cache and remove entries to stay under limit
+                    checkMultifileCacheSize(mCacheByteLimit);
+                    mMultifileCleanupPending = false;
+                });
+                deferredMultifileCleanupThread.detach();
+            }
+        } else {
+            BlobCache* bc = getBlobCacheLocked();
+            bc->set(key, keySize, value, valueSize);
+
+            if (!mSavePending) {
+                mSavePending = true;
+                std::thread deferredSaveThread([this]() {
+                    sleep(deferredSaveDelay);
+                    std::lock_guard<std::mutex> lock(mMutex);
+                    if (mInitialized && mBlobCache) {
+                        mBlobCache->writeToFile();
+                    }
+                    mSavePending = false;
+                });
+                deferredSaveThread.detach();
+            }
         }
     }
 }
@@ -145,13 +189,17 @@
     std::lock_guard<std::mutex> lock(mMutex);
 
     if (keySize < 0 || valueSize < 0) {
-        ALOGW("EGL_ANDROID_blob_cache set: negative sizes are not allowed");
+        ALOGW("EGL_ANDROID_blob_cache get: negative sizes are not allowed");
         return 0;
     }
 
     if (mInitialized) {
-        BlobCache* bc = getBlobCacheLocked();
-        return bc->get(key, keySize, value, valueSize);
+        if (mMultifileMode) {
+            return getBlobMultifile(key, keySize, value, valueSize, mFilename);
+        } else {
+            BlobCache* bc = getBlobCacheLocked();
+            return bc->get(key, keySize, value, valueSize);
+        }
     }
     return 0;
 }
@@ -161,9 +209,34 @@
     mFilename = filename;
 }
 
+void egl_cache_t::setCacheLimit(int64_t cacheByteLimit) {
+    std::lock_guard<std::mutex> lock(mMutex);
+
+    if (!mMultifileMode) {
+        // If we're not in multifile mode, ensure the cache limit is only being lowered,
+        // not increasing above the hard coded platform limit
+        if (cacheByteLimit > maxTotalSize) {
+            return;
+        }
+    }
+
+    mCacheByteLimit = cacheByteLimit;
+}
+
+size_t egl_cache_t::getCacheSize() {
+    std::lock_guard<std::mutex> lock(mMutex);
+    if (mMultifileMode) {
+        return getMultifileCacheSize();
+    }
+    if (mBlobCache) {
+        return mBlobCache->getSize();
+    }
+    return 0;
+}
+
 BlobCache* egl_cache_t::getBlobCacheLocked() {
     if (mBlobCache == nullptr) {
-        mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, maxTotalSize, mFilename));
+        mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, mCacheByteLimit, mFilename));
     }
     return mBlobCache.get();
 }
diff --git a/opengl/libs/EGL/egl_cache.h b/opengl/libs/EGL/egl_cache.h
index d10a615..2dcd803 100644
--- a/opengl/libs/EGL/egl_cache.h
+++ b/opengl/libs/EGL/egl_cache.h
@@ -64,6 +64,12 @@
     // cache contents from one program invocation to another.
     void setCacheFilename(const char* filename);
 
+    // Allow the fixed cache limit to be overridden
+    void setCacheLimit(int64_t cacheByteLimit);
+
+    // Return the byte total for cache file(s)
+    size_t getCacheSize();
+
 private:
     // Creation and (the lack of) destruction is handled internally.
     egl_cache_t();
@@ -112,6 +118,16 @@
 
     // sCache is the singleton egl_cache_t object.
     static egl_cache_t sCache;
+
+    // Whether to use multiple files to store cache entries
+    bool mMultifileMode;
+
+    // Cache limit
+    int64_t mCacheByteLimit;
+
+    // Whether we've kicked off a side thread that will check the multifile
+    // cache size and remove entries if needed.
+    bool mMultifileCleanupPending;
 };
 
 }; // namespace android
diff --git a/opengl/libs/EGL/egl_cache_multifile.cpp b/opengl/libs/EGL/egl_cache_multifile.cpp
new file mode 100644
index 0000000..48e557f
--- /dev/null
+++ b/opengl/libs/EGL/egl_cache_multifile.cpp
@@ -0,0 +1,343 @@
+/*
+ ** Copyright 2022, 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.
+ */
+
+// #define LOG_NDEBUG 0
+
+#include "egl_cache_multifile.h"
+
+#include <android-base/properties.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <log/log.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <utime.h>
+
+#include <algorithm>
+#include <chrono>
+#include <fstream>
+#include <limits>
+#include <locale>
+#include <map>
+#include <sstream>
+#include <unordered_map>
+
+#include <utils/JenkinsHash.h>
+
+static std::string multifileDirName = "";
+
+using namespace std::literals;
+
+namespace {
+
+// Create a directory for tracking multiple files
+void setupMultifile(const std::string& baseDir) {
+    // If we've already set up the multifile dir in this base directory, we're done
+    if (!multifileDirName.empty() && multifileDirName.find(baseDir) != std::string::npos) {
+        return;
+    }
+
+    // Otherwise, create it
+    multifileDirName = baseDir + ".multifile";
+    if (mkdir(multifileDirName.c_str(), 0755) != 0 && (errno != EEXIST)) {
+        ALOGW("Unable to create directory (%s), errno (%i)", multifileDirName.c_str(), errno);
+    }
+}
+
+// Create a filename that is based on the hash of the key
+std::string getCacheEntryFilename(const void* key, EGLsizeiANDROID keySize,
+                                  const std::string& baseDir) {
+    // Hash the key into a string
+    std::stringstream keyName;
+    keyName << android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
+
+    // Build a filename using dir and hash
+    return baseDir + "/" + keyName.str();
+}
+
+// Determine file age based on stat modification time
+// Newer files have a higher age (time since epoch)
+time_t getFileAge(const std::string& filePath) {
+    struct stat st;
+    if (stat(filePath.c_str(), &st) == 0) {
+        ALOGD("getFileAge returning %" PRId64 " for file age", static_cast<uint64_t>(st.st_mtime));
+        return st.st_mtime;
+    } else {
+        ALOGW("Failed to stat %s", filePath.c_str());
+        return 0;
+    }
+}
+
+size_t getFileSize(const std::string& filePath) {
+    struct stat st;
+    if (stat(filePath.c_str(), &st) != 0) {
+        ALOGE("Unable to stat %s", filePath.c_str());
+        return 0;
+    }
+    return st.st_size;
+}
+
+// Walk through directory entries and track age and size
+// Then iterate through the entries, oldest first, and remove them until under the limit.
+// This will need to be updated if we move to a multilevel cache dir.
+bool applyLRU(size_t cacheLimit) {
+    // Build a multimap of files indexed by age.
+    // They will be automatically sorted smallest (oldest) to largest (newest)
+    std::multimap<time_t, std::string> agesToFiles;
+
+    // Map files to sizes
+    std::unordered_map<std::string, size_t> filesToSizes;
+
+    size_t totalCacheSize = 0;
+
+    DIR* dir;
+    struct dirent* entry;
+    if ((dir = opendir(multifileDirName.c_str())) != nullptr) {
+        while ((entry = readdir(dir)) != nullptr) {
+            if (entry->d_name == "."s || entry->d_name == ".."s) {
+                continue;
+            }
+
+            // Look up each file age
+            std::string fullPath = multifileDirName + "/" + entry->d_name;
+            time_t fileAge = getFileAge(fullPath);
+
+            // Track the files, sorted by age
+            agesToFiles.insert(std::make_pair(fileAge, fullPath));
+
+            // Also track the size so we know how much room we have freed
+            size_t fileSize = getFileSize(fullPath);
+            filesToSizes[fullPath] = fileSize;
+            totalCacheSize += fileSize;
+        }
+        closedir(dir);
+    } else {
+        ALOGE("Unable to open filename: %s", multifileDirName.c_str());
+        return false;
+    }
+
+    if (totalCacheSize <= cacheLimit) {
+        // If LRU was called on a sufficiently small cache, no need to remove anything
+        return true;
+    }
+
+    // Walk through the map of files until we're under the cache size
+    for (const auto& cacheEntryIter : agesToFiles) {
+        time_t entryAge = cacheEntryIter.first;
+        const std::string entryPath = cacheEntryIter.second;
+
+        ALOGD("Removing %s with age %ld", entryPath.c_str(), entryAge);
+        if (std::remove(entryPath.c_str()) != 0) {
+            ALOGE("Error removing %s: %s", entryPath.c_str(), std::strerror(errno));
+            return false;
+        }
+
+        totalCacheSize -= filesToSizes[entryPath];
+        if (totalCacheSize <= cacheLimit) {
+            // Success
+            ALOGV("Reduced cache to %zu", totalCacheSize);
+            return true;
+        } else {
+            ALOGD("Cache size is still too large (%zu), removing more files", totalCacheSize);
+        }
+    }
+
+    // Should never reach this return
+    return false;
+}
+
+} // namespace
+
+namespace android {
+
+void setBlobMultifile(const void* key, EGLsizeiANDROID keySize, const void* value,
+                      EGLsizeiANDROID valueSize, const std::string& baseDir) {
+    if (baseDir.empty()) {
+        return;
+    }
+
+    setupMultifile(baseDir);
+    std::string filename = getCacheEntryFilename(key, keySize, multifileDirName);
+
+    ALOGD("Attempting to open filename for set: %s", filename.c_str());
+    std::ofstream outfile(filename, std::ofstream::binary);
+    if (outfile.fail()) {
+        ALOGW("Unable to open filename: %s", filename.c_str());
+        return;
+    }
+
+    // First write the key
+    outfile.write(static_cast<const char*>(key), keySize);
+    if (outfile.bad()) {
+        ALOGW("Unable to write key to filename: %s", filename.c_str());
+        outfile.close();
+        return;
+    }
+    ALOGD("Wrote %i bytes to out file for key", static_cast<int>(outfile.tellp()));
+
+    // Then write the value
+    outfile.write(static_cast<const char*>(value), valueSize);
+    if (outfile.bad()) {
+        ALOGW("Unable to write value to filename: %s", filename.c_str());
+        outfile.close();
+        return;
+    }
+    ALOGD("Wrote %i bytes to out file for full entry", static_cast<int>(outfile.tellp()));
+
+    outfile.close();
+}
+
+EGLsizeiANDROID getBlobMultifile(const void* key, EGLsizeiANDROID keySize, void* value,
+                                 EGLsizeiANDROID valueSize, const std::string& baseDir) {
+    if (baseDir.empty()) {
+        return 0;
+    }
+
+    setupMultifile(baseDir);
+    std::string filename = getCacheEntryFilename(key, keySize, multifileDirName);
+
+    // Open the hashed filename path
+    ALOGD("Attempting to open filename for get: %s", filename.c_str());
+    int fd = open(filename.c_str(), O_RDONLY);
+
+    // File doesn't exist, this is a MISS, return zero bytes read
+    if (fd == -1) {
+        ALOGD("Cache MISS - failed to open filename: %s, error: %s", filename.c_str(),
+              std::strerror(errno));
+        return 0;
+    }
+
+    ALOGD("Cache HIT - opened filename: %s", filename.c_str());
+
+    // Get the size of the file
+    size_t entrySize = getFileSize(filename);
+    if (keySize > entrySize) {
+        ALOGW("keySize (%lu) is larger than entrySize (%zu). This is a hash collision or modified "
+              "file",
+              keySize, entrySize);
+        close(fd);
+        return 0;
+    }
+
+    // Memory map the file
+    uint8_t* cacheEntry =
+            reinterpret_cast<uint8_t*>(mmap(nullptr, entrySize, PROT_READ, MAP_PRIVATE, fd, 0));
+    if (cacheEntry == MAP_FAILED) {
+        ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
+        close(fd);
+        return 0;
+    }
+
+    // Compare the incoming key with our stored version (the beginning of the entry)
+    int compare = memcmp(cacheEntry, key, keySize);
+    if (compare != 0) {
+        ALOGW("Cached key and new key do not match! This is a hash collision or modified file");
+        munmap(cacheEntry, entrySize);
+        close(fd);
+        return 0;
+    }
+
+    // Keys matched, so remaining cache is value size
+    size_t cachedValueSize = entrySize - keySize;
+
+    // Return actual value size if valueSize is not large enough
+    if (cachedValueSize > valueSize) {
+        ALOGD("Skipping file read, not enough room provided (valueSize): %lu, "
+              "returning required space as %zu",
+              valueSize, cachedValueSize);
+        munmap(cacheEntry, entrySize);
+        close(fd);
+        return cachedValueSize;
+    }
+
+    // Remaining entry following the key is the value
+    uint8_t* cachedValue = cacheEntry + keySize;
+    memcpy(value, cachedValue, cachedValueSize);
+    munmap(cacheEntry, entrySize);
+    close(fd);
+
+    ALOGD("Read %zu bytes from %s", cachedValueSize, filename.c_str());
+    return cachedValueSize;
+}
+
+// Walk through the files in our flat directory, checking the size of each one.
+// Return the total size of normal files in the directory.
+// This will need to be updated if we move to a multilevel cache dir.
+size_t getMultifileCacheSize() {
+    if (multifileDirName.empty()) {
+        return 0;
+    }
+
+    DIR* dir;
+    struct dirent* entry;
+    size_t size = 0;
+
+    ALOGD("Using %s as the multifile cache dir ", multifileDirName.c_str());
+
+    if ((dir = opendir(multifileDirName.c_str())) != nullptr) {
+        while ((entry = readdir(dir)) != nullptr) {
+            if (entry->d_name == "."s || entry->d_name == ".."s) {
+                continue;
+            }
+
+            // Add up the size of all files in the dir
+            std::string fullPath = multifileDirName + "/" + entry->d_name;
+            size += getFileSize(fullPath);
+        }
+        closedir(dir);
+    } else {
+        ALOGW("Unable to open filename: %s", multifileDirName.c_str());
+        return 0;
+    }
+
+    return size;
+}
+
+// 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
+constexpr uint32_t kCacheLimitDivisor = 2;
+
+// Calculate the cache size and remove old entries until under the limit
+void checkMultifileCacheSize(size_t cacheByteLimit) {
+    // Start with the value provided by egl_cache
+    size_t limit = cacheByteLimit;
+
+    // Check for a debug value
+    int debugCacheSize = base::GetIntProperty("debug.egl.blobcache.bytelimit", -1);
+    if (debugCacheSize >= 0) {
+        ALOGV("Overriding cache limit %zu with %i from debug.egl.blobcache.bytelimit", limit,
+              debugCacheSize);
+        limit = debugCacheSize;
+    }
+
+    // Tally up the initial amount of cache in use
+    size_t size = getMultifileCacheSize();
+    ALOGD("Multifile cache dir size: %zu", size);
+
+    // If size is larger than the threshold, remove files using LRU
+    if (size > limit) {
+        ALOGV("Multifile cache size is larger than %zu, removing old entries", cacheByteLimit);
+        if (!applyLRU(limit / kCacheLimitDivisor)) {
+            ALOGE("Error when clearing multifile shader cache");
+            return;
+        }
+    }
+    ALOGD("Multifile cache size after reduction: %zu", getMultifileCacheSize());
+}
+
+}; // namespace android
\ No newline at end of file
diff --git a/opengl/libs/EGL/egl_cache_multifile.h b/opengl/libs/EGL/egl_cache_multifile.h
new file mode 100644
index 0000000..ee5fe81
--- /dev/null
+++ b/opengl/libs/EGL/egl_cache_multifile.h
@@ -0,0 +1,36 @@
+/*
+ ** Copyright 2022, 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.
+ */
+
+#ifndef ANDROID_EGL_CACHE_MULTIFILE_H
+#define ANDROID_EGL_CACHE_MULTIFILE_H
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <string>
+
+namespace android {
+
+void setBlobMultifile(const void* key, EGLsizeiANDROID keySize, const void* value,
+                      EGLsizeiANDROID valueSize, const std::string& baseDir);
+EGLsizeiANDROID getBlobMultifile(const void* key, EGLsizeiANDROID keySize, void* value,
+                                 EGLsizeiANDROID valueSize, const std::string& baseDir);
+size_t getMultifileCacheSize();
+void checkMultifileCacheSize(size_t cacheByteLimit);
+
+}; // namespace android
+
+#endif // ANDROID_EGL_CACHE_MULTIFILE_H