Instead of allocating separate memory for each index entry in the hash table, use a single array (backed by a mapping of anonymous memory pages, which allows efficient array resizes) which holds a copy of the index file contents. The hash table now references each entry via its offset in the index file, so that the array address can change on resize. This eliminates some index file reads and reduces memory management overhead for the hash table entries. It should be more efficient in general. Reviewed-by: Dmitry Osipenko <dmitry.osipenko@collabora.com> Tested-by: Dmitry Osipenko <dmitry.osipenko@collabora.com> Acked-by: Timothy Arceri <tarceri@itsqueeze.com> Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/30988>
1089 lines
27 KiB
C
1089 lines
27 KiB
C
/*
|
|
* 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 <fcntl.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/file.h>
|
|
#include <sys/mman.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_debug.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;
|
|
};
|
|
|
|
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 int
|
|
mesa_db_flock(FILE *file, int op)
|
|
{
|
|
int ret;
|
|
|
|
do {
|
|
ret = flock(fileno(file), op);
|
|
} while (ret < 0 && errno == EINTR);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
mesa_db_lock(struct mesa_cache_db *db)
|
|
{
|
|
simple_mtx_lock(&db->flock_mtx);
|
|
|
|
if (mesa_db_flock(db->cache.file, LOCK_EX) < 0)
|
|
goto unlock_mtx;
|
|
|
|
if (mesa_db_flock(db->index.file, LOCK_EX) < 0)
|
|
goto unlock_cache;
|
|
|
|
return true;
|
|
|
|
unlock_cache:
|
|
mesa_db_flock(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)
|
|
{
|
|
mesa_db_flock(db->index.file, LOCK_UN);
|
|
mesa_db_flock(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 struct mesa_index_db_file_entry *
|
|
mesa_db_index_entry_get(struct mesa_cache_db *db, size_t offset)
|
|
{
|
|
return (struct mesa_index_db_file_entry *)
|
|
((char*)db->index_entries + offset);
|
|
}
|
|
|
|
static void
|
|
mesa_db_index_entry_insert(struct mesa_cache_db *db,
|
|
struct mesa_index_db_file_entry *index_entry)
|
|
{
|
|
size_t offset = (char*)index_entry - (char*)db->index_entries;
|
|
|
|
offset += sizeof(struct mesa_db_file_header);
|
|
_mesa_hash_table_u64_insert(db->index_db, index_entry->hash, (char*)(intptr_t)offset);
|
|
}
|
|
|
|
static struct mesa_index_db_file_entry *
|
|
mesa_db_index_entry_search(struct mesa_cache_db *db, uint64_t key)
|
|
{
|
|
size_t index_offset = (intptr_t)_mesa_hash_table_u64_search(db->index_db, key);
|
|
|
|
if (!index_offset)
|
|
return NULL;
|
|
|
|
return mesa_db_index_entry_get(db, index_offset - sizeof(struct mesa_db_file_header));
|
|
}
|
|
|
|
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_resize_index_entries(struct mesa_cache_db *db, off_t size)
|
|
{
|
|
int page_size = getpagesize();
|
|
size_t page_mask = page_size - 1;
|
|
off_t old_num_pages, new_num_pages;
|
|
|
|
if (db->index_entries_size == size)
|
|
return true;
|
|
|
|
new_num_pages = (size + page_mask) / page_size;
|
|
|
|
if (size) {
|
|
if (db->index_entries_size) {
|
|
old_num_pages = (db->index_entries_size + page_mask) / page_size;
|
|
|
|
if (new_num_pages != old_num_pages) {
|
|
db->index_entries = mremap(db->index_entries, old_num_pages * page_size,
|
|
new_num_pages * page_size, MREMAP_MAYMOVE);
|
|
if (db->index_entries == MAP_FAILED) {
|
|
fprintf(stderr, "%s: mremap failed with error %d (%s)\n",
|
|
__func__, errno, strerror(errno));
|
|
goto error;
|
|
}
|
|
}
|
|
} else {
|
|
db->index_entries = mmap(NULL, new_num_pages * page_size, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
|
|
if (db->index_entries == MAP_FAILED) {
|
|
fprintf(stderr, "%s: mmap failed with error %d (%s)\n",
|
|
__func__, errno, strerror(errno));
|
|
goto error;
|
|
}
|
|
}
|
|
} else {
|
|
if (db->index_entries_size) {
|
|
old_num_pages = (db->index_entries_size + page_mask) / page_size;
|
|
|
|
munmap(db->index_entries, old_num_pages * page_size);
|
|
}
|
|
|
|
db->index_entries = NULL;
|
|
}
|
|
|
|
db->index_entries_size = size;
|
|
return true;
|
|
|
|
error:
|
|
_mesa_hash_table_u64_clear(db->index_db);
|
|
db->index_entries = NULL;
|
|
db->index_entries_size = 0;
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
mesa_db_update_index(struct mesa_cache_db *db)
|
|
{
|
|
struct mesa_index_db_file_entry *index_entry;
|
|
size_t file_length;
|
|
size_t old_entries, new_entries;
|
|
int i;
|
|
|
|
if (!mesa_db_seek_end(db->index.file))
|
|
return false;
|
|
|
|
file_length = ftell(db->index.file);
|
|
if (file_length < db->index.offset)
|
|
return false;
|
|
|
|
if (!mesa_db_seek(db->index.file, db->index.offset))
|
|
return false;
|
|
|
|
new_entries = (file_length - db->index.offset) / sizeof(*index_entry);
|
|
if (!new_entries)
|
|
return true;
|
|
|
|
old_entries = db->index_entries_size / sizeof(*index_entry);
|
|
|
|
if (!mesa_db_resize_index_entries(db, (old_entries + new_entries) * sizeof(*index_entry)))
|
|
return false;
|
|
|
|
_mesa_hash_table_reserve(db->index_db->table, old_entries + new_entries);
|
|
|
|
index_entry = mesa_db_index_entry_get(db, old_entries * sizeof(*index_entry));
|
|
if (!mesa_db_read_data(db->index.file, index_entry, new_entries * sizeof(*index_entry)))
|
|
return false;
|
|
|
|
for (i = 0; i < new_entries; i++, index_entry++) {
|
|
/* Check whether the index entry looks valid or we have a corrupted DB */
|
|
if (!mesa_db_index_entry_valid(index_entry))
|
|
break;
|
|
|
|
mesa_db_index_entry_insert(db, index_entry);
|
|
|
|
db->index.offset += sizeof(*index_entry);
|
|
}
|
|
|
|
return mesa_db_seek(db->index.file, db->index.offset) &&
|
|
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);
|
|
|
|
mesa_db_resize_index_entries(db, 0);
|
|
}
|
|
|
|
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) {
|
|
|
|
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);
|
|
|
|
/* The update failed so we assume the files are corrupt and
|
|
* recreate them.
|
|
*/
|
|
if (!mesa_db_update_index(db)) {
|
|
mesa_db_recreate_files(db);
|
|
db->index.offset = ftell(db->index.file);
|
|
|
|
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 bool
|
|
mesa_db_remove_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;
|
|
|
|
unlink(db_file->path);
|
|
|
|
return true;
|
|
}
|
|
|
|
struct sort_entry {
|
|
struct mesa_index_db_file_entry *index_entry;
|
|
bool evicted;
|
|
};
|
|
|
|
static int
|
|
entry_sort_lru(const void *_a, const void *_b, void *arg)
|
|
{
|
|
const struct mesa_index_db_file_entry *a = ((const struct sort_entry *)_a)->index_entry;
|
|
const struct mesa_index_db_file_entry *b = ((const struct sort_entry *)_b)->index_entry;
|
|
|
|
/* 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_file_entry *a = ((const struct sort_entry *)_a)->index_entry;
|
|
const struct mesa_index_db_file_entry *b = ((const struct sort_entry *)_b)->index_entry;
|
|
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,
|
|
struct mesa_index_db_file_entry *remove_entry)
|
|
{
|
|
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 sort_entry *entries;
|
|
bool success = false, compact = false;
|
|
void *buffer = NULL;
|
|
unsigned int i = 0;
|
|
|
|
/* reload index to sync the last access times */
|
|
if (!remove_entry && !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;
|
|
|
|
for (i = 0, index_entry = db->index_entries; i < num_entries; i++, index_entry++) {
|
|
entries[i].index_entry = index_entry;
|
|
entries[i].evicted = index_entry == remove_entry;
|
|
buffer_size = MAX2(buffer_size, blob_file_size(index_entry->size));
|
|
}
|
|
|
|
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].index_entry->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++) {
|
|
struct mesa_index_db_file_entry *index_entry = entries[i].index_entry;
|
|
|
|
blob_size = blob_file_size(index_entry->size);
|
|
|
|
/* Sanity-check the cache-read offset */
|
|
if (ftell(db->cache.file) != index_entry->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))
|
|
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;
|
|
|
|
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) != index_entry->cache_db_file_offset)
|
|
goto cleanup;
|
|
|
|
/* Jump over the unchanged entry */
|
|
if (!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;
|
|
}
|
|
|
|
bool
|
|
mesa_db_wipe_path(const char *cache_path)
|
|
{
|
|
struct mesa_cache_db db = {0};
|
|
bool success = true;
|
|
|
|
if (!mesa_db_remove_file(&db.cache, cache_path, "mesa_cache.db") ||
|
|
!mesa_db_remove_file(&db.index, cache_path, "mesa_cache.idx"))
|
|
success = false;
|
|
|
|
free(db.cache.path);
|
|
free(db.index.path);
|
|
|
|
return success;
|
|
}
|
|
|
|
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_resize_index_entries(db, 0);
|
|
if (db->index_entries) {
|
|
munmap(db->index_entries, 0);
|
|
db->index_entries = NULL;
|
|
}
|
|
|
|
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;
|
|
long seek_pos;
|
|
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;
|
|
|
|
index_entry = mesa_db_index_entry_search(db, hash);
|
|
if (!index_entry)
|
|
goto fail;
|
|
|
|
if (!mesa_db_seek(db->cache.file, index_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;
|
|
|
|
index_entry->last_access_time = os_time_get_nano();
|
|
|
|
seek_pos = ((char*)index_entry - (char*)db->index_entries) +
|
|
sizeof(struct mesa_db_file_header);
|
|
|
|
if (!mesa_db_seek(db->index.file, seek_pos) ||
|
|
!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;
|
|
}
|
|
|
|
static bool
|
|
mesa_cache_db_has_space_locked(struct mesa_cache_db *db, size_t blob_size)
|
|
{
|
|
return ftell(db->cache.file) + blob_file_size(blob_size) -
|
|
sizeof(struct mesa_db_file_header) <= db->max_cache_size;
|
|
}
|
|
|
|
static size_t
|
|
mesa_cache_db_eviction_size(struct mesa_cache_db *db)
|
|
{
|
|
return db->max_cache_size / 2 - sizeof(struct mesa_db_file_header);
|
|
}
|
|
|
|
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_cache_db_file_entry cache_entry;
|
|
struct mesa_index_db_file_entry *index_entry;
|
|
off_t index_offset;
|
|
|
|
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 (!mesa_cache_db_has_space_locked(db, blob_size)) {
|
|
if (!mesa_db_compact(db, MAX2(blob_size, mesa_cache_db_eviction_size(db)),
|
|
NULL))
|
|
goto fail_fatal;
|
|
} else {
|
|
if (!mesa_db_update_index(db))
|
|
goto fail_fatal;
|
|
}
|
|
|
|
index_entry = mesa_db_index_entry_search(db, hash);
|
|
if (index_entry)
|
|
goto fail;
|
|
|
|
if (!mesa_db_seek_end(db->cache.file) ||
|
|
!mesa_db_seek_end(db->index.file))
|
|
goto fail_fatal;
|
|
|
|
index_offset = db->index_entries_size;
|
|
if (!mesa_db_resize_index_entries(db, index_offset + sizeof(*index_entry)))
|
|
goto fail;
|
|
|
|
index_entry = mesa_db_index_entry_get(db, index_offset);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
mesa_db_index_entry_insert(db, index_entry);
|
|
|
|
db->index.offset = ftell(db->index.file);
|
|
|
|
mesa_db_unlock(db);
|
|
|
|
return true;
|
|
|
|
fail_fatal:
|
|
mesa_db_zap(db);
|
|
fail:
|
|
mesa_db_unlock(db);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
mesa_cache_db_entry_remove(struct mesa_cache_db *db,
|
|
const uint8_t *cache_key_160bit)
|
|
{
|
|
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;
|
|
|
|
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;
|
|
|
|
index_entry = mesa_db_index_entry_search(db, hash);
|
|
if (!index_entry)
|
|
goto fail;
|
|
|
|
if (!mesa_db_seek(db->cache.file, index_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;
|
|
|
|
if (!mesa_db_compact(db, 0, index_entry))
|
|
goto fail_fatal;
|
|
|
|
mesa_db_unlock(db);
|
|
|
|
return true;
|
|
|
|
fail_fatal:
|
|
mesa_db_zap(db);
|
|
fail:
|
|
mesa_db_unlock(db);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
mesa_cache_db_has_space(struct mesa_cache_db *db, size_t blob_size)
|
|
{
|
|
bool has_space;
|
|
|
|
if (!mesa_db_lock(db))
|
|
return false;
|
|
|
|
if (!mesa_db_seek_end(db->cache.file))
|
|
goto fail_fatal;
|
|
|
|
has_space = mesa_cache_db_has_space_locked(db, blob_size);
|
|
|
|
mesa_db_unlock(db);
|
|
|
|
return has_space;
|
|
|
|
fail_fatal:
|
|
mesa_db_zap(db);
|
|
mesa_db_unlock(db);
|
|
|
|
return false;
|
|
}
|
|
|
|
static uint64_t
|
|
mesa_cache_db_eviction_2x_score_period(void)
|
|
{
|
|
const uint64_t nsec_per_sec = 1000000000ull;
|
|
static uint64_t period = 0;
|
|
|
|
if (period)
|
|
return period;
|
|
|
|
period = debug_get_num_option("MESA_DISK_CACHE_DATABASE_EVICTION_SCORE_2X_PERIOD",
|
|
30 * 24 * 60 * 60) * nsec_per_sec;
|
|
|
|
return period;
|
|
}
|
|
|
|
double
|
|
mesa_cache_db_eviction_score(struct mesa_cache_db *db)
|
|
{
|
|
int64_t eviction_size = mesa_cache_db_eviction_size(db);
|
|
struct mesa_index_db_file_entry *index_entry;
|
|
struct sort_entry *entries;
|
|
unsigned num_entries, i = 0;
|
|
double eviction_score = 0;
|
|
|
|
if (!mesa_db_lock(db))
|
|
return 0;
|
|
|
|
if (!db->alive)
|
|
goto fail;
|
|
|
|
if (!mesa_db_reload(db))
|
|
goto fail_fatal;
|
|
|
|
num_entries = _mesa_hash_table_num_entries(db->index_db->table);
|
|
entries = calloc(num_entries, sizeof(*entries));
|
|
if (!entries)
|
|
goto fail;
|
|
|
|
for (i = 0, index_entry = db->index_entries; i < num_entries; i++)
|
|
entries[i].index_entry = index_entry++;
|
|
|
|
util_qsort_r(entries, num_entries, sizeof(*entries),
|
|
entry_sort_lru, db);
|
|
|
|
for (i = 0; eviction_size > 0 && i < num_entries; i++) {
|
|
index_entry = entries[i].index_entry;
|
|
uint64_t entry_age = os_time_get_nano() - index_entry->last_access_time;
|
|
unsigned entry_size = blob_file_size(index_entry->size);
|
|
|
|
/* Eviction score is a sum of weighted cache entry sizes,
|
|
* where weight doubles for each month of entry's age.
|
|
*/
|
|
uint64_t period = mesa_cache_db_eviction_2x_score_period();
|
|
double entry_scale = 1 + (double)entry_age / period;
|
|
double entry_score = entry_size * entry_scale;
|
|
|
|
eviction_score += entry_score;
|
|
eviction_size -= entry_size;
|
|
}
|
|
|
|
free(entries);
|
|
|
|
mesa_db_unlock(db);
|
|
|
|
return eviction_score;
|
|
|
|
fail_fatal:
|
|
mesa_db_zap(db);
|
|
fail:
|
|
mesa_db_unlock(db);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#endif /* DETECT_OS_WINDOWS */
|