feat(scheduler-sim-discrete-clock): pin the simulation clock so it moves only at recorded timestamps (task 2.1)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-29 04:41:14 +00:00
parent 4f653d8fbe
commit 3a6b503f07
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
5 changed files with 865 additions and 19 deletions

View file

@ -12,7 +12,7 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu
- _Boundary: CliCompat, scheduler test build_
- [ ] 2. Core: pin the simulator clock, remove the REPL speed parameter, retire the CLI option
- [ ] 2.1 (P) Pin the simulation-mode clock so it moves only at recorded timestamps
- [x] 2.1 (P) Pin the simulation-mode clock so it moves only at recorded timestamps
- Write the tests first in the new test binary, each passing a non-zero speed through the still-present simulator argument so they fail on the current code: after start the clock equals the chosen snapshot's timestamp and stays equal after a real sleep, with the core holding the frozen speed setting; with a stand-in allocator that sleeps and a recorded session start that is not a whole second, the session start seen inside the session equals the recorded one exactly, and simulation output has that session start, a zero session duration and every decision timestamp equal to it, with at least one decision per session; a session start earlier than the preceding event, and events earlier than the start snapshot, set the clock to exactly those values and the replay completes; a mid-replay snapshot followed by the end marker leaves the clock at the last event's timestamp after the run and after a sleep, with the start timestamp pinned so the real skip branch runs; the same recording replayed twice gives identical logical timestamps and decisions in both outputs, ignoring only the wall-clock header field (design.md, Testing Strategy: Unit (SimClock), Integration (replay loop)).
- Make simulator startup always select the frozen clock setting and drop the speed parameter from the start-clock function. The start-clock function keeps writing the shadow copy, now always the frozen setting, because `info config` and the startup test still read it; the simulator's speed argument is no longer read. Both are removed in 3.1.
- Update the speed-drift and never-runs-backwards comments in the simulator driver, replay loop and simulation-output sources; the simulator header's comments are left to 3.1 (design.md, Modified Files).
@ -68,3 +68,4 @@ Test-first throughout (red → green → refactor). Every sub-task leaves the bu
## Implementation Notes
- Build dir: `bash src/tools/xce-build.sh scheduler asan xce.build.discrete` builds into `src/xce.build.discrete`; iterate with `meson test -C src/xce.build.discrete --suite scheduler` and grep its testlog for `:FAIL`.
- 2.1 added `args->sim_speed = DISCRETE_CLOCK_RED_SPEED` (and that define) to `test_sim_discrete_clock.c` to prove RED; 3.1 removes both. Its fixture writer leaves mkdtemp dirs under `/tmp` (as `test_sim_record_output.c` does).

View file

