From f414cd874b33daffa4a40cb55a0b29f5375741c8 Mon Sep 17 00:00:00 2001 From: jacob Date: Tue, 30 Apr 2024 14:04:18 -0500 Subject: [PATCH] sheet eviction when over memory budget --- src/game.c | 3 +- src/sheet.c | 286 ++++++++++++++++++++++++------------------------ src/sys_win32.c | 2 + src/work.c | 6 +- 4 files changed, 151 insertions(+), 146 deletions(-) diff --git a/src/game.c b/src/game.c index e020f9c2..55ba0970 100644 --- a/src/game.c +++ b/src/game.c @@ -9,6 +9,7 @@ #include "scratch.h" #include "atomic.h" #include "app.h" +#include "log.h" GLOBAL struct { struct atomic_i32 game_thread_shutdown; @@ -305,7 +306,6 @@ INTERNAL void game_update(void) e->valid = true; e->rel_xform = XFORM_IDENT; - entity_enable_prop(e, ENTITY_PROP_CAMERA); entity_enable_prop(e, ENTITY_PROP_CAMERA_ACTIVE); e->camera_follow = player_ent->handle; @@ -373,6 +373,7 @@ INTERNAL void game_update(void) /* Clear level */ case GAME_CMD_KIND_CLEAR_ALL: { + logf_info("Clearing level"); for (u64 i = 0; i < entities_array.count; ++i) { struct entity *ent = &entities_array.entities[i]; if (ent->valid) { diff --git a/src/sheet.c b/src/sheet.c index aa5f4742..61988414 100644 --- a/src/sheet.c +++ b/src/sheet.c @@ -20,12 +20,10 @@ #define MAX_LOADER_THREADS 4 -#if 0 /* Size of cache memory until evictor starts evicting */ #define CACHE_MEMORY_BUDGET MEGABYTE(8) -#endif -/* How long between evictor thread checks */ +/* How long between evictor thread scans */ #define EVICTOR_CHECK_INTERVAl 0.500 /* Time a cache entry spends unused it's considered evictable */ @@ -52,8 +50,7 @@ enum cache_node_state { CACHE_NODE_STATE_NONE, CACHE_NODE_STATE_QUEUED, CACHE_NODE_STATE_WORKING, - CACHE_NODE_STATE_LOADED, - CACHE_NODE_STATE_EVICTED + CACHE_NODE_STATE_LOADED }; struct cache_node { @@ -63,6 +60,7 @@ struct cache_node { struct atomic_u64 last_refcount0_ts; /* Last time that refcount reached 0 */ /* Allocated data */ + u64 memory_usage; struct arena arena; struct sheet *sheet; @@ -86,6 +84,7 @@ struct cache_bucket { }; struct cache { + struct atomic_u64 memory_usage; struct arena arena; struct cache_bucket *buckets; struct sys_mutex node_pool_mutex; @@ -325,6 +324,8 @@ INTERNAL void sheet_load(struct cache_node *n, struct sheet_tag tag) } } arena_set_readonly(&n->arena); + n->memory_usage = n->arena.committed; + atomic_u64_eval_add(&G.cache.memory_usage, n->memory_usage); logf_info("Finished loading sheet \"%F\" in %F seconds (final size: %F bytes).", FMT_STR(path), @@ -579,7 +580,6 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sheet_loader_thread_entry_point, arg) (UNUSED)arg; while (true) { - sys_mutex_lock(&G.loaders_mutex); if (G.loaders_shutdown) { @@ -588,17 +588,11 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sheet_loader_thread_entry_point, arg) break; } else if (!G.first_loader_cmd) { /* Wait for work */ -#if 1 sys_condition_variable_wait(&G.loaders_cv, &G.loaders_mutex); -#else - sys_mutex_unlock(&G.loaders_mutex); - sys_sleep(0.5); - sys_mutex_lock(&G.loaders_mutex); -#endif } - if (G.first_loader_cmd) { + while (G.first_loader_cmd && !G.loaders_shutdown) { /* Pull cmd from queue */ struct loader_cmd *cmd = G.first_loader_cmd; G.first_loader_cmd = cmd->next; @@ -626,127 +620,145 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sheet_loader_thread_entry_point, arg) * Evictor thread * ========================== */ +struct evict_node { + b32 force_evict; + u64 last_used_ts; + struct cache_node *cache_node; + struct cache_bucket *cache_bucket; + struct evict_node *next_unsorted; + struct evict_node *next_sorted; + struct evict_node *next_evicted; + +}; + INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sheet_evictor_thread_entry_point, arg) { (UNUSED)arg; - struct evict_node { - b32 evicted; - struct cache_node *cache_node; - struct evict_node *next; - }; - - struct evict_bucket { - b32 contains_evicted_node; - struct cache_bucket *cache_bucket; - struct evict_node *head; - struct evict_bucket *next; - }; - while (true) { struct temp_arena scratch = scratch_begin_no_conflict(); - struct evict_bucket *head_evict_bucket = NULL; - b32 any_node_evicted = false; + struct evict_node *head_unsorted = NULL; + struct evict_node *oldest_sorted = NULL; + struct evict_node *head_evicted = NULL; - b32 abort_thread_loop = false; + b32 abort = false; sys_mutex_lock(&G.evictor_mutex); { + /* Thread shutdown */ if (G.evictor_shutdown) { - /* Thread shutdown */ - abort_thread_loop = true; - goto abort_thread_loop; + abort = true; + } else { + /* Wait */ + sys_condition_variable_wait_time(&G.evictor_cv, &G.evictor_mutex, EVICTOR_CHECK_INTERVAl); } - /* Wait */ - sys_condition_variable_wait_time(&G.evictor_cv, &G.evictor_mutex, EVICTOR_CHECK_INTERVAl); + if (!G.evictor_shutdown) { + sys_timestamp_t cur_timestamp = sys_timestamp(); + f64 cur_time = sys_timestamp_seconds(cur_timestamp); - sys_timestamp_t cur_timestamp = sys_timestamp(); - f64 cur_time = sys_timestamp_seconds(cur_timestamp); + b32 cache_over_budget = atomic_u64_eval(&G.cache.memory_usage) > CACHE_MEMORY_BUDGET; - /* Scan for evictable nodes */ - { - __profscope(eviction_scan); + /* Scan for evictable nodes */ + { + __profscope(eviction_scan); - for (u64 i = 0; i < CACHE_BUCKETS_COUNT; ++i) { - struct cache_bucket *bucket = &G.cache.buckets[i]; - struct evict_node *head_evict_node = NULL; - sys_rw_mutex_lock_shared(&bucket->rw_mutex); - { - struct cache_node *n = bucket->first; - while (n) { - b32 should_evict_node = false; - - if ((atomic_u32_eval(&n->state) == CACHE_NODE_STATE_LOADED) && (atomic_i32_eval(&n->refcount) <= 0)) { - /* Check usage time */ - if (!should_evict_node) { - /* TODO: Only evict if over memory budget (in LRU order until under memory budget) */ - sys_timestamp_t last_used_ts = atomic_u64_raw(&n->last_refcount0_ts); - f64 last_used_time = sys_timestamp_seconds(last_used_ts); - if (cur_time - last_used_time > EVICTOR_GRACE_PERIOD) { - /* Cache entry unused for too long */ - should_evict_node = true; - } - } + for (u64 i = 0; i < CACHE_BUCKETS_COUNT; ++i) { + struct cache_bucket *bucket = &G.cache.buckets[i]; + sys_rw_mutex_lock_shared(&bucket->rw_mutex); + { + struct cache_node *n = bucket->first; + while (n) { + b32 consider_for_eviction = false; + b32 force_evict = false; + if (atomic_u32_eval(&n->state) == CACHE_NODE_STATE_LOADED) { + sys_timestamp_t last_used_ts = atomic_u64_eval(&n->last_refcount0_ts); + if (atomic_i32_eval(&n->refcount) <= 0) { #if RESOURCE_RELOADING - /* Check for file changes for resource reloading */ - if (!should_evict_node) { - should_evict_node = true; - struct string path = string_from_cstr_len((char *)n->tag_path, n->tag_path_len); - if (sys_is_file(path)) { - struct sys_file file = sys_file_open_read(path); - struct sys_file_time ft = sys_file_get_time(file); - sys_file_close(file); + /* Check if file changed for resource reloading */ + if (!consider_for_eviction) { + struct string path = string_from_cstr_len((char *)n->tag_path, n->tag_path_len); + if (!sys_is_file(path)) { + consider_for_eviction = true; + } else { + struct sys_file file = sys_file_open_read(path); + struct sys_file_time ft = sys_file_get_time(file); + sys_file_close(file); - struct sys_datetime initial_file_time = n->initial_resource_file_modified_time; - struct sys_datetime current_file_time = ft.modified; + struct sys_datetime initial_file_time = n->initial_resource_file_modified_time; + struct sys_datetime current_file_time = ft.modified; - if (MEMCMP_STRUCT(&initial_file_time, ¤t_file_time) != 0) { - logf_info("Resource file for sheet \"%F\" has changed. Evicting to allow for reloading.", FMT_STR(path)); - should_evict_node = true; - } else { - /* File unchanged */ - should_evict_node = false; + if (MEMCMP_STRUCT(&initial_file_time, ¤t_file_time) != 0) { + logf_info("Resource file for sheet \"%F\" has changed. Evicting to allow for reloading.", FMT_STR(path)); + consider_for_eviction = true; + force_evict = true; + } + } } - } - } #endif - /* Add node to bucket evict list */ - if (should_evict_node) { - struct evict_node *evict_node = arena_push_zero(scratch.arena, struct evict_node); - evict_node->cache_node = n; - evict_node->next = head_evict_node; - head_evict_node = evict_node; - } - n = n->next_hash; + /* Check usage time */ + if (!consider_for_eviction && cache_over_budget && (cur_time - sys_timestamp_seconds(last_used_ts) > EVICTOR_GRACE_PERIOD)) { + /* Cache is over budget and node hasn't been referenced in a while */ + consider_for_eviction = true; + } + + } + + /* Add node to evict list */ + if (consider_for_eviction) { + struct evict_node *evict_node = arena_push_zero(scratch.arena, struct evict_node); + evict_node->cache_node = n; + evict_node->cache_bucket = bucket; + evict_node->last_used_ts = last_used_ts; + evict_node->force_evict = force_evict; + evict_node->next_unsorted = head_unsorted; + head_unsorted = evict_node; + } + + n = n->next_hash; + } } + sys_rw_mutex_unlock_shared(&bucket->rw_mutex); } } - sys_rw_mutex_unlock_shared(&bucket->rw_mutex); + } - /* Add bucket with nodes to evict list */ - if (head_evict_node) { - struct evict_bucket *evict_bucket = arena_push_zero(scratch.arena, struct evict_bucket); - evict_bucket->cache_bucket = bucket; - evict_bucket->head = head_evict_node; - evict_bucket->next = head_evict_bucket; - head_evict_bucket = evict_bucket; + /* Sort evict nodes by usage time */ + { + /* TODO: Optimize sort if necessary. Currently O(n^2). */ + __profscope(eviction_sort); + for (struct evict_node *en = head_unsorted; en; en = en->next_unsorted) { + sys_timestamp_t ts = en->last_used_ts; + struct evict_node *prev = NULL; + struct evict_node *next = oldest_sorted; + while (next && !(ts <= next->last_used_ts || en->force_evict)) { + prev = next; + next = next->next_sorted; + } + if (prev) { + prev->next_sorted = en; + } else { + oldest_sorted = en; + } + en->next_sorted = next; } } - } - /* Remove evictable nodes from cache table */ - { - __profscope(eviction_cache_removal); - for (struct evict_bucket *eb = head_evict_bucket; eb; eb = eb->next) { - struct cache_bucket *bucket = eb->cache_bucket; - sys_rw_mutex_lock_exclusive(&bucket->rw_mutex); - { - for (struct evict_node *en = eb->head; en; en = en->next) { - struct cache_node *n = en->cache_node; - /* Check that cache node is still loaded and unreferenced */ - if ((atomic_u32_eval(&n->state) == CACHE_NODE_STATE_LOADED) && (atomic_i32_eval(&n->refcount) <= 0)) { + /* Remove evictable nodes from cache table until under budget */ + { + __profscope(eviction_cache_removal); + for (struct evict_node *en = oldest_sorted; en; en = en->next_sorted) { + struct cache_bucket *bucket = en->cache_bucket; + struct cache_node *n = en->cache_node; + + sys_rw_mutex_lock_exclusive(&bucket->rw_mutex); + { + if (*atomic_i32_raw(&n->refcount) > 0 || (!en->force_evict && (*atomic_u64_raw(&n->last_refcount0_ts) != en->last_used_ts))) { + /* Cache node has been referenced since scan, skip eviction. */ + continue; + } + if (en->force_evict || atomic_u64_eval(&G.cache.memory_usage) > CACHE_MEMORY_BUDGET) { /* Remove from cache table */ if (n->prev_hash) { n->prev_hash->next_hash = n->next_hash; @@ -756,55 +768,47 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sheet_evictor_thread_entry_point, arg) if (n->next_hash) { n->next_hash->prev_hash = n->prev_hash; } - en->evicted = true; - eb->contains_evicted_node = true; - any_node_evicted = true; - } - } - } - sys_rw_mutex_unlock_exclusive(&bucket->rw_mutex); - } - } - - if (any_node_evicted) { - /* Release evicted node memory */ - __profscope(eviction_memory_release); - for (struct evict_bucket *eb = head_evict_bucket; eb; eb = eb->next) { - if (eb->contains_evicted_node) { - for (struct evict_node *en = eb->head; en; en = en->next) { - if (en->evicted) { - struct cache_node *n = en->cache_node; - arena_release(&n->arena); + atomic_u64_eval_add(&G.cache.memory_usage, -((i64)n->memory_usage)); + /* Add to evicted list */ + en->next_evicted = head_evicted; + head_evicted = en; + } else { + /* Cache is no longer over budget or force evicting, stop iteration */ + break; } } + sys_rw_mutex_unlock_exclusive(&bucket->rw_mutex); } } - /* Add evicted nodes to free list */ - __profscope(eviction_free_list_append); - sys_mutex_lock(&G.cache.node_pool_mutex); - { - for (struct evict_bucket *eb = head_evict_bucket; eb; eb = eb->next) { - if (eb->contains_evicted_node) { - for (struct evict_node *en = eb->head; en; en = en->next) { - if (en->evicted) { - struct cache_node *n = en->cache_node; - n->next_free = G.cache.node_pool_first_free; - G.cache.node_pool_first_free = n; - } - } + if (head_evicted) { + /* Release evicted node memory */ + { + __profscope(eviction_memory_release); + for (struct evict_node *en = head_evicted; en; en = en->next_evicted) { + struct cache_node *n = en->cache_node; + arena_release(&n->arena); } } + + /* Add evicted nodes to free list */ + sys_mutex_lock(&G.cache.node_pool_mutex); + { + __profscope(eviction_free_list_append); + for (struct evict_node *en = head_evicted; en; en = en->next_evicted) { + struct cache_node *n = en->cache_node; + n->next_free = G.cache.node_pool_first_free; + G.cache.node_pool_first_free = n; + } + } + sys_mutex_unlock(&G.cache.node_pool_mutex); } - sys_mutex_unlock(&G.cache.node_pool_mutex); } } - abort_thread_loop: - sys_mutex_unlock(&G.evictor_mutex); scratch_end(scratch); - if (abort_thread_loop) { + if (abort) { break; } } diff --git a/src/sys_win32.c b/src/sys_win32.c index 35ae2caf..ce392cde 100644 --- a/src/sys_win32.c +++ b/src/sys_win32.c @@ -519,6 +519,8 @@ struct sys_file_time sys_file_get_time(struct sys_file file) struct sys_file_map sys_file_map_open_read(struct sys_file file) { + __prof; + u64 size = sys_file_get_size(file); u8 *base_ptr = NULL; HANDLE map_handle = 0; diff --git a/src/work.c b/src/work.c index b490eac8..8d2fe230 100644 --- a/src/work.c +++ b/src/work.c @@ -398,15 +398,13 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(worker_thread_entry_point, thread_data) if (G.workers_shutdown) { sys_mutex_unlock(&G.mutex); break; - } - - if (!G.scheduled_work_head) { + } else if (!G.scheduled_work_head) { /* Wait for work */ sys_condition_variable_wait(&G.cv, &G.mutex); } /* Do work from top */ - if (G.scheduled_work_head) { + while (G.scheduled_work_head && !G.workers_shutdown) { struct work *work = G.scheduled_work_head; if (work) { __profscope(work_pool_task);