diff options
-rw-r--r-- | opengl/libs/Android.bp | 7 | ||||
-rw-r--r-- | opengl/libs/EGL/MultifileBlobCache.cpp | 689 | ||||
-rw-r--r-- | opengl/libs/EGL/MultifileBlobCache.h | 167 | ||||
-rw-r--r-- | opengl/libs/EGL/MultifileBlobCache_test.cpp | 200 | ||||
-rw-r--r-- | opengl/libs/EGL/egl_cache.cpp | 105 | ||||
-rw-r--r-- | opengl/libs/EGL/egl_cache.h | 21 | ||||
-rw-r--r-- | opengl/libs/EGL/egl_cache_multifile.cpp | 343 | ||||
-rw-r--r-- | opengl/libs/EGL/egl_cache_multifile.h | 36 | ||||
-rw-r--r-- | opengl/tests/EGLTest/egl_cache_test.cpp | 56 |
9 files changed, 1178 insertions, 446 deletions
diff --git a/opengl/libs/Android.bp b/opengl/libs/Android.bp index 750338bd84..49e1cbafb4 100644 --- a/opengl/libs/Android.bp +++ b/opengl/libs/Android.bp @@ -144,6 +144,7 @@ cc_library_static { srcs: [ "EGL/BlobCache.cpp", "EGL/FileBlobCache.cpp", + "EGL/MultifileBlobCache.cpp", ], export_include_dirs: ["EGL"], } @@ -160,7 +161,6 @@ cc_library_shared { 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 @@ cc_test { 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 0000000000..99af299f8d --- /dev/null +++ b/opengl/libs/EGL/MultifileBlobCache.cpp @@ -0,0 +1,689 @@ +/* + ** 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 <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()) { + ALOGV("INIT: no baseDir provided in MultifileBlobCache constructor, returning early."); + 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; + + ALOGV("INIT: Initializing multifile blobcache with maxKeySize=%zu and maxValueSize=%zu", + mMaxKeySize, mMaxValueSize); + + // Initialize our cache with the contents of the directory + mTotalCacheSize = 0; + + // Create the worker thread + mTaskThread = std::thread(&MultifileBlobCache::processTasks, this); + + // 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)); + + ALOGV("INIT: Checking entry %u", entryHash); + + // 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); + + // If the cache entry is damaged or no good, remove it + // TODO: Perform any other checks + if (valueSize <= 0 || st.st_size <= 0 || st.st_atime <= 0) { + ALOGV("INIT: Entry %u has a problem! Removing.", entryHash); + if (remove(fullPath.c_str()) != 0) { + ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno)); + } + continue; + } + + ALOGV("INIT: Entry %u is good, tracking it now.", entryHash); + + // Note: Converting from off_t (signed) to size_t (unsigned) + size_t fileSize = static_cast<size_t>(st.st_size); + time_t accessTime = st.st_atime; + + // 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); + } + } + + mInitialized = true; +} + +MultifileBlobCache::~MultifileBlobCache() { + if (!mInitialized) { + return; + } + + // 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(); + if (mTaskThread.joinable()) { + 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(entryHash, 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() { + if (!mInitialized) { + return; + } + + // 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; + 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 0000000000..c0cc9dc2a9 --- /dev/null +++ b/opengl/libs/EGL/MultifileBlobCache.h @@ -0,0 +1,167 @@ +/* + ** 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), mEntryHash(0), mBuffer(nullptr), mBufferSize(0) {} + + TaskCommand getTaskCommand() { return mCommand; } + + void initWriteToDisk(uint32_t entryHash, std::string fullPath, uint8_t* buffer, + size_t bufferSize) { + mCommand = TaskCommand::WriteToDisk; + mEntryHash = entryHash; + mFullPath = std::move(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; } + +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); + + void trimCache(size_t cacheByteLimit); + 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 for deferred writes + + // 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 0000000000..1a55a4fcdd --- /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 1e8a34863d..b00ee33374 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; - -// Multifile cache size limit -constexpr size_t kMultifileCacheByteLimit = 64 * 1024 * 1024; +static const unsigned int kDeferredMonolithicSaveDelay = 4; -// 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 @@ static EGLsizeiANDROID getBlob(const void* key, EGLsizeiANDROID keySize, void* v // 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 @@ void egl_cache_t::initialize(egl_display_t* display) { 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 @@ void egl_cache_t::initialize(egl_display_t* display) { } } - // 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 @@ void egl_cache_t::terminate() { mBlobCache->writeToFile(); } mBlobCache = nullptr; - if (mMultifileMode) { - checkMultifileCacheSize(mCacheByteLimit); + if (mMultifileBlobCache) { + mMultifileBlobCache->finish(); } - mMultifileMode = false; + mMultifileBlobCache = nullptr; mInitialized = false; } @@ -151,20 +169,8 @@ void egl_cache_t::setBlob(const void* key, EGLsizeiANDROID keySize, const void* 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 @@ void egl_cache_t::setBlob(const void* key, EGLsizeiANDROID keySize, const void* 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 @@ EGLsizeiANDROID egl_cache_t::getBlob(const void* key, EGLsizeiANDROID keySize, v 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 @@ void egl_cache_t::setCacheLimit(int64_t cacheByteLimit) { 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 @@ void egl_cache_t::setCacheLimit(int64_t cacheByteLimit) { 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 @@ size_t egl_cache_t::getCacheSize() { 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 2dcd803324..1399368dd8 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 egl_display_t; 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 @@ public: // 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 @@ private: // 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 @@ private: // 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 @@ private: 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 48e557f190..0000000000 --- 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 ee5fe8108d..0000000000 --- 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 diff --git a/opengl/tests/EGLTest/egl_cache_test.cpp b/opengl/tests/EGLTest/egl_cache_test.cpp index 265bec492e..2b3e3a46af 100644 --- a/opengl/tests/EGLTest/egl_cache_test.cpp +++ b/opengl/tests/EGLTest/egl_cache_test.cpp @@ -24,7 +24,7 @@ #include <android-base/test_utils.h> #include "egl_cache.h" -#include "egl_cache_multifile.h" +#include "MultifileBlobCache.h" #include "egl_display.h" #include <memory> @@ -33,12 +33,16 @@ using namespace std::literals; namespace android { -class EGLCacheTest : public ::testing::Test { +class EGLCacheTest : public ::testing::TestWithParam<egl_cache_t::EGLCacheMode> { protected: virtual void SetUp() { - mCache = egl_cache_t::get(); + // Terminate to clean up any previous cache in this process + mCache->terminate(); + mTempFile.reset(new TemporaryFile()); mCache->setCacheFilename(&mTempFile->path[0]); + mCache->setCacheLimit(1024); + mCache->setCacheMode(mCacheMode); } virtual void TearDown() { @@ -49,11 +53,12 @@ protected: std::string getCachefileName(); - egl_cache_t* mCache; + egl_cache_t* mCache = egl_cache_t::get(); std::unique_ptr<TemporaryFile> mTempFile; + egl_cache_t::EGLCacheMode mCacheMode = GetParam(); }; -TEST_F(EGLCacheTest, UninitializedCacheAlwaysMisses) { +TEST_P(EGLCacheTest, UninitializedCacheAlwaysMisses) { uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->setBlob("abcd", 4, "efgh", 4); ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf, 4)); @@ -63,7 +68,7 @@ TEST_F(EGLCacheTest, UninitializedCacheAlwaysMisses) { ASSERT_EQ(0xee, buf[3]); } -TEST_F(EGLCacheTest, InitializedCacheAlwaysHits) { +TEST_P(EGLCacheTest, InitializedCacheAlwaysHits) { uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); mCache->setBlob("abcd", 4, "efgh", 4); @@ -74,7 +79,7 @@ TEST_F(EGLCacheTest, InitializedCacheAlwaysHits) { ASSERT_EQ('h', buf[3]); } -TEST_F(EGLCacheTest, TerminatedCacheAlwaysMisses) { +TEST_P(EGLCacheTest, TerminatedCacheAlwaysMisses) { uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); mCache->setBlob("abcd", 4, "efgh", 4); @@ -86,7 +91,7 @@ TEST_F(EGLCacheTest, TerminatedCacheAlwaysMisses) { ASSERT_EQ(0xee, buf[3]); } -TEST_F(EGLCacheTest, ReinitializedCacheContainsValues) { +TEST_P(EGLCacheTest, ReinitializedCacheContainsValues) { uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); mCache->setBlob("abcd", 4, "efgh", 4); @@ -101,12 +106,12 @@ TEST_F(EGLCacheTest, ReinitializedCacheContainsValues) { std::string EGLCacheTest::getCachefileName() { // Return the monolithic filename unless we find the multifile dir - std::string cachefileName = &mTempFile->path[0]; - std::string multifileDirName = cachefileName + ".multifile"; + std::string cachePath = &mTempFile->path[0]; + std::string multifileDirName = cachePath + ".multifile"; + std::string cachefileName = ""; struct stat info; if (stat(multifileDirName.c_str(), &info) == 0) { - // Ensure we only have one file to manage int realFileCount = 0; @@ -121,6 +126,9 @@ std::string EGLCacheTest::getCachefileName() { cachefileName = multifileDirName + "/" + entry->d_name; realFileCount++; } + } else { + printf("Unable to open %s, error: %s\n", + multifileDirName.c_str(), std::strerror(errno)); } if (realFileCount != 1) { @@ -128,14 +136,19 @@ std::string EGLCacheTest::getCachefileName() { // violates test assumptions cachefileName = ""; } + } else { + printf("Unable to stat %s, error: %s\n", + multifileDirName.c_str(), std::strerror(errno)); } return cachefileName; } -TEST_F(EGLCacheTest, ModifiedCacheMisses) { - // Turn this back on if multifile becomes the default - GTEST_SKIP() << "Skipping test designed for multifile, see b/263574392 and b/246966894"; +TEST_P(EGLCacheTest, ModifiedCacheMisses) { + // Skip if not in multifile mode + if (mCacheMode == egl_cache_t::EGLCacheMode::Monolithic) { + GTEST_SKIP() << "Skipping test designed for multifile"; + } uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); @@ -147,13 +160,13 @@ TEST_F(EGLCacheTest, ModifiedCacheMisses) { ASSERT_EQ('g', buf[2]); ASSERT_EQ('h', buf[3]); + // Ensure the cache file is written to disk + mCache->terminate(); + // Depending on the cache mode, the file will be in different locations std::string cachefileName = getCachefileName(); ASSERT_TRUE(cachefileName.length() > 0); - // Ensure the cache file is written to disk - mCache->terminate(); - // Stomp on the beginning of the cache file, breaking the key match const long stomp = 0xbadf00d; FILE *file = fopen(cachefileName.c_str(), "w"); @@ -164,14 +177,15 @@ TEST_F(EGLCacheTest, ModifiedCacheMisses) { // Ensure no cache hit mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); uint8_t buf2[4] = { 0xee, 0xee, 0xee, 0xee }; - ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf2, 4)); + // getBlob may return junk for required size, but should not return a cache hit + mCache->getBlob("abcd", 4, buf2, 4); ASSERT_EQ(0xee, buf2[0]); ASSERT_EQ(0xee, buf2[1]); ASSERT_EQ(0xee, buf2[2]); ASSERT_EQ(0xee, buf2[3]); } -TEST_F(EGLCacheTest, TerminatedCacheBelowCacheLimit) { +TEST_P(EGLCacheTest, TerminatedCacheBelowCacheLimit) { uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee }; mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); @@ -204,4 +218,8 @@ TEST_F(EGLCacheTest, TerminatedCacheBelowCacheLimit) { ASSERT_LE(mCache->getCacheSize(), 4); } +INSTANTIATE_TEST_CASE_P(MonolithicCacheTests, + EGLCacheTest, ::testing::Values(egl_cache_t::EGLCacheMode::Monolithic)); +INSTANTIATE_TEST_CASE_P(MultifileCacheTests, + EGLCacheTest, ::testing::Values(egl_cache_t::EGLCacheMode::Multifile)); } |