vo_gpu_next: save cache to separate files

Save the cache to separate files to avoid loading/saving a huge combined
libplacebo.cache. This approach allows the saving of only new cache
objects and avoids resaving the entire cache, especially even if only a
tiny change was made.

This commit improves the cold start time of mpv and avoids saving data
when it's not necessary.

Number of changes were made:
- each cached object is saved in its own file
- cache files are prefixed with the name of cached object
- cache directory is cleaned on each uninit
    - the least recently used cache files are removed if cumulative cache
      size is above limit
    - files used in the recent 24 hours are not removed to allow changes
      to mpv.conf without worrying about the cache being removed during
      experimentation
- shader cache size limit is set to 128 MiB
- icc cache size limit is set to 1.5 GiB
- cache objects are loaded/saved as needed

This commit eliminates the runtime performance penalty associated with
the size cache. While we continue to maintain the cache limit to prevent
retaining stale objects, mpv now only loads a small subset of files that
are currently required for playback, instead of loading all files.
This commit is contained in:
Kacper Michajłow 2023-11-17 19:31:51 +01:00 committed by Dudemanguy
parent a59b8edb96
commit 69891c4070
2 changed files with 194 additions and 55 deletions

View File

@ -6869,8 +6869,11 @@ them.
files contain uncompressed LUTs. Their size depends on the
``--icc-3dlut-size``, and can be very big.
NOTE: On ``--vo=gpu``, this is not cleaned automatically, so old, unused
cache files may stick around indefinitely.
On `--vo=gpu-next`, files that have not been accessed in the last 24 hours
may be cleared if the cache limit (1.5 GiB) is exceeded.
On ``--vo=gpu``, this is not cleaned automatically, so old, unused cache
files may stick around indefinitely.
``--icc-cache-dir``
The directory where icc cache is stored. Cache is stored in the system's
@ -7018,11 +7021,14 @@ them.
``--gpu-shader-cache``
Store and load compiled GLSL shaders in the cache directory (Default:
``yes``). Normally, shader compilation is very fast, so this is not usually
needed. It mostly matters for anything based on D3D11 (including ANGLE), as
well as on some other proprietary drivers. Enabling this can improve startup
performance on these platforms.
needed. It mostly matters for anything involving GLSL to SPIR-V conversion,
that is: D3D11, ANGLE or Vulkan, as well as on some other proprietary
drivers. Enabling this can improve startup performance on these platforms.
NOTE: On ``--vo=gpu``, is not cleaned automatically, so old, unused cache
On `--vo=gpu-next`, files that have not been accessed in the last 24 hours
may be cleared if the cache limit (128 MiB) is exceeded.
On ``--vo=gpu``, this is not cleaned automatically, so old, unused cache
files may stick around indefinitely.
``--gpu-shader-cache-dir``

View File

