736 lines
27 KiB
C++
736 lines
27 KiB
C++
|
|
/*
|
||
|
|
** 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;
|
||
|
|
|
||
|
|
constexpr uint32_t kMultifileMagic = 'MFB$';
|
||
|
|
constexpr uint32_t kCrcPlaceholder = 0;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
// 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 maxKeySize, size_t maxValueSize, size_t maxTotalSize,
|
||
|
|
const std::string& baseDir)
|
||
|
|
: mInitialized(false),
|
||
|
|
mMaxKeySize(maxKeySize),
|
||
|
|
mMaxValueSize(maxValueSize),
|
||
|
|
mMaxTotalSize(maxTotalSize),
|
||
|
|
mTotalCacheSize(0),
|
||
|
|
mHotCacheLimit(0),
|
||
|
|
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 the hotcache limit to be large enough to contain one max entry
|
||
|
|
// This ensure the hot cache is always large enough for single entry
|
||
|
|
mHotCacheLimit = mMaxKeySize + mMaxValueSize + sizeof(MultifileHeader);
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the cache entry is damaged or no good, remove it
|
||
|
|
if (st.st_size <= 0 || st.st_atime <= 0) {
|
||
|
|
ALOGE("INIT: Entry %u has invalid stats! Removing.", entryHash);
|
||
|
|
if (remove(fullPath.c_str()) != 0) {
|
||
|
|
ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno));
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Read the beginning of the file to get header
|
||
|
|
MultifileHeader header;
|
||
|
|
size_t result = read(fd, static_cast<void*>(&header), sizeof(MultifileHeader));
|
||
|
|
if (result != sizeof(MultifileHeader)) {
|
||
|
|
ALOGE("Error reading MultifileHeader from cache entry (%s): %s",
|
||
|
|
fullPath.c_str(), std::strerror(errno));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify header magic
|
||
|
|
if (header.magic != kMultifileMagic) {
|
||
|
|
ALOGE("INIT: Entry %u has bad magic (%u)! Removing.", entryHash, header.magic);
|
||
|
|
if (remove(fullPath.c_str()) != 0) {
|
||
|
|
ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno));
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Note: Converting from off_t (signed) to size_t (unsigned)
|
||
|
|
size_t fileSize = static_cast<size_t>(st.st_size);
|
||
|
|
|
||
|
|
// 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));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ensure we have a good CRC
|
||
|
|
if (header.crc !=
|
||
|
|
crc32c(mappedEntry + sizeof(MultifileHeader),
|
||
|
|
fileSize - sizeof(MultifileHeader))) {
|
||
|
|
ALOGE("INIT: Entry %u failed CRC check! Removing.", entryHash);
|
||
|
|
if (remove(fullPath.c_str()) != 0) {
|
||
|
|
ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno));
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the cache entry is damaged or no good, remove it
|
||
|
|
if (header.keySize <= 0 || header.valueSize <= 0) {
|
||
|
|
ALOGE("INIT: Entry %u has a bad header keySize (%lu) or valueSize (%lu), "
|
||
|
|
"removing.",
|
||
|
|
entryHash, header.keySize, header.valueSize);
|
||
|
|
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);
|
||
|
|
|
||
|
|
// Track details for rapid lookup later
|
||
|
|
trackEntry(entryHash, header.valueSize, fileSize, st.st_atime);
|
||
|
|
|
||
|
|
// Track the total size
|
||
|
|
increaseTotalCacheSize(fileSize);
|
||
|
|
|
||
|
|
// Preload the entry for fast retrieval
|
||
|
|
if ((mHotCacheSize + fileSize) < mHotCacheLimit) {
|
||
|
|
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 {
|
||
|
|
// If we're not keeping it in hot cache, unmap it now
|
||
|
|
munmap(mappedEntry, fileSize);
|
||
|
|
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) {
|
||
|
|
ALOGW("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();
|
||
|
|
}
|
||
|
|
|
||
|
|
ALOGV("SET: Add %u to cache", entryHash);
|
||
|
|
|
||
|
|
uint8_t* buffer = new uint8_t[fileSize];
|
||
|
|
|
||
|
|
// Write placeholders for magic and CRC until deferred thread completes the write
|
||
|
|
android::MultifileHeader header = {kMultifileMagic, kCrcPlaceholder, keySize, valueSize};
|
||
|
|
memcpy(static_cast<void*>(buffer), static_cast<const void*>(&header),
|
||
|
|
sizeof(android::MultifileHeader));
|
||
|
|
// Write the key and value after the header
|
||
|
|
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("SET: Failed to add %u to hot cache", entryHash);
|
||
|
|
delete[] buffer;
|
||
|
|
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
|
||
|
|
{
|
||
|
|
// Synchronize access to deferred write status
|
||
|
|
std::lock_guard<std::mutex> lock(mDeferredWriteStatusMutex);
|
||
|
|
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) {
|
||
|
|
ALOGW("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);
|
||
|
|
|
||
|
|
// Wait for writes to complete if there is an outstanding write for this entry
|
||
|
|
bool wait = false;
|
||
|
|
{
|
||
|
|
// Synchronize access to deferred write status
|
||
|
|
std::lock_guard<std::mutex> lock(mDeferredWriteStatusMutex);
|
||
|
|
wait = mDeferredWrites.find(entryHash) != mDeferredWrites.end();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (wait) {
|
||
|
|
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
|
||
|
|
ALOGV("HOTCACHE(ADD): Waiting for work to complete for %u", newEntryHash);
|
||
|
|
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
|
||
|
|
ALOGV("HOTCACHE(REMOVE): Waiting for work to complete for %u", entryHash);
|
||
|
|
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 empty");
|
||
|
|
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() {
|
||
|
|
// Wait for all deferred writes to complete
|
||
|
|
ALOGV("TRIM: Waiting for work to complete.");
|
||
|
|
waitForWorkComplete();
|
||
|
|
|
||
|
|
ALOGV("TRIM: Reducing multifile cache size to %zu", mMaxTotalSize / kCacheLimitDivisor);
|
||
|
|
if (!applyLRU(mMaxTotalSize / 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());
|
||
|
|
|
||
|
|
// Add CRC check to the header (always do this last!)
|
||
|
|
MultifileHeader* header = reinterpret_cast<MultifileHeader*>(buffer);
|
||
|
|
header->crc =
|
||
|
|
crc32c(buffer + sizeof(MultifileHeader), bufferSize - sizeof(MultifileHeader));
|
||
|
|
|
||
|
|
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
|
||
|
|
{
|
||
|
|
// Synchronize access to deferred write status
|
||
|
|
std::lock_guard<std::mutex> lock(mDeferredWriteStatusMutex);
|
||
|
|
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
|