EGL: Refactor multifile blobcache
After more testing, it became apparent that we need to
avoid accessing the filesystem to have decent speed.
This CL does that by:
* Moving the multifile cache to a class
* Tracking all entries in a bookkeeping system
* Remove use of STL for read/write
* Keep recent entries in a hot cache
* Deferring file writes to a worker thread
* Allowing devices to opt in via config
For more data and details on the design philosophy:
go/improving-multifile-blobcache-speed
Also added a new sequence of tests that bypass EGL
and directly invoke the mulfifile cache.
Test: pubg_mobile_launch ANGLE trace
Test: /data/nativetest64/EGL_test/EGL_test
Test: /data/nativetest64/libEGL_test/libEGL_test
Bug: b/266725576
Change-Id: Ia19147522a6f68f05fbe47c1545e4c9d69e513c6
diff --git a/opengl/libs/Android.bp b/opengl/libs/Android.bp
index 750338b..49e1cba 100644
--- a/opengl/libs/Android.bp
+++ b/opengl/libs/Android.bp
@@ -144,6 +144,7 @@
srcs: [
"EGL/BlobCache.cpp",
"EGL/FileBlobCache.cpp",
+ "EGL/MultifileBlobCache.cpp",
],
export_include_dirs: ["EGL"],
}
@@ -160,7 +161,6 @@
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",
@@ -205,6 +205,11 @@
srcs: [
"EGL/BlobCache.cpp",
"EGL/BlobCache_test.cpp",
+ "EGL/MultifileBlobCache.cpp",
+ "EGL/MultifileBlobCache_test.cpp",
+ ],
+ shared_libs: [
+ "libutils",
],
}
diff --git a/opengl/libs/EGL/MultifileBlobCache.cpp b/opengl/libs/EGL/MultifileBlobCache.cpp
new file mode 100644
index 0000000..48b184b
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache.cpp
@@ -0,0 +1,668 @@
+/*
+ ** 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 "MultifileBlobCache.h"
+
+#include <android-base/properties.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <log/log.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <algorithm>
+#include <chrono>
+#include <limits>
+#include <locale>
+
+#include <utils/JenkinsHash.h>
+
+using namespace std::literals;
+
+namespace {
+
+// Open the file and determine the size of the value it contains
+size_t getValueSizeFromFile(int fd, const std::string& entryPath) {
+ // Read the beginning of the file to get header
+ android::MultifileHeader header;
+ size_t result = read(fd, static_cast<void*>(&header), sizeof(android::MultifileHeader));
+ if (result != sizeof(android::MultifileHeader)) {
+ ALOGE("Error reading MultifileHeader from cache entry (%s): %s", entryPath.c_str(),
+ std::strerror(errno));
+ return 0;
+ }
+
+ return header.valueSize;
+}
+
+// Helper function to close entries or free them
+void freeHotCacheEntry(android::MultifileHotCache& entry) {
+ if (entry.entryFd != -1) {
+ // If we have an fd, then this entry was added to hot cache via INIT or GET
+ // We need to unmap and close the entry
+ munmap(entry.entryBuffer, entry.entrySize);
+ close(entry.entryFd);
+ } else {
+ // Otherwise, this was added to hot cache during SET, so it was never mapped
+ // and fd was only on the deferred thread.
+ delete[] entry.entryBuffer;
+ }
+}
+
+} // namespace
+
+namespace android {
+
+MultifileBlobCache::MultifileBlobCache(size_t maxTotalSize, size_t maxHotCacheSize,
+ const std::string& baseDir)
+ : mInitialized(false),
+ mMaxTotalSize(maxTotalSize),
+ mTotalCacheSize(0),
+ mHotCacheLimit(maxHotCacheSize),
+ mHotCacheSize(0),
+ mWorkerThreadIdle(true) {
+ if (baseDir.empty()) {
+ return;
+ }
+
+ // Establish the name of our multifile directory
+ mMultifileDirName = baseDir + ".multifile";
+
+ // Set a limit for max key and value, ensuring at least one entry can always fit in hot cache
+ mMaxKeySize = mHotCacheLimit / 4;
+ mMaxValueSize = mHotCacheLimit / 2;
+
+ // Initialize our cache with the contents of the directory
+ mTotalCacheSize = 0;
+
+ // See if the dir exists, and initialize using its contents
+ struct stat st;
+ if (stat(mMultifileDirName.c_str(), &st) == 0) {
+ // Read all the files and gather details, then preload their contents
+ DIR* dir;
+ struct dirent* entry;
+ if ((dir = opendir(mMultifileDirName.c_str())) != nullptr) {
+ while ((entry = readdir(dir)) != nullptr) {
+ if (entry->d_name == "."s || entry->d_name == ".."s) {
+ continue;
+ }
+
+ std::string entryName = entry->d_name;
+ std::string fullPath = mMultifileDirName + "/" + entryName;
+
+ // The filename is the same as the entryHash
+ uint32_t entryHash = static_cast<uint32_t>(strtoul(entry->d_name, nullptr, 10));
+
+ // Look up the details of the file
+ struct stat st;
+ if (stat(fullPath.c_str(), &st) != 0) {
+ ALOGE("Failed to stat %s", fullPath.c_str());
+ return;
+ }
+
+ // Open the file so we can read its header
+ int fd = open(fullPath.c_str(), O_RDONLY);
+ if (fd == -1) {
+ ALOGE("Cache error - failed to open fullPath: %s, error: %s", fullPath.c_str(),
+ std::strerror(errno));
+ return;
+ }
+
+ // Look up the details we track about each file
+ size_t valueSize = getValueSizeFromFile(fd, fullPath);
+ size_t fileSize = st.st_size;
+ time_t accessTime = st.st_atime;
+
+ // If the cache entry is damaged or no good, remove it
+ // TODO: Perform any other checks
+ if (valueSize <= 0 || fileSize <= 0 || accessTime <= 0) {
+ if (remove(fullPath.c_str()) != 0) {
+ ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno));
+ }
+ continue;
+ }
+
+ // Track details for rapid lookup later
+ trackEntry(entryHash, valueSize, fileSize, accessTime);
+
+ // Track the total size
+ increaseTotalCacheSize(fileSize);
+
+ // Preload the entry for fast retrieval
+ if ((mHotCacheSize + fileSize) < mHotCacheLimit) {
+ // Memory map the file
+ uint8_t* mappedEntry = reinterpret_cast<uint8_t*>(
+ mmap(nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0));
+ if (mappedEntry == MAP_FAILED) {
+ ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
+ }
+
+ ALOGV("INIT: Populating hot cache with fd = %i, cacheEntry = %p for "
+ "entryHash %u",
+ fd, mappedEntry, entryHash);
+
+ // Track the details of the preload so they can be retrieved later
+ if (!addToHotCache(entryHash, fd, mappedEntry, fileSize)) {
+ ALOGE("INIT Failed to add %u to hot cache", entryHash);
+ munmap(mappedEntry, fileSize);
+ close(fd);
+ return;
+ }
+ } else {
+ close(fd);
+ }
+ }
+ closedir(dir);
+ } else {
+ ALOGE("Unable to open filename: %s", mMultifileDirName.c_str());
+ }
+ } else {
+ // If the multifile directory does not exist, create it and start from scratch
+ if (mkdir(mMultifileDirName.c_str(), 0755) != 0 && (errno != EEXIST)) {
+ ALOGE("Unable to create directory (%s), errno (%i)", mMultifileDirName.c_str(), errno);
+ }
+ }
+
+ mTaskThread = std::thread(&MultifileBlobCache::processTasks, this);
+
+ mInitialized = true;
+}
+
+MultifileBlobCache::~MultifileBlobCache() {
+ // Inform the worker thread we're done
+ ALOGV("DESCTRUCTOR: Shutting down worker thread");
+ DeferredTask task(TaskCommand::Exit);
+ queueTask(std::move(task));
+
+ // Wait for it to complete
+ ALOGV("DESCTRUCTOR: Waiting for worker thread to complete");
+ waitForWorkComplete();
+ mTaskThread.join();
+}
+
+// Set will add the entry to hot cache and start a deferred process to write it to disk
+void MultifileBlobCache::set(const void* key, EGLsizeiANDROID keySize, const void* value,
+ EGLsizeiANDROID valueSize) {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Ensure key and value are under their limits
+ if (keySize > mMaxKeySize || valueSize > mMaxValueSize) {
+ ALOGV("SET: keySize (%lu vs %zu) or valueSize (%lu vs %zu) too large", keySize, mMaxKeySize,
+ valueSize, mMaxValueSize);
+ return;
+ }
+
+ // Generate a hash of the key and use it to track this entry
+ uint32_t entryHash = android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
+
+ 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) {
+ ALOGV("SET: Cache is full, calling trimCache to clear space");
+ trimCache(mMaxTotalSize);
+ }
+
+ ALOGV("SET: Add %u to cache", entryHash);
+
+ uint8_t* buffer = new uint8_t[fileSize];
+
+ // Write the key and value after the header
+ android::MultifileHeader header = {keySize, valueSize};
+ memcpy(static_cast<void*>(buffer), static_cast<const void*>(&header),
+ sizeof(android::MultifileHeader));
+ memcpy(static_cast<void*>(buffer + sizeof(MultifileHeader)), static_cast<const void*>(key),
+ keySize);
+ memcpy(static_cast<void*>(buffer + sizeof(MultifileHeader) + keySize),
+ static_cast<const void*>(value), valueSize);
+
+ std::string fullPath = mMultifileDirName + "/" + std::to_string(entryHash);
+
+ // Track the size and access time for quick recall
+ trackEntry(entryHash, valueSize, fileSize, time(0));
+
+ // Update the overall cache size
+ increaseTotalCacheSize(fileSize);
+
+ // Keep the entry in hot cache for quick retrieval
+ ALOGV("SET: Adding %u to hot cache.", entryHash);
+
+ // Sending -1 as the fd indicates we don't have an fd for this
+ if (!addToHotCache(entryHash, -1, buffer, fileSize)) {
+ ALOGE("GET: Failed to add %u to hot cache", entryHash);
+ return;
+ }
+
+ // Track that we're creating a pending write for this entry
+ // Include the buffer to handle the case when multiple writes are pending for an entry
+ mDeferredWrites.insert(std::make_pair(entryHash, buffer));
+
+ // Create deferred task to write to storage
+ ALOGV("SET: Adding task to queue.");
+ DeferredTask task(TaskCommand::WriteToDisk);
+ task.initWriteToDisk(fullPath, buffer, fileSize);
+ queueTask(std::move(task));
+}
+
+// Get will check the hot cache, then load it from disk if needed
+EGLsizeiANDROID MultifileBlobCache::get(const void* key, EGLsizeiANDROID keySize, void* value,
+ EGLsizeiANDROID valueSize) {
+ if (!mInitialized) {
+ return 0;
+ }
+
+ // Ensure key and value are under their limits
+ if (keySize > mMaxKeySize || valueSize > mMaxValueSize) {
+ ALOGV("GET: keySize (%lu vs %zu) or valueSize (%lu vs %zu) too large", keySize, mMaxKeySize,
+ valueSize, mMaxValueSize);
+ return 0;
+ }
+
+ // Generate a hash of the key and use it to track this entry
+ uint32_t entryHash = android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
+
+ // See if we have this file
+ if (!contains(entryHash)) {
+ ALOGV("GET: Cache MISS - cache does not contain entry: %u", entryHash);
+ return 0;
+ }
+
+ // Look up the data for this entry
+ MultifileEntryStats entryStats = getEntryStats(entryHash);
+
+ size_t cachedValueSize = entryStats.valueSize;
+ if (cachedValueSize > valueSize) {
+ ALOGV("GET: Cache MISS - valueSize not large enough (%lu) for entry %u, returning required"
+ "size (%zu)",
+ valueSize, entryHash, cachedValueSize);
+ return cachedValueSize;
+ }
+
+ // We have the file and have enough room to write it out, return the entry
+ ALOGV("GET: Cache HIT - cache contains entry: %u", entryHash);
+
+ // Look up the size of the file
+ size_t fileSize = entryStats.fileSize;
+ if (keySize > fileSize) {
+ ALOGW("keySize (%lu) is larger than entrySize (%zu). This is a hash collision or modified "
+ "file",
+ keySize, fileSize);
+ return 0;
+ }
+
+ std::string fullPath = mMultifileDirName + "/" + std::to_string(entryHash);
+
+ // Open the hashed filename path
+ uint8_t* cacheEntry = 0;
+
+ // Check hot cache
+ if (mHotCache.find(entryHash) != mHotCache.end()) {
+ ALOGV("GET: HotCache HIT for entry %u", entryHash);
+ cacheEntry = mHotCache[entryHash].entryBuffer;
+ } else {
+ ALOGV("GET: HotCache MISS for entry: %u", entryHash);
+
+ if (mDeferredWrites.find(entryHash) != mDeferredWrites.end()) {
+ // Wait for writes to complete if there is an outstanding write for this entry
+ ALOGV("GET: Waiting for write to complete for %u", entryHash);
+ waitForWorkComplete();
+ }
+
+ // Open the entry file
+ int fd = open(fullPath.c_str(), O_RDONLY);
+ if (fd == -1) {
+ ALOGE("Cache error - failed to open fullPath: %s, error: %s", fullPath.c_str(),
+ std::strerror(errno));
+ return 0;
+ }
+
+ // Memory map the file
+ cacheEntry =
+ reinterpret_cast<uint8_t*>(mmap(nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0));
+ if (cacheEntry == MAP_FAILED) {
+ ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
+ close(fd);
+ return 0;
+ }
+
+ ALOGV("GET: Adding %u to hot cache", entryHash);
+ if (!addToHotCache(entryHash, fd, cacheEntry, fileSize)) {
+ ALOGE("GET: Failed to add %u to hot cache", entryHash);
+ return 0;
+ }
+
+ cacheEntry = mHotCache[entryHash].entryBuffer;
+ }
+
+ // Ensure the header matches
+ MultifileHeader* header = reinterpret_cast<MultifileHeader*>(cacheEntry);
+ if (header->keySize != keySize || header->valueSize != valueSize) {
+ ALOGW("Mismatch on keySize(%ld vs. cached %ld) or valueSize(%ld vs. cached %ld) compared "
+ "to cache header values for fullPath: %s",
+ keySize, header->keySize, valueSize, header->valueSize, fullPath.c_str());
+ removeFromHotCache(entryHash);
+ return 0;
+ }
+
+ // Compare the incoming key with our stored version (the beginning of the entry)
+ uint8_t* cachedKey = cacheEntry + sizeof(MultifileHeader);
+ int compare = memcmp(cachedKey, key, keySize);
+ if (compare != 0) {
+ ALOGW("Cached key and new key do not match! This is a hash collision or modified file");
+ removeFromHotCache(entryHash);
+ return 0;
+ }
+
+ // Remaining entry following the key is the value
+ uint8_t* cachedValue = cacheEntry + (keySize + sizeof(MultifileHeader));
+ memcpy(value, cachedValue, cachedValueSize);
+
+ return cachedValueSize;
+}
+
+void MultifileBlobCache::finish() {
+ // Wait for all deferred writes to complete
+ ALOGV("FINISH: Waiting for work to complete.");
+ waitForWorkComplete();
+
+ // Close all entries in the hot cache
+ for (auto hotCacheIter = mHotCache.begin(); hotCacheIter != mHotCache.end();) {
+ uint32_t entryHash = hotCacheIter->first;
+ MultifileHotCache entry = hotCacheIter->second;
+
+ ALOGV("FINISH: Closing hot cache entry for %u", entryHash);
+ freeHotCacheEntry(entry);
+
+ mHotCache.erase(hotCacheIter++);
+ }
+}
+
+void MultifileBlobCache::trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
+ time_t accessTime) {
+ mEntries.insert(entryHash);
+ mEntryStats[entryHash] = {valueSize, fileSize, accessTime};
+}
+
+bool MultifileBlobCache::contains(uint32_t hashEntry) const {
+ return mEntries.find(hashEntry) != mEntries.end();
+}
+
+MultifileEntryStats MultifileBlobCache::getEntryStats(uint32_t entryHash) {
+ return mEntryStats[entryHash];
+}
+
+void MultifileBlobCache::increaseTotalCacheSize(size_t fileSize) {
+ mTotalCacheSize += fileSize;
+}
+
+void MultifileBlobCache::decreaseTotalCacheSize(size_t fileSize) {
+ mTotalCacheSize -= fileSize;
+}
+
+bool MultifileBlobCache::addToHotCache(uint32_t newEntryHash, int newFd, uint8_t* newEntryBuffer,
+ size_t newEntrySize) {
+ ALOGV("HOTCACHE(ADD): Adding %u to hot cache", newEntryHash);
+
+ // Clear space if we need to
+ if ((mHotCacheSize + newEntrySize) > mHotCacheLimit) {
+ ALOGV("HOTCACHE(ADD): mHotCacheSize (%zu) + newEntrySize (%zu) is to big for "
+ "mHotCacheLimit "
+ "(%zu), freeing up space for %u",
+ mHotCacheSize, newEntrySize, mHotCacheLimit, newEntryHash);
+
+ // Wait for all the files to complete writing so our hot cache is accurate
+ waitForWorkComplete();
+
+ // Free up old entries until under the limit
+ for (auto hotCacheIter = mHotCache.begin(); hotCacheIter != mHotCache.end();) {
+ uint32_t oldEntryHash = hotCacheIter->first;
+ MultifileHotCache oldEntry = hotCacheIter->second;
+
+ // Move our iterator before deleting the entry
+ hotCacheIter++;
+ if (!removeFromHotCache(oldEntryHash)) {
+ ALOGE("HOTCACHE(ADD): Unable to remove entry %u", oldEntryHash);
+ return false;
+ }
+
+ // Clear at least half the hot cache
+ if ((mHotCacheSize + newEntrySize) <= mHotCacheLimit / 2) {
+ ALOGV("HOTCACHE(ADD): Freed enough space for %zu", mHotCacheSize);
+ break;
+ }
+ }
+ }
+
+ // Track it
+ mHotCache[newEntryHash] = {newFd, newEntryBuffer, newEntrySize};
+ mHotCacheSize += newEntrySize;
+
+ ALOGV("HOTCACHE(ADD): New hot cache size: %zu", mHotCacheSize);
+
+ return true;
+}
+
+bool MultifileBlobCache::removeFromHotCache(uint32_t entryHash) {
+ if (mHotCache.find(entryHash) != mHotCache.end()) {
+ ALOGV("HOTCACHE(REMOVE): Removing %u from hot cache", entryHash);
+
+ // Wait for all the files to complete writing so our hot cache is accurate
+ waitForWorkComplete();
+
+ ALOGV("HOTCACHE(REMOVE): Closing hot cache entry for %u", entryHash);
+ MultifileHotCache entry = mHotCache[entryHash];
+ freeHotCacheEntry(entry);
+
+ // Delete the entry from our tracking
+ mHotCacheSize -= entry.entrySize;
+ size_t count = mHotCache.erase(entryHash);
+
+ return true;
+ }
+
+ return false;
+}
+
+bool MultifileBlobCache::applyLRU(size_t cacheLimit) {
+ // 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;
+
+ ALOGV("LRU: Removing entryHash %u", entryHash);
+
+ // Track the overall size
+ MultifileEntryStats entryStats = getEntryStats(entryHash);
+ decreaseTotalCacheSize(entryStats.fileSize);
+
+ // Remove it from hot cache if present
+ removeFromHotCache(entryHash);
+
+ // Remove it from the system
+ std::string entryPath = mMultifileDirName + "/" + std::to_string(entryHash);
+ if (remove(entryPath.c_str()) != 0) {
+ ALOGE("LRU: Error removing %s: %s", entryPath.c_str(), std::strerror(errno));
+ return false;
+ }
+
+ // Increment the iterator before clearing the entry
+ cacheEntryIter++;
+
+ // Delete the entry from our tracking
+ size_t count = mEntryStats.erase(entryHash);
+ if (count != 1) {
+ ALOGE("LRU: Failed to remove entryHash (%u) from mEntryStats", entryHash);
+ return false;
+ }
+
+ // See if it has been reduced enough
+ size_t totalCacheSize = getTotalSize();
+ if (totalCacheSize <= cacheLimit) {
+ // Success
+ ALOGV("LRU: Reduced cache to %zu", totalCacheSize);
+ return true;
+ }
+ }
+
+ ALOGV("LRU: Cache is emptry");
+ return false;
+}
+
+// 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 MultifileBlobCache::trimCache(size_t cacheByteLimit) {
+ // Start with the value provided by egl_cache
+ size_t limit = cacheByteLimit;
+
+ // Wait for all deferred writes to complete
+ waitForWorkComplete();
+
+ size_t size = getTotalSize();
+
+ // If size is larger than the threshold, remove files using LRU
+ if (size > limit) {
+ ALOGV("TRIM: Multifile cache size is larger than %zu, removing old entries",
+ cacheByteLimit);
+ if (!applyLRU(limit / kCacheLimitDivisor)) {
+ ALOGE("Error when clearing multifile shader cache");
+ return;
+ }
+ }
+}
+
+// This function performs a task. It only knows how to write files to disk,
+// but it could be expanded if needed.
+void MultifileBlobCache::processTask(DeferredTask& task) {
+ switch (task.getTaskCommand()) {
+ case TaskCommand::Exit: {
+ ALOGV("DEFERRED: Shutting down");
+ return;
+ }
+ case TaskCommand::WriteToDisk: {
+ uint32_t entryHash = task.getEntryHash();
+ std::string& fullPath = task.getFullPath();
+ uint8_t* buffer = task.getBuffer();
+ size_t bufferSize = task.getBufferSize();
+
+ // Create the file or reset it if already present, read+write for user only
+ int fd = open(fullPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
+ if (fd == -1) {
+ ALOGE("Cache error in SET - failed to open fullPath: %s, error: %s",
+ fullPath.c_str(), std::strerror(errno));
+ return;
+ }
+
+ ALOGV("DEFERRED: Opened fd %i from %s", fd, fullPath.c_str());
+
+ ssize_t result = write(fd, buffer, bufferSize);
+ if (result != bufferSize) {
+ ALOGE("Error writing fileSize to cache entry (%s): %s", fullPath.c_str(),
+ std::strerror(errno));
+ return;
+ }
+
+ ALOGV("DEFERRED: Completed write for: %s", fullPath.c_str());
+ close(fd);
+
+ // Erase the entry from mDeferredWrites
+ // Since there could be multiple outstanding writes for an entry, find the matching one
+ typedef std::multimap<uint32_t, uint8_t*>::iterator entryIter;
+ std::pair<entryIter, entryIter> iterPair = mDeferredWrites.equal_range(entryHash);
+ for (entryIter it = iterPair.first; it != iterPair.second; ++it) {
+ if (it->second == buffer) {
+ ALOGV("DEFERRED: Marking write complete for %u at %p", it->first, it->second);
+ mDeferredWrites.erase(it);
+ break;
+ }
+ }
+
+ return;
+ }
+ default: {
+ ALOGE("DEFERRED: Unhandled task type");
+ return;
+ }
+ }
+}
+
+// This function will wait until tasks arrive, then execute them
+// If the exit command is submitted, the loop will terminate
+void MultifileBlobCache::processTasksImpl(bool* exitThread) {
+ while (true) {
+ std::unique_lock<std::mutex> lock(mWorkerMutex);
+ if (mTasks.empty()) {
+ ALOGV("WORKER: No tasks available, waiting");
+ mWorkerThreadIdle = true;
+ mWorkerIdleCondition.notify_all();
+ // Only wake if notified and command queue is not empty
+ mWorkAvailableCondition.wait(lock, [this] { return !mTasks.empty(); });
+ }
+
+ ALOGV("WORKER: Task available, waking up.");
+ mWorkerThreadIdle = false;
+ DeferredTask task = std::move(mTasks.front());
+ mTasks.pop();
+
+ if (task.getTaskCommand() == TaskCommand::Exit) {
+ ALOGV("WORKER: Exiting work loop.");
+ *exitThread = true;
+ mWorkerThreadIdle = true;
+ mWorkerIdleCondition.notify_one();
+ return;
+ }
+
+ lock.unlock();
+ processTask(task);
+ }
+}
+
+// Process tasks until the exit task is submitted
+void MultifileBlobCache::processTasks() {
+ while (true) {
+ bool exitThread = false;
+ processTasksImpl(&exitThread);
+ if (exitThread) {
+ break;
+ }
+ }
+}
+
+// Add a task to the queue to be processed by the worker thread
+void MultifileBlobCache::queueTask(DeferredTask&& task) {
+ std::lock_guard<std::mutex> queueLock(mWorkerMutex);
+ mTasks.emplace(std::move(task));
+ mWorkAvailableCondition.notify_one();
+}
+
+// Wait until all tasks have been completed
+void MultifileBlobCache::waitForWorkComplete() {
+ std::unique_lock<std::mutex> lock(mWorkerMutex);
+ mWorkerIdleCondition.wait(lock, [this] { return (mTasks.empty() && mWorkerThreadIdle); });
+}
+
+}; // namespace android
\ No newline at end of file
diff --git a/opengl/libs/EGL/MultifileBlobCache.h b/opengl/libs/EGL/MultifileBlobCache.h
new file mode 100644
index 0000000..dcdfe47
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache.h
@@ -0,0 +1,164 @@
+/*
+ ** 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_MULTIFILE_BLOB_CACHE_H
+#define ANDROID_MULTIFILE_BLOB_CACHE_H
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <future>
+#include <map>
+#include <queue>
+#include <string>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace android {
+
+struct MultifileHeader {
+ EGLsizeiANDROID keySize;
+ EGLsizeiANDROID valueSize;
+};
+
+struct MultifileEntryStats {
+ EGLsizeiANDROID valueSize;
+ size_t fileSize;
+ time_t accessTime;
+};
+
+struct MultifileHotCache {
+ int entryFd;
+ uint8_t* entryBuffer;
+ size_t entrySize;
+};
+
+enum class TaskCommand {
+ Invalid = 0,
+ WriteToDisk,
+ Exit,
+};
+
+class DeferredTask {
+public:
+ DeferredTask(TaskCommand command) : mCommand(command) {}
+
+ TaskCommand getTaskCommand() { return mCommand; }
+
+ void initWriteToDisk(std::string fullPath, uint8_t* buffer, size_t bufferSize) {
+ mCommand = TaskCommand::WriteToDisk;
+ mFullPath = fullPath;
+ mBuffer = buffer;
+ mBufferSize = bufferSize;
+ }
+
+ uint32_t getEntryHash() { return mEntryHash; }
+ std::string& getFullPath() { return mFullPath; }
+ uint8_t* getBuffer() { return mBuffer; }
+ size_t getBufferSize() { return mBufferSize; };
+
+private:
+ TaskCommand mCommand;
+
+ // Parameters for WriteToDisk
+ uint32_t mEntryHash;
+ std::string mFullPath;
+ uint8_t* mBuffer;
+ size_t mBufferSize;
+};
+
+class MultifileBlobCache {
+public:
+ MultifileBlobCache(size_t maxTotalSize, size_t maxHotCacheSize, const std::string& baseDir);
+ ~MultifileBlobCache();
+
+ void set(const void* key, EGLsizeiANDROID keySize, const void* value,
+ EGLsizeiANDROID valueSize);
+ EGLsizeiANDROID get(const void* key, EGLsizeiANDROID keySize, void* value,
+ EGLsizeiANDROID valueSize);
+
+ void finish();
+
+ size_t getTotalSize() const { return mTotalCacheSize; }
+ void trimCache(size_t cacheByteLimit);
+
+private:
+ void trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
+ time_t accessTime);
+ bool contains(uint32_t entryHash) const;
+ bool removeEntry(uint32_t entryHash);
+ MultifileEntryStats getEntryStats(uint32_t entryHash);
+
+ size_t getFileSize(uint32_t entryHash);
+ size_t getValueSize(uint32_t entryHash);
+
+ void increaseTotalCacheSize(size_t fileSize);
+ void decreaseTotalCacheSize(size_t fileSize);
+
+ bool addToHotCache(uint32_t entryHash, int fd, uint8_t* entryBufer, size_t entrySize);
+ bool removeFromHotCache(uint32_t entryHash);
+
+ bool applyLRU(size_t cacheLimit);
+
+ bool mInitialized;
+ std::string mMultifileDirName;
+
+ std::unordered_set<uint32_t> mEntries;
+ std::unordered_map<uint32_t, MultifileEntryStats> mEntryStats;
+ std::unordered_map<uint32_t, MultifileHotCache> mHotCache;
+
+ size_t mMaxKeySize;
+ size_t mMaxValueSize;
+ size_t mMaxTotalSize;
+ size_t mTotalCacheSize;
+ size_t mHotCacheLimit;
+ size_t mHotCacheEntryLimit;
+ size_t mHotCacheSize;
+
+ // Below are the components used to allow a deferred write
+
+ // Track whether we have pending writes for an entry
+ std::multimap<uint32_t, uint8_t*> mDeferredWrites;
+
+ // Functions to work through tasks in the queue
+ void processTasks();
+ void processTasksImpl(bool* exitThread);
+ void processTask(DeferredTask& task);
+
+ // Used by main thread to create work for the worker thread
+ void queueTask(DeferredTask&& task);
+
+ // Used by main thread to wait for worker thread to complete all outstanding work.
+ void waitForWorkComplete();
+
+ std::thread mTaskThread;
+ std::queue<DeferredTask> mTasks;
+ std::mutex mWorkerMutex;
+
+ // This condition will block the worker thread until a task is queued
+ std::condition_variable mWorkAvailableCondition;
+
+ // This condition will block the main thread while the worker thread still has tasks
+ std::condition_variable mWorkerIdleCondition;
+
+ // This bool will track whether all tasks have been completed
+ bool mWorkerThreadIdle;
+};
+
+}; // namespace android
+
+#endif // ANDROID_MULTIFILE_BLOB_CACHE_H
diff --git a/opengl/libs/EGL/MultifileBlobCache_test.cpp b/opengl/libs/EGL/MultifileBlobCache_test.cpp
new file mode 100644
index 0000000..1a55a4f
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache_test.cpp
@@ -0,0 +1,200 @@
+/*
+ ** Copyright 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.
+ */
+
+#include "MultifileBlobCache.h"
+
+#include <android-base/test_utils.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <stdio.h>
+
+#include <memory>
+
+namespace android {
+
+template <typename T>
+using sp = std::shared_ptr<T>;
+
+constexpr size_t kMaxTotalSize = 32 * 1024;
+constexpr size_t kMaxPreloadSize = 8 * 1024;
+
+constexpr size_t kMaxKeySize = kMaxPreloadSize / 4;
+constexpr size_t kMaxValueSize = kMaxPreloadSize / 2;
+
+class MultifileBlobCacheTest : public ::testing::Test {
+protected:
+ virtual void SetUp() {
+ mTempFile.reset(new TemporaryFile());
+ mMBC.reset(new MultifileBlobCache(kMaxTotalSize, kMaxPreloadSize, &mTempFile->path[0]));
+ }
+
+ virtual void TearDown() { mMBC.reset(); }
+
+ std::unique_ptr<TemporaryFile> mTempFile;
+ std::unique_ptr<MultifileBlobCache> mMBC;
+};
+
+TEST_F(MultifileBlobCacheTest, CacheSingleValueSucceeds) {
+ unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+ mMBC->set("abcd", 4, "efgh", 4);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+ ASSERT_EQ('e', buf[0]);
+ ASSERT_EQ('f', buf[1]);
+ ASSERT_EQ('g', buf[2]);
+ ASSERT_EQ('h', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, CacheTwoValuesSucceeds) {
+ unsigned char buf[2] = {0xee, 0xee};
+ mMBC->set("ab", 2, "cd", 2);
+ mMBC->set("ef", 2, "gh", 2);
+ ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+ ASSERT_EQ('c', buf[0]);
+ ASSERT_EQ('d', buf[1]);
+ ASSERT_EQ(size_t(2), mMBC->get("ef", 2, buf, 2));
+ ASSERT_EQ('g', buf[0]);
+ ASSERT_EQ('h', buf[1]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetSetTwiceSucceeds) {
+ unsigned char buf[2] = {0xee, 0xee};
+ mMBC->set("ab", 2, "cd", 2);
+ ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+ ASSERT_EQ('c', buf[0]);
+ ASSERT_EQ('d', buf[1]);
+ // Use the same key, but different value
+ mMBC->set("ab", 2, "ef", 2);
+ ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+ ASSERT_EQ('e', buf[0]);
+ ASSERT_EQ('f', buf[1]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetOnlyWritesInsideBounds) {
+ unsigned char buf[6] = {0xee, 0xee, 0xee, 0xee, 0xee, 0xee};
+ mMBC->set("abcd", 4, "efgh", 4);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf + 1, 4));
+ ASSERT_EQ(0xee, buf[0]);
+ ASSERT_EQ('e', buf[1]);
+ ASSERT_EQ('f', buf[2]);
+ ASSERT_EQ('g', buf[3]);
+ ASSERT_EQ('h', buf[4]);
+ ASSERT_EQ(0xee, buf[5]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetOnlyWritesIfBufferIsLargeEnough) {
+ unsigned char buf[3] = {0xee, 0xee, 0xee};
+ mMBC->set("abcd", 4, "efgh", 4);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 3));
+ ASSERT_EQ(0xee, buf[0]);
+ ASSERT_EQ(0xee, buf[1]);
+ ASSERT_EQ(0xee, buf[2]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetDoesntAccessNullBuffer) {
+ mMBC->set("abcd", 4, "efgh", 4);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, nullptr, 0));
+}
+
+TEST_F(MultifileBlobCacheTest, MultipleSetsCacheLatestValue) {
+ unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+ mMBC->set("abcd", 4, "efgh", 4);
+ mMBC->set("abcd", 4, "ijkl", 4);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+ ASSERT_EQ('i', buf[0]);
+ ASSERT_EQ('j', buf[1]);
+ ASSERT_EQ('k', buf[2]);
+ ASSERT_EQ('l', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, SecondSetKeepsFirstValueIfTooLarge) {
+ unsigned char buf[kMaxValueSize + 1] = {0xee, 0xee, 0xee, 0xee};
+ mMBC->set("abcd", 4, "efgh", 4);
+ mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
+ ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+ ASSERT_EQ('e', buf[0]);
+ ASSERT_EQ('f', buf[1]);
+ ASSERT_EQ('g', buf[2]);
+ ASSERT_EQ('h', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, DoesntCacheIfKeyIsTooBig) {
+ char key[kMaxKeySize + 1];
+ unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+ for (int i = 0; i < kMaxKeySize + 1; i++) {
+ key[i] = 'a';
+ }
+ mMBC->set(key, kMaxKeySize + 1, "bbbb", 4);
+ ASSERT_EQ(size_t(0), mMBC->get(key, kMaxKeySize + 1, buf, 4));
+ ASSERT_EQ(0xee, buf[0]);
+ ASSERT_EQ(0xee, buf[1]);
+ ASSERT_EQ(0xee, buf[2]);
+ ASSERT_EQ(0xee, buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, DoesntCacheIfValueIsTooBig) {
+ char buf[kMaxValueSize + 1];
+ for (int i = 0; i < kMaxValueSize + 1; i++) {
+ buf[i] = 'b';
+ }
+ mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
+ for (int i = 0; i < kMaxValueSize + 1; i++) {
+ buf[i] = 0xee;
+ }
+ ASSERT_EQ(size_t(0), mMBC->get("abcd", 4, buf, kMaxValueSize + 1));
+ for (int i = 0; i < kMaxValueSize + 1; i++) {
+ SCOPED_TRACE(i);
+ ASSERT_EQ(0xee, buf[i]);
+ }
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMaxKeySizeSucceeds) {
+ char key[kMaxKeySize];
+ unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+ for (int i = 0; i < kMaxKeySize; i++) {
+ key[i] = 'a';
+ }
+ mMBC->set(key, kMaxKeySize, "wxyz", 4);
+ ASSERT_EQ(size_t(4), mMBC->get(key, kMaxKeySize, buf, 4));
+ ASSERT_EQ('w', buf[0]);
+ ASSERT_EQ('x', buf[1]);
+ ASSERT_EQ('y', buf[2]);
+ ASSERT_EQ('z', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMaxValueSizeSucceeds) {
+ char buf[kMaxValueSize];
+ for (int i = 0; i < kMaxValueSize; i++) {
+ buf[i] = 'b';
+ }
+ mMBC->set("abcd", 4, buf, kMaxValueSize);
+ for (int i = 0; i < kMaxValueSize; i++) {
+ buf[i] = 0xee;
+ }
+ mMBC->get("abcd", 4, buf, kMaxValueSize);
+ for (int i = 0; i < kMaxValueSize; i++) {
+ SCOPED_TRACE(i);
+ ASSERT_EQ('b', buf[i]);
+ }
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMinKeyAndValueSizeSucceeds) {
+ unsigned char buf[1] = {0xee};
+ mMBC->set("x", 1, "y", 1);
+ ASSERT_EQ(size_t(1), mMBC->get("x", 1, buf, 1));
+ ASSERT_EQ('y', buf[0]);
+}
+
+} // namespace android
diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp
index 1e8a348..b00ee33 100644
--- a/opengl/libs/EGL/egl_cache.cpp
+++ b/opengl/libs/EGL/egl_cache.cpp
@@ -14,6 +14,8 @@
** limitations under the License.
*/
+// #define LOG_NDEBUG 0
+
#include "egl_cache.h"
#include <android-base/properties.h>
@@ -25,22 +27,19 @@
#include <thread>
#include "../egl_impl.h"
-#include "egl_cache_multifile.h"
#include "egl_display.h"
// 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;
+static const size_t kMaxMonolithicKeySize = 12 * 1024;
+static const size_t kMaxMonolithicValueSize = 64 * 1024;
+static const size_t kMaxMonolithicTotalSize = 2 * 1024 * 1024;
// The time in seconds to wait before saving newly inserted monolithic cache entries.
-static const unsigned int deferredSaveDelay = 4;
+static const unsigned int kDeferredMonolithicSaveDelay = 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;
+// Multifile cache size limits
+constexpr uint32_t kMultifileHotCacheLimit = 8 * 1024 * 1024;
+constexpr uint32_t kMultifileCacheByteLimit = 32 * 1024 * 1024;
namespace android {
@@ -68,10 +67,7 @@
// egl_cache_t definition
//
egl_cache_t::egl_cache_t()
- : mInitialized(false),
- mMultifileMode(false),
- mCacheByteLimit(maxTotalSize),
- mMultifileCleanupPending(false) {}
+ : mInitialized(false), mMultifileMode(false), mCacheByteLimit(kMaxMonolithicTotalSize) {}
egl_cache_t::~egl_cache_t() {}
@@ -85,7 +81,7 @@
std::lock_guard<std::mutex> lock(mMutex);
egl_connection_t* const cnx = &gEGLImpl;
- if (cnx->dso && cnx->major >= 0 && cnx->minor >= 0) {
+ if (display && cnx->dso && cnx->major >= 0 && cnx->minor >= 0) {
const char* exts = display->disp.queryString.extensions;
size_t bcExtLen = strlen(BC_EXT_STR);
size_t extsLen = strlen(exts);
@@ -114,14 +110,36 @@
}
}
- // 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\"");
+ // Check the device config to decide whether multifile should be used
+ if (base::GetBoolProperty("ro.egl.blobcache.multifile", false)) {
+ mMultifileMode = true;
+ ALOGV("Using multifile EGL blobcache");
+ }
+
+ // Allow forcing the mode for debug purposes
+ std::string mode = base::GetProperty("debug.egl.blobcache.multifile", "");
+ if (mode == "true") {
+ ALOGV("Forcing multifile cache due to debug.egl.blobcache.multifile == %s", mode.c_str());
+ mMultifileMode = true;
+ } else if (mode == "false") {
+ ALOGV("Forcing monolithic cache due to debug.egl.blobcache.multifile == %s", mode.c_str());
mMultifileMode = false;
}
if (mMultifileMode) {
- mCacheByteLimit = kMultifileCacheByteLimit;
+ mCacheByteLimit = static_cast<size_t>(
+ base::GetUintProperty<uint32_t>("ro.egl.blobcache.multifile_limit",
+ kMultifileCacheByteLimit));
+
+ // Check for a debug value
+ int debugCacheSize = base::GetIntProperty("debug.egl.blobcache.multifile_limit", -1);
+ if (debugCacheSize >= 0) {
+ ALOGV("Overriding cache limit %zu with %i from debug.egl.blobcache.multifile_limit",
+ mCacheByteLimit, debugCacheSize);
+ mCacheByteLimit = debugCacheSize;
+ }
+
+ ALOGV("Using multifile EGL blobcache limit of %zu bytes", mCacheByteLimit);
}
mInitialized = true;
@@ -133,10 +151,10 @@
mBlobCache->writeToFile();
}
mBlobCache = nullptr;
- if (mMultifileMode) {
- checkMultifileCacheSize(mCacheByteLimit);
+ if (mMultifileBlobCache) {
+ mMultifileBlobCache->finish();
}
- mMultifileMode = false;
+ mMultifileBlobCache = nullptr;
mInitialized = false;
}
@@ -151,20 +169,8 @@
if (mInitialized) {
if (mMultifileMode) {
- setBlobMultifile(key, keySize, value, valueSize, mFilename);
-
- 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();
- }
+ MultifileBlobCache* mbc = getMultifileBlobCacheLocked();
+ mbc->set(key, keySize, value, valueSize);
} else {
BlobCache* bc = getBlobCacheLocked();
bc->set(key, keySize, value, valueSize);
@@ -172,7 +178,7 @@
if (!mSavePending) {
mSavePending = true;
std::thread deferredSaveThread([this]() {
- sleep(deferredSaveDelay);
+ sleep(kDeferredMonolithicSaveDelay);
std::lock_guard<std::mutex> lock(mMutex);
if (mInitialized && mBlobCache) {
mBlobCache->writeToFile();
@@ -196,15 +202,21 @@
if (mInitialized) {
if (mMultifileMode) {
- return getBlobMultifile(key, keySize, value, valueSize, mFilename);
+ MultifileBlobCache* mbc = getMultifileBlobCacheLocked();
+ return mbc->get(key, keySize, value, valueSize);
} else {
BlobCache* bc = getBlobCacheLocked();
return bc->get(key, keySize, value, valueSize);
}
}
+
return 0;
}
+void egl_cache_t::setCacheMode(EGLCacheMode cacheMode) {
+ mMultifileMode = (cacheMode == EGLCacheMode::Multifile);
+}
+
void egl_cache_t::setCacheFilename(const char* filename) {
std::lock_guard<std::mutex> lock(mMutex);
mFilename = filename;
@@ -216,7 +228,7 @@
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) {
+ if (cacheByteLimit > kMaxMonolithicTotalSize) {
return;
}
}
@@ -226,8 +238,8 @@
size_t egl_cache_t::getCacheSize() {
std::lock_guard<std::mutex> lock(mMutex);
- if (mMultifileMode) {
- return getMultifileCacheSize();
+ if (mMultifileBlobCache) {
+ return mMultifileBlobCache->getTotalSize();
}
if (mBlobCache) {
return mBlobCache->getSize();
@@ -237,9 +249,18 @@
BlobCache* egl_cache_t::getBlobCacheLocked() {
if (mBlobCache == nullptr) {
- mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, mCacheByteLimit, mFilename));
+ mBlobCache.reset(new FileBlobCache(kMaxMonolithicKeySize, kMaxMonolithicValueSize,
+ mCacheByteLimit, mFilename));
}
return mBlobCache.get();
}
+MultifileBlobCache* egl_cache_t::getMultifileBlobCacheLocked() {
+ if (mMultifileBlobCache == nullptr) {
+ mMultifileBlobCache.reset(
+ new MultifileBlobCache(mCacheByteLimit, kMultifileHotCacheLimit, mFilename));
+ }
+ return mMultifileBlobCache.get();
+}
+
}; // namespace android
diff --git a/opengl/libs/EGL/egl_cache.h b/opengl/libs/EGL/egl_cache.h
index 2dcd803..1399368 100644
--- a/opengl/libs/EGL/egl_cache.h
+++ b/opengl/libs/EGL/egl_cache.h
@@ -25,6 +25,7 @@
#include <string>
#include "FileBlobCache.h"
+#include "MultifileBlobCache.h"
namespace android {
@@ -32,6 +33,11 @@
class EGLAPI egl_cache_t {
public:
+ enum class EGLCacheMode {
+ Monolithic,
+ Multifile,
+ };
+
// get returns a pointer to the singleton egl_cache_t object. This
// singleton object will never be destroyed.
static egl_cache_t* get();
@@ -64,6 +70,9 @@
// cache contents from one program invocation to another.
void setCacheFilename(const char* filename);
+ // Allow setting monolithic or multifile modes
+ void setCacheMode(EGLCacheMode cacheMode);
+
// Allow the fixed cache limit to be overridden
void setCacheLimit(int64_t cacheByteLimit);
@@ -85,6 +94,9 @@
// possible.
BlobCache* getBlobCacheLocked();
+ // Get or create the multifile blobcache
+ MultifileBlobCache* getMultifileBlobCacheLocked();
+
// mInitialized indicates whether the egl_cache_t is in the initialized
// state. It is initialized to false at construction time, and gets set to
// true when initialize is called. It is set back to false when terminate
@@ -98,6 +110,9 @@
// first time it's needed.
std::unique_ptr<FileBlobCache> mBlobCache;
+ // The multifile version of blobcache allowing larger contents to be stored
+ std::unique_ptr<MultifileBlobCache> mMultifileBlobCache;
+
// mFilename is the name of the file for storing cache contents in between
// program invocations. It is initialized to an empty string at
// construction time, and can be set with the setCacheFilename method. An
@@ -123,11 +138,7 @@
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;
+ size_t mCacheByteLimit;
};
}; // namespace android
diff --git a/opengl/libs/EGL/egl_cache_multifile.cpp b/opengl/libs/EGL/egl_cache_multifile.cpp
deleted file mode 100644
index 48e557f..0000000
--- a/opengl/libs/EGL/egl_cache_multifile.cpp
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- ** 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
deleted file mode 100644
index ee5fe81..0000000
--- a/opengl/libs/EGL/egl_cache_multifile.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- ** 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