myxce/.kiro/specs/scheduler-sim-discrete-clock/research.md
Rongsong Shen 4b6872d053
docs(spec): scheduler-sim-discrete-clock requirements, design and tasks approved
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 04:16:17 +00:00

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), and speed_num = 0 makes it return virtual_clock_base exactly. 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 the endSession return) and per session beginRound (sim_loop.c:1528, before xce_core_begin_session() sets session_start). REPL set output/snapshot only read it. With speed 0 these are the only clock movements (Req 1.5), and session_start, the written beginRound, decision ts and durationNs all derive from xce_core_now() at beginRound (Req 1.4, 1.7, 1.8).
    • parse_opt runs before mylog_init() (main.c:265 vs :325); a WARN issued before mylog_init() goes to stdout, not --logfile. The deprecation warning therefore belongs right after mylog_init() and before the --sim branch, which covers both modes.
    • repl_cmd_set() checks ntok < 3 (repl_params.c:400) before dispatching on the parameter name, so bare set speed never reaches the speed branch today (Req 3.1 needs the name check first).

Research Log

Clock mechanics and who writes the clock atoms

  • Context: Requirement 1 (event-driven clock) and whether speed_num/speed_den can 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 reads speed_num/speed_den except xce_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-speed is stored as a raw string; argp keeps the last occurrence; a trailing --sim-speed with no value is an argp error. Validation (strtoull, exit 1) happens only in the --sim branch; normal mode ignores the option silently. Logging before mylog_init() goes to stdout.
  • Implications: Drop the numeric validation and the sim_args.sim_speed plumbing; warn once after mylog_init(). main() is not unit-testable, so the warning goes in a small function a test can call with the existing capture_stdout_around() pattern (test_repl_help.c:437-460; test binaries never call mylog_init(), so WARN reaches 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); every set path returns false. info config prints speed=%llu from the shadow copy sim->sim_speed_num, its only production reader. Help line at repl.c:314.
  • Implications: Handle speed before the argument-count check, print a fixed removal message, return false. Delete repl_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) and test_repl_snapshot_params.c also carry behavioral assertions; test_clock.c, test_sim_startup.c, test_repl_snapshot_params.c and test_repl_help.c test speed behavior, the help text or the info config line; test_sim_event_loop.c, test_maxtasks_wiring_sim_driver_parity.c and test_sched_alloc_from_stamp.c write the core atoms directly (unaffected, since the core keeps them). No test runs the xce_scheduler binary 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-speed is run-ha-soak.sh:738, which counts only decision verification MISMATCH lines.

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) sets virtual_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) in src/scheduler/src/cli_compat.c, called once from main() after mylog_init() and before the --sim branch. It logs one WARN naming the value when the pointer is non-NULL, and nothing otherwise.
  • Rationale: Goes to --logfile in 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(), if ntok >= 2 and tokens[1] is speed, print the removal message and return false; then the usual ntok < 3 usage 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, the sim_speed_num/sim_speed_den shadow copy, repl_set_speed() and the numeric validation in main.c; keep the core fields (Option B rejected).

Review Gate

  • Pass 1 (independent reviewer): GATE PASS, no must-fix. Ten nits applied: --help test mechanism (popen of 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 popen test of the binary with default options and with every --sim-speed variant was added. (2) The snapshot's session_duration_ns claim 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_TEST lines, SimClock rationale).

  • Design review round 2: one must-fix issue fixed. The --sim branch returns without mylog_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-speed and piped set speed).

  • Design review round 3: one must-fix issue fixed. Interactive runs exit through the REPL's exit(0) and never return from main(), and calling tlog_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 while main() 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 (freopen of a script file), the unreachable -ENOSYS branch 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_ts pinned 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 pinned endSession timestamp 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.c failure-path tests set speed 1): they fail before the clock matters; mechanical removal of the assignment.
  • Code outside the scheduler parses speed= from info config: none found (grep of xceadmin, harness, tools, frontend, .claude).
  • The soak script's log now contains a warning: it counts only decision verification MISMATCH lines (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.