@ -17,6 +17,9 @@
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
*/
#include <dirent.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <libplacebo/colorspace.h>
@ -90,9 +93,12 @@ struct frame_info {
};
struct cache {
char *path;
struct mp_log *log;
struct mpv_global *global;
char *dir;
const char *name;
size_t size_limit;
pl_cache cache;
uint64_t sig;
};
struct priv {
@ -1499,84 +1505,211 @@ static void wait_events(struct vo *vo, int64_t until_time_ns)
}
}
#if PL_API_VER < 342
static inline void xor_hash(void *hash, pl_cache_obj obj)
static char *cache_filepath(void *ta_ctx, char *dir, const char *prefix, uint64_t key)
{
*((uint64_t *) hash) ^= obj.key;
bstr filename = {0};
bstr_xappend_asprintf(ta_ctx, &filename, "%s_%016" PRIx64, prefix, key);
return mp_path_join_bstr(ta_ctx, bstr0(dir), filename);
}
static inline uint64_t pl_cache_signature(pl_cache cache)
static void cache_save_file(void *ta_ctx, char *filepath, void *data, size_t size)
{
uint64_t hash = 0;
pl_cache_iterate(cache, xor_hash, &hash);
return hash;
if (!data || !size)
return;
char *tmp = talloc_asprintf(ta_ctx, "%sXXXXXX", filepath);
int fd = mkstemp(tmp);
if (fd < 0)
return;
FILE *cache = fdopen(fd, "wb");
if (!cache) {
close(fd);
unlink(tmp);
return;
}
size_t written = fwrite(data, size, 1, cache);
int ret = fclose(cache);
if (written > 0 && !ret) {
ret = rename(tmp, filepath);
} else {
unlink(tmp);
}
}
static pl_cache_obj cache_load_obj(void *p, uint64_t key)
{
struct cache *c = p;
void *ta_ctx = talloc_new(NULL);
pl_cache_obj obj = {0};
if (!c->dir)
goto done;
char *filepath = cache_filepath(ta_ctx, c->dir, c->name, key);
if (!filepath)
goto done;
if (stat(filepath, &(struct stat){0}))
goto done;
int64_t load_start = mp_time_ns();
struct bstr data = stream_read_file(filepath, ta_ctx, c->global, STREAM_MAX_READ_SIZE);
int64_t load_end = mp_time_ns();
MP_DBG(c, "%s: key(%" PRIx64 "), size(%zu), load time(%.3f ms)\n",
__func__, key, data.len,
MP_TIME_NS_TO_MS(load_end - load_start));
obj = (pl_cache_obj){
.key = key,
.data = talloc_steal(NULL, data.start),
.size = data.len,
.free = talloc_free,
};
done:
talloc_free(ta_ctx);
return obj;
}
static void cache_save_obj(void *p, pl_cache_obj obj)
{
const struct cache *c = p;
void *ta_ctx = talloc_new(NULL);
if (!c->dir)
goto done;
char *filepath = cache_filepath(ta_ctx, c->dir, c->name, obj.key);
if (!filepath)
goto done;
// Don't save if already exists
if (!stat(filepath, &(struct stat){0})) {
MP_DBG(c, "%s: key(%"PRIx64"), size(%zu)\n", __func__, obj.key, obj.size);
goto done;
}
int64_t save_start = mp_time_ns();
cache_save_file(ta_ctx, filepath, obj.data, obj.size);
int64_t save_end = mp_time_ns();
MP_DBG(c, "%s: key(%" PRIx64 "), size(%zu), save time(%.3f ms)\n",
__func__, obj.key, obj.size,
MP_TIME_NS_TO_MS(save_end - save_start));
done:
talloc_free(ta_ctx);
}
#endif
static void cache_init(struct vo *vo, struct cache *cache, size_t max_size,
const char *dir_opt)
{
struct priv *p = vo->priv;
const char *name = cache == &p->shader_cache ? "shader.cache" : "icc.cache";
const char *name = cache == &p->shader_cache ? "shader" : "icc";
const size_t limit = cache == &p->shader_cache ? 128 << 20 : 1536 << 20;
char *dir;
if (dir_opt && dir_opt[0]) {
dir = mp_get_user_path(NULL, p->global, dir_opt);
dir = mp_get_user_path(vo, p->global, dir_opt);
} else {
dir = mp_find_user_file(NULL, p->global, "cache", "");
dir = mp_find_user_file(vo, p->global, "cache", "");
}
if (!dir || !dir[0])
goto done;
return;
mp_mkdirp(dir);
cache->path = mp_path_join(vo, dir, name);
cache->cache = pl_cache_create(pl_cache_params(
.log = p->pllog,
.max_total_size = max_size,
));
*cache = (struct cache){
.log = p->log,
.global = p->global,
.dir = dir,
.name = name,
.size_limit = limit,
.cache = pl_cache_create(pl_cache_params(
.log = p->pllog,
.get = cache_load_obj,
.set = cache_save_obj,
.priv = cache
)),
};
}
FILE *file = fopen(cache->path, "rb");
if (file) {
int ret = pl_cache_load_file(cache->cache, file);
fclose(file);
if (ret < 0)
MP_WARN(p, "Failed loading cache from %s\n", cache->path);
}
struct file_entry {
char *filepath;
size_t size;
time_t atime;
};
cache->sig = pl_cache_signature(cache->cache);
done:
talloc_free(dir);
static int compare_atime(const void *a, const void *b)
{
return (((struct file_entry *)b)->atime - ((struct file_entry *)a)->atime);
}
static void cache_uninit(struct priv *p, struct cache *cache)
{
if (!cache->cache)
goto done;
if (pl_cache_signature(cache->cache) == cache->sig)
goto done; // skip re-saving identical cache
return;
assert(cache->path);
char *tmp = talloc_asprintf(cache->path, "%sXXXXXX", cache->path);
int fd = mkstemp(tmp);
if (fd < 0)
goto done;
FILE *file = fdopen(fd, "wb");
if (!file) {
close(fd);
unlink(tmp);
void *ta_ctx = talloc_new(NULL);
struct file_entry *files = NULL;
size_t num_files = 0;
assert(cache->dir);
assert(cache->name);
DIR *d = opendir(cache->dir);
if (!d)
goto done;
struct dirent *dir;
while ((dir = readdir(d)) != NULL) {
char *filepath = mp_path_join(ta_ctx, cache->dir, dir->d_name);
if (!filepath)
continue;
struct stat filestat;
if (stat(filepath, &filestat))
continue;
if (!S_ISREG(filestat.st_mode))
continue;
bstr fname = bstr0(dir->d_name);
if (!bstr_eatstart0(&fname, cache->name))
continue;
if (!bstr_eatstart0(&fname, "_"))
continue;
if (fname.len != 16) // %016x
continue;
MP_TARRAY_APPEND(ta_ctx, files, num_files,
(struct file_entry){
.filepath = filepath,
.size = filestat.st_size,
.atime = filestat.st_atime,
});
}
int ret = pl_cache_save_file(cache->cache, file);
fclose(file);
if (ret >= 0)
ret = rename(tmp, cache->path);
if (ret < 0) {
MP_WARN(p, "Failed saving cache to %s\n", cache->path);
unlink(tmp);
closedir(d);
if (!num_files)
goto done;
qsort(files, num_files, sizeof(struct file_entry), compare_atime);
time_t t = time(NULL);
size_t cache_size = 0;
size_t cache_limit = cache->size_limit ? cache->size_limit : SIZE_MAX;
for (int i = 0; i < num_files; i++) {
// Remove files that exceed the size limit but are older than one day.
// This allows for temporary maintaining a larger cache size while
// adjusting the configuration. The cache will be cleared the next day
// for unused entries. We don't need to be overly aggressive with cache
// cleaning; in most cases, it will not grow much, and in others, it may
// actually be useful to cache more.
cache_size += files[i].size;
double rel_use = difftime(t, files[i].atime);
if (cache_size > cache_limit && rel_use > 60 * 60 * 24) {
MP_VERBOSE(p, "Removing %s | size: %9zu bytes | last used: %9d seconds ago\n",
files[i].filepath, files[i].size, (int)rel_use);
unlink(files[i].filepath);
}
}
// fall through
done:
talloc_free(ta_ctx);
pl_cache_destroy(&cache->cache);
}