From a716ef16bf96bb65e2df2adf5a7627676bd31c3a Mon Sep 17 00:00:00 2001 From: fy59 Date: Fri, 7 Aug 2026 08:03:56 +0200 Subject: [PATCH] feat(governor): add thread-safe wait for reservation changes --- include/lardon3d/resource_governor.h | 8 ++ src/resource_governor.c | 84 ++++++++++++ tests/test_resource_governor.c | 188 ++++++++++++++++++++++++++- 3 files changed, 279 insertions(+), 1 deletion(-) diff --git a/include/lardon3d/resource_governor.h b/include/lardon3d/resource_governor.h index 76d733c..69f69e2 100644 --- a/include/lardon3d/resource_governor.h +++ b/include/lardon3d/resource_governor.h @@ -176,6 +176,14 @@ bool lardon3d_resource_governor_availability( const Lardon3DResourceSnapshot *snapshot, Lardon3DResourceAvailability *availability ); +uint64_t lardon3d_resource_governor_generation( + Lardon3DResourceGovernor *governor +); +bool lardon3d_resource_governor_wait_for_change( + Lardon3DResourceGovernor *governor, + uint64_t observed_generation, + uint64_t timeout_ns +); const char *lardon3d_resource_decision_name( Lardon3DResourceDecisionKind kind ); diff --git a/src/resource_governor.c b/src/resource_governor.c index f34579c..ac61f03 100644 --- a/src/resource_governor.c +++ b/src/resource_governor.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -13,6 +14,8 @@ struct Lardon3DResourceReservation { struct Lardon3DResourceGovernor { pthread_mutex_t mutex; + pthread_cond_t cond; + uint64_t generation; Lardon3DHardwareProfile profile; Lardon3DResourcePolicy policy; uint64_t next_reservation_id; @@ -203,6 +206,26 @@ lardon3d_resource_governor_create( free(governor); return NULL; } + pthread_condattr_t attr; + if (pthread_condattr_init(&attr) != 0) { + (void)pthread_mutex_destroy(&governor->mutex); + free(governor); + return NULL; + } + if (pthread_condattr_setclock(&attr, CLOCK_MONOTONIC) != 0) { + (void)pthread_condattr_destroy(&attr); + (void)pthread_mutex_destroy(&governor->mutex); + free(governor); + return NULL; + } + if (pthread_cond_init(&governor->cond, &attr) != 0) { + (void)pthread_condattr_destroy(&attr); + (void)pthread_mutex_destroy(&governor->mutex); + free(governor); + return NULL; + } + (void)pthread_condattr_destroy(&attr); + governor->generation = 0; governor->profile = *profile; governor->policy = *policy; governor->next_reservation_id = 1; @@ -227,6 +250,7 @@ lardon3d_resource_governor_destroy(Lardon3DResourceGovernor *governor) free(reservation); reservation = next; } + (void)pthread_cond_destroy(&governor->cond); (void)pthread_mutex_destroy(&governor->mutex); free(governor); } @@ -347,6 +371,62 @@ lardon3d_resource_governor_availability( return success; } +uint64_t +lardon3d_resource_governor_generation( + Lardon3DResourceGovernor *governor +) +{ + if (!governor) { + return 0; + } + (void)pthread_mutex_lock(&governor->mutex); + uint64_t generation = governor->generation; + (void)pthread_mutex_unlock(&governor->mutex); + return generation; +} + +bool +lardon3d_resource_governor_wait_for_change( + Lardon3DResourceGovernor *governor, + uint64_t observed_generation, + uint64_t timeout_ns +) +{ + if (!governor) { + return false; + } + struct timespec deadline; + if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) { + return false; + } + deadline.tv_sec += (time_t)(timeout_ns / 1000000000ULL); + uint64_t remainder = timeout_ns % 1000000000ULL; + deadline.tv_nsec += (long)remainder; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec += 1; + deadline.tv_nsec -= 1000000000L; + } + (void)pthread_mutex_lock(&governor->mutex); + bool changed = false; + while (governor->generation == observed_generation) { + int result = pthread_cond_timedwait( + &governor->cond, + &governor->mutex, + &deadline + ); + if (result == ETIMEDOUT) { + break; + } + if (result != 0) { + (void)pthread_mutex_unlock(&governor->mutex); + return false; + } + } + changed = governor->generation != observed_generation; + (void)pthread_mutex_unlock(&governor->mutex); + return changed; +} + static void evaluate_locked( Lardon3DResourceGovernor *governor, @@ -554,6 +634,8 @@ lardon3d_resource_governor_reserve( governor->io_slots_reserved += decision->io_slots; ++governor->active_count; *reservation = created; + ++governor->generation; + (void)pthread_cond_broadcast(&governor->cond); (void)pthread_mutex_unlock(&governor->mutex); return true; } @@ -635,6 +717,8 @@ lardon3d_resource_governor_release( current->information.state = LARDON3D_RESERVATION_RELEASED; current->next = governor->released; governor->released = current; + ++governor->generation; + (void)pthread_cond_broadcast(&governor->cond); (void)pthread_mutex_unlock(&governor->mutex); return true; } diff --git a/tests/test_resource_governor.c b/tests/test_resource_governor.c index 705c1cd..907f49b 100644 --- a/tests/test_resource_governor.c +++ b/tests/test_resource_governor.c @@ -25,6 +25,26 @@ typedef struct { atomic_bool *failed; } ThreadContext; +typedef struct { + Lardon3DResourceGovernor *governor; + uint64_t observed_generation; + uint64_t timeout_ns; + atomic_bool *result; +} WaitContext; + +static void * +wait_for_change_thread(void *argument) +{ + WaitContext *context = argument; + bool changed = lardon3d_resource_governor_wait_for_change( + context->governor, + context->observed_generation, + context->timeout_ns + ); + atomic_store(context->result, changed); + return NULL; +} + static void * decide_repeatedly(void *argument) { @@ -207,8 +227,174 @@ run_test(void) return true; } +static bool +run_generation_test(void) +{ + Lardon3DHardwareProfile profile = { + .logical_cpu_count = 16, + .page_size_bytes = 4096, + .memory_total_bytes = GIBIBYTES(16), + .cpu_architecture = "test", + }; + Lardon3DResourcePolicy policy = { + .system_memory_reserve_bytes = GIBIBYTES(2), + .gpu_memory_reserve_bytes = 0, + .system_cpu_reserve = 1, + .maximum_cpu_load_ratio = 0.90, + .maximum_io_pressure_avg10 = 80.0, + .io_slot_capacity = 8, + }; + Lardon3DResourceGovernor *governor = lardon3d_resource_governor_create( + &profile, + &policy + ); + CHECK(governor); + + /* Test 1 : génération initiale = 0. */ + CHECK(lardon3d_resource_governor_generation(governor) == 0); + + Lardon3DResourceSnapshot snapshot = { + .memory_available_bytes = GIBIBYTES(10), + .cpu_load_1m = 2.0, + }; + Lardon3DResourceEstimate estimate = { + .memory_bytes_per_item = GIBIBYTES(1), + .minimum_batch_size = 2, + .maximum_batch_size = 8, + .desired_cpu_threads = 4, + .task_class = LARDON3D_RESOURCE_TASK_GENERAL, + }; + Lardon3DResourceDecision decision; + Lardon3DResourceReservation *reservation = NULL; + + /* Test 2 : WAIT ne change pas la génération. */ + snapshot.cpu_load_1m = 15.0; + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(decision.kind == LARDON3D_RESOURCE_WAIT); + CHECK(reservation == NULL); + CHECK(lardon3d_resource_governor_generation(governor) == 0); + snapshot.cpu_load_1m = 2.0; + + /* Test 3 : REJECT ne change pas la génération. */ + estimate.memory_bytes_per_item = GIBIBYTES(8); + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(decision.kind == LARDON3D_RESOURCE_REJECT); + CHECK(reservation == NULL); + CHECK(lardon3d_resource_governor_generation(governor) == 0); + estimate.memory_bytes_per_item = GIBIBYTES(1); + + /* Test 4 : création d'une réservation change la génération (0 -> 1). */ + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(decision.kind == LARDON3D_RESOURCE_START + || decision.kind == LARDON3D_RESOURCE_REDUCE_BATCH); + CHECK(reservation != NULL); + CHECK(lardon3d_resource_governor_generation(governor) == 1); + + /* Test 5 : libération change la génération (1 -> 2). */ + CHECK(lardon3d_resource_governor_release(governor, reservation)); + CHECK(lardon3d_resource_governor_generation(governor) == 2); + + /* Test 6 : double libération refusée ne change pas la génération. */ + CHECK(!lardon3d_resource_governor_release(governor, reservation)); + CHECK(lardon3d_resource_governor_generation(governor) == 2); + + /* Test 7 : un thread attendant est réveillé après une libération. */ + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(reservation != NULL); + uint64_t generation = lardon3d_resource_governor_generation(governor); + atomic_bool wait_result = false; + WaitContext wait_context = { + .governor = governor, + .observed_generation = generation, + .timeout_ns = 5000000000ULL, + .result = &wait_result, + }; + pthread_t waiter; + CHECK(pthread_create(&waiter, NULL, wait_for_change_thread, &wait_context) == 0); + CHECK(lardon3d_resource_governor_release(governor, reservation)); + CHECK(pthread_join(waiter, NULL) == 0); + CHECK(atomic_load(&wait_result) == true); + + /* Test 8 : l'attente expire proprement (timeout court). */ + generation = lardon3d_resource_governor_generation(governor); + CHECK(!lardon3d_resource_governor_wait_for_change( + governor, + generation, + 1000000ULL + )); + + /* Test 9 : une décision WAIT ne modifie pas la génération et + * wait_for_change() expire sans faux changement. */ + generation = lardon3d_resource_governor_generation(governor); + snapshot.cpu_load_1m = 15.0; + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(decision.kind == LARDON3D_RESOURCE_WAIT); + CHECK(reservation == NULL); + snapshot.cpu_load_1m = 2.0; + CHECK(lardon3d_resource_governor_generation(governor) == generation); + CHECK(!lardon3d_resource_governor_wait_for_change( + governor, + generation, + 1000000ULL + )); + + /* Test 10 : plusieurs attentes successives fonctionnent. */ + for (size_t index = 0; index < 3; ++index) { + CHECK(lardon3d_resource_governor_reserve( + governor, + &snapshot, + &estimate, + &decision, + &reservation + )); + CHECK(reservation != NULL); + generation = lardon3d_resource_governor_generation(governor); + CHECK(lardon3d_resource_governor_release(governor, reservation)); + CHECK(lardon3d_resource_governor_wait_for_change( + governor, + generation, + 5000000000ULL + )); + } + + lardon3d_resource_governor_destroy(governor); + return true; +} + int main(void) { - return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; + return (run_test() && run_generation_test()) + ? EXIT_SUCCESS + : EXIT_FAILURE; }