feat(scheduler): show event time/session in sim echo, total sessions on replay load

repl_print_event_summary() (interactive REPL event echo) now prefixes
each echoed event with "[sim] t=<ns> (<human>) session=<N>", using the
same xce_core_now()/repl_format_ts_human()/session_count sources "info
session" already reports. Uses session_count+1, not the raw count:
events stream in and are echoed individually before the ReplaySession/
begin_round message that increments session_count is even read (file
order is event, event, ..., ReplaySession), so an echoed event always
belongs to the session about to begin.

Add sim_prescan_session_count() (sim.c): scans the replay file from the
current position via shape-only classification (mirroring sim_loop.c's
own ReplayEvent/ReplaySession/SnapshotEntry dispatch), counts
ReplaySession messages, then rewinds via fseek back to the exact ftell()
position -- same rewind technique sim_locate_and_load_snapshot() already
uses. Wired into sched_sim_driver_start(): prints "[sim] replay file
contains N sessions" once at load, non-fatal on scan failure.

Add test_replay_load_reports_total_session_count; extend
test_step_echoes_events_before_session_decisions with t=/session=1
assertions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-08-20 07:48:43 +00:00
parent 4e3e6332c5
commit 8abab049f4
No known key found for this signature in database
GPG key ID: 5BCDAAE0123E9FE2
4 changed files with 221 additions and 3 deletions

View file

