diff --git a/.kiro/specs/libxce-alloc-preason-equality/tasks.md b/.kiro/specs/libxce-alloc-preason-equality/tasks.md index bc824487..8b1a3ee3 100644 --- a/.kiro/specs/libxce-alloc-preason-equality/tasks.md +++ b/.kiro/specs/libxce-alloc-preason-equality/tasks.md @@ -29,7 +29,7 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu - _Depends: 2.1_ - [ ] 3. Core: reproducible reservation start time in the scheduler -- [ ] 3.1 (P) Record each session's start from the same clock reading as the session start +- [x] 3.1 (P) Record each session's start from the same clock reading as the session start - Write the test first, in its own test function (not shared with 3.2): with recording enabled, a recorded session's begin time equals the session's start time exactly. - Take the recorded begin time from the session start instead of a second clock reading (design.md, Components: Recording). - Done when the test passes and the existing recording tests still pass. @@ -88,3 +88,4 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu ## Implementation Notes - `xce-build.sh` resolves a relative build dir under `src/`: `bash src/tools/xce-build.sh lib asan xce.build.alloceq` builds into `src/xce.build.alloceq`, so iterate with `meson test -C src/xce.build.alloceq`. - `xce-build.sh lib` runs only the `libxce test` binary; run `meson test -C src/xce.build.alloceq "libxce alloc OOM test"` (or plain `meson test`) for the allocation OOM binary, which also wraps `vasprintf`/`__vasprintf_chk` since 2.1. +- Simulation mode (`sim_loop.c`) still takes its own session-begin clock reading before `xce_core_begin_session()`; by design that is left to spec scheduler-sim-discrete-clock (3.1 fixed only the normal-mode recorder). diff --git a/src/scheduler/src/driver.c b/src/scheduler/src/driver.c index cacd3642..a30b9f36 100644 --- a/src/scheduler/src/driver.c +++ b/src/scheduler/src/driver.c @@ -1024,10 +1024,17 @@ void xce_sched_driver_run_normal_mode_round(void* driver_ctx) * the single boolean check + zero-allocation gate (Req 5 AC 10, * "Performance") -- it returns NULL immediately when `sched->recording` * is false, and every hook below is a cheap `rec_ctx != NULL` pointer - * check with no further cost on that path. */ + * check with no further cost on that path. + * + * beginRound is core->session_start, the clock reading + * xce_core_begin_session() took above -- NOT a second xce_core_now() + * read (libxce-alloc-preason-equality design.md Decision 8): every + * allocation's `from` is that same session start, so the recorded + * beginRound and `from` must come from one reading or a replay can + * differ by one second. Proven by test_driver_session_ordering.c's + * test_recorded_begin_round_equals_session_start. */ if (sched->recording) { - rec_ctx - = sched_record_session_begin(sched, xce_core_now(&(sched->core))); + rec_ctx = sched_record_session_begin(sched, sched->core.session_start); } xce_core_signal_go(&(sched->core)); diff --git a/src/scheduler/testsuite/test_driver_session_ordering.c b/src/scheduler/testsuite/test_driver_session_ordering.c index 28fc764f..97c8085b 100644 --- a/src/scheduler/testsuite/test_driver_session_ordering.c +++ b/src/scheduler/testsuite/test_driver_session_ordering.c @@ -267,7 +267,10 @@ int sched_fill_limit_requests(xce_sched_alloc_req_t* alloc_req, * xce_sched_driver_run_normal_mode_round() never calls * sched_record_session_begin() with `sched.recording == true` -- either * way, these stubs never need to do real work for this file's ordering - * proof, which is not about recording. Never links recording.c's real + * proof, which is not about recording. (Exception: + * test_recorded_begin_round_equals_session_start sets `sched.recording` + * so the sched_record_session_begin() stub below captures its begin + * time.) Never links recording.c's real * implementations (they need xce_sched_core_save_snapshot(), snapshot.c, * an even bigger dependency surface this file's own fixture doesn't * otherwise need). */ @@ -305,11 +308,20 @@ int sched_record_stop(xce_scheduler_t* sched) return 0; } +/* libxce-alloc-preason-equality task 3.1: records the begin time + * xce_sched_driver_run_normal_mode_round() hands the recorder, so + * test_recorded_begin_round_equals_session_start can compare it with + * core->session_start. Returns NULL (no real recording context), so the + * round's per-decision/end hooks stay on their `rec_ctx == NULL` path. */ +static int g_record_begin_calls; +static uint64_t g_record_begin_ts; + sched_record_session_ctx_t* sched_record_session_begin(xce_scheduler_t* sched, uint64_t begin_round_ts) { (void)sched; - (void)begin_round_ts; + g_record_begin_calls++; + g_record_begin_ts = begin_round_ts; return NULL; } @@ -868,11 +880,70 @@ static void test_null_driver_ctx_is_safe_noop(void) TEST_ASSERT_EQUAL_INT(0, atomic_load(&g_observer_end_calls)); } +/* + * test_recorded_begin_round_equals_session_start - libxce-alloc-preason- + * equality task 3.1 (Req 4.2, 6.5; design.md Decision 8): with recording + * enabled, the begin time a normal-mode round hands + * sched_record_session_begin() (the recorded beginRound) is exactly + * core->session_start, the one clock reading xce_core_begin_session() + * took -- not a second xce_core_now() read after plugin_begin_session()/ + * sched_account_jobs(). The same session start later becomes every + * allocation's `from` (task 3.2), so a replay reproduces both from one + * value. + */ +static void test_recorded_begin_round_equals_session_start(void) +{ + xce_scheduler_t sched; + pthread_t picker_tid; + pthread_t allocator_tid; + test_driver_ctx_t driver_ctx; + int rc; + + memset(&g_events, 0, sizeof(g_events)); + memset(&sched, 0, sizeof(sched)); + memset(&driver_ctx, 0, sizeof(driver_ctx)); + g_record_begin_calls = 0; + g_record_begin_ts = 0; + + rc = xce_sched_core_init(&(sched.core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + lock(sched.core.plugins, RTTI_LOCK_WRITE); + rc = key_add(sched.core.plugins, (void*)g_stub_picker_plugin.name, + (void*)&g_stub_picker_plugin); + TEST_ASSERT_GREATER_OR_EQUAL(0, rc); + unlock(sched.core.plugins); + + sched.core.job_pick_plugin = g_stub_picker_plugin.name; + sched.recording = true; + + rc = xce_core_set_decision_cb(&(sched.core), test_on_decision, &driver_ctx); + TEST_ASSERT_EQUAL_INT(0, rc); + + rc = pthread_create(&picker_tid, NULL, picker_thread_fn, &(sched.core)); + TEST_ASSERT_EQUAL_INT(0, rc); + rc = pthread_create(&allocator_tid, NULL, stand_in_allocator_thread_fn, + &(sched.core)); + TEST_ASSERT_EQUAL_INT(0, rc); + + xce_sched_driver_run_normal_mode_round(&sched); + + rc = pthread_join(picker_tid, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + rc = pthread_join(allocator_tid, NULL); + TEST_ASSERT_EQUAL_INT(0, rc); + + TEST_ASSERT_EQUAL_INT(1, g_record_begin_calls); + TEST_ASSERT_NOT_EQUAL(0, sched.core.session_start); + TEST_ASSERT_EQUAL_UINT64(sched.core.session_start, g_record_begin_ts); +} + int main(void) { UNITY_BEGIN(); RUN_TEST( test_round_runs_plugin_begin_and_account_jobs_before_picker_signaled); RUN_TEST(test_null_driver_ctx_is_safe_noop); + RUN_TEST(test_recorded_begin_round_equals_session_start); return UNITY_END(); }