util/fossilize_db: add runtime RO foz db loading via FOZ_DBS_DYNAMIC_LIST

Add a new environment varible
MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMICE_LIST that specifies a text
file containing a list of RO fossilize caches to load. The list file
is modifiable at runtime to allow for loading RO caches after
initialization unlike MESA_DISK_CACHE_READ_ONLY_FOZ_DBS.

The implementation spawns an updater thread that uses inotify to monitor
the list file for modifications, attempting to load new foz dbs added to
the list. Removing files from the list will not evict a loaded cache.

MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST takes an absolute path.
The file must exist at initialization for updating to occur.

File names of foz dbs in the list file are new-line separated and take
relative paths to the default cache directory like
MESA_DISK_CACHE_READ_ONLY_FOZ_DBS.

The maximum number of RO foz dbs is kept to 8 and is shared between
MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST and
MESA_DISK_CACHE_READ_ONLY_FOZ_DBS.

The intended use case for this feature is to allow prebuilt caches
to be downloaded and loaded asynchronously during app runtime.
Prebuilt caches be large (several GB) and depending on network
conditions would otherwise present extended wait time for caches
to be availible before app launch.
This will be used in Chrome OS.

Signed-off-by: Juston Li <justonli@google.com>
Part-of: <https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/19328>
This commit is contained in:
Juston Li
2022-10-24 20:03:56 +00:00
committed by Marge Bot
parent eea2033b3e
commit 3b69b67545
4 changed files with 391 additions and 8 deletions

View File

@@ -207,7 +207,8 @@ Core Mesa environment variables
cache implementation instead of the default multi-file cache
implementation. This implementation reduces the overall disk usage by
the shader cache and also allows for loading of precompiled cache
DBs via :envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS`. This
DBs via :envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS` or
:envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST`. This
implementation does not support cache size limits via
:envvar:`MESA_SHADER_CACHE_MAX_SIZE`. If
:envvar:`MESA_SHADER_CACHE_DIR` is not set, the cache will be stored
@@ -223,7 +224,8 @@ Core Mesa environment variables
relative to the cache directory and do not include suffixes,
referencing both the cache DB and its index file. E.g.
MESA_DISK_CACHE_SINGLE_FILE=filename1 refers to filename1.foz and
filename1_idx.foz. A limit of 8 DBs can be loaded.
filename1_idx.foz. A limit of 8 DBs can be loaded and this limit is
shared with :envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST.`
.. envvar:: MESA_DISK_CACHE_DATABASE
@@ -237,6 +239,20 @@ Core Mesa environment variables
or else within ``.cache/mesa_shader_cache_db`` within the user's home
directory.
.. envvar:: MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST
if set with :envvar:`MESA_DISK_CACHE_SINGLE_FILE` enabled, references
a text file that contains a new-line separated list of read only
Fossilize DB shader caches to load. The list file is modifiable at
runtime to allow for loading read only caches after initialization
unlike :envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS`. This variable
takes an absolute path to the list file. The list file must exist at
initialization for updating to occur. Cache files in the list take
relative paths to the current cache directory like
:envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS`. A limit of 8 DBs can be
loaded and this limit is shared with
:envvar:`MESA_DISK_CACHE_READ_ONLY_FOZ_DBS`.
.. envvar:: MESA_DISK_CACHE_COMBINE_RW_WITH_RO_FOZ
if set to 1, enables simultaneous use of :abbr:`RW (read-write)` and

View File

