From 3b69b67545b678da2970654b9490cc3902cdf738 Mon Sep 17 00:00:00 2001 From: Juston Li Date: Mon, 24 Oct 2022 20:03:56 +0000 Subject: [PATCH] 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 Part-of: --- docs/envvars.rst | 20 +++- src/util/fossilize_db.c | 200 +++++++++++++++++++++++++++++++++- src/util/fossilize_db.h | 9 ++ src/util/tests/cache_test.cpp | 170 +++++++++++++++++++++++++++++ 4 files changed, 391 insertions(+), 8 deletions(-) diff --git a/docs/envvars.rst b/docs/envvars.rst index fb563ceae8e..26186cc789b 100644 --- a/docs/envvars.rst +++ b/docs/envvars.rst @@ -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 diff --git a/src/util/fossilize_db.c b/src/util/fossilize_db.c index c462af437d2..25f4decba98 100644 --- a/src/util/fossilize_db.c +++ b/src/util/fossilize_db.c @@ -37,6 +37,8 @@ #include #include #include +#include +#include #include #include @@ -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++) { diff --git a/src/util/fossilize_db.h b/src/util/fossilize_db.h index e05aef9e4ca..90ebe526045 100644 --- a/src/util/fossilize_db.h +++ b/src/util/fossilize_db.h @@ -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 diff --git a/src/util/tests/cache_test.cpp b/src/util/tests/cache_test.cpp index 60e8627c754..1bc8411b903 100644 --- a/src/util/tests/cache_test.cpp +++ b/src/util/tests/cache_test.cpp @@ -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 +}