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

88 lines
12 KiB
Markdown

# 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.