util/disk_cache: Add new mesa-db cache type

Introduce new cache type, the Mesa-DB. This is a single-file read/write
cache that is based on the read-only Fossilize DB cache. Mesa-DB supports
cache size capping. It's a much more efficient cache than the multi-file
cache because Mesa-DB doesn't have the inode overhead. The plan is to make
Mesa-DB the default cache implementation once it will be deemed as stable
and well tested. For now users have to set the new MESA_DISK_CACHE_DATABASE
environment variable in order to active the Mesa-DB cache.

Mesa-DB cache is resilient to corrupted cache files and doesn't require
maintenance from users and developers. The size capping is implemented
by evicting least recently used cache items and compacting the cache
database files with the evicted entries. In order to prevent frequent
compaction of the cache, at minimum a half of cache is evicted when cache
is full.

Acked-by: Timothy Arceri <tarceri@itsqueeze.com>
Reviewed-by: Emil Velikov <emil.velikov@collabora.com>
Signed-off-by: Dmitry Osipenko <dmitry.osipenko@collabora.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/16888>
This commit is contained in:
Dmitry Osipenko
2022-06-13 19:31:20 +03:00
committed by Marge Bot
parent f64f74b8f2
commit 32211788d0
9 changed files with 1164 additions and 0 deletions

View File

@@ -1,2 +1,3 @@
GL_ARB_shader_clock on llvmpipe
VK_KHR_shader_clock on lavapipe
Mesa-DB, the new single file cache type

View File

