blob: 3d2021dfc3aae3f4404ace1256169dbaf05852ea [file] [log] [blame]
/*
* Copyright (C) 2025 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 "PipelineCache.h"
#include <SkData.h>
#include <SkRefCnt.h>
#include <SkString.h>
#include <android-base/unique_fd.h>
#include <errno.h>
#include <fcntl.h>
#include <log/log.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utils/Trace.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
namespace {
using key_size_t = uint32_t;
void releaseProc(const void* ptr, void* context) {
const auto size = reinterpret_cast<size_t>(context);
release(Memory{.data = const_cast<void*>(ptr), .size = size});
}
struct PipelineCacheData {
key_size_t keySize;
sk_sp<SkData> key;
sk_sp<SkData> data;
struct LoadResult {
enum Outcome {
Success,
CouldNotAcquire,
ZeroSize,
NoKeySize,
NoKey,
};
Outcome outcome;
AcquireResult acquireResult;
};
static LoadResult load(const std::string& path, PipelineCacheData& cache) {
Memory memory;
auto result = acquire(path, memory);
if (result.outcome != AcquireResult::Success) {
return LoadResult{.outcome = LoadResult::CouldNotAcquire, .acquireResult = result};
}
if (memory.size == 0) {
release(memory);
return LoadResult{.outcome = LoadResult::ZeroSize};
}
if (memory.size < sizeof(key_size_t)) {
release(memory);
return LoadResult{.outcome = LoadResult::NoKeySize};
}
memcpy(&cache.keySize, memory.data, sizeof(key_size_t));
if (memory.size < (sizeof(key_size_t) + cache.keySize)) {
release(memory);
return LoadResult{.outcome = LoadResult::NoKey};
}
cache.key = SkData::MakeWithCopy(static_cast<uint8_t*>(memory.data) + sizeof(key_size_t),
cache.keySize);
auto dataSize = memory.size - sizeof(key_size_t) - cache.keySize;
cache.data = SkData::MakeWithProc(
static_cast<uint8_t*>(memory.data) + sizeof(key_size_t) + cache.keySize, dataSize,
releaseProc, reinterpret_cast<void*>(memory.size));
return LoadResult{.outcome = LoadResult::Success};
}
};
void logLoadWarning(PipelineCacheData::LoadResult result, const char* message) {
if (result.outcome == PipelineCacheData::LoadResult::CouldNotAcquire) {
// Missing file is a normal case (cache was never written - there is no failure)
if ((result.acquireResult.outcome == AcquireResult::OpenFailed) &&
(result.acquireResult.errnoValue == ENOENT)) {
return;
}
ALOGW("%s; acquire outcome=%d, errnoValue=%d", message, result.acquireResult.outcome,
result.acquireResult.errnoValue);
return;
}
ALOGW("%s; outcome=%d", message, result.outcome);
}
} // namespace
AcquireResult acquire(const std::string& path, Memory& memory) {
android::base::unique_fd fd(open(path.c_str(), O_RDONLY));
if (fd.get() == -1) {
return AcquireResult{.outcome = AcquireResult::OpenFailed, .errnoValue = errno};
}
struct stat stat;
auto result = fstat(fd.get(), &stat);
if (result == -1) {
return AcquireResult{.outcome = AcquireResult::FstatFailed, .errnoValue = errno};
}
if (stat.st_size == 0) {
return AcquireResult{.outcome = AcquireResult::CannotMmapZeroSizeFile};
}
auto data = mmap(nullptr, stat.st_size, PROT_READ, MAP_SHARED, fd.get(), 0);
if (data == reinterpret_cast<void*>(-1)) {
return AcquireResult{.outcome = AcquireResult::MmapFailed, .errnoValue = errno};
}
memory.data = data;
memory.size = stat.st_size;
return AcquireResult{.outcome = AcquireResult::Success};
}
ReleaseResult release(Memory memory) {
auto result = munmap(memory.data, memory.size);
if (result == -1) {
return ReleaseResult{.outcome = ReleaseResult::MunmapFailed, .errnoValue = errno};
}
return ReleaseResult{.outcome = ReleaseResult::Success};
}
PipelineCacheStore::PipelineCacheStore(useconds_t writeThrottleInterval)
: mWriteThrottleInterval(writeThrottleInterval)
, mMutex()
, mConditionVariable()
, mStoreRequest()
, mExit(false)
, mThread(&PipelineCacheStore::runThread, this) {}
PipelineCacheStore::~PipelineCacheStore() {
mExit = true;
mConditionVariable.notify_one();
mThread.join();
}
void PipelineCacheStore::runThread() {
while (true) {
{
std::unique_lock<std::mutex> lock(mMutex);
mConditionVariable.wait(lock, [this] { return mExit || mStoreRequest.has_value(); });
}
if (mExit) {
return;
}
{
ATRACE_NAME("PipelineCacheStore::runThread (delay to throttle cache requests)");
// Frequent sequential cache writes will be written at most once per interval to reduce
// I/O activity.
usleep(mWriteThrottleInterval);
}
StoreRequest storeRequest;
{
std::lock_guard<std::mutex> lock(mMutex);
storeRequest = std::move(mStoreRequest.value());
mStoreRequest.reset();
}
{
ATRACE_NAME("PipelineCacheStore::runThread (write to file cache)");
android::base::unique_fd fd(creat(storeRequest.path.c_str(), S_IRUSR | S_IWUSR));
if (fd.get() == -1) {
ALOGE("PipelineCacheStore::runThread: could not open pipeline cache file (errno = "
"%d)",
errno);
continue;
}
auto written = write(fd.get(), storeRequest.data.data(), storeRequest.data.size());
if (written == -1) {
ALOGE("PipelineCacheStore::runThread: could not write to pipeline cache file "
"(errno = %d)",
errno);
continue;
}
ATRACE_INT64("HWUI pipeline cache size", written);
}
}
}
void PipelineCacheStore::store(std::string path, std::vector<uint8_t> data) {
ATRACE_NAME("PipelineCacheStore::store (lock mutex and notify condition)");
{
std::lock_guard<std::mutex> lock(mMutex);
mStoreRequest = StoreRequest{
.path = std::move(path),
.data = std::move(data),
};
}
mConditionVariable.notify_one();
}
PipelineCache::PipelineCache(std::string storePath, useconds_t writeThrottleInterval)
: mStorePath(std::move(storePath))
, mPipelineCacheStore(writeThrottleInterval)
, mKey(SkData::MakeEmpty())
, mData(SkData::MakeEmpty()) {
PipelineCacheData cache;
auto result = PipelineCacheData::load(mStorePath, cache);
if (result.outcome != PipelineCacheData::LoadResult::Success) {
logLoadWarning(
result,
"PipelineCache::PipelineCache: could not load cache key (cache will be dropped)");
return;
}
mKey = cache.key;
mData = cache.data;
}
sk_sp<SkData> PipelineCache::tryLoad(const SkData& key) {
ATRACE_NAME("PipelineCache::tryLoad");
if (!key.equals(mKey.get())) {
return nullptr;
}
if (mData == nullptr) {
ALOGW("PipelineCache::tryLoad: multiple data loads, incurring a load cost on the critical "
"path");
PipelineCacheData cache;
auto result = PipelineCacheData::load(mStorePath, cache);
if (result.outcome != PipelineCacheData::LoadResult::Success) {
logLoadWarning(
result,
"PipelineCache::tryLoad: could not load cache key (cache will be dropped)");
return nullptr;
}
return std::move(cache.data);
}
return std::move(mData);
}
bool PipelineCache::canStore(const SkString& description) const {
return description == SkString("VkPipelineCache");
}
void PipelineCache::store(const SkData& key, const SkData& data) {
ATRACE_NAME("PipelineCache::store");
mKey = SkData::MakeWithCopy(key.data(), key.size());
auto dataSize = sizeof(key_size_t) + key.size() + data.size();
std::vector<uint8_t> pendingData(dataSize);
auto ptr = pendingData.data();
auto keySize = static_cast<key_size_t>(key.size());
memcpy(ptr, &keySize, sizeof(key_size_t));
ptr += sizeof(key_size_t);
memcpy(ptr, key.data(), key.size());
ptr += key.size();
memcpy(ptr, data.data(), data.size());
mPipelineCacheStore.store(mStorePath, std::move(pendingData));
}