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 +}