@ -569,10 +569,31 @@ bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line)
* sim_loop.c), leaving every input event silent unless a breakpoint
* happened to match it. Only ever called from xce_repl_on_event() below,
* itself only reached when sim->interactive (repl.h's own gate) -- zero
* output added to batch `--sim` mode.
* output added to batch `--sim` mode. Prefixed with a "[sim] t=... (...)
* session=..." line (same xce_core_now()/repl_format_ts_human()/
* rs->session_count sources `info session`/`info config`, repl_info.c,
* already use) so an operator reading a step/continue transcript can tell
* WHEN each event happened and which session it belongs to, not just what
* happened. Uses session_count+1, NOT the raw count: sim_loop.c's replay
* reader calls xce_repl_check_event() (-> here) for every raw event AS IT
* STREAMS IN, and only reads that batch's own ReplaySession/begin_round
* message (which is what increments session_count, xce_repl_on_begin_round)
* AFTER all of that session's events -- file order is event, event, ...,
* ReplaySession. So every event echoed here always belongs to the session
* about to begin, never one already counted.
*/
static void repl_print_event_summary(const xce_core_event_t* ev)
static void repl_print_event_summary(xce_sched_sim_driver_t* sim,
const xce_core_event_t* ev)
{
xce_sched_repl_state_t* rs = sim->repl_state;
char ts_human[32];
repl_format_ts_human(xce_core_now(&(sim->core)), ts_human,
sizeof(ts_human));
fprintf(stdout, "[sim] t=%llu (%s) session=%llu\n",
(unsigned long long)xce_core_now(&(sim->core)), ts_human,
(unsigned long long)((rs != NULL) ? rs->session_count + 1 : 1));
switch (ev->type) {
case XCE_CORE_EVENT_CONFIG_PUSH:
fprintf(stdout, "[sim] event: CONFIG_PUSH\n");
@ -699,7 +720,7 @@ void xce_repl_on_event(xce_sched_sim_driver_t* sim, xce_core_event_t* ev)
/* task 12.5, bug-1595: unconditional echo, printed before the
* breakpoint check below so the operator sees "event happened" before
* (if it also matches) "breakpoint hit". */
repl_print_event_summary(ev);
repl_print_event_summary(sim, ev);
bp = repl_find_event_breakpoint(rs, ev);
if (bp != NULL) {

View file

@ -327,6 +327,87 @@ static void sim_set_virtual_clock(xce_sched_sim_driver_t* sim,
sim->sim_speed_den = 1;
}
/*
* sim_prescan_session_count - counts the total number of ReplaySession
* (BEGIN_ROUND) messages remaining in sim->replay_file from the current
* position onward, then rewinds the file back to that exact position, so
* sched_sim_driver_start() can report "this replay file has N sessions" up
* front, before sched_sim_driver_run()'s real event loop (sim_loop.c)
* starts consuming the same stream. Uses the identical shape-only
* classification (msg_root_has_shape()) sim_loop.c's own main loop uses to
* tell ReplayEvent/ReplaySession/SnapshotEntry messages apart, and the same
* ftell()/fseek() rewind technique sim_locate_and_load_snapshot() above
* already establishes -- safe because this codebase's capn_init_fp() calls
* are always unpacked (packed=0), which reads via a single plain fread()
* per message with no internal read-ahead buffer (msg_root_has_shape()'s
* own file header comment), so ftell()/fseek() stay exactly in sync with
* message boundaries. Never decodes a message's fields, only its wire
* shape -- counting needs nothing more. Scope: only sim->replay_file's own
* session count is reported -- a run under --sim-alt-events substitutes a
* DIFFERENT event stream at run time (sim_loop.c), which this scan does
* not see; a niche enough case that reporting the primary file's own count
* is still the right default.
*/
static int sim_prescan_session_count(xce_sched_sim_driver_t* sim,
uint64_t* out_count)
{
FILE* f;
long resume_at;
uint64_t count = 0;
if ((sim == NULL) || (sim->replay_file == NULL) || (out_count == NULL)) {
return -EINVAL;
}
f = sim->replay_file;
resume_at = ftell(f);
if (resume_at < 0) {
return -errno;
}
for (;;) {
struct capn c;
capn_ptr root;
if (capn_init_fp(&c, f, 0) != 0) {
break; /* end of file (or malformed trailing data) -- same
* "nonzero means no more messages" convention every
* other reader in this file follows. */
}
if (msg_root_has_shape(&c, &root, ReplaySession_word_count,
ReplaySession_pointer_count)) {
count++;
capn_free(&c);
continue;
}
if (msg_root_has_shape(&c, &root, ReplayEvent_word_count,
ReplayEvent_pointer_count)) {
capn_free(&c);
continue;
}
if (msg_root_has_shape(&c, &root, SnapshotEntry_word_count,
SnapshotEntry_pointer_count)) {
capn_free(&c); /* mid-replay checkpoint -- skip, don't stop */
continue;
}
/* Unrecognized shape -- end of the message stream, same stop
* condition sim_loop.c's own main loop uses. */
capn_free(&c);
break;
}
if (fseek(f, resume_at, SEEK_SET) != 0) {
return -errno;
}
*out_count = count;
return 0;
}
int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
const sched_sim_args_t* args)
{
@ -393,6 +474,22 @@ int sched_sim_driver_start(xce_sched_sim_driver_t* sim,
sim_set_virtual_clock(sim, snapshot_ts, args->sim_speed);
/* Orientation for an operator about to `step`/`continue` through this
* file (or watch a batch --sim run go by): how many sessions are there
* in total? Purely informational -- a scan failure (e.g. a stat error
* on an already-open, already-validated file) is not fatal to starting
* the simulation, so it is silently skipped rather than aborting
* startup over a diagnostic nicety. */
{
uint64_t total_sessions = 0;
if (sim_prescan_session_count(sim, &total_sessions) == 0) {
fprintf(stdout, "[sim] replay file contains %llu session%s\n",
(unsigned long long)total_sessions,
(total_sessions == 1) ? "" : "s");
}
}
/* Req 5 AC 3 (design review follow-up, 2026-08-19): activate the
* virtualized clock for tlog logging now that it's seeded -- every
* ERROR/WARN/INFO/TRACE line emitted for the rest of this run shows

View file

@ -1779,11 +1779,74 @@ static void test_step_echoes_events_before_session_decisions(void)
"events must be echoed BEFORE the session's "
"resulting decision (bug-1595)");
{
char* ts_line_pos = strstr(captured, "[sim] t=");
TEST_ASSERT_NOT_NULL_MESSAGE(
ts_line_pos,
"each echoed event must be preceded by a t=.../session=... "
"line so an operator can tell when/which session it happened "
"in");
TEST_ASSERT_TRUE_MESSAGE(
ts_line_pos < jobpush_pos,
"the t=.../session=... line must precede the event it "
"describes");
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "session=1"),
"the first (and only) session's events must be tagged "
"session=1 -- xce_repl_on_begin_round() increments "
"session_count before this session's event batch is pushed");
}
free(captured);
printf("test_step_echoes_events_before_session_decisions: PASS\n");
}
/*
* New requirement (2026-08-20): loading a replay file reports its total
* session count up front (sim.c's sched_sim_driver_start(), via
* sim_prescan_session_count()'s ftell/fseek pre-scan), so an operator knows
* how many `step`s exist before starting to walk through them.
*/
static void test_replay_load_reports_total_session_count(void)
{
char replay_path[PATH_MAX];
char aux_dir[PATH_MAX];
char script_path[PATH_MAX];
char capture_path[PATH_MAX];
FILE* f;
char* captured;
f = write_header_and_snapshot(replay_path, sizeof(replay_path),
"_sessioncount");
TEST_ASSERT_NOT_NULL(f);
append_jobs_push_event(f, 1000ULL, 8181ULL, "normal");
append_replay_session(f, 1500ULL);
append_jobs_push_event(f, 2000ULL, 8182ULL, "normal");
append_replay_session(f, 2500ULL);
append_end_of_stream_marker(f);
fclose(f);
make_tmp_dir(aux_dir, sizeof(aux_dir), "_sessioncountaux");
snprintf(script_path, sizeof(script_path), "%s/script.txt", aux_dir);
snprintf(capture_path, sizeof(capture_path), "%s/capture.out", aux_dir);
write_text_file(script_path, "quit\n");
captured = run_group_d_and_capture(replay_path, script_path,
capture_path);
TEST_ASSERT_NOT_NULL_MESSAGE(
strstr(captured, "[sim] replay file contains 2 sessions"),
"loading a 2-session replay file must report the total session "
"count up front, before any step/continue command runs");
free(captured);
printf("test_replay_load_reports_total_session_count: PASS\n");
}
int main(void)
{
UNITY_BEGIN();
@ -1818,6 +1881,7 @@ int main(void)
RUN_TEST(test_event_breakpoint_pauses_repl_at_matching_event);
RUN_TEST(test_decision_breakpoint_pauses_after_decision_output);
RUN_TEST(test_step_echoes_events_before_session_decisions);
RUN_TEST(test_replay_load_reports_total_session_count);
return UNITY_END();
}