delay & deduplicate resource watch events

This commit is contained in:
jacob 2025-05-13 05:33:59 -05:00
parent 9fd87d9675
commit 79ce7a9d6e
4 changed files with 150 additions and 58 deletions

View File

@ -3,6 +3,7 @@
#include "arena.h" #include "arena.h"
#include "tar.h" #include "tar.h"
#include "incbin.h" #include "incbin.h"
#include "util.h"
/* ========================== * /* ========================== *
* Global data * Global data
@ -22,9 +23,16 @@ GLOBAL struct {
#endif #endif
#if RESOURCE_RELOADING #if RESOURCE_RELOADING
struct sys_thread resource_watch_thread; struct sys_thread resource_watch_monitor_thread;
struct sys_thread resource_watch_dispatch_thread;
struct sys_mutex watch_dispatcher_mutex;
struct arena watch_dispatcher_info_arena;
struct sys_watch_info_list watch_dispatcher_info_list;
struct sys_condition_variable watch_dispatcher_cv;
b32 watch_dispatcher_shutdown;
struct sys_mutex watch_callbacks_mutex; struct sys_mutex watch_callbacks_mutex;
b32 watch_stop;
resource_watch_callback *watch_callbacks[64]; resource_watch_callback *watch_callbacks[64];
u64 num_watch_callbacks; u64 num_watch_callbacks;
#endif #endif
@ -35,7 +43,8 @@ GLOBAL struct {
* ========================== */ * ========================== */
#if RESOURCE_RELOADING #if RESOURCE_RELOADING
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_thread_entry_point, _); INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_monitor_thread_entry_point, _);
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_dispatcher_thread_entry_point, _);
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(resource_shutdown); INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(resource_shutdown);
#endif #endif
@ -58,8 +67,14 @@ struct resource_startup_receipt resource_startup(void)
#if RESOURCE_RELOADING #if RESOURCE_RELOADING
G.watch_callbacks_mutex = sys_mutex_alloc(); G.watch_callbacks_mutex = sys_mutex_alloc();
G.watch_dispatcher_mutex = sys_mutex_alloc();
G.watch_dispatcher_info_arena = arena_alloc(GIGABYTE(64));
G.watch_dispatcher_cv = sys_condition_variable_alloc();
app_register_exit_callback(&resource_shutdown); app_register_exit_callback(&resource_shutdown);
G.resource_watch_thread = sys_thread_alloc(resource_watch_thread_entry_point, NULL, LIT("[P2] Resource watcher")); G.resource_watch_monitor_thread = sys_thread_alloc(resource_watch_monitor_thread_entry_point, NULL, LIT("[P2] Resource watch monitor"));
G.resource_watch_dispatch_thread = sys_thread_alloc(resource_watch_dispatcher_thread_entry_point, NULL, LIT("[P2] Resource watch dispatcher"));
#endif #endif
@ -132,12 +147,14 @@ b32 resource_exists(struct string path)
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(resource_shutdown) INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(resource_shutdown)
{ {
__prof; __prof;
/* Wait until any watch callbacks finish before shutting down */ /* Wait for dispatcher thread to finish before shutting down */
struct sys_lock lock = sys_mutex_lock_e(&G.watch_callbacks_mutex); struct sys_lock lock = sys_mutex_lock_e(&G.watch_dispatcher_mutex);
{ {
G.watch_stop = true; G.watch_dispatcher_shutdown = true;
} }
sys_mutex_unlock(&lock); sys_mutex_unlock(&lock);
sys_condition_variable_broadcast(&G.watch_dispatcher_cv);
sys_thread_wait_release(&G.resource_watch_dispatch_thread);
} }
void resource_register_watch_callback(resource_watch_callback *callback) void resource_register_watch_callback(resource_watch_callback *callback)
@ -153,34 +170,94 @@ void resource_register_watch_callback(resource_watch_callback *callback)
sys_mutex_unlock(&lock); sys_mutex_unlock(&lock);
} }
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_thread_entry_point, _) INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_monitor_thread_entry_point, _)
{ {
(UNUSED)_; (UNUSED)_;
struct temp_arena scratch = scratch_begin_no_conflict(); struct temp_arena scratch = scratch_begin_no_conflict();
struct sys_watch dir_watch = sys_watch_alloc(LIT("res")); struct sys_watch watch = sys_watch_alloc(LIT("res"));
/* NOTE: This thread is force-shutdown at the moment, however shutdown is guaranteed not to occur while the watch mutex is locked by the thread */ /* NOTE: We let OS force-shutdown this thread */
volatile i32 run = true; volatile i32 run = true;
while (run) { while (run) {
struct temp_arena temp = arena_temp_begin(scratch.arena); struct temp_arena temp = arena_temp_begin(scratch.arena);
struct sys_watch_info *info = sys_watch_wait(temp.arena, &dir_watch); struct sys_watch_info_list res = sys_watch_wait(temp.arena, &watch);
struct sys_lock lock = sys_mutex_lock_e(&G.watch_dispatcher_mutex);
{ {
struct sys_lock lock = sys_mutex_lock_s(&G.watch_callbacks_mutex); G.watch_dispatcher_info_list = sys_watch_info_copy(&G.watch_dispatcher_info_arena, res);
if (!G.watch_stop) { }
while (info) { sys_mutex_unlock(&lock);
sys_condition_variable_broadcast(&G.watch_dispatcher_cv);
arena_temp_end(temp);
}
sys_watch_release(&watch);
scratch_end(scratch);
}
/* NOTE: We separate the responsibilities of monitoring directory changes
* & dispatching watch callbacks into two separate threads so that we can delay
* the dispatch of these callbacks, allowing for deduplication of file
* modification notifications. */
#define WATCH_DISPATCHER_DELAY_SECONDS 0.100
#define WATCH_DISPATCHER_DEDUP_DICT_BINS 128
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(resource_watch_dispatcher_thread_entry_point, _)
{
(UNUSED)_;
struct temp_arena scratch = scratch_begin_no_conflict();
struct sys_lock watch_dispatcher_lock = sys_mutex_lock_e(&G.watch_dispatcher_mutex);
while (!G.watch_dispatcher_shutdown) {
if (G.watch_dispatcher_info_arena.pos > 0) {
/* Unlock and sleep a bit so duplicate events pile up */
{
sys_mutex_unlock(&watch_dispatcher_lock);
sys_sleep(WATCH_DISPATCHER_DELAY_SECONDS);
watch_dispatcher_lock = sys_mutex_lock_e(&G.watch_dispatcher_mutex);
}
if (!G.watch_dispatcher_shutdown) {
struct temp_arena temp = arena_temp_begin(scratch.arena);
/* Pull watch info from queue */
struct sys_watch_info_list watch_info_list = sys_watch_info_copy(temp.arena, G.watch_dispatcher_info_list);
arena_reset(&G.watch_dispatcher_info_arena);
/* Unlock and run callbacks */
sys_mutex_unlock(&watch_dispatcher_lock);
{
struct fixed_dict dedup_dict = fixed_dict_init(temp.arena, WATCH_DISPATCHER_DEDUP_DICT_BINS);
for (struct sys_watch_info *info = watch_info_list.first; info; info = info->next) {
b32 skip = false;
if (info->kind == SYS_WATCH_INFO_KIND_MODIFIED) {
/* Skip modified notifications for the same file */
if ((u64)fixed_dict_get(&dedup_dict, info->name) != 1) {
fixed_dict_set(temp.arena, &dedup_dict, info->name, (void *)1);
} else {
skip = true;
}
}
if (!skip) {
struct sys_lock callbacks_lock = sys_mutex_lock_s(&G.watch_callbacks_mutex);
for (u64 i = 0; i < G.num_watch_callbacks; ++i) { for (u64 i = 0; i < G.num_watch_callbacks; ++i) {
resource_watch_callback *callback = G.watch_callbacks[i]; resource_watch_callback *callback = G.watch_callbacks[i];
callback(info); callback(info);
} }
info = info->next; sys_mutex_unlock(&callbacks_lock);
} }
} }
sys_mutex_unlock(&lock);
} }
watch_dispatcher_lock = sys_mutex_lock_e(&G.watch_dispatcher_mutex);
arena_temp_end(temp); arena_temp_end(temp);
} }
}
if (!G.watch_dispatcher_shutdown) {
sys_condition_variable_wait(&G.watch_dispatcher_cv, &watch_dispatcher_lock);
}
}
sys_watch_release(&dir_watch);
scratch_end(scratch); scratch_end(scratch);
} }

