318 lines
34 KiB
Markdown
318 lines
34 KiB
Markdown
# 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-speed` keeps 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-speed` from the command line (a later release).
|
|
- Changing the `.replay` format, the core clock formula, or normal-mode clock behavior (Req 4).
|
|
- How `from` is computed and compared (spec libxce-alloc-preason-equality).
|
|
|
|
## Boundary Commitments
|
|
|
|
### This Spec Owns
|
|
- The simulation-mode clock setting: `sim_set_virtual_clock()` stores `speed_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_start` equals its recorded `beginRound` exactly, including when that is earlier than the previous clock value (Req 1.3, 1.4).
|
|
- The `--sim-speed` command-line behavior: accepted with any value in both modes, no effect, one startup warning (`sched_cli_warn_deprecated_options()`).
|
|
- The REPL `set speed` removal message, and the `help`, `set` usage and `info config` text without speed.
|
|
- The simulation sections of `src/docs/deployment.md`, and the speed-mode passages of `.kiro/specs/scheduler-core/requirements.md` and `design.md` listed in requirements.md 5.2.
|
|
|
|
### Out of Boundary
|
|
- `xce_core_now()`'s formula and the four clock atoms in `core.h` (kept; only their documentation of which values are used changes).
|
|
- Normal mode: `xce_core_set_wallclock_anchor()`, `driver.c`, recording (`recording*.c`).
|
|
- The `.replay` schema (`src/schema/controller.capnp`).
|
|
- `from` stamping and allocation equality (spec libxce-alloc-preason-equality).
|
|
- The superseded `scheduler-framework` spec, and scheduler-core `brief.md`/`tasks.md` history.
|
|
|
|
### 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()` in `src/include/utils.h` (`WARN`; the drain in `main.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 and `xce_core_begin_session()` in `sim_loop.c`, or `xce_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-speed` entirely (the later release): update `sched_cli_warn_deprecated_options()`, the help text, the soak script and `deployment.md`.
|
|
|
|
## Architecture
|
|
|
|
### Existing Architecture Analysis
|
|
- One core, two drivers (`.kiro/steering/scheduler-driver-equivalence.md`): `driver.c` (normal) and `sim_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 in `xce_core_set_wallclock_anchor()` (`core_clock.c:180`); `speed_num = 0` returns `virtual_clock_base` exactly.
|
|
- Simulation-mode clock writers: `sim_set_virtual_clock()` (`sim.c:309`, called once at `sim.c:594`), `sim_advance_virtual_clock()` at each event (`sim_loop.c:417`, after the `endSession` return at 412-415) and at each session (`sim_loop.c:1528`, immediately before the session record opens at 1538 and `xce_core_begin_session()` sets `session_start`). `repl_set_speed()` (`repl_params.c:202`) is the only other writer and is removed.
|
|
|
|
### Architecture Pattern & Boundary Map
|
|
|
|
```mermaid
|
|
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` — remove `sched_sim_args_t.sim_speed` (155-157) and `xce_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; store `speed_num = 0`, `speed_den = 1`; no shadow copy); caller at 594.
|
|
- `src/scheduler/src/main.c` — option help text (92-94); remove the `sim_speed` default, validation and `sim_args.sim_speed` assignment (339, 356-368, 373) and the comment at 296-299 that cites it; update the comments at 67-72 (flag meanings live in `sched_sim_args_t`, which no longer has a speed field) and 74 (the `sim_speed` field); call `sched_cli_warn_deprecated_options(arguments.sim_speed)` once after `mylog_init()` (325) and before the `--sim` branch (335); when `arguments.sim_speed != NULL`, immediately after that call (still single-threaded: no picker, allocator or driver thread exists before `sched_sim_driver_run()` or `sched_init()`), call `mylog_fini()` (`utils.c:303`, `tlog_exit()`) and then `mylog_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_dep` at `src/scheduler/meson.build:62`: no `atexit` or 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()` clears `root_format`, tlog.c:2222, which `tlog_init()` checks at 2135; the file is reopened with `O_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 `--sim` branch's returns (main.c:352, 391, 396, 399; 366 is removed), the REPL's `exit(0)` on `quit` or end of input (repl.c:350), and normal-mode shutdown. `tlog_exit()` is deliberately not called at the `--sim` returns or in the REPL's quit: the picker and allocator threads are never joined (sim_loop.c:1374-1385), and `tlog_exit()` is unsafe while another thread logs (tlog.c:833-841 vs 2213-2219). The unreachable `-ENOSYS` branch (main.c:380-392; no simulation path returns `-ENOSYS`) is left untouched.
|
|
- `src/scheduler/src/repl_params.c` — delete `repl_set_speed()` and its doc comment (174-227; keep and edit the section banner at 170-172); in `repl_cmd_set()` handle `speed` first; remove `speed` from the usage string (400-402); comment references (22, 44-48, 348).
|
|
- `src/scheduler/src/repl_info.c` — `info config` line without `speed=` (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; also `repl_params.c:171` (section banner) and `:388-394` (the `repl_cmd_set()` doc comment naming `set speed <N>`); `sim.c:227-230` (says `--sim-output` timestamps come from a clock that "never runs backwards", which Req 1.3 contradicts); `testsuite/meson.build` comments at 318-319, 566, 1178 and 1211.
|
|
- `src/scheduler/meson.build` — add `'src/cli_compat.c'` to `scheduler_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 with `foreach f : scheduler_srcs` pick it up harmlessly.
|
|
- `src/scheduler/testsuite/meson.build` — register `sched_sim_discrete_clock_test` from `['test_sim_discrete_clock.c', '../src/cli_compat.c']` plus the white-box sim `#include`s the other sim tests use (it must not link `scheduler_srcs`, whose `sched_election.c` would clash with the test's stand-in picker/allocator); pass `-DXCE_SCHEDULER_BIN="<path>"` from `scheduler.full_path()` (`src/scheduler/meson.build:67`, defined before `subdir('testsuite')` at `:171`) and register it with `test('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.h` fields breaks compilation of `main.c`, `sim.c`, `repl_params.c`, `repl_info.c` and every test file that assigns `sim_speed` (plus the assignments in `test_repl_snapshot_params.c:1164-1165`, its shadow-field assertions at 896-914, and `test_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 the `core.speed_num == 0`/`speed_den == 1` assertions (now unconditional), drop the shadow-copy assertions (442-443); `test_repl_snapshot_params.c` — replace `test_set_speed_updates_shadow_and_core_atomics` with a removal-message test, update `test_info_config_reports_current_parameters` (no `speed=`; assert it is absent) and the help assertion (1212); `test_repl_help.c` — drop `set speed <N>` from the lists at 655 and 711 and the comments at 8, 555, 649; comment-only updates in `test_repl_snapshot_params.c` (18-28, 1044-1052, 1125-1135, 1144), `test_repl_step_verbosity.c` (30-32, 637) and `test_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 their `RUN_TEST` lines 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.c` and `test_sched_alloc_from_stamp.c` (they write the core atoms, which remain). (requirements.md's "27" is the 24 files above that assign `sim_speed` plus three that write the core speed atoms directly: these two and `test_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.c` mode rows (760), "Simulation fast mode" (783).
|
|
|
|
## System Flows
|
|
|
|
```mermaid
|
|
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). `endSession` does 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()` sets `virtual_clock_base` (and `real_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) and `xce_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 from `xce_core_now()`, not `core.session_start`, because it opens before `xce_core_begin_session()` sets that field.
|
|
|
|
**Contracts**: State [x]
|
|
|
|
##### State Management
|
|
```c
|
|
/* 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`, so `xce_core_now(&sim->core) == snapshot_ts` until the next `sim_advance_virtual_clock()`.
|
|
- Invariant (simulation mode): `xce_core_now()` returns the most recent value passed to `sim_set_virtual_clock()` or `sim_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
|
|
```c
|
|
/* 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 == NULL` logs nothing; otherwise exactly one `WARN` line 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.c` keeps `OPT_SIM_SPEED` with argument name `"N"` so a trailing `--sim-speed` stays 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()`: if `ntok >= 2` and `tokens[1]` is `"speed"`, print `[sim] set speed: the speed parameter was removed; the simulator clock moves only at recorded timestamps` and return false (re-prompt, same as `help`); nothing else changes. Then the existing `ntok < 3` usage check, whose text becomes `set 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.c` help drops the `set speed <N>` line; `set output`, `set verify`, `set units` lines 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-speed` never 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 speed` is not an error; it returns to the prompt.
|
|
- When `--sim-speed` is given, `main()` drains the logger right after logging the warning (`mylog_fini()` then `mylog_init()`, while still single-threaded), so the warning is on disk before any mode-specific startup, whether the process later returns from `main()` or exits through the REPL's `exit(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 `WARN` is the only new log line; it goes to `--logfile` when 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 holds `speed_num == 0`, `speed_den == 1` (1.1, 1.5).
|
|
- A replay whose second session's `beginRound` is earlier than the preceding event's timestamp: that session's `session_start` equals its `beginRound`, and the replay completes without error. Second case, as the normal-mode recorder produces: events whose `ts` is earlier than the start snapshot's timestamp; after each such event `xce_core_now()` equals that event's `ts` (observed by calling the `static` `sim_process_replay_event_message()` directly, reachable through the white-box `#include` of `sim_loop.c`), and the replay completes (1.3). A fixture whose last recorded event (`ts = T`) is followed by a second SnapshotEntry with `ts = S` and then the `endSession` marker (whose `ts` differs from both `T` and `S`, e.g. 0 as `test_sim_record_output.c`'s `append_end_of_stream_marker()` writes), where `S` differs from `T` and is greater than the initial snapshot's `ts` (a SnapshotEntry can be hand-built as `test_sim_startup.c:369-390` does): run it through `sched_sim_driver_run()` in batch mode with the stand-in picker/allocator and `args.sim_start_ts` set to the initial snapshot's `ts`, so `sim.c:218` does not pick `S` as the start snapshot and the real skip branch at `sim_loop.c:1684-1692` runs; after the run returns 0, and again after a real sleep (≥ 20 ms), `xce_core_now()` equals `T` (1.5: mid-replay snapshot skip, and after the end of the recording).
|
|
- **Integration (replay loop)**:
|
|
- With a stand-in allocator that sleeps (≥ 10 ms) before deciding and a recorded `beginRound` that is not a whole second: `core->session_start`, recorded by the stand-in allocator on every `cbuf_get` return (including the `jobid == 0` terminator, so a session with no pending jobs is still observed), equals `beginRound` exactly (1.2, 1.4), and simulation output (`sim->record_file` to a temp file) has each session's `beginRound` equal to the recorded one, `summary.durationNs == 0`, and every `ReplayDecision.ts == beginRound` (1.7, 1.8).
|
|
- The same recording replayed twice to two output files: identical session start times, `ReplayDecision.ts`, `durationNs`, snapshot timestamps and `recordingStartTs`, and identical decision sequences; `recordingStartWall` is not compared (1.6). The REPL's reported position (prompt and `info session`) reads `xce_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-ha` soak's recordings (4.2).
|
|
- **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 call `mylog_init()`, so `WARN` takes 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"; `NULL` produces no output (2.3, 2.4, 2.5).
|
|
- **REPL** (update `test_repl_snapshot_params.c`, `test_repl_help.c`): `set speed 7` and bare `set speed` print the removal message, return false, and leave `xce_core_now()` and the core atoms unchanged; `set` usage and `help` do not contain `speed`; `info config` does not contain `speed=` and still shows `output=`, `verify=`, `events_processed=`, `current_ts=`, `sessions=`; `set output`, `set verify`, `set units` tests pass unchanged; `set speed 7` followed by `info config` and `quit` in a script file that a forked child `freopen`s as stdin (the `child_step_snapshot_quit()` pattern, `test_repl_snapshot_params.c:656-690`; stdout `freopen`ed to a capture file; the parent `waitpid`s): the child exits 0 and the capture contains both the removal message and the `[sim] config:` line (a non-tty stdin exercises the same `readline()` 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.c` normal-mode 1/1 test and `test_sched_alloc_from_stamp.c` normal-mode test unchanged (4.1); `test_core_isolation.c` passes (core untouched).
|
|
- **Manual/soak (5.3)**: `/xce-test-ha` runs its `--sim-verify --sim-speed=0` step 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-speed` entry 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`, via `popen()` of `XCE_SCHEDULER_BIN` (the child inherits the test's `ASAN_OPTIONS=detect_leaks=0`) against a fixture the test writes, whose `beginRound` is not a whole second. The in-process tests above cannot fail on the pre-change code, because a zero-initialized `sched_sim_args_t` already selected speed 0; this test is the one that does (default speed 1 at `main.c:339`, `strtoull` rejection at `main.c:356-368`).
|
|
- (a) `--sim FIXTURE --sim-output OUT0 --logfile LOG0` with no `--sim-speed` exits 0; OUT0's sessions have `beginRound` equal to the recorded value, `summary.durationNs == 0`, and every `ReplayDecision.ts == beginRound`; LOG0 is absent or has no `--sim-speed` line (1.7, 1.8, 2.5; 1.4 on the default command line follows because `session_start` (`core_session.c:429`) and the written `beginRound` (`sim_loop.c:1538`) are both `xce_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 (as `test_sim_record_output.c`'s `append_queue_update_event()`/`append_jobs_push_event()` do), and the test asserts at least one decision per session, so the `ts` check is not vacuous.
|
|
- (b) The same command, but with a fresh `--sim-output OUTn` and `--logfile LOGn` for each run, plus each of `--sim-speed 7`, `--sim-speed abc`, `--sim-speed -1` and `--sim-speed=` exits 0, produces output identical to OUT0 in every field except `recordingStartWall` (compared byte-for-byte after the first message; the `ReplayHeader` is decoded with `decode_ReplayHeader` and compared field by field except `recordingStartWall`) (2.2), and logs exactly one line containing `--sim-speed` and the quoted value (2.1, 2.3, 2.4).
|
|
- (c) `--sim-speed 1 --sim-speed 9`, with its own fresh `--sim-output` and `--logfile`, logs exactly one such line, naming `"9"` (2.3).
|
|
- (d) `--sim FIXTURE --sim-speed` with 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-speed` and `"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/null` in the command), since the child prints decision lines, and takes the exit code from `WEXITSTATUS(pclose(...))`.
|
|
- Normal mode (2.1, 2.3) is covered by the CliCompat unit test plus the call site's placement before the `--sim` branch in `main.c`, because normal mode needs a controller.
|
|
|
|
## Project Invariant Cross-Check
|
|
|
|
Scheduler dimensions from `.claude/skills/xce-audit-invariants/SKILL.md`:
|
|
1. Driver state-equivalence: only the simulation driver's clock setting changes; the session protocol calls in `sim_loop.c` and `driver.c` are untouched. A replayed `session_start` and written `beginRound` now equal the recorded values exactly; `durationNs = 0` and decision `ts = beginRound` are intentional simulation-output differences (requirements, Adjacent expectations); decisions and accounting are unaffected. Pass.
|
|
2. Core module I/O isolation: no core-family source changes except comments in `core.h`. Pass.
|
|
3. Picker→allocator `jbuf` capacity-1 rendezvous: untouched. Pass.
|
|
4. Snapshot save/load field completeness: no persistence change; `session_start` and `session_duration_ns` still round-trip unchanged, and after a replayed session their values are `beginRound` and 0. Pass.
|