12 KiB
Research & Design Decisions
Summary
- Feature:
scheduler-sim-discrete-clock - Discovery Scope: Extension (light discovery): the simulator is already event-driven; the work is to pin its clock, retire a CLI option, remove a REPL parameter, and align docs/specs.
- Key Findings:
- The core already has a frozen-clock mode:
xce_core_now() = virtual_clock_base + (monotonic_now - real_clock_base) * speed_num / speed_den(core_clock.c:80-119), andspeed_num = 0makes it returnvirtual_clock_baseexactly. Every simulator test that runs a replay already uses it. Pinning simulation mode to 0/1 delivers Requirement 1 without touching the core formula. - In simulation mode exactly three places write the clock:
sim_set_virtual_clock()at start (sim.c:309-329),sim_advance_virtual_clock()per event (sim_loop.c:417, after theendSessionreturn) and per sessionbeginRound(sim_loop.c:1528, beforexce_core_begin_session()setssession_start). REPLset output/snapshotonly read it. With speed 0 these are the only clock movements (Req 1.5), andsession_start, the writtenbeginRound, decisiontsanddurationNsall derive fromxce_core_now()atbeginRound(Req 1.4, 1.7, 1.8). parse_optruns beforemylog_init()(main.c:265vs:325); aWARNissued beforemylog_init()goes to stdout, not--logfile. The deprecation warning therefore belongs right aftermylog_init()and before the--simbranch, which covers both modes.repl_cmd_set()checksntok < 3(repl_params.c:400) before dispatching on the parameter name, so bareset speednever reaches the speed branch today (Req 3.1 needs the name check first).
- The core already has a frozen-clock mode:
Research Log
Clock mechanics and who writes the clock atoms
- Context: Requirement 1 (event-driven clock) and whether
speed_num/speed_dencan be dropped from the core. - Sources Consulted:
src/scheduler/src/core_clock.c,include/core.h:264-302,706-716,core.c:130-143,sim.c:309-329,594,sim_loop.c:175-202,411-417,1520-1545,repl_params.c:202-226,scheduler.c:757. - Findings: Writers of the four atoms: core init (speed 1/1),
xce_core_set_wallclock_anchor()(normal mode, 1/1),sim_set_virtual_clock()(sim start,speed_num = args->sim_speed),sim_advance_virtual_clock()(bases only),repl_set_speed()(speed). No production code readsspeed_num/speed_denexceptxce_core_now(). Normal mode is always 1/1. - Implications: Removing the fields from the core would need a replacement freeze flag and would churn 28 test files and the core-isolation surface for no behavioral gain. Keep them; simulation mode sets 0/1 unconditionally; nothing else ever sets another value.
CLI parsing and logging order
- Context: Requirement 2 (deprecated no-op in both modes, one warning naming the last value).
- Sources Consulted:
src/scheduler/src/main.c:53,74,92-94,149-151,265-400;src/include/utils.h:154-160; vendored tlog early-print path. - Findings:
--sim-speedis stored as a raw string; argp keeps the last occurrence; a trailing--sim-speedwith no value is an argp error. Validation (strtoull, exit 1) happens only in the--simbranch; normal mode ignores the option silently. Logging beforemylog_init()goes to stdout. - Implications: Drop the numeric validation and the
sim_args.sim_speedplumbing; warn once aftermylog_init().main()is not unit-testable, so the warning goes in a small function a test can call with the existingcapture_stdout_around()pattern (test_repl_help.c:437-460; test binaries never callmylog_init(), soWARNreaches stdout —test_scheduler_decision_publish_retry.c:61-70).
REPL surface
- Context: Requirement 3.
- Sources Consulted:
repl_params.c:171-226,388-435,repl_info.c:1770-1806,repl.c:253-254,314,401-463,repl_internal.h. - Findings:
repl_dispatch()returns true (resume) or false (re-prompt); everysetpath returns false.info configprintsspeed=%llufrom the shadow copysim->sim_speed_num, its only production reader. Help line atrepl.c:314. - Implications: Handle
speedbefore the argument-count check, print a fixed removal message, return false. Deleterepl_set_speed()and the shadow copy.
Tests, docs and specs touched
- Context: File Structure Plan and Requirement 5.
- Findings: 22 test files only assign
args.sim_speed = 0(mechanical removal);test_sim_startup.c(which also assigns 1 in its failure-path tests) andtest_repl_snapshot_params.calso carry behavioral assertions;test_clock.c,test_sim_startup.c,test_repl_snapshot_params.candtest_repl_help.ctest speed behavior, the help text or theinfo configline;test_sim_event_loop.c,test_maxtasks_wiring_sim_driver_parity.candtest_sched_alloc_from_stamp.cwrite the core atoms directly (unaffected, since the core keeps them). No test runs thexce_schedulerbinary as a subprocess. Docs:deployment.md:214,220,223. scheduler-core requirements/design passages listed in requirements.md 5.2. The only in-repo command line passing--sim-speedisrun-ha-soak.sh:738, which counts onlydecision verification MISMATCHlines.
Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| A. Pin sim to speed 0/1 (selected) | Keep the core formula; sim_set_virtual_clock() always stores 0/1; remove every sim/CLI/REPL path that sets another speed |
Smallest change; reuses the exact mode all replay tests already exercise; core untouched | Core formula still supports N/M values no product path uses | Document the two values in core.h |
B. Replace speed with a clock_frozen flag |
Core formula becomes vbase + (frozen ? 0 : elapsed) |
Removes unused generality | Touches core, core isolation surface, 28 test files; no behavioral gain | Rejected (simplification lens) |
| C. Simulator-private clock | Sim stops using the core clock | None over A | Breaks the "one core, two consumers" model and driver equivalence | Rejected |
Design Decisions
Decision: Frozen clock via existing speed_num = 0
- Context: Req 1.1-1.8.
- Alternatives Considered: Options B and C above.
- Selected Approach:
sim_set_virtual_clock(sim, snapshot_ts)setsvirtual_clock_base = snapshot_ts,speed_num = 0,speed_den = 1;sim_advance_virtual_clock()unchanged in behavior (exact, unclamped set). - Rationale: Delivers exactness by construction; zero change to the core module.
- Trade-offs: The core formula keeps a generality nothing uses; documented as "normal mode 1/1, simulation mode 0/1".
- Follow-up: Tests prove that real time elapsing inside a replay does not move the clock.
Decision: Deprecation warning in a testable helper, emitted after mylog_init()
- Context: Req 2.1-2.6.
- Selected Approach: New
sched_cli_warn_deprecated_options(const char* sim_speed)insrc/scheduler/src/cli_compat.c, called once frommain()aftermylog_init()and before the--simbranch. It logs oneWARNnaming the value when the pointer is non-NULL, and nothing otherwise. - Rationale: Goes to
--logfilein both modes; unit-testable; argp already gives last-value and missing-value semantics. - Trade-offs: One small new file for one function; justified by testability and by giving the later removal a single place.
Decision: set speed handled before the argument-count check
- Context: Req 3.1, 3.2.
- Selected Approach: In
repl_cmd_set(), ifntok >= 2andtokens[1]isspeed, print the removal message and return false; then the usualntok < 3usage check.
Generalization / Build vs. Adopt / Simplification
- Generalization: Requirements 1.1-1.8 are one property ("the sim clock moves only at recorded timestamps"); a single mechanism (speed 0/1 plus the three existing writers) satisfies all of them.
- Build vs. Adopt: Adopt the core's existing frozen mode; no new dependency.
- Simplification: Remove
sched_sim_args_t.sim_speed, thesim_speed_num/sim_speed_denshadow copy,repl_set_speed()and the numeric validation inmain.c; keep the core fields (Option B rejected).
Review Gate
-
Pass 1 (independent reviewer): GATE PASS, no must-fix. Ten nits applied:
--helptest mechanism (popenof the built binary via-DXCE_SCHEDULER_BIN), meson source-list placement, default log file path, line ranges, stale comments, a stronger warning test, a realistic 1.3 case, 4.2 wording, invariant-1 wording, and the test-file count here. -
Design review round 1: two must-fix issues fixed. (1) The in-process clock tests cannot fail on the pre-change code (zero-initialized args already meant speed 0), so an end-to-end
popentest of the binary with default options and with every--sim-speedvariant was added. (2) The snapshot'ssession_duration_nsclaim was corrected: snapshots written before the first replayed session carry the recorded value. Nine nits applied (test registration, line ranges, stale comments, observation points, build-order constraint,RUN_TESTlines, SimClock rationale). -
Design review round 2: one must-fix issue fixed. The
--simbranch returns withoutmylog_fini(), and tlog writes asynchronously with no exit hook, so a startup warning could be lost; superseded in round 3. Seven nits applied (1.4 proof attribution, non-vacuous decision check, popen pipe handling, absent log file, output comparison method, line references, tests for a trailing--sim-speedand pipedset speed). -
Design review round 3: one must-fix issue fixed. Interactive runs exit through the REPL's
exit(0)and never return frommain(), and callingtlog_exit()at the returns or at quit is unsafe because the picker/allocator threads are never joined. The design now drains the logger (mylog_fini()+mylog_init()) right after the warning whilemain()is still single-threaded, covering every exit path; an interactive end-to-end case (e) was added. Nits applied: tlog citation, the real scripted-REPL test pattern (freopenof a script file), the unreachable-ENOSYSbranch noted, a mid-replay snapshot observation for 1.5. -
Design review round 4: one must-fix issue fixed. The mid-replay snapshot check could not fail (wrong code path, masked by the next event, and the start-snapshot scan would select the later snapshot); it now runs the real skip branch with
sim_start_tspinned and checks the clock after the run. The end-to-end cases now use a fresh output and log file per run. Nits applied (tlog line reference, drain in the sequence diagram, 5.3 coverage in the components table, drain side effects, REPL position under 1.6, test-local capture helper). -
Design review round 5: one must-fix issue fixed (1.5 coverage disagreed between the Traceability and Components tables; ReplSet now covers 1.5 because removing
repl_set_speed()removes the only other clock writer). Nits applied: line ranges, a pinnedendSessiontimestamp in the mid-replay test, fresh output file in E2E case (c), stale four-mode comments, component names in Traceability, drain calls in Allowed Dependencies. -
Design review round 6: no must-fix issues (40+ line references verified). Four nits applied: a line range, the neutral 1/1 core-init citation, the drain's repeated gzip notice noted against Req 2.2, and the test-file count reconciled with requirements.md.
Risks & Mitigations
- A test relies on drift at speed 1 (the three
test_sim_startup.cfailure-path tests set speed 1): they fail before the clock matters; mechanical removal of the assignment. - Code outside the scheduler parses
speed=frominfo config: none found (grep of xceadmin, harness, tools, frontend, .claude). - The soak script's log now contains a warning: it counts only
decision verification MISMATCHlines (run-ha-soak.sh:737-739); verified by 5.3's check. - Old recordings whose events precede their first snapshot's timestamp move the clock backwards: accepted by Req 1.3; exact set, no error.
References
.kiro/steering/scheduler-driver-equivalence.md— one core, two drivers; the change stays in the sim driver..claude/skills/xce-audit-invariants/SKILL.md— scheduler dimensions 1-4 (cross-checked in design.md)..kiro/specs/libxce-alloc-preason-equality— task 5.3 depends on Req 1.2-1.4.