View File

@ -20,10 +20,10 @@
#define MAX_LOADER_THREADS 4 #define MAX_LOADER_THREADS 4
/* How long between evictor thread scans */ /* How long between evictor thread scans */
#define EVICTOR_CYCLE_INTERVAL (RESOURCE_RELOADING ? 0.100 : 0.500) #define EVICTOR_CYCLE_INTERVAL_NS NS_FROM_SECONDS(0.5)
/* Time a cache entry spends unused until it's considered evictable (rounded up to multiple of of EVICTOR_CYCLE_INTERVAL) */ /* Time a cache entry spends unused until it's considered evictable (rounded up to multiple of of EVICTOR_CYCLE_INTERVAL) */
#define EVICTOR_GRACE_PERIOD 10.000 #define EVICTOR_GRACE_PERIOD_NS NS_FROM_SECONDS(10)
#define TCTX_ARENA_RESERVE MEGABYTE(64) #define TCTX_ARENA_RESERVE MEGABYTE(64)
@ -94,8 +94,6 @@ struct cache_node {
#if RESOURCE_RELOADING #if RESOURCE_RELOADING
struct atomic_i32 out_of_date; /* Has the resource changed since this node was loaded */ struct atomic_i32 out_of_date; /* Has the resource changed since this node was loaded */
u64 tag_path_len;
u8 tag_path[4096];
#endif #endif
}; };
@ -317,7 +315,7 @@ b32 sprite_tag_eq(struct sprite_tag t1, struct sprite_tag t2)
INTERNAL struct cache_node_hash cache_node_hash_from_tag_hash(u64 tag_hash, enum cache_node_kind kind) INTERNAL struct cache_node_hash cache_node_hash_from_tag_hash(u64 tag_hash, enum cache_node_kind kind)
{ {
return (struct cache_node_hash) { .v = hash_fnv64(tag_hash, STRING(1, (u8 *)&kind)) }; return (struct cache_node_hash) { .v = rand_u64_from_seed(tag_hash + kind) };
} }
/* ========================== * /* ========================== *
@ -355,7 +353,7 @@ INTERNAL void cache_node_load_texture(struct cache_node *n, struct sprite_tag ta
atomic_u32_eval_exchange(&n->state, CACHE_NODE_STATE_WORKING); atomic_u32_eval_exchange(&n->state, CACHE_NODE_STATE_WORKING);
struct string path = tag.path; struct string path = tag.path;
logf_info("Loading sprite texture \"%F\"", FMT_STR(path)); logf_info("Loading sprite texture [%F] \"%F\"", FMT_HEX(n->hash.v), FMT_STR(path));
i64 start_ns = sys_time_ns(); i64 start_ns = sys_time_ns();
ASSERT(string_ends_with(path, LIT(".ase"))); ASSERT(string_ends_with(path, LIT(".ase")));
@ -383,20 +381,16 @@ INTERNAL void cache_node_load_texture(struct cache_node *n, struct sprite_tag ta
/* TODO: Query renderer for more accurate texture size in VRAM */ /* TODO: Query renderer for more accurate texture size in VRAM */
memory_size += (decoded.image.width * decoded.image.height) * sizeof(*decoded.image.pixels); memory_size += (decoded.image.width * decoded.image.height) * sizeof(*decoded.image.pixels);
} else { } else {
logf_error("Sprite \"%F\" not found", FMT_STR(path)); logf_error("Sprite [%F] \"%F\" not found", FMT_HEX(n->hash.v), FMT_STR(path));
} }
#if RESOURCE_RELOADING
u64 cpy_len = min_u64(tag.path.len, ARRAY_COUNT(n->tag_path));
n->tag_path_len = cpy_len;
MEMCPY(n->tag_path, tag.path.text, cpy_len);
#endif
} }
arena_set_readonly(&n->arena); arena_set_readonly(&n->arena);
n->memory_usage = n->arena.committed + memory_size; n->memory_usage = n->arena.committed + memory_size;
atomic_u64_eval_add_u64(&G.cache.memory_usage, n->memory_usage); atomic_u64_eval_add_u64(&G.cache.memory_usage, n->memory_usage);
f64 elapsed = SECONDS_FROM_NS(sys_time_ns() - start_ns); f64 elapsed = SECONDS_FROM_NS(sys_time_ns() - start_ns);
logf_info("Finished loading sprite texture \"%F\" in %F seconds (cache size: %F bytes).", logf_info("Finished loading sprite texture [%F] \"%F\" in %F seconds (cache size: %F bytes).",
FMT_HEX(n->hash.v),
FMT_STR(path), FMT_STR(path),
FMT_FLOAT(elapsed), FMT_FLOAT(elapsed),
FMT_UINT(n->memory_usage)); FMT_UINT(n->memory_usage));
@ -658,7 +652,7 @@ INTERNAL void cache_node_load_sheet(struct cache_node *n, struct sprite_tag tag)
atomic_u32_eval_exchange(&n->state, CACHE_NODE_STATE_WORKING); atomic_u32_eval_exchange(&n->state, CACHE_NODE_STATE_WORKING);
struct string path = tag.path; struct string path = tag.path;
logf_info("Loading sprite sheet \"%F\"", FMT_STR(path)); logf_info("Loading sprite sheet [%F] \"%F\"", FMT_HEX(n->hash.v), FMT_STR(path));
i64 start_ns = sys_time_ns(); i64 start_ns = sys_time_ns();
//ASSERT(string_ends_with(path, LIT(".ase"))); //ASSERT(string_ends_with(path, LIT(".ase")));
@ -683,17 +677,13 @@ INTERNAL void cache_node_load_sheet(struct cache_node *n, struct sprite_tag tag)
logf_error("Sprite \"%F\" not found", FMT_STR(path)); logf_error("Sprite \"%F\" not found", FMT_STR(path));
} }
} }
#if RESOURCE_RELOADING
u64 cpy_len = min_u64(tag.path.len, ARRAY_COUNT(n->tag_path));
n->tag_path_len = cpy_len;
MEMCPY(n->tag_path, tag.path.text, cpy_len);
#endif
arena_set_readonly(&n->arena); arena_set_readonly(&n->arena);
n->memory_usage = n->arena.committed; n->memory_usage = n->arena.committed;
atomic_u64_eval_add_u64(&G.cache.memory_usage, n->memory_usage); atomic_u64_eval_add_u64(&G.cache.memory_usage, n->memory_usage);
f64 elapsed = SECONDS_FROM_NS(sys_time_ns() - start_ns); f64 elapsed = SECONDS_FROM_NS(sys_time_ns() - start_ns);
logf_info("Finished loading sprite sheet \"%F\" in %F seconds (cache size: %F bytes).", logf_info("Finished loading sprite sheet [%F] \"%F\" in %F seconds (cache size: %F bytes).",
FMT_HEX(n->hash.v),
FMT_STR(path), FMT_STR(path),
FMT_FLOAT(elapsed), FMT_FLOAT(elapsed),
FMT_UINT(n->memory_usage)); FMT_UINT(n->memory_usage));
@ -1090,7 +1080,7 @@ INTERNAL RESOURCE_WATCH_CALLBACK_FUNC_DEF(sprite_resource_watch_callback, info)
{ {
struct string name = info->name; struct string name = info->name;
struct sprite_tag tag = sprite_tag_from_path(name); struct sprite_tag tag = sprite_tag_from_path(name);
for (u64 kind = 0; kind < NUM_CACHE_NODE_KINDS; ++kind) { for (enum cache_node_kind kind = 0; kind < NUM_CACHE_NODE_KINDS; ++kind) {
struct cache_node_hash hash = cache_node_hash_from_tag_hash(tag.hash, kind); struct cache_node_hash hash = cache_node_hash_from_tag_hash(tag.hash, kind);
u64 cache_bin_index = hash.v % CACHE_BINS_COUNT; u64 cache_bin_index = hash.v % CACHE_BINS_COUNT;
struct cache_bin *bin = &G.cache.bins[cache_bin_index]; struct cache_bin *bin = &G.cache.bins[cache_bin_index];
@ -1154,14 +1144,13 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sprite_evictor_thread_entry_point, arg)
#if RESOURCE_RELOADING #if RESOURCE_RELOADING
/* Check if file changed for resource reloading */ /* Check if file changed for resource reloading */
if (!consider_for_eviction) { if (!consider_for_eviction) {
struct string path = STRING(n->tag_path_len, n->tag_path);
if (atomic_i32_eval(&n->out_of_date)) { if (atomic_i32_eval(&n->out_of_date)) {
switch (n->kind) { switch (n->kind) {
case CACHE_NODE_KIND_TEXTURE: { case CACHE_NODE_KIND_TEXTURE: {
logf_info("Resource file for sprite texture \"%F\" has changed. Evicting to allow for reloading.", FMT_STR(path)); logf_info("Resource file for sprite texture [%F] has changed. Evicting to allow for reloading.", FMT_HEX(n->hash.v));
} break; } break;
case CACHE_NODE_KIND_SHEET: { case CACHE_NODE_KIND_SHEET: {
logf_info("Resource file for sprite sheet \"%F\" has changed. Evicting to allow for reloading.", FMT_STR(path)); logf_info("Resource file for sprite sheet [%F] has changed. Evicting to allow for reloading.", FMT_HEX(n->hash.v));
} break; } break;
default: { sys_panic(LIT("Unknown sprite cache node kind")); } break; default: { sys_panic(LIT("Unknown sprite cache node kind")); } break;
} }
@ -1173,8 +1162,8 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sprite_evictor_thread_entry_point, arg)
/* Check usage time */ /* Check usage time */
u32 last_used_cycle = refcount.last_modified_cycle; u32 last_used_cycle = refcount.last_modified_cycle;
f64 time_since_use = (f64)(cur_cycle - last_used_cycle) * EVICTOR_CYCLE_INTERVAL; i64 time_since_use_ns = ((i64)cur_cycle - (i64)last_used_cycle) * EVICTOR_CYCLE_INTERVAL_NS;
if (time_since_use > EVICTOR_GRACE_PERIOD) { if (time_since_use_ns > EVICTOR_GRACE_PERIOD_NS) {
/* Cache is over budget and node hasn't been referenced in a while */ /* Cache is over budget and node hasn't been referenced in a while */
consider_for_eviction = true; consider_for_eviction = true;
} }
@ -1285,7 +1274,7 @@ INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(sprite_evictor_thread_entry_point, arg)
scratch_end(scratch); scratch_end(scratch);
/* Wait */ /* Wait */
sys_condition_variable_wait_time(&G.evictor_cv, &evictor_lock, EVICTOR_CYCLE_INTERVAL); sys_condition_variable_wait_time(&G.evictor_cv, &evictor_lock, SECONDS_FROM_NS(EVICTOR_CYCLE_INTERVAL_NS));
} }
sys_mutex_unlock(&evictor_lock); sys_mutex_unlock(&evictor_lock);
} }

View File

@ -276,11 +276,18 @@ struct sys_watch_info {
enum sys_watch_info_kind kind; enum sys_watch_info_kind kind;
struct string name; struct string name;
struct sys_watch_info *next; struct sys_watch_info *next;
struct sys_watch_info *prev;
};
struct sys_watch_info_list {
struct sys_watch_info *first;
struct sys_watch_info *last;
}; };
struct sys_watch sys_watch_alloc(struct string path); struct sys_watch sys_watch_alloc(struct string path);
void sys_watch_release(struct sys_watch *dw); void sys_watch_release(struct sys_watch *dw);
struct sys_watch_info *sys_watch_wait(struct arena *arena, struct sys_watch *dw); struct sys_watch_info_list sys_watch_wait(struct arena *arena, struct sys_watch *dw);
struct sys_watch_info_list sys_watch_info_copy(struct arena *arena, struct sys_watch_info_list src);
/* ========================== * /* ========================== *
* Window * Window

View File

@ -748,12 +748,11 @@ void sys_watch_release(struct sys_watch *dw)
sys_mutex_unlock(&lock); sys_mutex_unlock(&lock);
} }
struct sys_watch_info *sys_watch_wait(struct arena *arena, struct sys_watch *dw) struct sys_watch_info_list sys_watch_wait(struct arena *arena, struct sys_watch *dw)
{ {
__prof; __prof;
struct win32_watch *w32_watch = (struct win32_watch *)dw->handle; struct win32_watch *w32_watch = (struct win32_watch *)dw->handle;
struct sys_watch_info *first_info = NULL; struct sys_watch_info_list list = ZI;
struct sys_watch_info *last_info = NULL;
DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME | DWORD filter = FILE_NOTIFY_CHANGE_FILE_NAME |
FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_DIR_NAME |
@ -781,10 +780,12 @@ struct sys_watch_info *sys_watch_wait(struct arena *arena, struct sys_watch *dw)
FILE_NOTIFY_INFORMATION *res = (FILE_NOTIFY_INFORMATION *)(w32_watch->results_buff + offset); FILE_NOTIFY_INFORMATION *res = (FILE_NOTIFY_INFORMATION *)(w32_watch->results_buff + offset);
struct sys_watch_info *info = arena_push_zero(arena, struct sys_watch_info); struct sys_watch_info *info = arena_push_zero(arena, struct sys_watch_info);
if (last_info) { if (list.last) {
last_info->next = info; list.last->next = info;
info->prev = list.last;
list.last = info;
} else { } else {
first_info = info; list.first = info;
} }
struct string16 name16 = ZI; struct string16 name16 = ZI;
@ -840,7 +841,25 @@ struct sys_watch_info *sys_watch_wait(struct arena *arena, struct sys_watch *dw)
} }
} }
return first_info; return list;
}
struct sys_watch_info_list sys_watch_info_copy(struct arena *arena, struct sys_watch_info_list src_list)
{
struct sys_watch_info_list dst_list = ZI;
for (struct sys_watch_info *src = src_list.first; src; src = src->next) {
struct sys_watch_info *dst = arena_push_zero(arena, struct sys_watch_info);
dst->kind = src->kind;
dst->name = string_copy(arena, src->name);
if (dst_list.last) {
dst_list.last->next = dst;
dst->prev = dst_list.last;
dst_list.last = dst;
} else {
dst_list.first = dst;
}
}
return dst_list;
} }
/* ========================== * /* ========================== *