@@ -121,6 +121,11 @@ disk_cache_create(const char *gpu_name, const char *driver_id,
if (env_var_as_boolean("MESA_DISK_CACHE_SINGLE_FILE", false)) {
if (!disk_cache_load_cache_index_foz(local, cache))
goto path_fail;
} else if (env_var_as_boolean("MESA_DISK_CACHE_DATABASE", false)) {
if (!disk_cache_db_load_cache_index(local, cache))
goto path_fail;
cache->use_cache_db = true;
}
if (!disk_cache_mmap_cache_index(local, cache, path))
@@ -176,6 +181,9 @@ disk_cache_create(const char *gpu_name, const char *driver_id,
cache->max_size = max_size;
if (cache->use_cache_db)
mesa_cache_db_set_size_limit(&cache->cache_db, cache->max_size);
/* 4 threads were chosen below because just about all modern CPUs currently
* available that run Mesa have *at least* 4 cores. For these CPUs allowing
* more threads can result in the queue being processed faster, thus
@@ -252,6 +260,9 @@ disk_cache_destroy(struct disk_cache *cache)
if (env_var_as_boolean("MESA_DISK_CACHE_SINGLE_FILE", false))
foz_destroy(&cache->foz_db);
if (cache->use_cache_db)
mesa_cache_db_close(&cache->cache_db);
disk_cache_destroy_mmap(cache);
}
@@ -354,6 +365,8 @@ cache_put(void *job, void *gdata, int thread_index)
if (env_var_as_boolean("MESA_DISK_CACHE_SINGLE_FILE", false)) {
disk_cache_write_item_to_disk_foz(dc_job);
} else if (dc_job->cache->use_cache_db) {
disk_cache_db_write_item_to_disk(dc_job);
} else {
filename = disk_cache_get_cache_filename(dc_job->cache, dc_job->key);
if (filename == NULL)
@@ -452,6 +465,8 @@ disk_cache_get(struct disk_cache *cache, const cache_key key, size_t *size)
if (env_var_as_boolean("MESA_DISK_CACHE_SINGLE_FILE", false)) {
return disk_cache_load_item_foz(cache, key, size);
} else if (cache->use_cache_db) {
return disk_cache_db_load_item(cache, key, size);
} else {
char *filename = disk_cache_get_cache_filename(cache, key);
if (filename == NULL)

View File

@@ -45,6 +45,7 @@ extern "C" {
#define CACHE_DIR_NAME "mesa_shader_cache"
#define CACHE_DIR_NAME_SF "mesa_shader_cache_sf"
#define CACHE_DIR_NAME_DB "mesa_shader_cache_db"
typedef uint8_t cache_key[CACHE_KEY_SIZE];

View File

@@ -831,6 +831,8 @@ disk_cache_generate_cache_dir(void *mem_ctx, const char *gpu_name,
char *cache_dir_name = CACHE_DIR_NAME;
if (env_var_as_boolean("MESA_DISK_CACHE_SINGLE_FILE", false))
cache_dir_name = CACHE_DIR_NAME_SF;
else if (env_var_as_boolean("MESA_DISK_CACHE_DATABASE", false))
cache_dir_name = CACHE_DIR_NAME_DB;
char *path = getenv("MESA_SHADER_CACHE_DIR");
@@ -1042,6 +1044,45 @@ disk_cache_destroy_mmap(struct disk_cache *cache)
{
munmap(cache->index_mmap, cache->index_mmap_size);
}
void *
disk_cache_db_load_item(struct disk_cache *cache, const cache_key key,
size_t *size)
{
size_t cache_tem_size = 0;
void *cache_item = mesa_cache_db_read_entry(&cache->cache_db, key,
&cache_tem_size);
if (!cache_item)
return NULL;
uint8_t *uncompressed_data =
parse_and_validate_cache_item(cache, cache_item, cache_tem_size, size);
free(cache_item);
return uncompressed_data;
}
bool
disk_cache_db_write_item_to_disk(struct disk_cache_put_job *dc_job)
{
struct blob cache_blob;
blob_init(&cache_blob);
if (!create_cache_item_header_and_blob(dc_job, &cache_blob))
return false;
bool r = mesa_cache_db_entry_write(&dc_job->cache->cache_db, dc_job->key,
cache_blob.data, cache_blob.size);
blob_finish(&cache_blob);
return r;
}
bool
disk_cache_db_load_cache_index(void *mem_ctx, struct disk_cache *cache)
{
return mesa_cache_db_open(&cache->cache_db, cache->path);
}
#endif
#endif /* ENABLE_SHADER_CACHE */

View File

@@ -33,6 +33,7 @@
#else
#include "util/fossilize_db.h"
#include "util/mesa_cache_db.h"
#ifdef __cplusplus
extern "C" {
@@ -57,6 +58,10 @@ struct disk_cache {
struct foz_db foz_db;
struct mesa_cache_db cache_db;
bool use_cache_db;
/* Seed for rand, which is used to pick a random directory */
uint64_t seed_xorshift128plus[2];
@@ -145,6 +150,16 @@ disk_cache_mmap_cache_index(void *mem_ctx, struct disk_cache *cache,
void
disk_cache_destroy_mmap(struct disk_cache *cache);
void *
disk_cache_db_load_item(struct disk_cache *cache, const cache_key key,
size_t *size);
bool
disk_cache_db_write_item_to_disk(struct disk_cache_put_job *dc_job);
bool
disk_cache_db_load_cache_index(void *mem_ctx, struct disk_cache *cache);
#ifdef __cplusplus
}
#endif

810
src/util/mesa_cache_db.c Normal file
View File

@@ -0,0 +1,810 @@
/*
* Copyright © 2022 Collabora, Ltd.
*
* Based on Fossilize DB:
* Copyright © 2020 Valve Corporation
*
* SPDX-License-Identifier: MIT
*/
#include "detect_os.h"
#if DETECT_OS_WINDOWS == 0
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <unistd.h>
#include "crc32.h"
#include "disk_cache.h"
#include "hash_table.h"
#include "mesa-sha1.h"
#include "mesa_cache_db.h"
#include "os_time.h"
#include "ralloc.h"
#include "u_qsort.h"
#define MESA_CACHE_DB_VERSION 1
#define MESA_CACHE_DB_MAGIC "MESA_DB"
struct PACKED mesa_db_file_header {
char magic[8];
uint32_t version;
uint64_t uuid;
};
struct PACKED mesa_cache_db_file_entry {
cache_key key;
uint32_t crc;
uint32_t size;
};
struct PACKED mesa_index_db_file_entry {
uint64_t hash;
uint32_t size;
uint64_t last_access_time;
uint64_t cache_db_file_offset;
};
struct mesa_index_db_hash_entry {
uint64_t cache_db_file_offset;
uint64_t index_db_file_offset;
uint64_t last_access_time;
uint32_t size;
bool evicted;
};
static inline bool mesa_db_seek_end(FILE *file)
{
return !fseek(file, 0, SEEK_END);
}
static inline bool mesa_db_seek(FILE *file, long pos)
{
return !fseek(file, pos, SEEK_SET);
}
static inline bool mesa_db_seek_cur(FILE *file, long pos)
{
return !fseek(file, pos, SEEK_CUR);
}
static inline bool mesa_db_read_data(FILE *file, void *data, size_t size)
{
return fread(data, 1, size, file) == size;
}
#define mesa_db_read(file, var) mesa_db_read_data(file, var, sizeof(*(var)))
static inline bool mesa_db_write_data(FILE *file, const void *data, size_t size)
{
return fwrite(data, 1, size, file) == size;
}
#define mesa_db_write(file, var) mesa_db_write_data(file, var, sizeof(*(var)))
static inline bool mesa_db_truncate(FILE *file, long pos)
{
return !ftruncate(fileno(file), pos);
}
static bool
mesa_db_lock(struct mesa_cache_db *db)
{
simple_mtx_lock(&db->flock_mtx);
if (flock(fileno(db->cache.file), LOCK_EX) == -1)
goto unlock_mtx;
if (flock(fileno(db->index.file), LOCK_EX) == -1)
goto unlock_cache;
return true;
unlock_cache:
flock(fileno(db->cache.file), LOCK_UN);
unlock_mtx:
simple_mtx_unlock(&db->flock_mtx);
return false;
}
static void
mesa_db_unlock(struct mesa_cache_db *db)
{
flock(fileno(db->index.file), LOCK_UN);
flock(fileno(db->cache.file), LOCK_UN);
simple_mtx_unlock(&db->flock_mtx);
}
static uint64_t to_mesa_cache_db_hash(const uint8_t *cache_key_160bit)
{
uint64_t hash = 0;
for (unsigned i = 0; i < 8; i++)
hash |= ((uint64_t)cache_key_160bit[i]) << i * 8;
return hash;
}
static uint64_t
mesa_db_generate_uuid(void)
{
/* This simple UUID implementation is sufficient for our needs
* because UUID is updated rarely. It's nice to make UUID meaningful
* and incremental by adding the timestamp to it, which also prevents
* the potential collisions. */
return ((os_time_get() / 1000000) << 32) | rand();
}
static bool
mesa_db_read_header(FILE *file, struct mesa_db_file_header *header)
{
rewind(file);
fflush(file);
if (!mesa_db_read(file, header))
return false;
if (strncmp(header->magic, MESA_CACHE_DB_MAGIC, sizeof(header->magic)) ||
header->version != MESA_CACHE_DB_VERSION || !header->uuid)
return false;
return true;
}
static bool
mesa_db_load_header(struct mesa_cache_db_file *db_file)
{
struct mesa_db_file_header header;
if (!mesa_db_read_header(db_file->file, &header))
return false;
db_file->uuid = header.uuid;
return true;
}
static bool mesa_db_uuid_changed(struct mesa_cache_db *db)
{
struct mesa_db_file_header cache_header;
struct mesa_db_file_header index_header;
if (!mesa_db_read_header(db->cache.file, &cache_header) ||
!mesa_db_read_header(db->index.file, &index_header) ||
cache_header.uuid != index_header.uuid ||
cache_header.uuid != db->uuid)
return true;
return false;
}
static bool
mesa_db_write_header(struct mesa_cache_db_file *db_file,
uint64_t uuid, bool reset)
{
struct mesa_db_file_header header;
rewind(db_file->file);
sprintf(header.magic, "MESA_DB");
header.version = MESA_CACHE_DB_VERSION;
header.uuid = uuid;
if (!mesa_db_write(db_file->file, &header))
return false;
if (reset) {
if (!mesa_db_truncate(db_file->file, ftell(db_file->file)))
return false;
}
fflush(db_file->file);
return true;
}
/* Wipe out all database cache files.
*
* Whenever we get an unmanageable error on reading or writing to the
* database file, wipe out the whole database and start over. All the
* cached entries will be lost, but the broken cache will be auto-repaired
* reliably. Normally cache shall never get corrupted and losing cache
* entries is acceptable, hence it's more practical to repair DB using
* the simplest method.
*/
static bool
mesa_db_zap(struct mesa_cache_db *db)
{
/* Disable cache to prevent the recurring faults */
db->alive = false;
/* Zap corrupted database files to start over from a clean slate */
if (!mesa_db_truncate(db->cache.file, 0) ||
!mesa_db_truncate(db->index.file, 0))
return false;
fflush(db->cache.file);
fflush(db->index.file);
return true;
}
static bool
mesa_db_index_entry_valid(struct mesa_index_db_file_entry *entry)
{
return entry->size && entry->hash &&
(int64_t)entry->cache_db_file_offset >= sizeof(struct mesa_db_file_header);
}
static bool
mesa_db_cache_entry_valid(struct mesa_cache_db_file_entry *entry)
{
return entry->size && entry->crc;
}
static bool
mesa_db_update_index(struct mesa_cache_db *db)
{
struct mesa_index_db_hash_entry *hash_entry;
struct mesa_index_db_file_entry index_entry;
size_t file_length;
if (!mesa_db_seek_end(db->index.file))
return false;
file_length = ftell(db->index.file);
if (!mesa_db_seek(db->index.file, db->index.offset))
return false;
while (db->index.offset < file_length) {
if (!mesa_db_read(db->index.file, &index_entry))
break;
/* Check whether the index entry looks valid or we have a corrupted DB */
if (!mesa_db_index_entry_valid(&index_entry))
break;
hash_entry = ralloc(db->mem_ctx, struct mesa_index_db_hash_entry);
if (!hash_entry)
break;
hash_entry->cache_db_file_offset = index_entry.cache_db_file_offset;
hash_entry->index_db_file_offset = db->index.offset;
hash_entry->last_access_time = index_entry.last_access_time;
hash_entry->size = index_entry.size;
_mesa_hash_table_u64_insert(db->index_db, index_entry.hash, hash_entry);
db->index.offset += sizeof(index_entry);
}
if (!mesa_db_seek(db->index.file, db->index.offset))
return false;
return db->index.offset == file_length;
}
static void
mesa_db_hash_table_reset(struct mesa_cache_db *db)
{
_mesa_hash_table_u64_clear(db->index_db);
ralloc_free(db->mem_ctx);
db->mem_ctx = ralloc_context(NULL);
}
static bool
mesa_db_recreate_files(struct mesa_cache_db *db)
{
db->uuid = mesa_db_generate_uuid();
if (!mesa_db_write_header(&db->cache, db->uuid, true) ||
!mesa_db_write_header(&db->index, db->uuid, true))
return false;
return true;
}
static bool
mesa_db_load(struct mesa_cache_db *db, bool reload)
{
/* reloading must be done under the held lock */
if (!reload) {
if (!mesa_db_lock(db))
return false;
}
/* If file headers are invalid, then zap database files and start over */
if (!mesa_db_load_header(&db->cache) ||
!mesa_db_load_header(&db->index) ||
db->cache.uuid != db->index.uuid) {
/* This is unexpected to happen on reload, bail out */
if (reload)
goto fail;
if (!mesa_db_recreate_files(db))
goto fail;
} else {
db->uuid = db->cache.uuid;
}
db->index.offset = ftell(db->index.file);
if (reload)
mesa_db_hash_table_reset(db);
if (!mesa_db_update_index(db))
goto fail;
if (!reload)
mesa_db_unlock(db);
db->alive = true;
return true;
fail:
if (!reload)
mesa_db_unlock(db);
return false;
}
static bool
mesa_db_reload(struct mesa_cache_db *db)
{
fflush(db->cache.file);
fflush(db->index.file);
return mesa_db_load(db, true);
}
static void
touch_file(const char* path)
{
close(open(path, O_CREAT | O_CLOEXEC, 0644));
}
static bool
mesa_db_open_file(struct mesa_cache_db_file *db_file,
const char *cache_path,
const char *filename)
{
if (asprintf(&db_file->path, "%s/%s", cache_path, filename) == -1)
return false;
/* The fopen("r+b") mode doesn't auto-create new file, hence we need to
* explicitly create the file first.
*/
touch_file(db_file->path);
db_file->file = fopen(db_file->path, "r+b");
if (!db_file->file) {
free(db_file->path);
return false;
}
return true;
}
static void
mesa_db_close_file(struct mesa_cache_db_file *db_file)
{
fclose(db_file->file);
free(db_file->path);
}
static int
entry_sort_lru(const void *_a, const void *_b, void *arg)
{
const struct mesa_index_db_hash_entry *a = *((const struct mesa_index_db_hash_entry **)_a);
const struct mesa_index_db_hash_entry *b = *((const struct mesa_index_db_hash_entry **)_b);
/* In practice it's unlikely that we will get two entries with the
* same timestamp, but technically it's possible to happen if OS
* timer's resolution is low. */
if (a->last_access_time == b->last_access_time)
return 0;
return a->last_access_time > b->last_access_time ? 1 : -1;
}
static int
entry_sort_offset(const void *_a, const void *_b, void *arg)
{
const struct mesa_index_db_hash_entry *a = *((const struct mesa_index_db_hash_entry **)_a);
const struct mesa_index_db_hash_entry *b = *((const struct mesa_index_db_hash_entry **)_b);
struct mesa_cache_db *db = arg;
/* Two entries will never have the identical offset, otherwise DB is
* corrupted. */
if (a->cache_db_file_offset == b->cache_db_file_offset)
mesa_db_zap(db);
return a->cache_db_file_offset > b->cache_db_file_offset ? 1 : -1;
}
static uint32_t blob_file_size(uint32_t blob_size)
{
return sizeof(struct mesa_cache_db_file_entry) + blob_size;
}
static bool
mesa_db_compact(struct mesa_cache_db *db, int64_t blob_size)
{
uint32_t num_entries, buffer_size = sizeof(struct mesa_index_db_file_entry);
struct mesa_db_file_header cache_header, index_header;
FILE *compacted_cache = NULL, *compacted_index = NULL;
struct mesa_index_db_file_entry index_entry;
struct mesa_index_db_hash_entry **entries;
bool success = false, compact = false;
void *buffer = NULL;
unsigned int i = 0;
/* reload index to sync the last access times */
if (!mesa_db_reload(db))
return false;
num_entries = _mesa_hash_table_num_entries(db->index_db->table);
entries = calloc(num_entries, sizeof(*entries));
if (!entries)
return false;
compacted_cache = fopen(db->cache.path, "r+b");
compacted_index = fopen(db->index.path, "r+b");
if (!compacted_cache || !compacted_index)
goto cleanup;
/* The database file has been replaced if UUID changed. We opened
* some other cache, stop processing this database. */
if (!mesa_db_read_header(compacted_cache, &cache_header) ||
!mesa_db_read_header(compacted_index, &index_header) ||
cache_header.uuid != db->uuid ||
index_header.uuid != db->uuid)
goto cleanup;
hash_table_foreach(db->index_db->table, entry) {
entries[i] = entry->data;
entries[i]->evicted = false;
buffer_size = MAX2(buffer_size, blob_file_size(entries[i]->size));
i++;
}
util_qsort_r(entries, num_entries, sizeof(*entries),
entry_sort_lru, db);
for (i = 0; blob_size > 0 && i < num_entries; i++) {
blob_size -= blob_file_size(entries[i]->size);
entries[i]->evicted = true;
}
util_qsort_r(entries, num_entries, sizeof(*entries),
entry_sort_offset, db);
/* entry_sort_offset() may zap the database */
if (!db->alive)
goto cleanup;
buffer = malloc(buffer_size);
if (!buffer)
goto cleanup;
/* Mark cache file invalid by writing zero-UUID header. If compaction will
* fail, then the file will remain to be invalid since we can't repair it. */
if (!mesa_db_write_header(&db->cache, 0, false) ||
!mesa_db_write_header(&db->index, 0, false))
goto cleanup;
/* Sync the file pointers */
if (!mesa_db_seek(compacted_cache, ftell(db->cache.file)) ||
!mesa_db_seek(compacted_index, ftell(db->index.file)))
goto cleanup;
/* Do the compaction */
for (i = 0; i < num_entries; i++) {
blob_size = blob_file_size(entries[i]->size);
/* Sanity-check the cache-read offset */
if (ftell(db->cache.file) != entries[i]->cache_db_file_offset)
goto cleanup;
if (entries[i]->evicted) {
/* Jump over the evicted entry */
if (!mesa_db_seek_cur(db->cache.file, blob_size) ||
!mesa_db_seek_cur(db->index.file, sizeof(index_entry)))
goto cleanup;
compact = true;
continue;
}
if (compact) {
/* Compact the cache file */
if (!mesa_db_read_data(db->cache.file, buffer, blob_size) ||
!mesa_db_cache_entry_valid(buffer) ||
!mesa_db_write_data(compacted_cache, buffer, blob_size))
goto cleanup;
/* Compact the index file */
if (!mesa_db_read(db->index.file, &index_entry) ||
!mesa_db_index_entry_valid(&index_entry) ||
index_entry.cache_db_file_offset != entries[i]->cache_db_file_offset ||
index_entry.size != entries[i]->size)
goto cleanup;
index_entry.cache_db_file_offset = ftell(compacted_cache) - blob_size;
if (!mesa_db_write(compacted_index, &index_entry))
goto cleanup;
} else {
/* Sanity-check the cache-write offset */
if (ftell(compacted_cache) != entries[i]->cache_db_file_offset)
goto cleanup;
/* Jump over the unchanged entry */
if (!mesa_db_seek_cur(db->index.file, sizeof(index_entry)) ||
!mesa_db_seek_cur(compacted_index, sizeof(index_entry)) ||
!mesa_db_seek_cur(db->cache.file, blob_size) ||
!mesa_db_seek_cur(compacted_cache, blob_size))
goto cleanup;
}
}
fflush(compacted_cache);
fflush(compacted_index);
/* Cut off the the freed space left after compaction */
if (!mesa_db_truncate(db->cache.file, ftell(compacted_cache)) ||
!mesa_db_truncate(db->index.file, ftell(compacted_index)))
goto cleanup;
/* Set the new UUID to let all cache readers know that the cache was changed */
db->uuid = mesa_db_generate_uuid();
if (!mesa_db_write_header(&db->cache, db->uuid, false) ||
!mesa_db_write_header(&db->index, db->uuid, false))
goto cleanup;
success = true;
cleanup:
free(buffer);
if (compacted_index)
fclose(compacted_index);
if (compacted_cache)
fclose(compacted_cache);
free(entries);
/* reload compacted index */
if (success && !mesa_db_reload(db))
success = false;
return success;
}
bool
mesa_cache_db_open(struct mesa_cache_db *db, const char *cache_path)
{
if (!mesa_db_open_file(&db->cache, cache_path, "mesa_cache.db"))
return false;
if (!mesa_db_open_file(&db->index, cache_path, "mesa_cache.idx"))
goto close_cache;
db->mem_ctx = ralloc_context(NULL);
if (!db->mem_ctx)
goto close_index;
simple_mtx_init(&db->flock_mtx, mtx_plain);
db->index_db = _mesa_hash_table_u64_create(NULL);
if (!db->index_db)
goto destroy_mtx;
if (!mesa_db_load(db, false))
goto destroy_hash;
return true;
destroy_hash:
_mesa_hash_table_u64_destroy(db->index_db);
destroy_mtx:
simple_mtx_destroy(&db->flock_mtx);
ralloc_free(db->mem_ctx);
close_index:
mesa_db_close_file(&db->index);
close_cache:
mesa_db_close_file(&db->cache);
return false;
}
void
mesa_cache_db_close(struct mesa_cache_db *db)
{
_mesa_hash_table_u64_destroy(db->index_db);
simple_mtx_destroy(&db->flock_mtx);
ralloc_free(db->mem_ctx);
mesa_db_close_file(&db->index);
mesa_db_close_file(&db->cache);
}
void
mesa_cache_db_set_size_limit(struct mesa_cache_db *db,
uint64_t max_cache_size)
{
db->max_cache_size = max_cache_size;
}
unsigned int
mesa_cache_db_file_entry_size(void)
{
return sizeof(struct mesa_cache_db_file_entry);
}
void *
mesa_cache_db_read_entry(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
size_t *size)
{
uint64_t hash = to_mesa_cache_db_hash(cache_key_160bit);
struct mesa_cache_db_file_entry cache_entry;
struct mesa_index_db_file_entry index_entry;
struct mesa_index_db_hash_entry *hash_entry;
void *data = NULL;
if (!mesa_db_lock(db))
return NULL;
if (!db->alive)
goto fail;
if (mesa_db_uuid_changed(db) && !mesa_db_reload(db))
goto fail_fatal;
if (!mesa_db_update_index(db))
goto fail_fatal;
hash_entry = _mesa_hash_table_u64_search(db->index_db, hash);
if (!hash_entry)
goto fail;
if (!mesa_db_seek(db->cache.file, hash_entry->cache_db_file_offset) ||
!mesa_db_read(db->cache.file, &cache_entry) ||
!mesa_db_cache_entry_valid(&cache_entry))
goto fail_fatal;
if (memcmp(cache_entry.key, cache_key_160bit, sizeof(cache_entry.key)))
goto fail;
data = malloc(cache_entry.size);
if (!data)
goto fail;
if (!mesa_db_read_data(db->cache.file, data, cache_entry.size) ||
util_hash_crc32(data, cache_entry.size) != cache_entry.crc)
goto fail_fatal;
if (!mesa_db_seek(db->index.file, hash_entry->index_db_file_offset) ||
!mesa_db_read(db->index.file, &index_entry) ||
!mesa_db_index_entry_valid(&index_entry) ||
index_entry.cache_db_file_offset != hash_entry->cache_db_file_offset ||
index_entry.size != hash_entry->size)
goto fail_fatal;
index_entry.last_access_time = os_time_get_nano();
hash_entry->last_access_time = index_entry.last_access_time;
if (!mesa_db_seek(db->index.file, hash_entry->index_db_file_offset) ||
!mesa_db_write(db->index.file, &index_entry))
goto fail_fatal;
fflush(db->index.file);
mesa_db_unlock(db);
*size = cache_entry.size;
return data;
fail_fatal:
mesa_db_zap(db);
fail:
free(data);
mesa_db_unlock(db);
return NULL;
}
bool
mesa_cache_db_entry_write(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
const void *blob, size_t blob_size)
{
uint64_t hash = to_mesa_cache_db_hash(cache_key_160bit);
struct mesa_index_db_hash_entry *hash_entry = NULL;
struct mesa_cache_db_file_entry cache_entry;
struct mesa_index_db_file_entry index_entry;
if (!mesa_db_lock(db))
return false;
if (!db->alive)
goto fail;
if (mesa_db_uuid_changed(db) && !mesa_db_reload(db))
goto fail_fatal;
if (!mesa_db_seek_end(db->cache.file))
goto fail_fatal;
if (ftell(db->cache.file) + blob_file_size(blob_size) > db->max_cache_size) {
if (!mesa_db_compact(db, MAX2(blob_size, db->max_cache_size / 2)))
goto fail_fatal;
} else {
if (!mesa_db_update_index(db))
goto fail_fatal;
}
hash_entry = _mesa_hash_table_u64_search(db->index_db, hash);
if (hash_entry) {
hash_entry = NULL;
goto fail;
}
if (!mesa_db_seek_end(db->cache.file) ||
!mesa_db_seek_end(db->index.file))
goto fail_fatal;
memcpy(cache_entry.key, cache_key_160bit, sizeof(cache_entry.key));
cache_entry.crc = util_hash_crc32(blob, blob_size);
cache_entry.size = blob_size;
index_entry.hash = hash;
index_entry.size = blob_size;
index_entry.last_access_time = os_time_get_nano();
index_entry.cache_db_file_offset = ftell(db->cache.file);
hash_entry = ralloc(db->mem_ctx, struct mesa_index_db_hash_entry);
if (!hash_entry)
goto fail;
hash_entry->cache_db_file_offset = index_entry.cache_db_file_offset;
hash_entry->index_db_file_offset = ftell(db->index.file);
hash_entry->last_access_time = index_entry.last_access_time;
hash_entry->size = index_entry.size;
if (!mesa_db_write(db->cache.file, &cache_entry) ||
!mesa_db_write_data(db->cache.file, blob, blob_size) ||
!mesa_db_write(db->index.file, &index_entry))
goto fail_fatal;
fflush(db->cache.file);
fflush(db->index.file);
db->index.offset = ftell(db->index.file);
_mesa_hash_table_u64_insert(db->index_db, hash, hash_entry);
mesa_db_unlock(db);
return true;
fail_fatal:
mesa_db_zap(db);
fail:
mesa_db_unlock(db);
if (hash_entry)
ralloc_free(hash_entry);
return false;
}
#endif /* DETECT_OS_WINDOWS */

110
src/util/mesa_cache_db.h Normal file
View File

@@ -0,0 +1,110 @@
/*
* Copyright © 2022 Collabora, Ltd.
*
* Based on Fossilize DB:
* Copyright © 2020 Valve Corporation
*
* SPDX-License-Identifier: MIT
*/
#ifndef MESA_CACHE_DB_H
#define MESA_CACHE_DB_H
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include "detect_os.h"
#include "simple_mtx.h"
#ifdef __cplusplus
extern "C" {
#endif
struct mesa_cache_db_file {
FILE *file;
char *path;
off_t offset;
uint64_t uuid;
};
struct mesa_cache_db {
struct hash_table_u64 *index_db;
struct mesa_cache_db_file cache;
struct mesa_cache_db_file index;
uint64_t max_cache_size;
simple_mtx_t flock_mtx;
void *mem_ctx;
uint64_t uuid;
bool alive;
};
#if DETECT_OS_WINDOWS == 0
bool
mesa_cache_db_open(struct mesa_cache_db *db, const char *cache_path);
void
mesa_cache_db_close(struct mesa_cache_db *db);
void
mesa_cache_db_set_size_limit(struct mesa_cache_db *db,
uint64_t max_cache_size);
unsigned int
mesa_cache_db_file_entry_size(void);
void *
mesa_cache_db_read_entry(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
size_t *size);
bool
mesa_cache_db_entry_write(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
const void *blob, size_t blob_size);
#else
static inline bool
mesa_cache_db_open(struct mesa_cache_db *db, const char *cache_path)
{
return false;
}
static inline void
mesa_cache_db_close(struct mesa_cache_db *db)
{
}
static inline void
mesa_cache_db_set_size_limit(struct mesa_cache_db *db,
uint64_t max_cache_size)
{
}
static inline unsigned int
mesa_cache_db_file_entry_size(void)
{
return 0;
}
static inline void *
mesa_cache_db_read_entry(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
size_t *size)
{
return NULL;
}
static inline bool
mesa_cache_db_entry_write(struct mesa_cache_db *db,
const uint8_t *cache_key_160bit,
const void *blob, size_t blob_size)
{
return false;
}
#endif /* DETECT_OS_WINDOWS */
#ifdef __cplusplus
}
#endif
#endif /* MESA_CACHE_DB_H */

View File

@@ -159,6 +159,8 @@ files_mesa_util = files(
'indices/u_indices_priv.h',
'indices/u_primconvert.c',
'indices/u_primconvert.h',
'mesa_cache_db.c',
'mesa_cache_db.h',
)
files_drirc = files('00-mesa-defaults.conf')

View File

@@ -39,6 +39,7 @@
#include "util/mesa-sha1.h"
#include "util/disk_cache.h"
#include "util/disk_cache_os.h"
#include "util/ralloc.h"
#ifdef ENABLE_SHADER_CACHE
@@ -520,6 +521,136 @@ test_put_and_get_between_instances(const char *driver_id)
disk_cache_destroy(cache1);
disk_cache_destroy(cache2);
}
static void
test_put_and_get_between_instances_with_eviction(const char *driver_id)
{
cache_key small_key[8], small_key2, big_key[2];
struct disk_cache *cache[2];
unsigned int i, n, k;
uint8_t *small;
uint8_t *big;
char *result;
size_t size;
#ifdef SHADER_CACHE_DISABLE_BY_DEFAULT
setenv("MESA_SHADER_CACHE_DISABLE", "false", 1);
#endif /* SHADER_CACHE_DISABLE_BY_DEFAULT */
setenv("MESA_SHADER_CACHE_MAX_SIZE", "2K", 1);
cache[0] = disk_cache_create("test_between_instances_with_eviction", driver_id, 0);
cache[1] = disk_cache_create("test_between_instances_with_eviction", driver_id, 0);
uint8_t two_KB[2048] = { 0 };
cache_key two_KB_key = { 'T', 'W', 'O', 'K', 'B' };
/* Flush the database by adding the dummy 2KB entry */
disk_cache_put(cache[0], two_KB_key, two_KB, sizeof(two_KB), NULL);
disk_cache_wait_for_idle(cache[0]);
int size_big = 1000;
size_big -= sizeof(struct cache_entry_file_data);
size_big -= mesa_cache_db_file_entry_size();
size_big -= cache[0]->driver_keys_blob_size;
size_big -= 4 + 8; /* cache_item_metadata size + room for alignment */
for (i = 0; i < ARRAY_SIZE(big_key); i++) {
big = (uint8_t *) malloc(size_big);
memset(big, i, size_big);
disk_cache_compute_key(cache[0], big, size_big, big_key[i]);
disk_cache_put(cache[0], big_key[i], big, size_big, NULL);
disk_cache_wait_for_idle(cache[0]);
result = (char *) disk_cache_get(cache[0], big_key[i], &size);
EXPECT_NE(result, nullptr) << "disk_cache_get with existent item (pointer)";
EXPECT_EQ(size, size_big) << "disk_cache_get with existent item (size)";
free(result);
free(big);
}
int size_small = 256;
size_small -= sizeof(struct cache_entry_file_data);
size_small -= mesa_cache_db_file_entry_size();
size_small -= cache[1]->driver_keys_blob_size;
size_small -= 4 + 8; /* cache_item_metadata size + room for alignment */
for (i = 0; i < ARRAY_SIZE(small_key); i++) {
small = (uint8_t *) malloc(size_small);
memset(small, i, size_small);
disk_cache_compute_key(cache[1], small, size_small, small_key[i]);
disk_cache_put(cache[1], small_key[i], small, size_small, NULL);
disk_cache_wait_for_idle(cache[1]);
/*
* At first we added two 1000KB entries to cache[0]. Now, when first
* 256KB entry is added, the two 1000KB entries are evicted because
* at minimum cache_max_size/2 is evicted on overflow.
*
* All four 256KB entries stay in the cache.
*/
for (k = 0; k < ARRAY_SIZE(cache); k++) {
for (n = 0; n <= i; n++) {
result = (char *) disk_cache_get(cache[k], big_key[0], &size);
EXPECT_EQ(result, nullptr) << "disk_cache_get with non-existent item (pointer)";
EXPECT_EQ(size, 0) << "disk_cache_get with non-existent item (size)";
free(result);
result = (char *) disk_cache_get(cache[k], big_key[1], &size);
EXPECT_EQ(result, nullptr) << "disk_cache_get with non-existent item (pointer)";
EXPECT_EQ(size, 0) << "disk_cache_get with non-existent item (size)";
free(result);
result = (char *) disk_cache_get(cache[k], small_key[n], &size);
EXPECT_NE(result, nullptr) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, size_small) << "disk_cache_get of existing item (size)";
free(result);
result = (char *) disk_cache_get(cache[k], small_key[n], &size);
EXPECT_NE(result, nullptr) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, size_small) << "disk_cache_get of existing item (size)";
free(result);
}
}
free(small);
}
small = (uint8_t *) malloc(size_small);
memset(small, i, size_small);
/* Add another 256KB entry. This will evict first five 256KB entries
* of eight that we added previously. */
disk_cache_compute_key(cache[0], small, size_small, small_key2);
disk_cache_put(cache[0], small_key2, small, size_small, NULL);
disk_cache_wait_for_idle(cache[0]);
free(small);
for (k = 0; k < ARRAY_SIZE(cache); k++) {
result = (char *) disk_cache_get(cache[k], small_key2, &size);
EXPECT_NE(result, nullptr) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, size_small) << "disk_cache_get of existing item (size)";
free(result);
}
for (i = 0, k = 0; k < ARRAY_SIZE(cache); k++) {
for (n = 0; n < ARRAY_SIZE(small_key); n++) {
result = (char *) disk_cache_get(cache[k], small_key[n], &size);
if (!result)
i++;
free(result);
}
}
EXPECT_EQ(i, 10) << "2x disk_cache_get with 5 non-existent 256KB items";
disk_cache_destroy(cache[0]);
disk_cache_destroy(cache[1]);
}
#endif /* ENABLE_SHADER_CACHE */
class Cache : public ::testing::Test {
@@ -604,3 +735,41 @@ run_tests:
}
#endif
}
TEST_F(Cache, Database)
{
const char *driver_id = "make_check_uncompressed";
#ifndef ENABLE_SHADER_CACHE
GTEST_SKIP() << "ENABLE_SHADER_CACHE not defined.";
#else
setenv("MESA_DISK_CACHE_DATABASE", "true", 1);
test_disk_cache_create(mem_ctx, CACHE_DIR_NAME_DB, driver_id);
/* We skip testing cache size limit as the single file cache compresses
* data much better than the multi-file cache, which results in the
* failing tests of the cache eviction function. We we will test the
* eviction separately with the disabled compression.
*/
test_put_and_get(false, driver_id);
int err = rmrf_local(CACHE_TEST_TMP);
EXPECT_EQ(err, 0) << "Removing " CACHE_TEST_TMP " again";
test_disk_cache_create(mem_ctx, CACHE_DIR_NAME_DB, driver_id);
test_put_and_get(true, driver_id);
test_put_key_and_get_key(driver_id);
test_put_and_get_between_instances(driver_id);
test_put_and_get_between_instances_with_eviction(driver_id);
setenv("MESA_DISK_CACHE_DATABASE", "false", 1);
err = rmrf_local(CACHE_TEST_TMP);
EXPECT_EQ(err, 0) << "Removing " CACHE_TEST_TMP " again";
#endif
}