@@ -37,6 +37,8 @@
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
@@ -92,7 +94,9 @@ check_files_opened_successfully(FILE *file, FILE *db_idx)
}
static bool
create_foz_db_filenames(char *cache_path, char *name, char **filename,
create_foz_db_filenames(const char *cache_path,
char *name,
char **filename,
char **idx_filename)
{
if (asprintf(filename, "%s/%s.foz", cache_path, name) == -1)
@@ -252,7 +256,16 @@ load_foz_dbs(struct foz_db *foz_db, FILE *db_idx, uint8_t file_idx,
flock(fileno(foz_db->file[file_idx]), LOCK_UN);
update_foz_index(foz_db, db_idx, file_idx);
if (foz_db->updater.thrd) {
/* If MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST is enabled, access to
* the foz_db hash table requires locking to prevent racing between this
* updated thread loading DBs at runtime and cache entry read/writes. */
simple_mtx_lock(&foz_db->mtx);
update_foz_index(foz_db, db_idx, file_idx);
simple_mtx_unlock(&foz_db->mtx);
} else {
update_foz_index(foz_db, db_idx, file_idx);
}
foz_db->alive = true;
return true;
@@ -263,7 +276,7 @@ fail:
}
static void
load_foz_dbs_ro(struct foz_db *foz_db, char *foz_dbs_ro, char *cache_path)
load_foz_dbs_ro(struct foz_db *foz_db, char *foz_dbs_ro)
{
uint8_t file_idx = 1;
char *filename = NULL;
@@ -275,8 +288,8 @@ load_foz_dbs_ro(struct foz_db *foz_db, char *foz_dbs_ro, char *cache_path)
filename = NULL;
idx_filename = NULL;
if (!create_foz_db_filenames(cache_path, foz_db_filename, &filename,
&idx_filename)) {
if (!create_foz_db_filenames(foz_db->cache_path, foz_db_filename,
&filename, &idx_filename)) {
free(foz_db_filename);
continue; /* Ignore invalid user provided filename and continue */
}
@@ -312,6 +325,165 @@ load_foz_dbs_ro(struct foz_db *foz_db, char *foz_dbs_ro, char *cache_path)
}
}
static bool
check_file_already_loaded(struct foz_db *foz_db,
FILE *db_file,
uint8_t max_file_idx)
{
struct stat new_file_stat;
if (fstat(fileno(db_file), &new_file_stat) == -1)
return false;
for (int i = 0; i < max_file_idx; i++) {
struct stat loaded_file_stat;
if (fstat(fileno(foz_db->file[i]), &loaded_file_stat) == -1)
continue;
if ((loaded_file_stat.st_dev == new_file_stat.st_dev) &&
(loaded_file_stat.st_ino == new_file_stat.st_ino))
return true;
}
return false;
}
static bool
load_from_list_file(struct foz_db *foz_db, const char *foz_dbs_list_filename)
{
uint8_t file_idx;
char list_entry[PATH_MAX];
/* Find the first empty file idx slot */
for (file_idx = 0; file_idx < FOZ_MAX_DBS; file_idx++) {
if (!foz_db->file[file_idx])
break;
}
if (file_idx >= FOZ_MAX_DBS)
return false;
FILE *foz_dbs_list_file = fopen(foz_dbs_list_filename, "rb");
if (!foz_dbs_list_file)
return false;
while (fgets(list_entry, sizeof(list_entry), foz_dbs_list_file)) {
char *db_filename = NULL;
char *idx_filename = NULL;
FILE *db_file = NULL;
FILE *idx_file = NULL;
list_entry[strcspn(list_entry, "\n")] = '\0';
if (!create_foz_db_filenames(foz_db->cache_path, list_entry,
&db_filename, &idx_filename))
continue;
db_file = fopen(db_filename, "rb");
idx_file = fopen(idx_filename, "rb");
free(db_filename);
free(idx_filename);
if (!check_files_opened_successfully(db_file, idx_file))
continue;
if (check_file_already_loaded(foz_db, db_file, file_idx)) {
fclose(db_file);
fclose(idx_file);
continue;
}
/* Must be set before calling load_foz_dbs() */
foz_db->file[file_idx] = db_file;
if (!load_foz_dbs(foz_db, idx_file, file_idx, true)) {
fclose(db_file);
fclose(idx_file);
foz_db->file[file_idx] = NULL;
continue;
}
fclose(idx_file);
file_idx++;
if (file_idx >= FOZ_MAX_DBS)
break;
}
fclose(foz_dbs_list_file);
return true;
}
static int
foz_dbs_list_updater_thrd(void *data)
{
char buf[10 * (sizeof(struct inotify_event) + NAME_MAX + 1)];
struct foz_db *foz_db = data;
struct foz_dbs_list_updater *updater = &foz_db->updater;
while (1) {
int len = read(updater->inotify_fd, buf, sizeof(buf));
if (len == -1 && errno != EAGAIN)
return errno;
int i = 0;
while (i < len) {
struct inotify_event *event = (struct inotify_event *)&buf[i];
i += sizeof(struct inotify_event) + event->len;
if (event->mask & IN_CLOSE_WRITE)
load_from_list_file(foz_db, foz_db->updater.list_filename);
/* List file deleted or watch removed by foz destroy */
if ((event->mask & IN_DELETE_SELF) || (event->mask & IN_IGNORED))
return 0;
}
}
return 0;
}
static bool
foz_dbs_list_updater_init(struct foz_db *foz_db, char *list_filename)
{
struct foz_dbs_list_updater *updater = &foz_db->updater;
/* Initial load */
if (!load_from_list_file(foz_db, list_filename))
return false;
updater->list_filename = list_filename;
int fd = inotify_init1(IN_CLOEXEC);
if (fd < 0)
return false;
int wd = inotify_add_watch(fd, foz_db->updater.list_filename,
IN_CLOSE_WRITE | IN_DELETE_SELF);
if (wd < 0) {
close(fd);
return false;
}
updater->inotify_fd = fd;
updater->inotify_wd = wd;
if (thrd_create(&updater->thrd, foz_dbs_list_updater_thrd, foz_db)) {
inotify_rm_watch(fd, wd);
close(fd);
return false;
}
return true;
}
/* Here we open mesa cache foz dbs files. If the files exist we load the index
* db into a hash table. The index db contains the offsets needed to later
* read cache entries from the foz db containing the actual cache entries.
@@ -326,6 +498,7 @@ foz_prepare(struct foz_db *foz_db, char *cache_path)
simple_mtx_init(&foz_db->flock_mtx, mtx_plain);
foz_db->mem_ctx = ralloc_context(NULL);
foz_db->index_db = _mesa_hash_table_u64_create(NULL);
foz_db->cache_path = cache_path;
/* Open the default foz dbs for read/write. If the files didn't already exist
* create them.
@@ -350,7 +523,12 @@ foz_prepare(struct foz_db *foz_db, char *cache_path)
char *foz_dbs_ro = getenv("MESA_DISK_CACHE_READ_ONLY_FOZ_DBS");
if (foz_dbs_ro)
load_foz_dbs_ro(foz_db, foz_dbs_ro, cache_path);
load_foz_dbs_ro(foz_db, foz_dbs_ro);
char *foz_dbs_list =
getenv("MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST");
if (foz_dbs_list)
foz_dbs_list_updater_init(foz_db, foz_dbs_list);
return true;
@@ -363,6 +541,16 @@ fail:
void
foz_destroy(struct foz_db *foz_db)
{
struct foz_dbs_list_updater *updater = &foz_db->updater;
if (updater->thrd) {
inotify_rm_watch(updater->inotify_fd, updater->inotify_wd);
/* inotify_rm_watch() triggers the IN_IGNORE event for the thread
* to exit.
*/
thrd_join(updater->thrd, NULL);
close(updater->inotify_fd);
}
if (foz_db->db_idx)
fclose(foz_db->db_idx);
for (unsigned i = 0; i < FOZ_MAX_DBS; i++) {

View File

@@ -72,6 +72,13 @@ struct foz_db_entry {
struct foz_payload_header header;
};
struct foz_dbs_list_updater {
int inotify_fd;
int inotify_wd; /* watch descriptor */
const char *list_filename;
thrd_t thrd;
};
struct foz_db {
FILE *file[FOZ_MAX_DBS]; /* An array of all foz dbs */
FILE *db_idx; /* The default writable foz db idx */
@@ -80,6 +87,8 @@ struct foz_db {
void *mem_ctx;
struct hash_table_u64 *index_db; /* Hash table of all foz db entries */
bool alive;
const char *cache_path;
struct foz_dbs_list_updater updater;
};
bool

View File

@@ -126,6 +126,24 @@ cache_exists(struct disk_cache *cache)
return result != NULL;
}
static void *
poll_disk_cache_get(struct disk_cache *cache,
const cache_key key,
size_t *size)
{
void *result;
for (int iter = 0; iter < 1000; ++iter) {
result = disk_cache_get(cache, key, size);
if (result)
return result;
usleep(1000);
}
return NULL;
}
#define CACHE_TEST_TMP "./cache-test-tmp"
static void
@@ -994,3 +1012,155 @@ TEST_F(Cache, Combined)
EXPECT_EQ(err, 0) << "Removing " CACHE_TEST_TMP " again";
#endif
}
TEST_F(Cache, List)
{
const char *driver_id = "make_check";
char blob[] = "This is a RO blob";
uint8_t blob_key[20];
char foz_rw_idx_file[1024];
char foz_ro_idx_file[1024];
char foz_rw_file[1024];
char foz_ro_file[1024];
char *result;
size_t size;
#ifndef ENABLE_SHADER_CACHE
GTEST_SKIP() << "ENABLE_SHADER_CACHE not defined.";
#else
setenv("MESA_DISK_CACHE_SINGLE_FILE", "true", 1);
#ifdef SHADER_CACHE_DISABLE_BY_DEFAULT
setenv("MESA_SHADER_CACHE_DISABLE", "false", 1);
#endif /* SHADER_CACHE_DISABLE_BY_DEFAULT */
test_disk_cache_create(mem_ctx, CACHE_DIR_NAME_SF, driver_id);
/* Create ro files for testing */
/* Create Fossilize writable cache. */
struct disk_cache *cache_sf_wr =
disk_cache_create("list_test", driver_id, 0);
disk_cache_compute_key(cache_sf_wr, blob, sizeof(blob), blob_key);
/* Ensure that disk_cache_get returns nothing before anything is added. */
result = (char *)disk_cache_get(cache_sf_wr, blob_key, &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)";
/* Put blob entry to the cache. */
disk_cache_put(cache_sf_wr, blob_key, blob, sizeof(blob), NULL);
disk_cache_wait_for_idle(cache_sf_wr);
result = (char *)disk_cache_get(cache_sf_wr, blob_key, &size);
EXPECT_STREQ(blob, result) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, sizeof(blob)) << "disk_cache_get of existing item (size)";
free(result);
/* Rename file foz_cache.foz -> ro_cache.foz */
sprintf(foz_rw_file, "%s/foz_cache.foz", cache_sf_wr->path);
sprintf(foz_ro_file, "%s/ro_cache.foz", cache_sf_wr->path);
EXPECT_EQ(rename(foz_rw_file, foz_ro_file), 0)
<< "foz_cache.foz renaming failed";
/* Rename file foz_cache_idx.foz -> ro_cache_idx.foz */
sprintf(foz_rw_idx_file, "%s/foz_cache_idx.foz", cache_sf_wr->path);
sprintf(foz_ro_idx_file, "%s/ro_cache_idx.foz", cache_sf_wr->path);
EXPECT_EQ(rename(foz_rw_idx_file, foz_ro_idx_file), 0)
<< "foz_cache_idx.foz renaming failed";
disk_cache_destroy(cache_sf_wr);
const char *list_filename = CACHE_TEST_TMP "/foz_dbs_list.txt";
setenv("MESA_DISK_CACHE_READ_ONLY_FOZ_DBS_DYNAMIC_LIST", list_filename, 1);
/* Create new empty file */
FILE *list_file = fopen(list_filename, "w");
fputs("ro_cache\n", list_file);
fclose(list_file);
/* Create Fossilize writable cache. */
struct disk_cache *cache_sf = disk_cache_create("list_test", driver_id, 0);
/* Blob entry must present because it shall be retrieved from the
* ro_cache.foz loaded from list at creation time */
result = (char *)disk_cache_get(cache_sf, blob_key, &size);
EXPECT_STREQ(blob, result) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, sizeof(blob)) << "disk_cache_get of existing item (size)";
free(result);
disk_cache_destroy(cache_sf);
remove(list_filename);
/* Test loading from a list populated at runtime */
/* Create new empty file */
list_file = fopen(list_filename, "w");
fclose(list_file);
/* Create Fossilize writable cache. */
cache_sf = disk_cache_create("list_test", driver_id, 0);
/* Ensure that disk_cache returns nothing before list file is populated */
result = (char *)disk_cache_get(cache_sf, blob_key, &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)";
/* Add ro_cache to list file for loading */
list_file = fopen(list_filename, "a");
fputs("ro_cache\n", list_file);
fclose(list_file);
/* Poll result to give time for updater to load ro cache */
result = (char *)poll_disk_cache_get(cache_sf, blob_key, &size);
/* Blob entry must present because it shall be retrieved from the
* ro_cache.foz loaded from list at runtime */
EXPECT_STREQ(blob, result) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, sizeof(blob)) << "disk_cache_get of existing item (size)";
free(result);
disk_cache_destroy(cache_sf);
remove(list_filename);
/* Test loading from a list with some invalid files */
/* Create new empty file */
list_file = fopen(list_filename, "w");
fclose(list_file);
/* Create Fossilize writable cache. */
cache_sf = disk_cache_create("list_test", driver_id, 0);
/* Ensure that disk_cache returns nothing before list file is populated */
result = (char *)disk_cache_get(cache_sf, blob_key, &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)";
/* Add non-existant list files for loading */
list_file = fopen(list_filename, "a");
fputs("no_cache\n", list_file);
fputs("no_cache2\n", list_file);
fputs("no_cache/no_cache3\n", list_file);
/* Add ro_cache to list file for loading */
fputs("ro_cache\n", list_file);
fclose(list_file);
/* Poll result to give time for updater to load ro cache */
result = (char *)poll_disk_cache_get(cache_sf, blob_key, &size);
/* Blob entry must present because it shall be retrieved from the
* ro_cache.foz loaded from list at runtime despite invalid files
* in the list */
EXPECT_STREQ(blob, result) << "disk_cache_get of existing item (pointer)";
EXPECT_EQ(size, sizeof(blob)) << "disk_cache_get of existing item (size)";
free(result);
disk_cache_destroy(cache_sf);
remove(list_filename);
int err = rmrf_local(CACHE_TEST_TMP);
EXPECT_EQ(err, 0) << "Removing " CACHE_TEST_TMP " again";
#endif
}