@ -224,12 +224,14 @@ static int sim_locate_and_load_snapshot(xce_sched_sim_driver_t* sim,
decode_SnapshotEntry(&decoded, &entry_wire);
capn_free(&c);
/* SnapshotEntry timestamps are monotonically non-decreasing
* across a well-formed `.replay` stream (both normal-mode
* recording and simulation --sim-output derive ts from
* xce_core_now(), which never runs backwards) -- so any
* qualifying candidate found later in the scan is always at
* least as recent as the current best. Freeing the OLD best
/* The LAST qualifying SnapshotEntry in file order wins
* (Req 1.1 of scheduler-sim-discrete-clock). Timestamps are
* normally non-decreasing across the stream, but not
* guaranteed: in simulation mode xce_core_now() is set exactly
* to each recorded timestamp, which may move it backwards (Req
* 1.3), so a --sim-output snapshot can carry an earlier ts than
* the one before it. Either way, a later candidate replaces
* the current best. Freeing the OLD best
* here is always safe: xce_sched_core_load_snapshot() is only
* ever called once, below, on whichever candidate survives to
* the end of this loop -- an intermediate candidate that gets
@ -305,9 +307,19 @@ static int sim_locate_and_load_snapshot(xce_sched_sim_driver_t* sim,
* boundary (test_core_isolation.c) -- that boundary is scoped to core.c
* itself, not to driver-level code legitimately seeding the core's clock
* anchors, exactly as AC 13's own pseudocode specifies.
*
* scheduler-sim-discrete-clock (Req 1.1, 1.5): simulation mode always runs
* the core clock frozen -- speed_num = 0, speed_den = 1 -- so
* xce_core_now() returns exactly snapshot_ts, and afterwards exactly the
* last value sim_advance_virtual_clock() (sim_loop.c) set, however much
* real time passes. This is the only place simulation mode sets the speed;
* the --sim-speed value (sched_sim_args_t.sim_speed) is no longer read.
* The sim_speed_num/sim_speed_den shadow copy is still written (always
* 0/1) for `info config` and test_sim_startup.c; the shadow copy and
* sched_sim_args_t.sim_speed are removed in task 3.1.
*/
static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim,
uint64_t snapshot_ts, uint64_t sim_speed)
uint64_t snapshot_ts)
{
struct timespec ts;
uint64_t real_clock_base_ns;
@ -320,11 +332,10 @@ static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim,
memory_order_release);
atomic_store_explicit(&(sim->core.real_clock_base), real_clock_base_ns,
memory_order_release);
atomic_store_explicit(&(sim->core.speed_num), sim_speed,
memory_order_release);
atomic_store_explicit(&(sim->core.speed_num), 0, memory_order_release);
atomic_store_explicit(&(sim->core.speed_den), 1, memory_order_release);
sim->sim_speed_num = sim_speed;
sim->sim_speed_num = 0;
sim->sim_speed_den = 1;
}
@ -591,7 +602,7 @@ int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
return rc;
}
sim_set_virtual_clock(sim, snapshot_ts, args->sim_speed);
sim_set_virtual_clock(sim, snapshot_ts);
/* Orientation for an operator about to `step`/`continue` through this
* file (or watch a batch --sim run go by): how many sessions are there

View file

@ -174,10 +174,14 @@ extern int sched_record_session_finalize(FILE* f,
/*
* sim_advance_virtual_clock - Req 5 AC 14 (task 11.2): re-anchors
* sim->core's virtual clock so xce_core_now(&sim->core) reports exactly
* new_virtual_ts at the instant this call returns (elapsed=0 against a
* freshly read real_clock_base), then evolves from there per whatever
* speed_num/speed_den sched_sim_driver_start() already set (task 4.5's
* two-anchor + speed formula, core_clock.c) -- speed settings are left
* new_virtual_ts. scheduler-sim-discrete-clock (Req 1.2, 1.3, 1.5): the
* set is exact and unconditional -- a new_virtual_ts earlier than the
* current clock (e.g. a recorder-stamped event batch older than the start
* snapshot, or out-of-order --sim-alt-events) is set as is, with no
* clamping, warning or error -- and, because sched_sim_driver_start()
* always pins speed_num/speed_den to 0/1 (sim.c's
* sim_set_virtual_clock()), the clock then stays at exactly that value,
* with no real-time drift, until the next call. Speed settings are left
* untouched here, only the two clock anchors jump. Same driver-level
* clock_gettime() legitimacy as sim.c's own sim_set_virtual_clock() (task
* 11.1, Step 4): core.c's "zero wall-clock calls" isolation boundary

View file

@ -84,9 +84,11 @@ extern int write_snapshot_entry(FILE* f, xce_sched_core_t* core, uint64_t ts);
* 19), not merely a same-shaped one taken later. `snapshot_ts` is the
* caller's own already-captured value from that load
* (xce_sched_sim_driver_t.snapshot_ts, sim.h's task 11.5 field) --
* deliberately NOT re-derived via xce_core_now() here; see sim.h's own doc
* comment on that field for why a re-derived value could drift under a
* non-frozen --sim-speed.
* deliberately NOT re-derived via xce_core_now() here: this message
* records the loaded snapshot's own timestamp, which must not depend on a
* clock read. (In simulation mode the clock moves only at recorded
* timestamps -- scheduler-sim-discrete-clock Req 1 -- and no recorded
* event has been read yet, so the two values are equal today.)
*
* configHash is left unset, same documented no-op limitation
* write_replay_header() itself already carries for this field (no utility

View file

@ -9,6 +9,22 @@
* non-numeric, negative, empty), and logs nothing when the option was not
* given (NULL).
*
* Task 2.1 (Requirements 1.1-1.8, 4.2; design.md, Components: SimClock,
* Testing Strategy: Unit (SimClock) and Integration (replay loop)): in
* simulation mode the logical clock moves only at the start snapshot, at
* each recorded event and at each recorded session, is set exactly (never
* clamped, even backwards), and never drifts with real time. Every clock
* test passes a non-zero speed through the still-present
* sched_sim_args_t.sim_speed (DISCRETE_CLOCK_RED_SPEED) to prove the
* simulator no longer reads it; task 3.1 removes that field and these
* assignments while keeping the pinned-clock assertions.
*
* White-box #include of the same simulator TU set test_sim_record_output.c
* uses, with its link-satisfying stubs and stand-in picker; the stand-in
* allocator here additionally records core->session_start on every
* cbuf_get() return (including the jobid==0 terminator) and can sleep
* before deciding, so real time visibly passes inside a session.
*
* This binary deliberately never calls mylog_init(): without it, tlog's
* early-print path writes WARN lines to stdout, which the test-local
* capture_stdout_around() below captures.
@ -18,6 +34,10 @@
* later tasks add to this file.
*/
#include <errno.h>
#include <limits.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@ -28,11 +48,292 @@
#include <unity/unity.h>
#include "cli_compat.h"
#include "controller.capnp.h"
#include "kvmgr.h"
#include "scheduler.h"
#include "xstring.h"
/* White-box #include of the simulator under test (same set and order as
* test_sim_record_output.c). */
#include "../src/core.c"
#include "../src/core_clock.c"
#include "../src/core_events.c"
#include "../src/core_session.c"
#include "../src/decision.c"
#include "../src/plugin.c"
#include "../src/plugins/builtin_fcfs.c"
#include "../src/recording.c"
#include "../src/recording_session.c"
#include "../src/repl.c"
#include "../src/repl_fmt.c"
#include "../src/repl_rollup.c"
#include "../src/repl_break.c"
#include "../src/repl_info.c"
#include "../src/repl_params.c"
#include "../src/sim.c"
#include "../src/sim_loop.c"
#include "../src/sim_picker.c"
#include "../src/sim_limit.c"
#include "../src/sim_record.c"
#include "../src/sim_verify.c"
#include "../src/snapshot.c"
#include "../src/snapshot_load.c"
#include "../src/snapshot_wire.c"
#if !defined(XCE_SCHEDULER_BIN)
#error "XCE_SCHEDULER_BIN must be defined by testsuite/meson.build"
#endif
/* Link-satisfying stubs and the stand-in picker below are copied from
* test_sim_record_output.c, whose comments give the rationale for each. */
void kill_sched_job(void* arg) { (void)arg; }
void kill_sched_queue(void* arg) { (void)arg; }
void kill_sched_worker(void* arg) { (void)arg; }
void kill_sched_user(void* arg) { (void)arg; }
void kill_sched_project(void* arg) { (void)arg; }
void kill_sched_resval(xce_val_t* val) { (void)val; }
var limit_get_configured_limits(xce_sched_core_t* core)
{
(void)core;
return NULL;
}
var limit_get_usage_instances(xce_sched_core_t* core)
{
(void)core;
return NULL;
}
xce_sched_plugin_t* limit_new_plugin(void)
{
return NULL;
}
int limit_seed_config(xce_sched_plugin_t* plugin, xce_sched_core_t* core,
xce_conf4sched_t* config)
{
(void)plugin;
(void)core;
(void)config;
return 0;
}
int sched_reconcile_job_resource_usage(xce_sched_core_t* c,
xce_job_account_t* a,
xce_job_rusages_t* r)
{
(void)c; (void)a; (void)r; return 0;
}
void sched_worker_release_host_ctx(xce_sched_worker_t* w) { (void)w; }
int sched_fill_limit_requests(xce_sched_alloc_req_t* alloc_req,
xlist_t* limits, xlist_t* rusages, int nhosts,
int nslots)
{
(void)alloc_req;
(void)limits;
(void)rusages;
(void)nhosts;
(void)nslots;
return 0;
}
int sched_account_jobs(xce_sched_core_t* core)
{
(void)core;
return 0;
}
int sched_check_host_satisfy(xce_sched_core_t* core, xce_sched_worker_t* w,
xvec_t* uncomitted, xvec_idval_t* wanted,
xvec_idval_t* unmeet)
{
(void)core;
(void)w;
(void)uncomitted;
(void)wanted;
(void)unmeet;
return 1;
}
int sched_check_shared_satisfy(xce_sched_core_t* core, xvec_t* uncomitted,
xvec_idval_t* wanted, xvec_idval_t* unmeet)
{
(void)core;
(void)uncomitted;
(void)wanted;
(void)unmeet;
return 1;
}
int sched_charge_account(xce_sched_core_t* core, xce_job_account_t* acct)
{
(void)core;
(void)acct;
return 0;
}
int sched_uncharge_account(xce_sched_core_t* core, xce_job_account_t* acct)
{
(void)core;
(void)acct;
return 0;
}
int sched_resolve_job_picker(xce_sched_core_t* core, xce_sched_plugin_t** out)
{
xce_sched_plugin_t* plugin = NULL;
int rc = 0;
if ((core == NULL) || (out == NULL)) {
return -EINVAL;
}
*out = NULL;
if (core->job_pick_plugin == NULL) {
return -EINVAL;
}
lock(core->plugins, RTTI_LOCK_READ);
rc = key_find(core->plugins, core->job_pick_plugin, (void**)&plugin);
unlock(core->plugins);
if (rc <= 0) {
return (rc < 0) ? rc : -ENOENT;
}
if ((plugin == NULL) || (plugin->picker_iface == NULL)
|| (plugin->picker_iface->next_job == NULL)
|| (plugin->picker_iface->begin_session == NULL)
|| (plugin->picker_iface->end_session == NULL)
|| (plugin->picker_iface->on_dispatch == NULL)
|| (plugin->picker_iface->on_pend == NULL)) {
return -EINVAL;
}
*out = plugin;
return 0;
}
void* sched_job_picker(void* arg)
{
xce_sched_core_t* core = (xce_sched_core_t*)arg;
xce_sched_plugin_t* picker = NULL;
for (;;) {
int go = 0;
uint64_t jobid = 0;
cbuf_get(&(core->wbuf), &go);
if (sched_resolve_job_picker(core, &picker) < 0) {
jobid = 0;
cbuf_put(&(core->jbuf), &jobid);
continue;
}
picker->picker_iface->begin_session(picker);
for (;;) {
xce_sched_job_t* job = picker->picker_iface->next_job(picker);
if (job == NULL) {
break;
}
jobid = job->jobid;
cbuf_put(&(core->jbuf), &jobid);
}
jobid = 0;
cbuf_put(&(core->jbuf), &jobid);
picker->picker_iface->end_session(picker);
}
return NULL;
}
/* Stand-in allocator (design.md, Testing Strategy: Integration): same
* PEND-only contract as test_sim_record_output.c's, plus two observation
* hooks for the core under observation (g_obs_core): every cbuf_get()
* return -- including the jobid==0 end-of-session terminator, so a session
* with no pending job is still observed -- records core->session_start
* and the jobid, and a real job decision is preceded by a real sleep
* (g_alloc_sleep_us) so wall-clock time visibly passes inside a session.
* Other cores (threads left parked by earlier runs, never joined) are
* served but not recorded. The main thread reads the records only after
* sched_sim_driver_run() returns, which it cannot do before this thread's
* last cbuf_put(&core->decisions) -- the cbuf's mutex orders the writes. */
#define MAX_OBS 64
static xce_sched_core_t* _Atomic g_obs_core;
static _Atomic unsigned g_alloc_sleep_us;
static int g_obs_n;
static uint64_t g_obs_session_start[MAX_OBS];
static uint64_t g_obs_jobid[MAX_OBS];
void* sched_allocator(void* arg)
{
xce_sched_core_t* core = (xce_sched_core_t*)arg;
xce_sched_plugin_t* picker = NULL;
for (;;) {
uint64_t jobid = 0;
xce_decision_t* decision = NULL;
xce_sched_job_t* job;
bool observed;
cbuf_get(&(core->jbuf), &jobid);
observed = (atomic_load(&g_obs_core) == core);
if (observed && (g_obs_n < MAX_OBS)) {
g_obs_session_start[g_obs_n] = core->session_start;
g_obs_jobid[g_obs_n] = jobid;
g_obs_n++;
}
if (jobid == 0) {
decision = NULL;
cbuf_put(&(core->decisions), &decision);
continue;
}
job = sched_get_job(core, jobid);
if (job == NULL) {
continue;
}
if (observed && (atomic_load(&g_alloc_sleep_us) > 0)) {
usleep(atomic_load(&g_alloc_sleep_us));
}
(void)sched_resolve_job_picker(core, &picker);
if (picker != NULL) {
picker->picker_iface->on_pend(picker, job->jobid);
}
decision = make_pend_decision(make_pendreason(job->jobid));
cbuf_put(&(core->decisions), &decision);
}
return NULL;
}
void sim_bootstrap_seqjob_allocator(xce_sched_sim_driver_t* sim)
{
(void)sim;
}
void sim_bootstrap_parjob_allocator(xce_sched_sim_driver_t* sim)
{
(void)sim;
}
void setUp(void) { return; }
void tearDown(void) { return; }
@ -149,6 +450,527 @@ static void test_no_warning_when_option_absent(void)
free(out);
}
/* ---------------------------------------------------------------------- */
/* SimClock fixtures (Requirements 1.1-1.8, 4.2). Fixture writers copied */
/* from test_sim_record_output.c; the fixture core's clock is frozen at a */
/* chosen value first, so the recorded start snapshot's ts is exact. */
/* ---------------------------------------------------------------------- */
/* Task 2.1 RED: a non-zero speed through the still-present simulator
* argument. The simulator must ignore it; removed with the field in 3.1. */
#define DISCRETE_CLOCK_RED_SPEED 7
#define SNAP_TS 1000000000000ULL /* start snapshot, 1000 s */
#define JOB_A 4101ULL
#define JOB_B 4102ULL
#define SLEEP_US 12000U /* stand-in allocator sleep per decision */
#define WAIT_US 25000U /* real time allowed to pass between reads */
static FILE* write_header_and_snapshot(char* out_path, size_t out_path_len,
uint64_t snap_ts)
{
char tmpl[] = "/tmp/sched_discrete_clock_XXXXXX";
xce_scheduler_t sched;
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
memset(&sched, 0, sizeof(sched));
mylock_init(&(sched.record_lock));
TEST_ASSERT_EQUAL_INT(0, xce_sched_core_init(&(sched.core)));
sched.core.job_pick_plugin = (char*)"builtin_fcfs";
sched.record_dir = tmpl;
atomic_store(&(sched.core.virtual_clock_base), snap_ts);
atomic_store(&(sched.core.speed_num), 0);
atomic_store(&(sched.core.speed_den), 1);
TEST_ASSERT_EQUAL_INT(0, sched_record_start(&sched, "fixture.replay"));
TEST_ASSERT_EQUAL_UINT64(snap_ts, sched.record_start_ts);
TEST_ASSERT_TRUE(strlen(sched.record_file_path) < out_path_len);
strcpy(out_path, sched.record_file_path);
return sched.record_file;
}
static void write_event_msg(FILE* f, replay_event_t* src)
{
struct capn c;
struct capn_segment* cs;
ReplayEvent_ptr p;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
encode_ReplayEvent_ptr(cs, &p, src);
capn_setp(capn_root(&c), 0, p.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void append_queue_update_event(FILE* f, uint64_t ts,
const char* queue_name)
{
xce_queue_t queue;
replay_event_t src;
memset(&queue, 0, sizeof(queue));
queue.queue = (char*)queue_name;
queue.status = QUEUE_STAT_OPEN;
queue.priority = 1;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_queueUpdate;
src.body.queue_update = &queue;
write_event_msg(f, &src);
}
static void append_jobs_push_event(FILE* f, uint64_t ts, uint64_t jobid,
const char* queue_name)
{
job_submit_data_t submit;
xce_job_t job;
xce_job_t* jobs[1];
xce_jobs_data_t jobs_data;
replay_event_t src;
memset(&submit, 0, sizeof(submit));
submit.queue = (char*)queue_name;
submit.user = (char*)"alice";
submit.mintasks = 1;
submit.maxtasks = 1;
memset(&job, 0, sizeof(job));
job.jobid = jobid;
job.state = JOB_STAT_PEND;
job.submit = &submit;
jobs[0] = &job;
memset(&jobs_data, 0, sizeof(jobs_data));
jobs_data.n_total = 1;
jobs_data.count = 1;
jobs_data.jobs = jobs;
memset(&src, 0, sizeof(src));
src.ts = ts;
src.body_which = ReplayEvent_body_jobsPush;
src.body.jobs_push = &jobs_data;
write_event_msg(f, &src);
}
static void append_replay_session(FILE* f, uint64_t begin_round)
{
struct capn c;
struct capn_segment* cs;
ReplaySession_ptr sp;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
sp = new_ReplaySession(cs);
ReplaySession_set_beginRound(sp, begin_round);
capn_setp(capn_root(&c), 0, sp.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
/* endSession marker with ts 0, as test_sim_record_output.c writes it. */
static void append_end_of_stream_marker(FILE* f)
{
replay_event_t src;
memset(&src, 0, sizeof(src));
src.body_which = ReplayEvent_body_endSession;
write_event_msg(f, &src);
}
/* Hand-built mid-replay SnapshotEntry (test_sim_startup.c's technique). */
static void append_snapshot_entry(FILE* f, uint64_t ts)
{
struct capn c;
struct capn_segment* cs;
SnapshotData_ptr sdata;
SnapshotEntry_ptr snap;
capn_init_malloc(&c);
cs = capn_root(&c).seg;
sdata = new_SnapshotData(cs);
snap = new_SnapshotEntry(cs);
SnapshotEntry_set_ts(snap, ts);
SnapshotEntry_set_data(snap, sdata);
capn_setp(capn_root(&c), 0, snap.p);
TEST_ASSERT_EQUAL_INT(0, write_capnp_message(f, &c));
capn_free(&c);
}
static void make_tmp_file(char* out, size_t out_len, const char* tag)
{
int fd;
snprintf(out, out_len, "/tmp/xce_discrete_clock_%s_XXXXXX", tag);
fd = mkstemp(out);
TEST_ASSERT_TRUE(fd >= 0);
close(fd);
}
static void sim_args_init(sched_sim_args_t* args, const char* replay_path)
{
memset(args, 0, sizeof(*args));
args->replay_path = replay_path;
args->sim_start_ts = SCHED_SIM_START_TS_LATEST;
args->sim_speed = DISCRETE_CLOCK_RED_SPEED;
}
/* Runs a batch replay with `sim` as the observed core. */
static int run_observed(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args, unsigned sleep_us)
{
g_obs_n = 0;
atomic_store(&g_alloc_sleep_us, sleep_us);
atomic_store(&g_obs_core, &(sim->core));
return sched_sim_driver_run(sim, args);
}
/* Asserts the stand-in allocator's records split (at each jobid==0
* terminator) into exactly n sessions, the k-th seeing session_start ==
* begin[k]. */
static void assert_observed_session_starts(const uint64_t* begin, int n)
{
int k = 0;
int i;
for (i = 0; i < g_obs_n; i++) {
TEST_ASSERT_TRUE_MESSAGE(k < n, "more sessions observed than "
"recorded");
TEST_ASSERT_EQUAL_UINT64_MESSAGE(begin[k], g_obs_session_start[i],
"session_start must equal the "
"recorded beginRound exactly");
if (g_obs_jobid[i] == 0) {
k++;
}
}
TEST_ASSERT_EQUAL_INT_MESSAGE(n, k, "every recorded session must be "
"observed");
}
/* Fixture shared by the output and determinism tests: two sessions whose
* beginRound is not a whole second, each with a pending job before it. */
#define OUT_B1 (SNAP_TS + 1234567891ULL)
#define OUT_B2 (OUT_B1 + 2000000333ULL)
static void write_two_session_fixture(char* path, size_t path_len)
{
FILE* f = write_header_and_snapshot(path, path_len, SNAP_TS);
append_queue_update_event(f, SNAP_TS + 100ULL, "normal");
append_jobs_push_event(f, SNAP_TS + 200ULL, JOB_A, "normal");
append_replay_session(f, OUT_B1);
append_jobs_push_event(f, OUT_B1 + 1000ULL, JOB_B, "normal");
append_replay_session(f, OUT_B2);
append_end_of_stream_marker(f);
fclose(f);
}
/* ---------------------------------------------------------------------- */
/* SimClock unit tests (Requirements 1.1, 1.3, 1.5) */
/* ---------------------------------------------------------------------- */
/* 1.1, 1.5: after start the clock is the start snapshot's ts, and stays
* there however much real time passes, with the core at speed 0/1. */
static void test_start_pins_clock_to_snapshot_ts(void)
{
static xce_sched_sim_driver_t sim;
char path[PATH_MAX];
sched_sim_args_t args;
FILE* f;
f = write_header_and_snapshot(path, sizeof(path), SNAP_TS);
append_end_of_stream_marker(f);
fclose(f);
sim_args_init(&args, path);
TEST_ASSERT_EQUAL_INT(0, sched_sim_driver_start(&sim, &args));
TEST_ASSERT_EQUAL_UINT64(SNAP_TS, xce_core_now(&(sim.core)));
usleep(WAIT_US);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(SNAP_TS, xce_core_now(&(sim.core)),
"clock must not drift with real time");
TEST_ASSERT_EQUAL_UINT64(0ULL, atomic_load(&(sim.core.speed_num)));
TEST_ASSERT_EQUAL_UINT64(1ULL, atomic_load(&(sim.core.speed_den)));
}
/* 1.3 (session case): a session whose beginRound is earlier than the
* preceding event's ts starts at exactly that beginRound; the replay
* completes. */
static void test_session_earlier_than_preceding_event_sets_exact(void)
{
static xce_sched_sim_driver_t sim;
char path[PATH_MAX];
sched_sim_args_t args;
FILE* f;
const uint64_t b1 = SNAP_TS + 1500000001ULL;
const uint64_t e = b1 + 900000000ULL;
const uint64_t b2 = b1 + 500000007ULL; /* < e */
const uint64_t begin[2] = { b1, b2 };
f = write_header_and_snapshot(path, sizeof(path), SNAP_TS);
append_queue_update_event(f, SNAP_TS + 100ULL, "normal");
append_jobs_push_event(f, SNAP_TS + 200ULL, JOB_A, "normal");
append_replay_session(f, b1);
append_queue_update_event(f, e, "padq"); /* inert padding event */
append_replay_session(f, b2);
append_end_of_stream_marker(f);
fclose(f);
sim_args_init(&args, path);
TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, SLEEP_US));
assert_observed_session_starts(begin, 2);
TEST_ASSERT_EQUAL_UINT64(b2, xce_core_now(&(sim.core)));
}
/* 1.3 (event case, as the normal-mode recorder produces): events whose ts
* is earlier than the start snapshot set the clock to exactly that ts --
* observed through the static sim_process_replay_event_message() -- and a
* full replay of the same file completes. */
static void test_events_earlier_than_start_snapshot_set_exact(void)
{
static xce_sched_sim_driver_t start_sim;
static xce_sched_sim_driver_t run_sim;
char path[PATH_MAX];
sched_sim_args_t args;
FILE* f;
const uint64_t ev_ts[2] = { SNAP_TS - 5000ULL,
SNAP_TS - 3000ULL };
const uint64_t begin[1] = { SNAP_TS + 1000000017ULL };
int i;
f = write_header_and_snapshot(path, sizeof(path), SNAP_TS);
append_queue_update_event(f, ev_ts[0], "normal");
append_jobs_push_event(f, ev_ts[1], JOB_A, "normal");
append_replay_session(f, begin[0]);
append_end_of_stream_marker(f);
fclose(f);
sim_args_init(&args, path);
TEST_ASSERT_EQUAL_INT(0, sched_sim_driver_start(&start_sim, &args));
TEST_ASSERT_EQUAL_UINT64(SNAP_TS, xce_core_now(&(start_sim.core)));
for (i = 0; i < 2; i++) {
struct capn c;
capn_ptr root;
xce_core_event_t ev;
int record_rc = 0;
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, start_sim.replay_file, 0));
TEST_ASSERT_TRUE(msg_root_has_shape(&c, &root, ReplayEvent_word_count,
ReplayEvent_pointer_count));
TEST_ASSERT_TRUE(sim_process_replay_event_message(&start_sim, &c, root,
&ev, &record_rc)
>= 0);
TEST_ASSERT_EQUAL_INT(0, record_rc);
usleep(WAIT_US / 5);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(ev_ts[i],
xce_core_now(&(start_sim.core)),
"an event earlier than the clock "
"must set it exactly, unclamped");
}
TEST_ASSERT_EQUAL_INT(0, run_observed(&run_sim, &args, 0));
assert_observed_session_starts(begin, 1);
}
/* 1.5: a mid-replay SnapshotEntry (ts S) followed by the endSession marker
* (ts 0) leaves the clock at the last event's ts T, after the run and
* after a real sleep. sim_start_ts is pinned to the initial snapshot so S
* is not chosen as the start snapshot and the real skip branch runs. */
static void test_mid_replay_snapshot_and_end_leave_clock_at_last_event(void)
{
static xce_sched_sim_driver_t sim;
char path[PATH_MAX];
sched_sim_args_t args;
FILE* f;
const uint64_t t = SNAP_TS + 700000011ULL;
const uint64_t s = SNAP_TS + 900000000ULL;
f = write_header_and_snapshot(path, sizeof(path), SNAP_TS);
append_queue_update_event(f, t, "padq");
append_snapshot_entry(f, s);
append_end_of_stream_marker(f);
fclose(f);
sim_args_init(&args, path);
args.sim_start_ts = SNAP_TS;
TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, 0));
TEST_ASSERT_EQUAL_UINT64(SNAP_TS, sim.snapshot_ts);
TEST_ASSERT_EQUAL_UINT64(t, xce_core_now(&(sim.core)));
usleep(WAIT_US);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(t, xce_core_now(&(sim.core)),
"clock must stay at the last event "
"after the end of the recording");
}
/* ---------------------------------------------------------------------- */
/* Replay-loop integration tests (Requirements 1.2, 1.4, 1.6, 1.7, 1.8) */
/* ---------------------------------------------------------------------- */
typedef struct {
uint64_t begin_round;
uint64_t duration_ns;
int n_decisions;
uint64_t decision_ts[8];
} out_session_t;
/* Decodes every ReplaySession in a simulation output file. */
static int read_output_sessions(const char* path, out_session_t* out, int max)
{
FILE* f = fopen(path, "r");
struct capn c;
int n = 0;
TEST_ASSERT_NOT_NULL(f);
while (capn_init_fp(&c, f, 0) == 0) {
capn_ptr root;
ReplaySession_ptr sp;
replay_session_t* dst = NULL;
int i;
if (!msg_root_has_shape(&c, &root, ReplaySession_word_count,
ReplaySession_pointer_count)) {
capn_free(&c);
continue;
}
TEST_ASSERT_TRUE(n < max);
sp.p = root;
decode_ReplaySession_ptr(&dst, sp);
capn_free(&c);
TEST_ASSERT_NOT_NULL(dst);
TEST_ASSERT_NOT_NULL(dst->summary);
TEST_ASSERT_TRUE(dst->n_decisions <= 8);
out[n].begin_round = dst->begin_round;
out[n].duration_ns = dst->summary->duration_ns;
out[n].n_decisions = dst->n_decisions;
for (i = 0; i < dst->n_decisions; i++) {
out[n].decision_ts[i] = dst->decisions[i]->ts;
}
free_ReplaySession_ptr(&dst);
n++;
}
fclose(f);
return n;
}
/* 1.2, 1.4, 1.7, 1.8: with an allocator that sleeps before deciding and
* beginRounds that are not whole seconds, session_start seen inside each
* session equals the recorded beginRound exactly, and the output has that
* beginRound, durationNs 0, and every decision ts equal to it. */
static void test_session_start_and_output_times_equal_begin_round(void)
{
static xce_sched_sim_driver_t sim;
char path[PATH_MAX];
char out_path[PATH_MAX];
sched_sim_args_t args;
const uint64_t begin[2] = { OUT_B1, OUT_B2 };
out_session_t sessions[4];
int n;
int k;
int i;
write_two_session_fixture(path, sizeof(path));
make_tmp_file(out_path, sizeof(out_path), "out");
sim_args_init(&args, path);
args.output_path = out_path;
TEST_ASSERT_EQUAL_INT(0, run_observed(&sim, &args, SLEEP_US));
assert_observed_session_starts(begin, 2);
n = read_output_sessions(out_path, sessions, 4);
TEST_ASSERT_EQUAL_INT(2, n);
for (k = 0; k < n; k++) {
TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].begin_round);
TEST_ASSERT_EQUAL_UINT64_MESSAGE(0ULL, sessions[k].duration_ns,
"no logical time passes inside "
"a replayed session");
TEST_ASSERT_TRUE_MESSAGE(sessions[k].n_decisions >= 1,
"each session must decide at least once");
for (i = 0; i < sessions[k].n_decisions; i++) {
TEST_ASSERT_EQUAL_UINT64(begin[k], sessions[k].decision_ts[i]);
}
}
unlink(out_path);
}
/* Reads a whole file; *hdr_end is the offset just past its first message
* (the ReplayHeader), whose fields other than recordingStartWall are
* returned in *hdr. */
static char* slurp_output(const char* path, long* len, long* hdr_end,
replay_header_t* hdr)
{
FILE* f = fopen(path, "r");
struct capn c;
ReplayHeader_ptr p;
struct ReplayHeader wire;
char* buf;
TEST_ASSERT_NOT_NULL(f);
TEST_ASSERT_EQUAL_INT(0, capn_init_fp(&c, f, 0));
p.p = capn_getp(capn_root(&c), 0, 1);
read_ReplayHeader(&wire, p);
memset(hdr, 0, sizeof(*hdr));
decode_ReplayHeader(hdr, &wire);
capn_free(&c);
*hdr_end = ftell(f);
TEST_ASSERT_EQUAL_INT(0, fseek(f, 0, SEEK_END));
*len = ftell(f);
buf = (char*)malloc((size_t)*len);
TEST_ASSERT_NOT_NULL(buf);
rewind(f);
TEST_ASSERT_EQUAL_UINT((size_t)*len, fread(buf, 1, (size_t)*len, f));
fclose(f);
return buf;
}
/* 1.6: the same recording replayed twice gives identical output (every
* logical timestamp and every decision), except recordingStartWall. */
static void test_two_replays_produce_identical_output(void)
{
static xce_sched_sim_driver_t sim1;
static xce_sched_sim_driver_t sim2;
char path[PATH_MAX];
char out[2][PATH_MAX];
sched_sim_args_t args;
replay_header_t hdr[2];
long len[2];
long hdr_end[2];
char* buf[2];
int r;
write_two_session_fixture(path, sizeof(path));
for (r = 0; r < 2; r++) {
make_tmp_file(out[r], sizeof(out[r]), "det");
sim_args_init(&args, path);
args.output_path = out[r];
TEST_ASSERT_EQUAL_INT(0, run_observed((r == 0) ? &sim1 : &sim2, &args,
SLEEP_US));
buf[r] = slurp_output(out[r], &len[r], &hdr_end[r], &hdr[r]);
}
TEST_ASSERT_EQUAL_UINT64(hdr[0].recording_start_ts,
hdr[1].recording_start_ts);
TEST_ASSERT_EQUAL_UINT64(SNAP_TS, hdr[0].recording_start_ts);
TEST_ASSERT_EQUAL_STRING(hdr[0].recorder_version, hdr[1].recorder_version);
TEST_ASSERT_EQUAL_STRING(hdr[0].sched_policy, hdr[1].sched_policy);
TEST_ASSERT_EQUAL_PTR(NULL, hdr[0].cluster);
TEST_ASSERT_EQUAL_PTR(NULL, hdr[1].cluster);
TEST_ASSERT_EQUAL_INT64(len[0] - hdr_end[0], len[1] - hdr_end[1]);
TEST_ASSERT_EQUAL_MEMORY_MESSAGE(buf[0] + hdr_end[0], buf[1] + hdr_end[1],
(size_t)(len[0] - hdr_end[0]),
"everything after the header must be "
"byte-identical across replays");
for (r = 0; r < 2; r++) {
free_ReplayHeader(&hdr[r]);
free(buf[r]);
unlink(out[r]);
}
}
int main(void)
{
UNITY_BEGIN();
@ -157,5 +979,11 @@ int main(void)
RUN_TEST(test_warn_negative_value);
RUN_TEST(test_warn_empty_value);
RUN_TEST(test_no_warning_when_option_absent);
RUN_TEST(test_start_pins_clock_to_snapshot_ts);
RUN_TEST(test_session_earlier_than_preceding_event_sets_exact);
RUN_TEST(test_events_earlier_than_start_snapshot_set_exact);
RUN_TEST(test_mid_replay_snapshot_and_end_leave_clock_at_last_event);
RUN_TEST(test_session_start_and_output_times_equal_begin_round);
RUN_TEST(test_two_replays_produce_identical_output);
return UNITY_END();
}