34 KiB
Design Document
Overview
Purpose: This feature makes the scheduler simulator's logical clock depend only on the recording, so a replay reproduces every clock-derived value (session start, written beginRound, decision timestamps, SessionSummary.durationNs, and values derived from the session start such as xce_alloc_t.from) exactly, on any host.
Users: Operators and engineers who replay recordings with xce_scheduler --sim (with --sim-verify, --sim-output, --sim-alt-events, or --interactive), and spec libxce-alloc-preason-equality, whose task 5.3 needs a replayed session to start at exactly the recorded beginRound.
Impact: The simulator already jumps its clock to each recorded timestamp; today the default --sim-speed 1 lets it drift with real time between jumps. This design pins simulation mode to the core's existing frozen-clock setting (speed_num = 0), turns --sim-speed into a deprecated option that only logs a warning, removes the REPL speed parameter, and updates the operator docs and the scheduler-core spec. The core clock formula and normal mode are unchanged.
Goals
- In simulation mode the logical clock changes only at the start snapshot, at each recorded event, and at each recorded session (Req 1).
--sim-speedkeeps existing command lines working in both modes, with one warning naming its value (Req 2).- The REPL no longer offers a speed setting (Req 3).
- Docs and the scheduler-core spec describe the simulator as it behaves (Req 5).
Non-Goals
- Pacing a replay against the wall clock (not provided before or after).
- Removing
--sim-speedfrom the command line (a later release). - Changing the
.replayformat, the core clock formula, or normal-mode clock behavior (Req 4). - How
fromis computed and compared (spec libxce-alloc-preason-equality).
Boundary Commitments
This Spec Owns
- The simulation-mode clock setting:
sim_set_virtual_clock()storesspeed_num = 0,speed_den = 1, and no simulation or REPL path sets any other speed. - The contract that, in simulation mode, a session's
core->session_startequals its recordedbeginRoundexactly, including when that is earlier than the previous clock value (Req 1.3, 1.4). - The
--sim-speedcommand-line behavior: accepted with any value in both modes, no effect, one startup warning (sched_cli_warn_deprecated_options()). - The REPL
set speedremoval message, and thehelp,setusage andinfo configtext without speed. - The simulation sections of
src/docs/deployment.md, and the speed-mode passages of.kiro/specs/scheduler-core/requirements.mdanddesign.mdlisted in requirements.md 5.2.
Out of Boundary
xce_core_now()'s formula and the four clock atoms incore.h(kept; only their documentation of which values are used changes).- Normal mode:
xce_core_set_wallclock_anchor(),driver.c, recording (recording*.c). - The
.replayschema (src/schema/controller.capnp). fromstamping and allocation equality (spec libxce-alloc-preason-equality).- The superseded
scheduler-frameworkspec, and scheduler-corebrief.md/tasks.mdhistory.
Allowed Dependencies
- The core clock atoms and
xce_core_now()(include/core.h,core_clock.c), read and written only through the existing sim driver functions. - The logging macros and
mylog_init()/mylog_fini()insrc/include/utils.h(WARN; the drain inmain.c). - argp as already used by
main.c.
Revalidation Triggers
- Any change to
sim_set_virtual_clock(),sim_advance_virtual_clock(), the order of the clock jump andxce_core_begin_session()insim_loop.c, orxce_core_now(): re-run libxce-alloc-preason-equality task 5.3 and this spec's clock tests. - Any new code in simulation mode that writes the clock atoms or reads real time.
- Removing
--sim-speedentirely (the later release): updatesched_cli_warn_deprecated_options(), the help text, the soak script anddeployment.md.
Architecture
Existing Architecture Analysis
- One core, two drivers (
.kiro/steering/scheduler-driver-equivalence.md):driver.c(normal) andsim_loop.c(simulation) drive the same core session protocol. This change is confined to the simulation driver's clock setting, the CLI entry point and the REPL. xce_core_now() = virtual_clock_base + (CLOCK_MONOTONIC_now - real_clock_base) * speed_num / speed_den(core_clock.c:80-119). Core init seeds a neutral 1/1 (core.c:142) and normal mode re-seeds 1/1 inxce_core_set_wallclock_anchor()(core_clock.c:180);speed_num = 0returnsvirtual_clock_baseexactly.- Simulation-mode clock writers:
sim_set_virtual_clock()(sim.c:309, called once atsim.c:594),sim_advance_virtual_clock()at each event (sim_loop.c:417, after theendSessionreturn at 412-415) and at each session (sim_loop.c:1528, immediately before the session record opens at 1538 andxce_core_begin_session()setssession_start).repl_set_speed()(repl_params.c:202) is the only other writer and is removed.
Architecture Pattern & Boundary Map
graph TB
subgraph CLI
Main[main.c argp]
Compat[cli_compat.c warn helper]
end
subgraph SimDriver
SimStart[sim.c sim_set_virtual_clock]
SimLoop[sim_loop.c sim_advance_virtual_clock]
Repl[repl_params.c repl.c repl_info.c]
end
subgraph Core
Clock[core_clock.c xce_core_now]
Session[core_session.c session_start]
end
Main --> Compat
Main --> SimStart
SimStart --> Clock
SimLoop --> Clock
Session --> Clock
Repl --> Clock
- Selected pattern: reuse the core's frozen-clock setting; no new abstraction (research.md, Option A).
- Existing patterns preserved: one core, two drivers; the core module stays free of new I/O (
test_core_isolation.c). - New component:
cli_compat.c, one function, so the deprecation warning is unit-testable and has one place to remove later. - The REPL only reads the clock after this change.
Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|---|---|---|---|
| CLI | argp (glibc), existing | --sim-speed stays an option taking a value |
No validation of the value |
| Scheduler | C11, existing sources | Sim driver, REPL, CLI helper | No new dependency |
| Logging | tlog via WARN (utils.h) |
Deprecation warning | Emitted after mylog_init() |
| Build/Test | Meson, Unity | New test binary, updated tests | src/scheduler/testsuite/meson.build |
File Structure Plan
Directory Structure
src/scheduler/
├── include/
│ ├── cli_compat.h # NEW: sched_cli_warn_deprecated_options() declaration
│ ├── core.h # doc only: speed atoms hold 1/1 (normal) or 0/1 (simulation)
│ └── sim.h # remove sched_sim_args_t.sim_speed and the sim_speed_num/den shadow copy; doc updates
├── src/
│ ├── cli_compat.c # NEW: the deprecation warning for --sim-speed
│ ├── main.c # help text; no validation; call the warning after mylog_init(); stop passing a speed
│ ├── sim.c # sim_set_virtual_clock(sim, snapshot_ts) stores speed 0/1
│ ├── sim_loop.c # comments on sim_advance_virtual_clock (no drift, exact unclamped set)
│ ├── sim_record.c # comment ~88-89 (no "non-frozen --sim-speed")
│ ├── repl_params.c # delete repl_set_speed(); set speed removal message before the ntok check; usage text
│ ├── repl_info.c # info config line without speed=; doc comment
│ ├── repl.c # help without "set speed <N>"
│ └── repl_internal.h # comment references only
├── meson.build # add src/cli_compat.c to scheduler_srcs only
└── testsuite/
├── test_sim_discrete_clock.c # NEW: Req 1 clock tests and Req 2 warning tests
├── meson.build # register the new test (links ../src/cli_compat.c, depends on the scheduler binary)
└── (existing tests, see Modified Files)
src/docs/deployment.md # simulation section
.kiro/specs/scheduler-core/requirements.md, design.md # speed-mode passages (Req 5.2)
Modified Files
src/scheduler/include/sim.h— removesched_sim_args_t.sim_speed(155-157) andxce_sched_sim_driver_t.sim_speed_num/sim_speed_den(90-91); update the comments at 44-50, 60-68, 136, 214-223.src/scheduler/src/sim.c—sim_set_virtual_clock(sim, snapshot_ts)(drop the third parameter; storespeed_num = 0,speed_den = 1; no shadow copy); caller at 594.src/scheduler/src/main.c— option help text (92-94); remove thesim_speeddefault, validation andsim_args.sim_speedassignment (339, 356-368, 373) and the comment at 296-299 that cites it; update the comments at 67-72 (flag meanings live insched_sim_args_t, which no longer has a speed field) and 74 (thesim_speedfield); callsched_cli_warn_deprecated_options(arguments.sim_speed)once aftermylog_init()(325) and before the--simbranch (335); whenarguments.sim_speed != NULL, immediately after that call (still single-threaded: no picker, allocator or driver thread exists beforesched_sim_driver_run()orsched_init()), callmylog_fini()(utils.c:303,tlog_exit()) and thenmylog_init()again with the same arguments. tlog writes the log file from its own thread and registers no exit hook (the tlog-1.0.0 Nix package,tinylog_depatsrc/scheduler/meson.build:62: noatexitor destructor in tlog.c);tlog_exit()joins that writer only after every buffered line is written (tlog.c:1725-1728, join at 2210), and re-initialising is supported (tlog_exit()clearsroot_format, tlog.c:2222, whichtlog_init()checks at 2135; the file is reopened withO_APPEND, tlog.c:1371). Side effects of the drain: when gzip is missing, tlog's "can not find gzip command" notice is printed to stderr twice (tlog.c:2161-2163), and if the first write rotated an oversized log the drain waits for that compression to finish (tlog.c:1484-1486); both are harmless while single-threaded. This puts the warning on disk before any mode-specific startup, so it survives every exit path: the--simbranch's returns (main.c:352, 391, 396, 399; 366 is removed), the REPL'sexit(0)onquitor end of input (repl.c:350), and normal-mode shutdown.tlog_exit()is deliberately not called at the--simreturns or in the REPL's quit: the picker and allocator threads are never joined (sim_loop.c:1374-1385), andtlog_exit()is unsafe while another thread logs (tlog.c:833-841 vs 2213-2219). The unreachable-ENOSYSbranch (main.c:380-392; no simulation path returns-ENOSYS) is left untouched.src/scheduler/src/repl_params.c— deleterepl_set_speed()and its doc comment (174-227; keep and edit the section banner at 170-172); inrepl_cmd_set()handlespeedfirst; removespeedfrom the usage string (400-402); comment references (22, 44-48, 348).src/scheduler/src/repl_info.c—info configline withoutspeed=(1801-1806); comments (23, 1770-1784).src/scheduler/src/repl.c— delete the help line at 314; comment at 253-254.src/scheduler/src/sim_loop.c,sim_record.c,repl_internal.h,include/core.h— comments only; alsorepl_params.c:171(section banner) and:388-394(therepl_cmd_set()doc comment namingset speed <N>);sim.c:227-230(says--sim-outputtimestamps come from a clock that "never runs backwards", which Req 1.3 contradicts);testsuite/meson.buildcomments at 318-319, 566, 1178 and 1211.src/scheduler/meson.build— add'src/cli_compat.c'toscheduler_srcs(the list starting at line 1) only; not to the sim isolation library list (~163), since it is not a sim file. The 22 test targets built withforeach f : scheduler_srcspick it up harmlessly.src/scheduler/testsuite/meson.build— registersched_sim_discrete_clock_testfrom['test_sim_discrete_clock.c', '../src/cli_compat.c']plus the white-box sim#includes the other sim tests use (it must not linkscheduler_srcs, whosesched_election.cwould clash with the test's stand-in picker/allocator); pass-DXCE_SCHEDULER_BIN="<path>"fromscheduler.full_path()(src/scheduler/meson.build:67, defined beforesubdir('testsuite')at:171) and register it withtest('scheduler sim discrete clock test', …, suite: 'scheduler', env: ['ASAN_OPTIONS=detect_leaks=0'], depends: scheduler)like the other scheduler tests.- Build-order constraint for tasks: removing the
sim.hfields breaks compilation ofmain.c,sim.c,repl_params.c,repl_info.cand every test file that assignssim_speed(plus the assignments intest_repl_snapshot_params.c:1164-1165, its shadow-field assertions at 896-914, andtest_sim_startup.c:442-443). These edits must land in one task. - Tests (mechanical, remove
args.sim_speed = 0;/sim_args.sim_speed = …;):test_repl_breakpoints_inspection.c,test_repl_equivalence.c,test_repl_event_echo.c,test_repl_execution_control.c,test_repl_limit_table.c,test_repl_prompt_context.c,test_repl_round_rollup.c,test_repl_step_verbosity.c,test_sim_alt_events.c,test_sim_contention_parity.c,test_sim_e2e_limit_visibility.c,test_sim_e2e_original_motivation.c,test_sim_equivalence.c,test_sim_event_loop.c(its direct core-atom writes at 703-704 stay),test_sim_live_allocation.c,test_sim_print_decision_gate.c,test_sim_record_output.c,test_sim_resource_contention.c,test_sim_round_boundary_reconciliation.c,test_sim_session_ordering.c,test_sim_snapshot_reconcile_gap.c,test_sim_verify.c,test_repl_snapshot_params.c(678, 814),test_sim_startup.c(422, 484, 521, 549, 571). - Tests (behavioral):
test_sim_startup.c— keep thecore.speed_num == 0/speed_den == 1assertions (now unconditional), drop the shadow-copy assertions (442-443);test_repl_snapshot_params.c— replacetest_set_speed_updates_shadow_and_core_atomicswith a removal-message test, updatetest_info_config_reports_current_parameters(nospeed=; assert it is absent) and the help assertion (1212);test_repl_help.c— dropset speed <N>from the lists at 655 and 711 and the comments at 8, 555, 649; comment-only updates intest_repl_snapshot_params.c(18-28, 1044-1052, 1125-1135, 1144),test_repl_step_verbosity.c(30-32, 637) andtest_repl_resource_formatting.c(16);test_clock.c— the three…_simulation_…speed tests (163-229) become formula tests named for the formula, not for product speed modes (including theirRUN_TESTlines at 496-499) and the header comment at 5-18; the normal (1/1) and frozen (0/1) tests stay. - Unchanged:
test_maxtasks_wiring_sim_driver_parity.candtest_sched_alloc_from_stamp.c(they write the core atoms, which remain). (requirements.md's "27" is the 24 files above that assignsim_speedplus three that write the core speed atoms directly: these two andtest_clock.c.) src/docs/deployment.md— 214 (example without--sim-speed), 220 (deprecated, no effect), 223 (no pacing difference between REPL and batch); describe the clock as moving only at recorded timestamps..kiro/specs/scheduler-core/requirements.md— flag table (359), AC 3 (383-405), AC 12 (750), AC 13 (786-791), AC 14 (797-798, including "strictly monotonically increasing"), AC 22 (1053, 1057, 1059, 1070-1072, 1102), Data Structure Changes Summary (1244)..kiro/specs/scheduler-core/design.md— 5.14 row (299), sim-driver fields (529),test_clock.cmode rows (760), "Simulation fast mode" (783).
System Flows
sequenceDiagram
participant Main as main.c
participant Sim as sim.c
participant Loop as sim_loop.c
participant Core as core
Main->>Main: argp parse, mylog_init
Main->>Main: sched_cli_warn_deprecated_options(sim_speed)
Main->>Main: if sim_speed given: mylog_fini, mylog_init (drain)
Main->>Sim: sched_sim_driver_run(args)
Sim->>Core: sim_set_virtual_clock(snapshot_ts): vbase=snapshot_ts, speed 0/1
loop each record
Loop->>Core: event: vbase = event.ts
Loop->>Core: session: vbase = beginRound
Loop->>Core: xce_core_begin_session: session_start = xce_core_now = beginRound
Loop->>Loop: decisions and output use xce_core_now = beginRound
end
- Every clock set is exact and unconditional; a value earlier than the current clock is set as is (Req 1.3).
endSessiondoes not move the clock (Req 1.2). - Because
speed_num = 0, nothing between two sets moves the clock: REPL waits, allocator work, output writing, the post-session pause and the end-of-replay park all read the last set value (Req 1.5).
Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1 | Clock = start snapshot's timestamp | SimClock | sim_set_virtual_clock() |
Startup |
| 1.2 | Exact set at each event/session | SimClock | sim_advance_virtual_clock() |
Replay loop |
| 1.3 | Earlier timestamps set exactly, no error | SimClock | sim_advance_virtual_clock() |
Replay loop |
| 1.4 | session_start == beginRound |
SimClock (reads core_session.c:429, unchanged) |
xce_core_begin_session() |
Replay loop |
| 1.5 | No other clock movement | SimClock, ReplSet | speed 0/1; repl_set_speed() removed, REPL read-only |
Replay loop |
| 1.6 | Identical logical timestamps and decisions across replays | SimClock | all of the above | Replay loop |
| 1.7 | Written beginRound = start time |
SimClock (sim_loop.c output writes) |
sched_record_session_ctx_new(xce_core_now()) |
Replay loop |
| 1.8 | durationNs 0, decision ts = beginRound |
SimClock (sim_loop.c output writes) |
finalize and decision writes | Replay loop |
| 2.1 | Accepted in both modes | CliCompat, main | argp option | Startup |
| 2.2 | No effect | main, SimClock | no speed plumbing | Startup |
| 2.3 | One warning naming the last value | CliCompat, main | sched_cli_warn_deprecated_options() |
Startup |
| 2.4 | Non-numeric/negative/empty accepted | main, CliCompat | no validation | Startup |
| 2.5 | No warning without the option | CliCompat | NULL argument | Startup |
| 2.6 | Help text | main | argp table | — |
| 3.1 | set speed message, no state change |
ReplSet | repl_cmd_set() |
REPL |
| 3.2 | Returns to prompt, keeps reading | ReplSet | returns false | REPL |
| 3.3 | Help and usage without speed | ReplSet, ReplHelp | text | REPL |
| 3.4 | info config without speed |
ReplInfo | text | REPL |
| 3.5 | Other set parameters unchanged |
ReplSet | repl_cmd_set() |
REPL |
| 4.1 | Normal mode unchanged | (none changed) | xce_core_set_wallclock_anchor() untouched |
— |
| 4.2 | Old recordings replay | SimClock | reader unchanged | Replay loop |
| 4.3 | .replay format unchanged |
(none changed) | schema untouched | — |
| 5.1 | Operator docs | Docs | deployment.md |
— |
| 5.2 | scheduler-core spec | Docs | requirements.md, design.md | — |
| 5.3 | In-repo callers keep passing | CliCompat, main | soak script unchanged | Startup |
Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies | Contracts |
|---|---|---|---|---|---|
SimClock (sim.c, sim_loop.c) |
Sim driver | Pin speed 0/1; exact sets | 1.1-1.8, 2.2, 4.2 | core clock atoms (P0) | State |
CliCompat (cli_compat.c) |
CLI | Deprecation warning | 2.1, 2.3, 2.4, 2.5, 5.3 | WARN (P0) |
Service |
main (main.c) |
CLI | Option table, call order, log drain | 2.1, 2.2, 2.3, 2.4, 2.6, 5.3 | argp, CliCompat (P0) | — |
ReplSet / ReplHelp / ReplInfo (repl_params.c, repl.c, repl_info.c) |
REPL | Remove speed | 1.5, 3.1-3.5 | — | — |
Docs (deployment.md, scheduler-core spec) |
Docs | Describe actual behavior | 5.1, 5.2 | — | — |
Sim driver
SimClock
| Field | Detail |
|---|---|
| Intent | Keep the simulation-mode logical clock at the last recorded timestamp set |
| Requirements | 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 2.2, 4.2 |
Responsibilities & Constraints
sim_set_virtual_clock()is the only place that sets the speed in simulation mode, and it sets 0/1.sim_advance_virtual_clock()setsvirtual_clock_base(andreal_clock_base) unconditionally: no clamping, no warning, no error for a value earlier than the current clock.- The clock jump to
beginRound(sim_loop.c:1528) stays before both the output session record opening (1538) andxce_core_begin_session()(1548); with speed 0 that is what makes 1.4, 1.7 and 1.8 hold (the relative order of the last two does not matter). The record is opened fromxce_core_now(), notcore.session_start, because it opens beforexce_core_begin_session()sets that field.
Contracts: State [x]
State Management
/* sim.c — replaces sim_set_virtual_clock(sim, snapshot_ts, sim_speed) */
static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim,
uint64_t snapshot_ts);
- Postconditions:
core.virtual_clock_base == snapshot_ts,core.speed_num == 0,core.speed_den == 1, soxce_core_now(&sim->core) == snapshot_tsuntil the nextsim_advance_virtual_clock(). - Invariant (simulation mode):
xce_core_now()returns the most recent value passed tosim_set_virtual_clock()orsim_advance_virtual_clock(), regardless of elapsed real time.
sched_sim_args_t loses sim_speed; xce_sched_sim_driver_t loses sim_speed_num and sim_speed_den. The core's speed_num/speed_den remain; core.h documents their two used values (1/1 normal, 0/1 simulation).
CLI
CliCompat
| Field | Detail |
|---|---|
| Intent | Log the one deprecation warning for --sim-speed |
| Requirements | 2.1, 2.3, 2.4, 2.5, 5.3 |
Contracts: Service [x]
Service Interface
/* include/cli_compat.h */
/*
* sched_cli_warn_deprecated_options - logs one WARN per deprecated option
* that was given. sim_speed is the raw --sim-speed value as argp stored it
* (the last occurrence), or NULL when the option was not given.
*/
void sched_cli_warn_deprecated_options(const char* sim_speed);
- Preconditions: called once, after
mylog_init(), before any mode-specific startup. - Postconditions:
sim_speed == NULLlogs nothing; otherwise exactly oneWARNline containing--sim-speed, the words "deprecated" and "no effect", "will be removed", and the value in quotes (so an empty value is visible as""). - Never fails, never exits, never parses the value.
Implementation Notes
main.ckeepsOPT_SIM_SPEEDwith argument name"N"so a trailing--sim-speedstays an argp error (Req 2.4); help text: "Deprecated, has no effect: the simulator clock moves only at recorded timestamps. Will be removed in a future release."
REPL
ReplSet / ReplHelp / ReplInfo
| Field | Detail |
|---|---|
| Intent | Remove the speed parameter from the REPL |
| Requirements | 1.5, 3.1, 3.2, 3.3, 3.4, 3.5 |
Implementation Notes
repl_cmd_set(): ifntok >= 2andtokens[1]is"speed", print[sim] set speed: the speed parameter was removed; the simulator clock moves only at recorded timestampsand return false (re-prompt, same ashelp); nothing else changes. Then the existingntok < 3usage check, whose text becomesset output <FILE>|off | set verify on|off | set units raw|human|both.repl_info_config()prints[sim] config: output=%s verify=%s events_processed=%llu current_ts=%llu (%s) sessions=%llu.repl.chelp drops theset speed <N>line;set output,set verify,set unitslines are unchanged.
Data Models
No persisted or wire data changes. The .replay schema is untouched (Req 4.3). In-memory changes: sched_sim_args_t.sim_speed and xce_sched_sim_driver_t.sim_speed_num/sim_speed_den are removed; the core's four clock atoms are unchanged. Simulation output values change only in content: SessionSummary.durationNs becomes 0 and ReplayDecision.ts equals the session's beginRound; in simulation mode the snapshot blob's diagnostic session_duration_ns is 0 after any replayed session, while a snapshot written before the first replayed session ends (the --sim-output initial SnapshotEntry, or a REPL snapshot/set output at the pre-replay pause) carries the value loaded from the recorded snapshot (snapshot_load.c:448-449).
Error Handling
--sim-speednever produces an error or a non-zero exit, except the argp error for a missing value, which is unchanged.- A recorded timestamp earlier than the current clock is not an error (Req 1.3).
set speedis not an error; it returns to the prompt.- When
--sim-speedis given,main()drains the logger right after logging the warning (mylog_fini()thenmylog_init(), while still single-threaded), so the warning is on disk before any mode-specific startup, whether the process later returns frommain()or exits through the REPL'sexit(0). The drain's only other observable effect, a repeated tlog "can not find gzip command" notice on stderr when gzip is absent, is accepted as outside Req 2.2's "only difference" (tlog's own diagnostic, not scheduler behavior; stdout, log content and simulation output are unaffected). - Monitoring: the startup
WARNis the only new log line; it goes to--logfilewhen given, otherwise to the default/tmp/xce_scheduler.log(main.c:216), in both modes.
Testing Strategy
New test binary test_sim_discrete_clock.c (registered as scheduler sim discrete clock test), using the in-process fixtures and stand-in picker/allocator pattern of the existing sim tests (e.g. test_sim_record_output.c, test_sim_session_ordering.c):
- Unit (SimClock):
- After
sched_sim_driver_start(),xce_core_now()equals the chosen snapshot's timestamp, stays equal after a real sleep (≥ 20 ms), and the core holdsspeed_num == 0,speed_den == 1(1.1, 1.5). - A replay whose second session's
beginRoundis earlier than the preceding event's timestamp: that session'ssession_startequals itsbeginRound, and the replay completes without error. Second case, as the normal-mode recorder produces: events whosetsis earlier than the start snapshot's timestamp; after each such eventxce_core_now()equals that event'sts(observed by calling thestaticsim_process_replay_event_message()directly, reachable through the white-box#includeofsim_loop.c), and the replay completes (1.3). A fixture whose last recorded event (ts = T) is followed by a second SnapshotEntry withts = Sand then theendSessionmarker (whosetsdiffers from bothTandS, e.g. 0 astest_sim_record_output.c'sappend_end_of_stream_marker()writes), whereSdiffers fromTand is greater than the initial snapshot'sts(a SnapshotEntry can be hand-built astest_sim_startup.c:369-390does): run it throughsched_sim_driver_run()in batch mode with the stand-in picker/allocator andargs.sim_start_tsset to the initial snapshot'sts, sosim.c:218does not pickSas the start snapshot and the real skip branch atsim_loop.c:1684-1692runs; after the run returns 0, and again after a real sleep (≥ 20 ms),xce_core_now()equalsT(1.5: mid-replay snapshot skip, and after the end of the recording).
- After
- Integration (replay loop):
- With a stand-in allocator that sleeps (≥ 10 ms) before deciding and a recorded
beginRoundthat is not a whole second:core->session_start, recorded by the stand-in allocator on everycbuf_getreturn (including thejobid == 0terminator, so a session with no pending jobs is still observed), equalsbeginRoundexactly (1.2, 1.4), and simulation output (sim->record_fileto a temp file) has each session'sbeginRoundequal to the recorded one,summary.durationNs == 0, and everyReplayDecision.ts == beginRound(1.7, 1.8). - The same recording replayed twice to two output files: identical session start times,
ReplayDecision.ts,durationNs, snapshot timestamps andrecordingStartTs, and identical decision sequences;recordingStartWallis not compared (1.6). The REPL's reported position (prompt andinfo session) readsxce_core_now(), so its equality across runs follows from the SimClock invariant proven above. - Fixtures written by the unchanged recorder and fixture writers (the format is untouched, 4.3) replay without error across the whole sim suite, as do the
/xce-test-hasoak's recordings (4.2).
- With a stand-in allocator that sleeps (≥ 10 ms) before deciding and a recorded
- Unit (CliCompat), with a local copy of the
capture_stdout_around()helper (each test file carries its own, e.g.test_repl_help.c); the test binary must not callmylog_init(), soWARNtakes tlog's early-print path to stdout (tlog.c:838-839):"0","abc","-1"and""each produce exactly one line naming the value and containing--sim-speed, "deprecated", "no effect" and "will be removed";NULLproduces no output (2.3, 2.4, 2.5). - REPL (update
test_repl_snapshot_params.c,test_repl_help.c):set speed 7and bareset speedprint the removal message, return false, and leavexce_core_now()and the core atoms unchanged;setusage andhelpdo not containspeed;info configdoes not containspeed=and still showsoutput=,verify=,events_processed=,current_ts=,sessions=;set output,set verify,set unitstests pass unchanged;set speed 7followed byinfo configandquitin a script file that a forked childfreopens as stdin (thechild_step_snapshot_quit()pattern,test_repl_snapshot_params.c:656-690; stdoutfreopened to a capture file; the parentwaitpids): the child exits 0 and the capture contains both the removal message and the[sim] config:line (a non-tty stdin exercises the samereadline()path as a pipe) (3.1-3.5). - Regression: the full scheduler suite (every sim and REPL test now runs at the pinned speed with no speed field),
test_clock.cnormal-mode 1/1 test andtest_sched_alloc_from_stamp.cnormal-mode test unchanged (4.1);test_core_isolation.cpasses (core untouched). - Manual/soak (5.3):
/xce-test-haruns its--sim-verify --sim-speed=0step unmodified and passes; the step's log contains the deprecation warning. - CLI help (2.6): in
test_sim_discrete_clock.c,popen(XCE_SCHEDULER_BIN " --help")and assert the--sim-speedentry contains "Deprecated" and the whole output contains neither "real-time" nor "as-fast-as-possible". - CLI end-to-end (1.4, 1.7, 1.8 on the default command line; 2.1-2.5), in
test_sim_discrete_clock.c, viapopen()ofXCE_SCHEDULER_BIN(the child inherits the test'sASAN_OPTIONS=detect_leaks=0) against a fixture the test writes, whosebeginRoundis not a whole second. The in-process tests above cannot fail on the pre-change code, because a zero-initializedsched_sim_args_talready selected speed 0; this test is the one that does (default speed 1 atmain.c:339,strtoullrejection atmain.c:356-368).- (a)
--sim FIXTURE --sim-output OUT0 --logfile LOG0with no--sim-speedexits 0; OUT0's sessions havebeginRoundequal to the recorded value,summary.durationNs == 0, and everyReplayDecision.ts == beginRound; LOG0 is absent or has no--sim-speedline (1.7, 1.8, 2.5; 1.4 on the default command line follows becausesession_start(core_session.c:429) and the writtenbeginRound(sim_loop.c:1538) are bothxce_core_now()reads with no clock write between them, and 1.4 itself is proven in-process). The fixture contains a queue update and a pending job before each session (astest_sim_record_output.c'sappend_queue_update_event()/append_jobs_push_event()do), and the test asserts at least one decision per session, so thetscheck is not vacuous. - (b) The same command, but with a fresh
--sim-output OUTnand--logfile LOGnfor each run, plus each of--sim-speed 7,--sim-speed abc,--sim-speed -1and--sim-speed=exits 0, produces output identical to OUT0 in every field exceptrecordingStartWall(compared byte-for-byte after the first message; theReplayHeaderis decoded withdecode_ReplayHeaderand compared field by field exceptrecordingStartWall) (2.2), and logs exactly one line containing--sim-speedand the quoted value (2.1, 2.3, 2.4). - (c)
--sim-speed 1 --sim-speed 9, with its own fresh--sim-outputand--logfile, logs exactly one such line, naming"9"(2.3). - (d)
--sim FIXTURE --sim-speedwith no value exits non-zero (argp usage error) (2.4). - (e)
--sim FIXTURE -i --sim-speed 3 --logfile LOGI < /dev/null(end of input →quit→exit(0), repl.c:350) exits 0, and LOGI contains exactly one line containing--sim-speedand"3"(2.3 on the REPL exit path). - Mechanics: the test reads the child's stdout to EOF before
pclose()(or redirects it to/dev/nullin the command), since the child prints decision lines, and takes the exit code fromWEXITSTATUS(pclose(...)). - Normal mode (2.1, 2.3) is covered by the CliCompat unit test plus the call site's placement before the
--simbranch inmain.c, because normal mode needs a controller.
- (a)
Project Invariant Cross-Check
Scheduler dimensions from .claude/skills/xce-audit-invariants/SKILL.md:
- Driver state-equivalence: only the simulation driver's clock setting changes; the session protocol calls in
sim_loop.canddriver.care untouched. A replayedsession_startand writtenbeginRoundnow equal the recorded values exactly;durationNs = 0and decisionts = beginRoundare intentional simulation-output differences (requirements, Adjacent expectations); decisions and accounting are unaffected. Pass. - Core module I/O isolation: no core-family source changes except comments in
core.h. Pass. - Picker→allocator
jbufcapacity-1 rendezvous: untouched. Pass. - Snapshot save/load field completeness: no persistence change;
session_startandsession_duration_nsstill round-trip unchanged, and after a replayed session their values arebeginRoundand 0. Pass.