feat(scheduler-core): implement REPL breakpoints and state inspection (task 12.2)
Adds the interactive simulation REPL's breakpoint and state-inspection commands (Req 5 AC 22): `break <event_type> [key=value ...]` / `info break` / `delete <id>|all` on event and decision types, and `info session/job/queue/worker/resource/summary`. repl.c (370 lines after task 12.1) couldn't hold this task's full surface under the project's 800-line cap, so it's split three ways (matching the snapshot.c/snapshot_wire.c/snapshot_load.c precedent): repl.c (execution control + dispatch), repl_break.c (matching/CRUD + the three new driver-facing hooks), repl_info.c (info dumps), sharing state via repl_internal.h. sim_loop.c gained 3 new bare hook calls (repl.h inline wrappers, same pattern as task 12.1's two) at the points design.md's pseudocode requires -- per decoded event before it joins the batch, per session boundary, and per drained decision -- landing sim_loop.c at exactly 800/800 lines. Two real bugs found and fixed during development (bug-1506, bug-1507): an uninitialized xstr_table left breakpoint filter fields silently NULL instead of failing loud, and repl_breakpoint_free() called a bare free() on an RTTI-refcounted makeString() string, corrupting the heap. Review found one gap: `info job` printed only a resreq presence flag instead of the actual resreq text, when format_resreq() already existed in this repo for exactly this purpose. Fixed to match. Scheduler suite: 35/35 clean (was 34/34). RED-phase evidence (2 real bugs plus 2 synthetic probes, all reverted with confirmed zero residue) recorded in tasks.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
parent
02f7f48ff9
commit
2fecea3592
18 changed files with 3067 additions and 212 deletions
|
|
@ -5,36 +5,39 @@
|
|||
#include "sim.h"
|
||||
|
||||
/*
|
||||
* repl.h - interactive simulation REPL, execution-control slice (scheduler-
|
||||
* core spec, task 12.1, Req 5 AC 22). New file per design.md's File
|
||||
* Structure Plan ("repl.c -- NEW (Phase 3): interactive REPL command loop
|
||||
* (Req 5 AC 22)"), Phase 3, sibling to sim.h/sim.c/sim_loop.c.
|
||||
* repl.h - interactive simulation REPL (scheduler-core spec, Req 5 AC 22).
|
||||
* New file per design.md's File Structure Plan ("repl.c -- NEW (Phase 3):
|
||||
* interactive REPL command loop (Req 5 AC 22)"), Phase 3, sibling to
|
||||
* sim.h/sim.c/sim_loop.c. Task 12.1 built execution control (step/continue/
|
||||
* quit, pre-replay pause); task 12.2 (this revision) adds breakpoints
|
||||
* (event/BEGIN_ROUND/DECISION, with key=value filters) and the `info`
|
||||
* state-inspection command family.
|
||||
*
|
||||
* This header declares repl.c's two driver-facing entry points, plus two
|
||||
* `static inline` wrappers that fold the `sim->interactive` gate INTO the
|
||||
* header itself. That inline-wrapper indirection is a deliberate, judgment-
|
||||
* based deviation from this file family's usual "extern declared where
|
||||
* consumed, no dedicated header" convention (sim_verify.c/sim_record.c/
|
||||
* This header declares repl.c's driver-facing entry points, plus `static
|
||||
* inline` wrappers that fold the `sim->interactive` gate INTO the header
|
||||
* itself. That inline-wrapper indirection is a deliberate, judgment-based
|
||||
* deviation from this file family's usual "extern declared where consumed,
|
||||
* no dedicated header" convention (sim_verify.c/sim_record.c/
|
||||
* recording_session.c's entry points are all extern-declared directly
|
||||
* inside sim_loop.c -- see that file's own header comment): sim_loop.c was
|
||||
* already at 796/800 lines (this project's per-file cap, CLAUDE.md) before
|
||||
* this task, leaving no room for even one bare `if (sim->interactive) { ...
|
||||
* }` block at each of the two call sites design.md's REPL integration
|
||||
* pseudocode requires (pre-replay pause, post-session pause), let alone two.
|
||||
* Moving the gate into these inline wrappers keeps sim_loop.c's own diff to
|
||||
* two single-line, brace-free call expressions -- this task's own text
|
||||
* anticipates exactly this fallback ("move the hook logic into a tiny
|
||||
* static inline helper in repl.h, or extract something unrelated out of
|
||||
* sim_loop.c"). The two real functions (`xce_repl_start`/
|
||||
* `xce_repl_post_session_check`) still carry the actual logic; a `static
|
||||
* inline` here costs nothing at link time (each including TU gets its own
|
||||
* copy, trivially small, no ODR risk -- sim_loop.c is the only production
|
||||
* inside sim_loop.c -- see that file's own header comment): sim_loop.c sits
|
||||
* right at its 800-line cap (CLAUDE.md), leaving no room for even one bare
|
||||
* `if (sim->interactive) { ... }` block at any of the five call sites
|
||||
* design.md's REPL integration pseudocode requires (pre-replay pause,
|
||||
* post-session pause, per-event/per-BEGIN_ROUND/per-decision breakpoint
|
||||
* checks). Moving the gate into these inline wrappers keeps sim_loop.c's
|
||||
* own diff to five single-line, brace-free call expressions. The real
|
||||
* functions below still carry the actual logic; a `static inline` here
|
||||
* costs nothing at link time (each including TU gets its own copy,
|
||||
* trivially small, no ODR risk -- sim_loop.c is the only production
|
||||
* consumer).
|
||||
*
|
||||
* Boundary (Req 5 AC 17, "Simulation isolation", same as sim.h): this header
|
||||
* only depends on sim.h (itself core.h + stdbool/stdint/stdio) -- no
|
||||
* scheduler.h, no client.h, no capnp types. repl.c's own file header
|
||||
* comment documents the same boundary for its implementation.
|
||||
* scheduler.h, no client.h, no capnp types. xce_core_event_t (core.h) and
|
||||
* xce_decision_t (decision.h, included transitively via core.h) are both
|
||||
* already capnp-free, so the three new task 12.2 hooks below add no new
|
||||
* dependency. repl.c's own file header comment documents the same boundary
|
||||
* for its implementation.
|
||||
*/
|
||||
|
||||
/*
|
||||
|
|
@ -51,20 +54,47 @@
|
|||
extern void xce_repl_start(xce_sched_sim_driver_t* sim);
|
||||
|
||||
/*
|
||||
* xce_repl_post_session_check - the per-session pause-decision point (Req 5
|
||||
* AC 22's interaction model: "blocks on stdin ... at each breakpoint hit";
|
||||
* task 12.1's own narrower scope: only a `step N` budget reaching zero, no
|
||||
* breakpoints yet). Called once per completed ReplaySession, immediately
|
||||
* after xce_core_end_session() (sim_loop.c), when sim->interactive is set.
|
||||
* Internally decides whether to pause (a `step` budget hit zero; `continue`
|
||||
* mode never pauses here in 12.1's scope) and, if so, re-enters the same
|
||||
* blocking command loop xce_repl_start() uses. Task 12.2's own seam: it
|
||||
* extends the INTERNAL pause decision (repl.c) to also check a breakpoint
|
||||
* list -- this function's signature and sim_loop.c's own call site are not
|
||||
* expected to change.
|
||||
* xce_repl_post_session_check - the `step N` pause-decision point (Req 5 AC
|
||||
* 22's interaction model: a `step` budget reaching zero). Called once per
|
||||
* completed ReplaySession, immediately after xce_core_end_session()
|
||||
* (sim_loop.c), when sim->interactive is set. This is a SEPARATE pause path
|
||||
* from the breakpoint checks below -- it never inspects the breakpoint
|
||||
* list (that would be too coarse: a breakpoint must interrupt mid-batch, at
|
||||
* the exact matching event/decision, not merely once per completed
|
||||
* session). See xce_repl_check_begin_round()'s own doc comment for why
|
||||
* task 12.2 gave breakpoints their own, separately-triggered hooks instead
|
||||
* of folding them into this function.
|
||||
*/
|
||||
extern void xce_repl_post_session_check(xce_sched_sim_driver_t* sim);
|
||||
|
||||
/*
|
||||
* xce_repl_on_event / xce_repl_on_begin_round / xce_repl_on_decision (task
|
||||
* 12.2, Req 5 AC 22 "Breakpoints" + "REPL integration with the simulation
|
||||
* loop" pseudocode) - the three breakpoint-check hooks, one per granularity
|
||||
* the pseudocode's `check_breakpoints(...)` calls require: per decoded
|
||||
* ReplayEvent (before it is applied to core state), per ReplaySession
|
||||
* boundary (BEGIN_ROUND, before the accumulated event batch is pushed), and
|
||||
* per drained decision (DECISION, after verify/output-write, matching
|
||||
* sim_loop.c's existing per-decision loop body). Each also does lightweight
|
||||
* position bookkeeping (event_index/session_count/last-session decision
|
||||
* tally) that `info session` reports -- folded in here rather than given
|
||||
* separate hook call sites, since these are the only three places in
|
||||
* sim_loop.c's loop where "one event/session/decision was just processed"
|
||||
* is already known, and sim_loop.c has no line budget for a sixth call
|
||||
* site. Implemented in repl.c; each checks sim->repl_state's breakpoint
|
||||
* list for a match and, if found, prints a "[sim] hit breakpoint ..." line
|
||||
* and re-enters the same blocking command loop xce_repl_start() uses. A
|
||||
* NULL sim/ev/d, or a not-yet-allocated repl_state, is a safe no-op (same
|
||||
* "never load-bearing, degrade to unattended" discipline
|
||||
* repl_state_ensure()'s own doc comment establishes).
|
||||
*/
|
||||
extern void xce_repl_on_event(xce_sched_sim_driver_t* sim,
|
||||
xce_core_event_t* ev);
|
||||
extern void xce_repl_on_begin_round(xce_sched_sim_driver_t* sim,
|
||||
uint64_t begin_round);
|
||||
extern void xce_repl_on_decision(xce_sched_sim_driver_t* sim,
|
||||
xce_decision_t* d);
|
||||
|
||||
static inline void xce_repl_pre_replay_pause(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
if (sim->interactive) {
|
||||
|
|
@ -79,4 +109,28 @@ static inline void xce_repl_post_session_pause(xce_sched_sim_driver_t* sim)
|
|||
}
|
||||
}
|
||||
|
||||
static inline void xce_repl_check_event(xce_sched_sim_driver_t* sim,
|
||||
xce_core_event_t* ev)
|
||||
{
|
||||
if (sim->interactive) {
|
||||
xce_repl_on_event(sim, ev);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void xce_repl_check_begin_round(xce_sched_sim_driver_t* sim,
|
||||
uint64_t begin_round)
|
||||
{
|
||||
if (sim->interactive) {
|
||||
xce_repl_on_begin_round(sim, begin_round);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void xce_repl_check_decision(xce_sched_sim_driver_t* sim,
|
||||
xce_decision_t* d)
|
||||
{
|
||||
if (sim->interactive) {
|
||||
xce_repl_on_decision(sim, d);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ scheduler_srcs = [
|
|||
'src/sim_verify.c',
|
||||
'src/sim_record.c',
|
||||
'src/repl.c',
|
||||
'src/repl_break.c',
|
||||
'src/repl_info.c',
|
||||
'src/schedobj.c',
|
||||
'src/plugin.c',
|
||||
'src/resource.c',
|
||||
|
|
@ -78,18 +80,23 @@ sched_core_isolation_lib = static_library(
|
|||
# Task 11.3 (scheduler-core spec, Req 5 AC 17): isolated compile target for
|
||||
# the simulation driver's own files, consumed by
|
||||
# testsuite/test_sim_isolation.c to prove at build time that
|
||||
# sim.c/sim_loop.c/sim_record.c/repl.c reference zero controller-facing
|
||||
# symbols (client/netio/xceapi/delay/idgen). All files are compiled together
|
||||
# into ONE archive -- task 11.2 added sim_loop.c as a sibling file to task
|
||||
# 11.1's sim.c, task 11.5 added sim_record.c as a third, and task 12.1 adds
|
||||
# repl.c as a fourth; covering only a subset of them would repeat core.c's
|
||||
# own siblings' (core_events.c/core_session.c/core_clock.c)
|
||||
# sim.c/sim_loop.c/sim_record.c/repl.c/repl_break.c/repl_info.c reference
|
||||
# zero controller-facing symbols (client/netio/xceapi/delay/idgen). All
|
||||
# files are compiled together into ONE archive -- task 11.2 added
|
||||
# sim_loop.c as a sibling file to task 11.1's sim.c, task 11.5 added
|
||||
# sim_record.c as a third, task 12.1 added repl.c as a fourth, and task
|
||||
# 12.2 adds repl_break.c/repl_info.c as a fifth/sixth (task 12.2's own
|
||||
# 800-line-cap split of repl.c's breakpoint/state-inspection logic --
|
||||
# repl_internal.h's own "Why a split at all" section has the full
|
||||
# rationale); covering only a subset of them would repeat core.c's own
|
||||
# siblings' (core_events.c/core_session.c/core_clock.c)
|
||||
# never-mechanically-covered gap for the sim driver. Same "compiling to an
|
||||
# object is enough, no real linking needed" rationale as
|
||||
# sched_core_isolation_lib above.
|
||||
sched_sim_isolation_lib = static_library(
|
||||
'sched_sim_isolation',
|
||||
['src/sim.c', 'src/sim_loop.c', 'src/sim_record.c', 'src/repl.c'],
|
||||
['src/sim.c', 'src/sim_loop.c', 'src/sim_record.c', 'src/repl.c',
|
||||
'src/repl_break.c', 'src/repl_info.c'],
|
||||
include_directories: incdir,
|
||||
dependencies: scheduler_deps,
|
||||
install: false)
|
||||
|
|
|
|||
|
|
@ -6,20 +6,29 @@
|
|||
#include <editline/readline.h> /* libedit (BSD) -- readline()/add_history() */
|
||||
|
||||
#include "repl.h"
|
||||
#include "repl_internal.h"
|
||||
#include "utils.h"
|
||||
|
||||
/*
|
||||
* repl.c - interactive simulation REPL, execution-control slice (scheduler-
|
||||
* core spec, task 12.1, Req 5 AC 22). Implements a single input-loop REPL
|
||||
* (NOT a separate thread, per this file family's own established
|
||||
* single-threaded-driver discipline and design.md line 482/510) that pauses
|
||||
* before the first event is processed and blocks the simulation loop while
|
||||
* active. This task's own scope: `step [N]`/`continue`/`quit`/`help`, the
|
||||
* pre-replay pause, and the blocking interaction model. Breakpoints, state
|
||||
* inspection (`info job`/`info queue`/etc.), snapshot capture, and `set`/
|
||||
* `info config` are tasks 12.2/12.3's own jobs -- see repl_should_pause()
|
||||
* below for the single seam left for 12.2 to extend without restructuring
|
||||
* sim_loop.c's call sites (repl.h's own header comment).
|
||||
* repl.c - interactive simulation REPL: command loop and execution control
|
||||
* (scheduler-core spec, task 12.1, Req 5 AC 22). Implements a single
|
||||
* input-loop REPL (NOT a separate thread, per this file family's own
|
||||
* established single-threaded-driver discipline and design.md line
|
||||
* 482/510) that pauses before the first event is processed and blocks the
|
||||
* simulation loop while active.
|
||||
*
|
||||
* This file's own scope: `step [N]`/`continue`/`quit`/`help`, the
|
||||
* pre-replay pause, the blocking interaction model, and repl_dispatch()'s
|
||||
* command table (which now also hands off to repl_break.c's `break`/
|
||||
* `delete` and repl_info.c's `info` -- task 12.2, Req 5 AC 22
|
||||
* "Breakpoints"/"State inspection"). Breakpoint matching/storage and state-
|
||||
* inspection dumps are NOT in this file -- task 12.2's full scope did not
|
||||
* fit in one file under this project's 800-line cap, so it is split across
|
||||
* repl.c (this file)/repl_break.c/repl_info.c, sharing private state via
|
||||
* repl_internal.h -- see that header's own "Why a split at all" section for
|
||||
* the full rationale, matching the precedent snapshot.c/snapshot_wire.c/
|
||||
* snapshot_load.c (task 8.1) already established for the identical
|
||||
* problem.
|
||||
*
|
||||
* Sanctioned deviation from requirements.md's literal text (Req 5 AC 22,
|
||||
* "Implementation note": "The REPL is a single `fgets` loop"): this task's
|
||||
|
|
@ -40,44 +49,19 @@
|
|||
* the whole linked binary. Uses libedit (BSD/NetBSD license) instead, via
|
||||
* its editline/readline.h emulation header, which provides the identical
|
||||
* readline()/add_history() surface this file calls -- zero behavioral
|
||||
* difference, no GPL exposure. src/controller/testsuite/test.c's own REPL
|
||||
* was switched to the same libedit dependency for project-wide consistency
|
||||
* (see src/controller/meson.build's `libedit_dep`), even though xce_test's
|
||||
* standalone-binary status meant it was never GPL-exposed the way
|
||||
* xce_scheduler would have been.
|
||||
* difference, no GPL exposure.
|
||||
*
|
||||
* Boundary (Req 5 AC 17, "Simulation isolation", same as sim.h/sim.c/
|
||||
* sim_loop.c/sim_record.c): this file touches only xce_sched_sim_driver_t/
|
||||
* xce_sched_core_t (via repl.h -> sim.h -> core.h) plus stdio/libedit --
|
||||
* no xcecli_ctx_t, no xce_cluster_config_t, no capnp socket, no controller
|
||||
* RPC. test_sim_isolation.c (task 11.3) is extended by this task to cover
|
||||
* repl.c/repl.h too, matching the 3-file (now 5-file) precedent it already
|
||||
* established for sim.c/sim_loop.c/sim_record.c.
|
||||
* sim_loop.c/sim_record.c): this file (and its repl_break.c/repl_info.c
|
||||
* siblings) touch only xce_sched_sim_driver_t/xce_sched_core_t (via repl.h/
|
||||
* repl_internal.h -> sim.h -> core.h) plus stdio/libedit -- no
|
||||
* xcecli_ctx_t, no xce_cluster_config_t, no capnp socket, no controller
|
||||
* RPC. test_sim_isolation.c (task 11.3, extended by task 12.1 and again by
|
||||
* this task) scans all three .c files plus repl.h/repl_internal.h.
|
||||
*/
|
||||
|
||||
#define XCE_REPL_PROMPT "(sim) "
|
||||
|
||||
typedef enum {
|
||||
XCE_REPL_MODE_STEP = 0, /* pause once `step_budget` sessions complete */
|
||||
XCE_REPL_MODE_CONTINUE /* task 12.1 scope: run unattended until EOF --
|
||||
* no breakpoint mechanism exists yet */
|
||||
} xce_repl_run_mode_t;
|
||||
|
||||
/*
|
||||
* xce_sched_repl_state_s - the full definition of sim.h's opaque forward
|
||||
* declaration. Deliberately private to this file (not exposed even in
|
||||
* repl.h) -- sim_loop.c and every other production consumer touch it only
|
||||
* through the pointer, never its fields, matching core.h's own
|
||||
* xce_sched_core_t precedent for "opaque outside the owning module."
|
||||
* White-box test files (this test suite's established convention -- see
|
||||
* test_sim_isolation.c's own header comment on the genre) get full field
|
||||
* access anyway via `#include "../src/repl.c"`.
|
||||
*/
|
||||
struct xce_sched_repl_state_s {
|
||||
xce_repl_run_mode_t mode;
|
||||
long step_budget;
|
||||
};
|
||||
|
||||
/*
|
||||
* repl_state_ensure - lazily allocates sim->repl_state on first use
|
||||
* (xce_repl_start() is always the first caller in the real driver flow, but
|
||||
|
|
@ -88,7 +72,11 @@ struct xce_sched_repl_state_s {
|
|||
* correctness (design.md's "Deterministic replay is preserved" invariant --
|
||||
* the REPL only ever controls WHEN execution pauses, never WHAT is
|
||||
* processed, so degrading to "never pauses" on OOM is safe, just less
|
||||
* useful).
|
||||
* useful). Only called here, at startup -- by the time repl_break.c's
|
||||
* xce_repl_on_event()/on_begin_round()/on_decision() or any repl_dispatch()
|
||||
* command runs, sim->repl_state is already guaranteed non-NULL (xce_repl_start()
|
||||
* always runs first, sim_loop.c's pre-replay pause), so those files read
|
||||
* sim->repl_state directly rather than needing this function too.
|
||||
*/
|
||||
static xce_sched_repl_state_t* repl_state_ensure(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
|
|
@ -102,6 +90,44 @@ static xce_sched_repl_state_t* repl_state_ensure(xce_sched_sim_driver_t* sim)
|
|||
return sim->repl_state;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_split_tokens - splits `line` on whitespace into up to `max_tokens`
|
||||
* fixed-size tokens (repl_internal.h's own doc comment on why `break`/
|
||||
* `info`/`delete` all need more than a single token). Silently truncates an
|
||||
* over-length token rather than erroring -- a malformed breakpoint filter/
|
||||
* name is caught downstream by the specific command handler (unrecognized
|
||||
* event type, missing '=', etc.), matching this file's existing "fail loud
|
||||
* with a specific message" style rather than a generic parse-error here.
|
||||
* Returns the token count (0 if `line` is entirely whitespace).
|
||||
*/
|
||||
int repl_split_tokens(const char* line, char tokens[][XCE_REPL_TOKEN_LEN],
|
||||
int max_tokens)
|
||||
{
|
||||
const char* p = line;
|
||||
int n = 0;
|
||||
|
||||
while ((*p != '\0') && (n < max_tokens)) {
|
||||
int i = 0;
|
||||
|
||||
while (isspace((unsigned char)*p)) {
|
||||
p++;
|
||||
}
|
||||
if (*p == '\0') {
|
||||
break;
|
||||
}
|
||||
while ((*p != '\0') && !isspace((unsigned char)*p)) {
|
||||
if (i < (XCE_REPL_TOKEN_LEN - 1)) {
|
||||
tokens[n][i++] = *p;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
tokens[n][i] = '\0';
|
||||
n++;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_count_jobs - counts `core->jobs` via the SAME iter_make()/iter_next()
|
||||
* walk core_session.c's core_gc_select_nodes()/scheduler.c's own job-listing
|
||||
|
|
@ -159,14 +185,11 @@ static void repl_print_banner(const xce_sched_sim_driver_t* sim)
|
|||
|
||||
/*
|
||||
* repl_print_help - Req 5 AC 22's "help" command: "Print available commands
|
||||
* with one-line descriptions." Deliberately covers ONLY the commands this
|
||||
* task implements (step/continue/quit/help) -- AC 22's full table also
|
||||
* lists breakpoints, state inspection, snapshot, and `set`/`info config`
|
||||
* commands that do not exist yet; listing them here would document
|
||||
* commands repl_dispatch() below would then reject as unrecognized. Tasks
|
||||
* 12.2/12.3 extend this listing as they add those commands, matching this
|
||||
* task's own text ("Don't over-build entries for commands that don't exist
|
||||
* yet").
|
||||
* with one-line descriptions." Covers execution control (task 12.1),
|
||||
* breakpoints, and state inspection (task 12.2) -- AC 22's remaining rows
|
||||
* (`snapshot`, `set`/`info config`) are task 12.3's job and stay unlisted
|
||||
* until implemented, matching task 12.1's own "don't over-build entries for
|
||||
* commands that don't exist yet" convention.
|
||||
*/
|
||||
static void repl_print_help(void)
|
||||
{
|
||||
|
|
@ -175,6 +198,15 @@ static void repl_print_help(void)
|
|||
printf(" continue Run until next breakpoint hit or EOF.\n");
|
||||
printf(" quit Exit the scheduler immediately.\n");
|
||||
printf(" help Print this help listing.\n");
|
||||
printf(" break <EVENT_TYPE> [key=value ...] Set a breakpoint.\n");
|
||||
printf(" info break List breakpoints.\n");
|
||||
printf(" delete <id>|all Delete breakpoint(s).\n");
|
||||
printf(" info session Simulation position.\n");
|
||||
printf(" info job <jobid> Dump one job.\n");
|
||||
printf(" info queue <name>|all Dump queue(s).\n");
|
||||
printf(" info worker <name>|all Dump worker(s).\n");
|
||||
printf(" info resource <name>|all Dump resource(s).\n");
|
||||
printf(" info summary Quick overview.\n");
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -257,10 +289,12 @@ static bool repl_cmd_step(xce_sched_sim_driver_t* sim, const char* line)
|
|||
* repl_dispatch - parses one readline()-returned command line (already
|
||||
* known non-blank by the caller) and executes it. Returns true if control
|
||||
* should return to the driver's event loop (`step`/`continue`), false if
|
||||
* the REPL should re-prompt (`help`, an unrecognized command, or a `step`
|
||||
* parse error). `quit` never returns from repl_cmd_quit() -- its own return
|
||||
* value below is unreachable, kept only so every branch has a well-typed
|
||||
* value.
|
||||
* the REPL should re-prompt (`help`, `break`/`info`/`delete`, an
|
||||
* unrecognized command, or a `step` parse error). `quit` never returns from
|
||||
* repl_cmd_quit() -- its own return value below is unreachable, kept only
|
||||
* so every branch has a well-typed value. `break`/`delete`/`info` are
|
||||
* implemented in repl_break.c/repl_info.c (repl_internal.h's own extern
|
||||
* declarations) -- this file only routes to them.
|
||||
*/
|
||||
static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line)
|
||||
{
|
||||
|
|
@ -285,6 +319,15 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line)
|
|||
repl_print_help();
|
||||
return false;
|
||||
}
|
||||
if (strcmp(cmd, "break") == 0) {
|
||||
return repl_cmd_break(sim, line);
|
||||
}
|
||||
if (strcmp(cmd, "delete") == 0) {
|
||||
return repl_cmd_delete(sim, line);
|
||||
}
|
||||
if (strcmp(cmd, "info") == 0) {
|
||||
return repl_cmd_info(sim, line);
|
||||
}
|
||||
|
||||
printf("[sim] unknown command: '%s' (type 'help' for a list)\n", cmd);
|
||||
|
||||
|
|
@ -299,9 +342,11 @@ static bool repl_dispatch(xce_sched_sim_driver_t* sim, const char* line)
|
|||
* from readline() (Ctrl-D / stdin EOF) is treated exactly like an explicit
|
||||
* `quit` -- the same convention test.c's own loop follows
|
||||
* (`execute_line("quit")` on a NULL readline() return) -- never returns in
|
||||
* that case either.
|
||||
* that case either. Non-static (repl_internal.h) -- repl_break.c's
|
||||
* breakpoint-hit hooks re-enter it directly, the same way
|
||||
* xce_repl_post_session_check() below already does for a `step N` pause.
|
||||
*/
|
||||
static void repl_command_loop(xce_sched_sim_driver_t* sim)
|
||||
void repl_command_loop(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
for (;;) {
|
||||
char* line = readline(XCE_REPL_PROMPT);
|
||||
|
|
@ -337,13 +382,14 @@ void xce_repl_start(xce_sched_sim_driver_t* sim)
|
|||
}
|
||||
|
||||
/*
|
||||
* repl_should_pause - Req 5 AC 22's per-session pause decision, task 12.1's
|
||||
* own scope: pause only when a `step N` budget reaches zero. `continue`
|
||||
* mode (AC 22: "Run until next breakpoint hit or EOF") always returns false
|
||||
* here today -- no breakpoint mechanism exists yet (task 12.2's job). This
|
||||
* is the single seam task 12.2 extends (adding a breakpoint-list check
|
||||
* alongside the step-budget check below) without touching
|
||||
* xce_repl_post_session_check() or sim_loop.c's call site at all.
|
||||
* repl_should_pause - Req 5 AC 22's `step N` pause decision: pause only
|
||||
* when a `step N` budget reaches zero. `continue` mode always returns false
|
||||
* here -- this function is deliberately NOT where breakpoints are checked
|
||||
* (repl_break.c's xce_repl_on_event()/xce_repl_on_begin_round()/
|
||||
* xce_repl_on_decision() are: breakpoint matching happens at the exact
|
||||
* event/session/decision granularity, not once per completed session --
|
||||
* see repl.h's own doc comment on those three functions for the three call
|
||||
* sites in sim_loop.c).
|
||||
*/
|
||||
static bool repl_should_pause(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
|
|
|
|||
669
src/scheduler/src/repl_break.c
Normal file
669
src/scheduler/src/repl_break.c
Normal file
|
|
@ -0,0 +1,669 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp -- breakpoint filter value matching */
|
||||
|
||||
#include "repl.h"
|
||||
#include "repl_internal.h"
|
||||
#include "utils.h"
|
||||
#include "xstring.h" /* makeString() -- breakpoint token/filter string copies */
|
||||
|
||||
/*
|
||||
* repl_break.c - breakpoints (scheduler-core spec, task 12.2, Req 5 AC 22
|
||||
* "Breakpoints"). Sibling of repl.c/repl_info.c -- see repl_internal.h's
|
||||
* own "Why a split at all" section for the full rationale behind this
|
||||
* three-file layout.
|
||||
*
|
||||
* Owns: the `break`/`delete` command handlers, the breakpoint list (create/
|
||||
* find/remove/clear), event/decision key=value filter matching, and the
|
||||
* three driver-facing breakpoint-check hooks repl.h declares
|
||||
* (xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision()) --
|
||||
* called from sim_loop.c at the three points design.md's REPL integration
|
||||
* pseudocode requires: per decoded ReplayEvent (before it is applied to
|
||||
* core state), per ReplaySession boundary (BEGIN_ROUND, before the
|
||||
* accumulated batch is pushed), and per drained decision (DECISION, after
|
||||
* verify/output-write). See repl.h's own doc comment on those three
|
||||
* functions for the exact call-site reasoning.
|
||||
*
|
||||
* These hooks are a SEPARATE mechanism from repl.c's repl_should_pause()
|
||||
* (task 12.1's `step N` pause): repl_should_pause() only fires once per
|
||||
* completed ReplaySession, too coarse for "pause AT the matching event",
|
||||
* so breakpoints get their own hook sites instead of extending that
|
||||
* function -- task 12.1's own doc comment on repl_should_pause() originally
|
||||
* (wrongly) predicted the opposite; repl.c's current comment there
|
||||
* corrects the record.
|
||||
*
|
||||
* Boundary (Req 5 AC 17): same as repl.c -- only xce_sched_sim_driver_t/
|
||||
* xce_sched_core_t (via repl.h/repl_internal.h -> sim.h -> core.h) plus
|
||||
* stdio/string.h/strings.h/stdlib.h. sched_get_job() (core.h) is the only
|
||||
* core.h accessor this file calls, for a `queue=` decision filter's job
|
||||
* lookup.
|
||||
*/
|
||||
|
||||
/*
|
||||
* BP_KIND_TABLE - the `break <TOKEN>` vocabulary (Req 5 AC 22 "Breakpoints"
|
||||
* table's own examples: JOB_STATE_CHANGE, JOB_PUSH, BEGIN_ROUND, DECISION).
|
||||
* The eight remaining *_PUSH, *_UPDATE, *_RUSAGE tokens complete the set
|
||||
* against every xce_core_event_type_t member (core.h)
|
||||
* sim_process_replay_event_message() (sim_loop.c) can actually decode, so
|
||||
* no wire event type is unreachable from `break`. Token spelling
|
||||
* intentionally does NOT always match the xce_core_event_type_t enumerator
|
||||
* name (e.g. "JOB_PUSH" here vs. XCE_CORE_EVENT_JOBS_PUSH) -- AC 22's own
|
||||
* example text is the authority on spelling, not the internal C enum.
|
||||
*/
|
||||
static const struct {
|
||||
const char* token;
|
||||
xce_repl_bp_kind_t kind;
|
||||
} BP_KIND_TABLE[] = {
|
||||
{ "CONFIG_PUSH", XCE_REPL_BP_CONFIG_PUSH },
|
||||
{ "JOB_PUSH", XCE_REPL_BP_JOB_PUSH },
|
||||
{ "WORKERS_PUSH", XCE_REPL_BP_WORKERS_PUSH },
|
||||
{ "RESOURCES_PUSH", XCE_REPL_BP_RESOURCES_PUSH },
|
||||
{ "RESVALS_FULL", XCE_REPL_BP_RESVALS_FULL },
|
||||
{ "RESVALS_DELTA", XCE_REPL_BP_RESVALS_DELTA },
|
||||
{ "JOB_STATE_CHANGE", XCE_REPL_BP_JOB_STATE_CHANGE },
|
||||
{ "JOB_UPDATE", XCE_REPL_BP_JOB_UPDATE },
|
||||
{ "QUEUE_UPDATE", XCE_REPL_BP_QUEUE_UPDATE },
|
||||
{ "JOB_RUSAGE", XCE_REPL_BP_JOB_RUSAGE },
|
||||
{ "BEGIN_ROUND", XCE_REPL_BP_BEGIN_ROUND },
|
||||
{ "DECISION", XCE_REPL_BP_DECISION },
|
||||
};
|
||||
#define NUM_BP_KINDS (sizeof(BP_KIND_TABLE) / sizeof(BP_KIND_TABLE[0]))
|
||||
|
||||
static bool repl_bp_kind_from_token(const char* token, xce_repl_bp_kind_t* out)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NUM_BP_KINDS; i++) {
|
||||
if (strcasecmp(token, BP_KIND_TABLE[i].token) == 0) {
|
||||
*out = BP_KIND_TABLE[i].kind;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* repl_bp_kind_for_core_event - the reverse of BP_KIND_TABLE, keyed by
|
||||
* xce_core_event_type_t (core.h) instead of a user-typed token. Used by
|
||||
* xce_repl_on_event() (below) to find which breakpoints are even
|
||||
* candidates for a given decoded event, before running any filter. */
|
||||
static xce_repl_bp_kind_t repl_bp_kind_for_core_event(xce_core_event_type_t t)
|
||||
{
|
||||
switch (t) {
|
||||
case XCE_CORE_EVENT_CONFIG_PUSH:
|
||||
return XCE_REPL_BP_CONFIG_PUSH;
|
||||
case XCE_CORE_EVENT_JOBS_PUSH:
|
||||
return XCE_REPL_BP_JOB_PUSH;
|
||||
case XCE_CORE_EVENT_WORKERS_PUSH:
|
||||
return XCE_REPL_BP_WORKERS_PUSH;
|
||||
case XCE_CORE_EVENT_RESOURCES_PUSH:
|
||||
return XCE_REPL_BP_RESOURCES_PUSH;
|
||||
case XCE_CORE_EVENT_RESVALS_FULL:
|
||||
return XCE_REPL_BP_RESVALS_FULL;
|
||||
case XCE_CORE_EVENT_RESVALS_DELTA:
|
||||
return XCE_REPL_BP_RESVALS_DELTA;
|
||||
case XCE_CORE_EVENT_JOB_STATE_CHANGE:
|
||||
return XCE_REPL_BP_JOB_STATE_CHANGE;
|
||||
case XCE_CORE_EVENT_JOB_UPDATE:
|
||||
return XCE_REPL_BP_JOB_UPDATE;
|
||||
case XCE_CORE_EVENT_QUEUE_UPDATE:
|
||||
return XCE_REPL_BP_QUEUE_UPDATE;
|
||||
case XCE_CORE_EVENT_JOB_RUSAGE:
|
||||
return XCE_REPL_BP_JOB_RUSAGE;
|
||||
default:
|
||||
return (xce_repl_bp_kind_t)0; /* matches no BP_KIND_TABLE entry */
|
||||
}
|
||||
}
|
||||
|
||||
/* repl_job_state_matches - AC 22's `state=RUN`-style filter value, matched
|
||||
* against the JOB_IS_*() bitmask macros (xce/types.h) every other live
|
||||
* state check in this codebase already uses, rather than an exact bitwise
|
||||
* equality (a job's `state` can carry secondary bits, e.g. PEND|PSUSP). */
|
||||
static bool repl_job_state_matches(uint32_t state, const char* name)
|
||||
{
|
||||
if (strcasecmp(name, "RUN") == 0) {
|
||||
return JOB_IS_RUN(state);
|
||||
}
|
||||
if (strcasecmp(name, "PEND") == 0) {
|
||||
return JOB_IS_PEND(state);
|
||||
}
|
||||
if (strcasecmp(name, "EXIT") == 0) {
|
||||
return JOB_IS_EXIT(state);
|
||||
}
|
||||
if (strcasecmp(name, "DONE") == 0) {
|
||||
return JOB_IS_DONE(state);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* repl_u64_str_eq - compares a uint64_t field against a filter's string
|
||||
* value (every jobid= filter goes through this). */
|
||||
static bool repl_u64_str_eq(uint64_t v, const char* value)
|
||||
{
|
||||
char buf[32];
|
||||
|
||||
snprintf(buf, sizeof(buf), "%llu", (unsigned long long)v);
|
||||
return strcmp(buf, value) == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_event_matches_filter - one key=value clause against one decoded
|
||||
* xce_core_event_t (task 12.2, AC 22's `break JOB_STATE_CHANGE jobid=12345`/
|
||||
* `state=RUN` examples). A key not meaningful for this event's own type
|
||||
* (e.g. "state=" against a QUEUE_UPDATE) fails closed (never matches)
|
||||
* rather than erroring at match time -- `break` already validated the
|
||||
* event-type token itself; an unsupported key on a supported type is a
|
||||
* user typo this function has no channel to report from (called deep
|
||||
* inside the driver loop, not at command-entry time).
|
||||
*/
|
||||
static bool repl_event_matches_filter(const xce_core_event_t* ev,
|
||||
const char* key, const char* value)
|
||||
{
|
||||
switch (ev->type) {
|
||||
case XCE_CORE_EVENT_JOB_STATE_CHANGE:
|
||||
if (strcmp(key, "jobid") == 0) {
|
||||
return repl_u64_str_eq(ev->u.job_state_change.jobid, value);
|
||||
}
|
||||
if (strcmp(key, "state") == 0) {
|
||||
return repl_job_state_matches(ev->u.job_state_change.new_state,
|
||||
value);
|
||||
}
|
||||
return false;
|
||||
case XCE_CORE_EVENT_JOB_UPDATE:
|
||||
if (strcmp(key, "jobid") == 0) {
|
||||
return repl_u64_str_eq(ev->u.job_update.jobid, value);
|
||||
}
|
||||
return false;
|
||||
case XCE_CORE_EVENT_JOB_RUSAGE:
|
||||
if (strcmp(key, "jobid") == 0) {
|
||||
return repl_u64_str_eq(ev->u.job_rusage.jobid, value);
|
||||
}
|
||||
return false;
|
||||
case XCE_CORE_EVENT_JOBS_PUSH:
|
||||
if (strcmp(key, "jobid") == 0) {
|
||||
int i;
|
||||
|
||||
for (i = 0; i < ev->u.jobs_push.njobs; i++) {
|
||||
xce_job_t* j = ev->u.jobs_push.jobs[i];
|
||||
|
||||
if ((j != NULL) && repl_u64_str_eq(j->jobid, value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
case XCE_CORE_EVENT_QUEUE_UPDATE:
|
||||
if (strcmp(key, "queue") == 0) {
|
||||
return (ev->u.queue_update.queue != NULL)
|
||||
&& (strcmp(ev->u.queue_update.queue->queue, value) == 0);
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* repl_breakpoint_matches_event - AND's every filter of `bp` against `ev`;
|
||||
* a breakpoint with zero filters matches every event of its own kind. */
|
||||
static bool repl_breakpoint_matches_event(const xce_repl_breakpoint_t* bp,
|
||||
const xce_core_event_t* ev)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (bp->kind != repl_bp_kind_for_core_event(ev->type)) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < bp->nfilters; i++) {
|
||||
if (!repl_event_matches_filter(ev, bp->filters[i].key,
|
||||
bp->filters[i].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* decision_op_name - "DISPATCH"/"PEND"/"PREEMPT"/"NONE", the `type=`
|
||||
* filter's own vocabulary (AC 22: `break DECISION type=DISPATCH`) and the
|
||||
* same label sim_print_decision() (sim_loop.c) already prints per op. */
|
||||
static const char* decision_op_name(xce_decision_op_t op)
|
||||
{
|
||||
switch (op) {
|
||||
case XCE_DECISION_DISPATCH:
|
||||
return "DISPATCH";
|
||||
case XCE_DECISION_PEND:
|
||||
return "PEND";
|
||||
case XCE_DECISION_PREEMPT:
|
||||
return "PREEMPT";
|
||||
default:
|
||||
return "NONE";
|
||||
}
|
||||
}
|
||||
|
||||
/* decision_jobid - the one jobid every non-NONE xce_decision_t op carries,
|
||||
* regardless of which union arm is active. */
|
||||
static uint64_t decision_jobid(const xce_decision_t* d)
|
||||
{
|
||||
switch (d->op) {
|
||||
case XCE_DECISION_DISPATCH:
|
||||
return (d->u.alloc != NULL) ? d->u.alloc->jobid : 0;
|
||||
case XCE_DECISION_PEND:
|
||||
return (d->u.reason != NULL) ? d->u.reason->jobid : 0;
|
||||
case XCE_DECISION_PREEMPT:
|
||||
return (d->u.preempt != NULL) ? d->u.preempt->jobid : 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_decision_matches_filter - one key=value clause against one drained
|
||||
* xce_decision_t (task 12.2, AC 22's `break DECISION type=DISPATCH
|
||||
* queue=normal` example). `queue=` requires a job lookup (a decision itself
|
||||
* carries no queue field) -- sched_get_job() (core.h), the same accessor
|
||||
* `info job` (repl_info.c) uses; a decision for a jobid no longer in
|
||||
* core->jobs (freed after completion) never matches a queue= filter, a
|
||||
* documented, narrow gap rather than a crash.
|
||||
*/
|
||||
static bool repl_decision_matches_filter(xce_sched_core_t* core,
|
||||
const xce_decision_t* d,
|
||||
const char* key, const char* value)
|
||||
{
|
||||
if (strcmp(key, "type") == 0) {
|
||||
return strcasecmp(decision_op_name(d->op), value) == 0;
|
||||
}
|
||||
if (strcmp(key, "jobid") == 0) {
|
||||
return repl_u64_str_eq(decision_jobid(d), value);
|
||||
}
|
||||
if (strcmp(key, "queue") == 0) {
|
||||
xce_sched_job_t* job = sched_get_job(core, decision_jobid(d));
|
||||
|
||||
return (job != NULL) && (job->queue != NULL)
|
||||
&& (strcmp(job->queue->name, value) == 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool repl_breakpoint_matches_decision(xce_sched_core_t* core,
|
||||
const xce_repl_breakpoint_t* bp,
|
||||
const xce_decision_t* d)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (bp->kind != XCE_REPL_BP_DECISION) {
|
||||
return false;
|
||||
}
|
||||
for (i = 0; i < bp->nfilters; i++) {
|
||||
if (!repl_decision_matches_filter(core, d, bp->filters[i].key,
|
||||
bp->filters[i].value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* repl_find_event_breakpoint / repl_find_begin_round_breakpoint /
|
||||
* repl_find_decision_breakpoint - first-match scan of rs->breakpoints;
|
||||
* returns the matched entry (for its id + token, used by the "[sim] hit
|
||||
* breakpoint ..." message) or NULL. BEGIN_ROUND supports no filters today
|
||||
* (AC 22 shows no BEGIN_ROUND filter example) -- a bare "break BEGIN_ROUND"
|
||||
* matches every session boundary; any filters attached to one never match
|
||||
* (fail closed, same discipline repl_event_matches_filter() uses for an
|
||||
* unsupported key). */
|
||||
static xce_repl_breakpoint_t*
|
||||
repl_find_event_breakpoint(xce_sched_repl_state_t* rs,
|
||||
const xce_core_event_t* ev)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
if (repl_breakpoint_matches_event(rs->breakpoints[i], ev)) {
|
||||
return rs->breakpoints[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static xce_repl_breakpoint_t*
|
||||
repl_find_begin_round_breakpoint(xce_sched_repl_state_t* rs)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
xce_repl_breakpoint_t* bp = rs->breakpoints[i];
|
||||
|
||||
if ((bp->kind == XCE_REPL_BP_BEGIN_ROUND) && (bp->nfilters == 0)) {
|
||||
return bp;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static xce_repl_breakpoint_t*
|
||||
repl_find_decision_breakpoint(xce_sched_core_t* core,
|
||||
xce_sched_repl_state_t* rs,
|
||||
const xce_decision_t* d)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
if (repl_breakpoint_matches_decision(core, rs->breakpoints[i], d)) {
|
||||
return rs->breakpoints[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_breakpoint_free - releases one breakpoint's plain-malloc'd memory
|
||||
* (bug-fix, discovered during this task's own RED/GREEN cycle: see
|
||||
* .wolf/buglog.json). `bp->token` and each filter's key/value are
|
||||
* makeString()-interned xstrings -- RTTI reference-counted objects whose
|
||||
* returned pointer is NOT a plain malloc() chunk (xstring.c's own
|
||||
* xstring_dereference() is the only correct release path, via the RTTI
|
||||
* reference-counting interface, never a bare free()). This codebase's own
|
||||
* kill_sched_job()/kill_sched_queue() (schedobj.c) establish the identical
|
||||
* precedent for every OTHER makeString()'d field in this codebase
|
||||
* (xce_sched_job_t/xce_sched_queue_t names): never explicitly released,
|
||||
* living for the process's lifetime (matching this whole test suite's own
|
||||
* "detect_leaks=0 by design" convention). Calling free() on one instead
|
||||
* corrupts the heap allocator's bookkeeping ("free(): invalid pointer");
|
||||
* only the plain calloc'd `bp->filters` array and `bp` itself are
|
||||
* genuinely owned, plain-malloc'd memory this function may free.
|
||||
*/
|
||||
static void repl_breakpoint_free(xce_repl_breakpoint_t* bp)
|
||||
{
|
||||
if (bp == NULL) {
|
||||
return;
|
||||
}
|
||||
free(bp->filters);
|
||||
free(bp);
|
||||
}
|
||||
|
||||
/* repl_breakpoints_append - grows rs->breakpoints (a realloc'd pointer
|
||||
* array, same growth idiom sim_loop.c's own buf_events[] uses) and appends
|
||||
* `bp`. Returns false (and does NOT free `bp` -- the caller's job, so it
|
||||
* can report the allocation failure with its own context) on OOM. */
|
||||
static bool repl_breakpoints_append(xce_sched_repl_state_t* rs,
|
||||
xce_repl_breakpoint_t* bp)
|
||||
{
|
||||
xce_repl_breakpoint_t** grown = (xce_repl_breakpoint_t**)realloc(
|
||||
rs->breakpoints,
|
||||
(size_t)(rs->nbreakpoints + 1) * sizeof(*(rs->breakpoints)));
|
||||
|
||||
if (grown == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
rs->breakpoints = grown;
|
||||
rs->breakpoints[rs->nbreakpoints] = bp;
|
||||
rs->nbreakpoints++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* repl_breakpoints_clear - `delete all`. Frees every entry, resets the
|
||||
* array/count. `next_bp_id` is deliberately NOT reset -- ids are never
|
||||
* reused within one REPL session, matching this codebase's general
|
||||
* "monotonic id, never recycled" convention (e.g. core->nextid). */
|
||||
static void repl_breakpoints_clear(xce_sched_repl_state_t* rs)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
repl_breakpoint_free(rs->breakpoints[i]);
|
||||
}
|
||||
free(rs->breakpoints);
|
||||
rs->breakpoints = NULL;
|
||||
rs->nbreakpoints = 0;
|
||||
}
|
||||
|
||||
/* repl_breakpoint_remove - `delete <id>`. Compacts the array on a match
|
||||
* (memmove over the removed slot) so `nbreakpoints` always reflects the
|
||||
* live count with no NULL holes -- every scan above (repl_find_*) relies on
|
||||
* a hole-free array. Returns false if no breakpoint has `id`. */
|
||||
static bool repl_breakpoint_remove(xce_sched_repl_state_t* rs, int id)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
if (rs->breakpoints[i]->id == id) {
|
||||
repl_breakpoint_free(rs->breakpoints[i]);
|
||||
memmove(&(rs->breakpoints[i]), &(rs->breakpoints[i + 1]),
|
||||
(size_t)(rs->nbreakpoints - i - 1)
|
||||
* sizeof(*(rs->breakpoints)));
|
||||
rs->nbreakpoints--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_cmd_break - Req 5 AC 22's "break <event_type> [key=value ...]": "Set
|
||||
* breakpoint on event type ... Optional key=value filters narrow the
|
||||
* match ... Multiple filters are AND'd." `line` (the full command line,
|
||||
* including the "break" token) is re-tokenized via repl_split_tokens()
|
||||
* (repl.c) rather than repl_dispatch()'s single-token sscanf(). Each filter
|
||||
* token must contain exactly one '=' with a non-empty key -- a malformed
|
||||
* filter aborts the WHOLE command (no partially-applied breakpoint), same
|
||||
* "fail loud, no silent partial state" discipline repl.c's own
|
||||
* repl_cmd_step() doc comment establishes for a bad `step` argument.
|
||||
*/
|
||||
bool repl_cmd_break(xce_sched_sim_driver_t* sim, const char* line)
|
||||
{
|
||||
char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN];
|
||||
int ntok = repl_split_tokens(line, tokens,
|
||||
XCE_REPL_MAX_TOKENS);
|
||||
xce_repl_bp_kind_t kind;
|
||||
xce_repl_breakpoint_t* bp;
|
||||
int i;
|
||||
|
||||
if (ntok < 2) {
|
||||
printf("[sim] break: usage: break <EVENT_TYPE> [key=value ...]\n");
|
||||
return false;
|
||||
}
|
||||
if (!repl_bp_kind_from_token(tokens[1], &kind)) {
|
||||
printf("[sim] break: unrecognized event type '%s'\n", tokens[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
bp = (xce_repl_breakpoint_t*)calloc(1, sizeof(*bp));
|
||||
if (bp == NULL) {
|
||||
printf("[sim] break: out of memory\n");
|
||||
return false;
|
||||
}
|
||||
bp->kind = kind;
|
||||
bp->token = makeString(tokens[1]);
|
||||
|
||||
for (i = 2; i < ntok; i++) {
|
||||
char* eq = strchr(tokens[i], '=');
|
||||
xce_repl_filter_t* grown;
|
||||
|
||||
if ((eq == NULL) || (eq == tokens[i])) {
|
||||
printf("[sim] break: malformed filter '%s' -- expected "
|
||||
"key=value\n",
|
||||
tokens[i]);
|
||||
repl_breakpoint_free(bp);
|
||||
return false;
|
||||
}
|
||||
*eq = '\0';
|
||||
|
||||
grown = (xce_repl_filter_t*)realloc(
|
||||
bp->filters, (size_t)(bp->nfilters + 1) * sizeof(*(bp->filters)));
|
||||
if (grown == NULL) {
|
||||
printf("[sim] break: out of memory\n");
|
||||
repl_breakpoint_free(bp);
|
||||
return false;
|
||||
}
|
||||
bp->filters = grown;
|
||||
bp->filters[bp->nfilters].key = makeString(tokens[i]);
|
||||
bp->filters[bp->nfilters].value = makeString(eq + 1);
|
||||
bp->nfilters++;
|
||||
}
|
||||
|
||||
bp->id = ++(sim->repl_state->next_bp_id);
|
||||
if (!repl_breakpoints_append(sim->repl_state, bp)) {
|
||||
printf("[sim] break: out of memory\n");
|
||||
(sim->repl_state->next_bp_id)--;
|
||||
repl_breakpoint_free(bp);
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("[sim] breakpoint %d set: %s", bp->id, bp->token);
|
||||
for (i = 0; i < bp->nfilters; i++) {
|
||||
printf(" %s=%s", bp->filters[i].key, bp->filters[i].value);
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
return false; /* setting a breakpoint never hands control back */
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_cmd_delete - Req 5 AC 22's "delete <id>": "Delete breakpoint by ID.
|
||||
* `delete all` removes all breakpoints."
|
||||
*/
|
||||
bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line)
|
||||
{
|
||||
char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN];
|
||||
int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS);
|
||||
|
||||
if (ntok < 2) {
|
||||
printf("[sim] delete: usage: delete <id>|all\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strcmp(tokens[1], "all") == 0) {
|
||||
int n = sim->repl_state->nbreakpoints;
|
||||
|
||||
repl_breakpoints_clear(sim->repl_state);
|
||||
printf("[sim] deleted %d breakpoint(s)\n", n);
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
int id = atoi(tokens[1]);
|
||||
|
||||
if (!repl_breakpoint_remove(sim->repl_state, id)) {
|
||||
printf("[sim] delete: no breakpoint with id %d\n", id);
|
||||
} else {
|
||||
printf("[sim] breakpoint %d deleted\n", id);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* xce_repl_on_event - the per-decoded-ReplayEvent breakpoint-check hook
|
||||
* (task 12.2, Req 5 AC 22's `check_breakpoints(event)` pseudocode call,
|
||||
* BEFORE `core_push_events`). Called from sim_loop.c right after a
|
||||
* translatable event is decoded, before it is appended to the accumulating
|
||||
* batch -- so a matching breakpoint pauses the REPL with core state exactly
|
||||
* as it stood before this event's effect, i.e. "about to be processed"
|
||||
* (the event is not applied to core state until the next ReplaySession
|
||||
* boundary's xce_core_push_events() call). Also increments `event_index`,
|
||||
* the position `info session` (repl_info.c) reports -- see repl.h's own
|
||||
* doc comment on this function for why no separate hook site exists for
|
||||
* that bookkeeping.
|
||||
*/
|
||||
void xce_repl_on_event(xce_sched_sim_driver_t* sim, xce_core_event_t* ev)
|
||||
{
|
||||
xce_sched_repl_state_t* rs;
|
||||
xce_repl_breakpoint_t* bp;
|
||||
|
||||
if ((sim == NULL) || (ev == NULL) || (sim->repl_state == NULL)) {
|
||||
return;
|
||||
}
|
||||
rs = sim->repl_state;
|
||||
|
||||
rs->event_index++;
|
||||
|
||||
bp = repl_find_event_breakpoint(rs, ev);
|
||||
if (bp != NULL) {
|
||||
printf("[sim] hit breakpoint %d @ event #%llu: %s\n", bp->id,
|
||||
(unsigned long long)rs->event_index, bp->token);
|
||||
repl_command_loop(sim);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* xce_repl_on_begin_round - the BEGIN_ROUND breakpoint-check hook (task
|
||||
* 12.2): BEGIN_ROUND is the ReplaySession boundary itself, so this is
|
||||
* called from sim_loop.c right after `begin_round` is decoded, before the
|
||||
* accumulated event batch is pushed -- matching the pseudocode's
|
||||
* `check_breakpoints(event)` call ordering for the message that IS a
|
||||
* ReplaySession (see repl.h's own doc comment). Also resets the
|
||||
* last-session decision tally and increments `session_count`, both of
|
||||
* which `info session` (repl_info.c) reports.
|
||||
*/
|
||||
void xce_repl_on_begin_round(xce_sched_sim_driver_t* sim, uint64_t begin_round)
|
||||
{
|
||||
xce_sched_repl_state_t* rs;
|
||||
xce_repl_breakpoint_t* bp;
|
||||
|
||||
if ((sim == NULL) || (sim->repl_state == NULL)) {
|
||||
return;
|
||||
}
|
||||
rs = sim->repl_state;
|
||||
|
||||
rs->session_count++;
|
||||
rs->last_begin_round = begin_round;
|
||||
rs->last_n_dispatch = 0;
|
||||
rs->last_n_pend = 0;
|
||||
rs->last_n_preempt = 0;
|
||||
|
||||
bp = repl_find_begin_round_breakpoint(rs);
|
||||
if (bp != NULL) {
|
||||
printf("[sim] hit breakpoint %d @ BEGIN_ROUND round=%llu\n", bp->id,
|
||||
(unsigned long long)begin_round);
|
||||
repl_command_loop(sim);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* xce_repl_on_decision - the per-drained-decision breakpoint-check hook
|
||||
* (task 12.2): called from sim_loop.c's decision-drain loop right after
|
||||
* sim_print_decision(d), matching the pseudocode's
|
||||
* `check_breakpoints(decision)` ordering ("after verify/output-write,
|
||||
* before moving to the next decision" -- see repl.h's own doc comment).
|
||||
* Also accumulates the last-session dispatch/pend/preempt tally
|
||||
* `info session` (repl_info.c) reports.
|
||||
*/
|
||||
void xce_repl_on_decision(xce_sched_sim_driver_t* sim, xce_decision_t* d)
|
||||
{
|
||||
xce_sched_repl_state_t* rs;
|
||||
xce_repl_breakpoint_t* bp;
|
||||
|
||||
if ((sim == NULL) || (d == NULL) || (sim->repl_state == NULL)) {
|
||||
return;
|
||||
}
|
||||
rs = sim->repl_state;
|
||||
|
||||
switch (d->op) {
|
||||
case XCE_DECISION_DISPATCH:
|
||||
rs->last_n_dispatch++;
|
||||
break;
|
||||
case XCE_DECISION_PEND:
|
||||
rs->last_n_pend++;
|
||||
break;
|
||||
case XCE_DECISION_PREEMPT:
|
||||
rs->last_n_preempt++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
bp = repl_find_decision_breakpoint(&(sim->core), rs, d);
|
||||
if (bp != NULL) {
|
||||
printf("[sim] hit breakpoint %d @ decision: %s jobid=%llu\n", bp->id,
|
||||
decision_op_name(d->op), (unsigned long long)decision_jobid(d));
|
||||
repl_command_loop(sim);
|
||||
}
|
||||
}
|
||||
668
src/scheduler/src/repl_info.c
Normal file
668
src/scheduler/src/repl_info.c
Normal file
|
|
@ -0,0 +1,668 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "repl.h"
|
||||
#include "repl_internal.h"
|
||||
#include "resreq.h"
|
||||
#include "utils.h"
|
||||
|
||||
/*
|
||||
* repl_info.c - state inspection (scheduler-core spec, task 12.2, Req 5 AC
|
||||
* 22 "State inspection"). Sibling of repl.c/repl_break.c -- see
|
||||
* repl_internal.h's own "Why a split at all" section for the full
|
||||
* rationale behind this three-file layout.
|
||||
*
|
||||
* Owns: `info session`/`info job`/`info queue`/`info worker`/
|
||||
* `info resource`/`info summary`/`info break`, and the `repl_cmd_info()`
|
||||
* dispatcher repl.c's repl_dispatch() hands off to (repl_internal.h's own
|
||||
* extern declaration).
|
||||
*
|
||||
* Every dump below reads core->jobs/queues/workers/resmgr the SAME way
|
||||
* repl.c's own repl_count_jobs() and snapshot.c's
|
||||
* xce_sched_core_save_snapshot() already do -- no new traversal mechanism.
|
||||
* Field selection is deliberately cross-checked against
|
||||
* xce_sched_core_save_snapshot()'s own documented field set (snapshot.c's
|
||||
* file header comment, "Field-by-field mapping") rather than invented
|
||||
* fresh, matching AC 22's own text: inspection output should "report state
|
||||
* matching what a snapshot dump of the same moment would show." Where a
|
||||
* requested field genuinely does not exist in this codebase's data model
|
||||
* yet (e.g. xce_sched_queue_t has no RUNLIMIT/MAX_RUNNING field), that gap
|
||||
* is reported honestly ("n/a"), never fabricated -- the same discipline
|
||||
* snapshot.c's own "Scope decisions" section already establishes for
|
||||
* job-level priority.
|
||||
*
|
||||
* Boundary (Req 5 AC 17): same as repl.c/repl_break.c -- only
|
||||
* xce_sched_sim_driver_t/xce_sched_core_t (via repl.h/repl_internal.h ->
|
||||
* sim.h -> core.h) plus stdio/string.h/stdlib.h, plus resreq.h for
|
||||
* format_resreq() (`info job`'s resreq= field, see repl_info_job() below).
|
||||
* resreq.h carries no capnp/controller/client symbols of its own (its
|
||||
* #include set is kvmgr.h/xlist.h/xvec.h plus libc headers) and is already
|
||||
* part of this same isolation-boundary family -- snapshot_wire.c's own
|
||||
* build_wire_job() (task 8.1) already includes it and calls format_resreq()
|
||||
* for the identical purpose.
|
||||
*/
|
||||
|
||||
/* dup_val/arr_append/find_resource_name_by_idx -- implemented in
|
||||
* snapshot_wire.c (task 8.1's own 800-line-cap split); redeclared here
|
||||
* rather than pulling in a private header, same "extern declared where
|
||||
* consumed" idiom snapshot.c itself already uses for these exact symbols.
|
||||
* Only arr_append/find_resource_name_by_idx are needed here (`info worker`/
|
||||
* `info resource` resolve a resource idx to its name the SAME way
|
||||
* xce_sched_core_save_snapshot() already does, per this file's own header
|
||||
* comment). */
|
||||
extern int arr_append(void***, int*, int*, void*);
|
||||
extern const char* find_resource_name_by_idx(xce_resource_t**, int, int);
|
||||
|
||||
/* repl_job_state_name - a single readable label for `info job`'s state=
|
||||
* field (task 12.2, AC 22's "info job <jobid>: Dump one job: state, ..."
|
||||
* row). Priority order matches this codebase's own convention for a job
|
||||
* whose `state` bitmask carries more than one bit (e.g. PEND|PSUSP): the
|
||||
* most "advanced" lifecycle bit wins, mirroring repl_break.c's own
|
||||
* repl_job_state_matches() check order (used by `break ... state=`
|
||||
* filters) so a filter and this display never disagree about which single
|
||||
* word describes a given state. */
|
||||
static const char* repl_job_state_name(uint32_t state)
|
||||
{
|
||||
if (JOB_IS_RUN(state)) {
|
||||
return "RUN";
|
||||
}
|
||||
if (JOB_IS_EXIT(state)) {
|
||||
return "EXIT";
|
||||
}
|
||||
if (JOB_IS_DONE(state)) {
|
||||
return "DONE";
|
||||
}
|
||||
if (JOB_IS_PEND(state)) {
|
||||
return "PEND";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_job - AC 22's "info job <jobid>": "Dump one job: state, queue,
|
||||
* priority, queue_time, resreq, allocations." Field set cross-checked
|
||||
* against xce_sched_core_save_snapshot()'s own documented mapping
|
||||
* (snapshot.c file header, "Field-by-field mapping", jobs row) rather than
|
||||
* invented fresh -- e.g. `resreq` is formatted via format_resreq()
|
||||
* (resreq.h), the SAME print-form snapshot_wire.c's own build_wire_job()
|
||||
* (task 8.1) already calls to populate a restored job's `submit->resreq`
|
||||
* wire field, not a fabricated format. (job-level priority remains the one
|
||||
* genuine gap snapshot.c's own "Scope decisions" section documents -- no
|
||||
* wire slot exists for it at all, unlike resreq.) sched_get_job() (core.h)
|
||||
* is the exact accessor xce_core_push_events()'s own JOB_STATE_CHANGE/
|
||||
* JOB_UPDATE handling (core_events.c) already uses.
|
||||
*/
|
||||
static void repl_info_job(xce_sched_sim_driver_t* sim, const char* jobid_str)
|
||||
{
|
||||
uint64_t jobid = strtoull(jobid_str, NULL, 10);
|
||||
xce_sched_job_t* job = sched_get_job(&(sim->core), jobid);
|
||||
char* resreq_str = NULL;
|
||||
|
||||
if (job == NULL) {
|
||||
printf("[sim] info job: no such job %llu\n",
|
||||
(unsigned long long)jobid);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Same format_resreq() call shape/error-handling build_wire_job()
|
||||
* (snapshot_wire.c) already uses to populate a restored job's
|
||||
* `submit->resreq` wire field -- WARN + fall back to "(none)" on
|
||||
* failure, never fabricate a format. Unlike build_wire_job() (whose
|
||||
* caller owns the string via j->submit->resreq for the lifetime of the
|
||||
* wire job), this dump is transient -- resreq_str is freed below right
|
||||
* after printing. */
|
||||
if (job->resreq != NULL) {
|
||||
int rc = format_resreq(&resreq_str, job->resreq);
|
||||
|
||||
if (rc < 0) {
|
||||
WARN("%s format_resreq failed for jobid %llu (rc=%d); resreq "
|
||||
"will be reported as unavailable for this dump",
|
||||
__func__, (unsigned long long)job->jobid, rc);
|
||||
resreq_str = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
printf("[sim] job %llu: state=%s queue=%s priority=%d queue_time=%lld "
|
||||
"resreq=%s allocs=%d\n",
|
||||
(unsigned long long)job->jobid, repl_job_state_name(job->state),
|
||||
(job->queue != NULL) ? job->queue->name : "(none)", job->priority,
|
||||
(long long)job->queue_time,
|
||||
(resreq_str != NULL) ? resreq_str : "(none)",
|
||||
(job->allocs != NULL) ? job->allocs->nhosts : 0);
|
||||
|
||||
FREEUP(resreq_str);
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_queue_one - AC 22's "info queue <name>": "Dump one queue:
|
||||
* priority, state (open/closed), RUNLIMIT, MAX_RUNNING, job_pick_plugin,
|
||||
* list of queued job IDs with priorities." xce_sched_queue_t (sched/types.h)
|
||||
* has no RUNLIMIT/MAX_RUNNING field at all -- confirmed against both the
|
||||
* live struct and its wire xce_queue_t counterpart -- the same
|
||||
* pre-existing, documented gap snapshot.c's own "Scope decisions" section
|
||||
* already notes for other AC 21/22-adjacent fields (e.g. job-level
|
||||
* priority) that requirements.md names but no C struct in this codebase
|
||||
* carries yet. Reported honestly as "n/a" rather than a fabricated number.
|
||||
* "queued" below means JOB_IS_PEND() -- a running job is not queued.
|
||||
*/
|
||||
static void repl_info_queue_one(xce_sched_sim_driver_t* sim,
|
||||
xce_sched_queue_t* q)
|
||||
{
|
||||
iterator_t it;
|
||||
xce_sched_job_t* job = NULL;
|
||||
int rc;
|
||||
|
||||
printf("[sim] queue %s: priority=%d status=%s RUNLIMIT=n/a "
|
||||
"MAX_RUNNING=n/a job_pick_plugin=%s\n",
|
||||
q->name, q->priority,
|
||||
((q->status & QUEUE_STAT_CLOSED) != 0) ? "closed" : "open",
|
||||
(q->job_pick_plugin != NULL) ? q->job_pick_plugin : "(inherit)");
|
||||
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
if ((job->queue != NULL) && (strcmp(job->queue->name, q->name) == 0)
|
||||
&& JOB_IS_PEND(job->state)) {
|
||||
printf("[sim] queued job: jobid=%llu priority=%d\n",
|
||||
(unsigned long long)job->jobid, job->priority);
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
/* repl_info_queue - `info queue <name|all>` dispatch. "all" walks
|
||||
* core->queues (same iter_make()/iter_next() idiom every other walk in this
|
||||
* file uses); a specific name goes through sched_get_queue() (core.h). */
|
||||
static void repl_info_queue(xce_sched_sim_driver_t* sim, const char* arg)
|
||||
{
|
||||
if (strcmp(arg, "all") == 0) {
|
||||
iterator_t it;
|
||||
xce_sched_queue_t* q = NULL;
|
||||
int rc = iter_make(&it, sim->core.queues);
|
||||
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&q);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
repl_info_queue_one(sim, q);
|
||||
}
|
||||
iter_destroy(&it);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
xce_sched_queue_t* q = sched_get_queue(&(sim->core), (char*)arg);
|
||||
|
||||
if (q == NULL) {
|
||||
printf("[sim] info queue: no such queue '%s'\n", arg);
|
||||
return;
|
||||
}
|
||||
repl_info_queue_one(sim, q);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_worker_one - AC 22's "info worker <name>": "Dump one worker:
|
||||
* state, resources (total + available), assigned jobs." `w->resvals` holds
|
||||
* currently-AVAILABLE values (the same zarray the picker/allocator
|
||||
* accounting path reads/resets each session, core_events.c); `w->resInUse`
|
||||
* holds the currently-committed amount at the same index -- "total" below
|
||||
* is therefore computed as available+inUse, mirroring how this codebase's
|
||||
* own resource accounting treats the pair as a conservation invariant
|
||||
* (nothing elsewhere sums them into a literal "total" field to reuse).
|
||||
* Resource names are resolved via find_resource_name_by_idx() against a
|
||||
* resmgr walk built the same way xce_sched_core_save_snapshot() builds its
|
||||
* own `res_arr` (snapshot.c) -- not a new lookup mechanism. "assigned jobs"
|
||||
* scans core->jobs for any per-host allocation naming this worker
|
||||
* (xce_job_alloc_t.allocs[]->host, xce/types.h).
|
||||
*/
|
||||
static void repl_info_worker_one(xce_sched_sim_driver_t* sim,
|
||||
xce_sched_worker_t* w)
|
||||
{
|
||||
xce_resource_t** res_arr = NULL;
|
||||
int res_cap = 0;
|
||||
int res_n = 0;
|
||||
iterator_t it;
|
||||
xce_resource_t* r = NULL;
|
||||
xce_sched_job_t* job;
|
||||
int rc;
|
||||
int i;
|
||||
|
||||
printf("[sim] worker %s: status=%s\n", w->name,
|
||||
(IS_WORKER_UNAVAIL(w->status)) ? "unavail" : "online");
|
||||
|
||||
rc = iter_make(&it, sim->core.resmgr);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&r);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
arr_append((void***)&res_arr, &res_cap, &res_n, r);
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
for (i = 0; i < w->resvals.count; i++) {
|
||||
xce_val_t* avail_v = (xce_val_t*)zarray_get(&(w->resvals), i);
|
||||
xce_val_t* used_v = (xce_val_t*)zarray_get(&(w->resInUse), i);
|
||||
const char* rname = find_resource_name_by_idx(res_arr, res_n, i);
|
||||
double avail;
|
||||
double used;
|
||||
|
||||
if ((avail_v == NULL) || (rname == NULL)) {
|
||||
continue;
|
||||
}
|
||||
avail = (avail_v->which == XCE_VAL_NUMERIC) ? avail_v->u.fvalue : 0.0;
|
||||
used = ((used_v != NULL) && (used_v->which == XCE_VAL_NUMERIC))
|
||||
? used_v->u.fvalue
|
||||
: 0.0;
|
||||
printf("[sim] resource %s: total=%.2f available=%.2f\n", rname,
|
||||
avail + used, avail);
|
||||
}
|
||||
free(res_arr);
|
||||
|
||||
job = NULL;
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
if (job->allocs == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (i = 0; i < job->allocs->nhosts; i++) {
|
||||
xce_host_alloc_t* ha = job->allocs->allocs[i];
|
||||
|
||||
if ((ha != NULL) && (ha->host != NULL)
|
||||
&& (strcmp(ha->host, w->name) == 0)) {
|
||||
printf("[sim] assigned job: jobid=%llu\n",
|
||||
(unsigned long long)job->jobid);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
/* repl_info_worker - `info worker <name|all>` dispatch, same "all" vs.
|
||||
* sched_get_worker() split as repl_info_queue() above. */
|
||||
static void repl_info_worker(xce_sched_sim_driver_t* sim, const char* arg)
|
||||
{
|
||||
if (strcmp(arg, "all") == 0) {
|
||||
iterator_t it;
|
||||
xce_sched_worker_t* w = NULL;
|
||||
int rc = iter_make(&it, sim->core.workers);
|
||||
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&w);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
repl_info_worker_one(sim, w);
|
||||
}
|
||||
iter_destroy(&it);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
xce_sched_worker_t* w = sched_get_worker(&(sim->core), (char*)arg);
|
||||
|
||||
if (w == NULL) {
|
||||
printf("[sim] info worker: no such worker '%s'\n", arg);
|
||||
return;
|
||||
}
|
||||
repl_info_worker_one(sim, w);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_resource_one - AC 22's "info resource <name>": "Dump one
|
||||
* resource: type, total capacity, available, allocation count." `r` is the
|
||||
* DEFINITION object (xce/types.h's xce_resource_t -- valtype/ordtype/idx,
|
||||
* no per-worker numbers of its own, per snapshot.c's own "resources ->
|
||||
* core->resmgr ... already the wire type natively" note); total/available
|
||||
* are aggregated across every worker's resvals/resInUse at `r->idx`, the
|
||||
* same idx-indexed convention repl_info_worker_one() above already
|
||||
* establishes. "allocation count" counts distinct jobs currently holding
|
||||
* any per-host xce_alloc_t naming this resource (xce/types.h).
|
||||
*/
|
||||
static void repl_info_resource_one(xce_sched_sim_driver_t* sim,
|
||||
xce_resource_t* r)
|
||||
{
|
||||
iterator_t it;
|
||||
xce_sched_worker_t* w = NULL;
|
||||
xce_sched_job_t* job;
|
||||
double total = 0.0;
|
||||
double available = 0.0;
|
||||
int nallocs = 0;
|
||||
int rc;
|
||||
|
||||
rc = iter_make(&it, sim->core.workers);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
xce_val_t* avail_v;
|
||||
xce_val_t* used_v;
|
||||
|
||||
rc = iter_next(&it, (void**)&w);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
if (r->idx >= w->resvals.count) {
|
||||
continue;
|
||||
}
|
||||
avail_v = (xce_val_t*)zarray_get(&(w->resvals), r->idx);
|
||||
used_v = (xce_val_t*)zarray_get(&(w->resInUse), r->idx);
|
||||
if ((avail_v != NULL) && (avail_v->which == XCE_VAL_NUMERIC)) {
|
||||
available += avail_v->u.fvalue;
|
||||
total += avail_v->u.fvalue;
|
||||
}
|
||||
if ((used_v != NULL) && (used_v->which == XCE_VAL_NUMERIC)) {
|
||||
total += used_v->u.fvalue;
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
job = NULL;
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
int i;
|
||||
int matched = 0;
|
||||
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
if (job->allocs == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (i = 0; i < job->allocs->nhosts; i++) {
|
||||
xce_host_alloc_t* ha = job->allocs->allocs[i];
|
||||
int j;
|
||||
|
||||
if (ha == NULL) {
|
||||
continue;
|
||||
}
|
||||
for (j = 0; j < ha->nallocs; j++) {
|
||||
if ((ha->allocs[j] != NULL)
|
||||
&& (ha->allocs[j]->resname != NULL)
|
||||
&& (strcmp(ha->allocs[j]->resname, r->name) == 0)) {
|
||||
matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
nallocs++;
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
printf("[sim] resource %s: valtype=%s total=%.2f available=%.2f "
|
||||
"allocation_count=%d\n",
|
||||
r->name,
|
||||
(r->valtype == XCE_VAL_NUMERIC)
|
||||
? "numeric"
|
||||
: (r->valtype == XCE_VAL_STRING) ? "string" : "boolean",
|
||||
total, available, nallocs);
|
||||
}
|
||||
|
||||
/* repl_info_resource - `info resource <name|all>` dispatch. resmgr has no
|
||||
* exported sched_get_*() accessor (unlike jobs/queues/workers, core.h) --
|
||||
* key_find() directly, the same accessor core_events.c's own
|
||||
* QUEUE_UPDATE/RESOURCES_PUSH handling already uses against core->resmgr. */
|
||||
static void repl_info_resource(xce_sched_sim_driver_t* sim, const char* arg)
|
||||
{
|
||||
if (strcmp(arg, "all") == 0) {
|
||||
iterator_t it;
|
||||
xce_resource_t* r = NULL;
|
||||
int rc = iter_make(&it, sim->core.resmgr);
|
||||
|
||||
if (rc < 0) {
|
||||
return;
|
||||
}
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&r);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
repl_info_resource_one(sim, r);
|
||||
}
|
||||
iter_destroy(&it);
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
xce_resource_t* r = NULL;
|
||||
int rc = key_find(sim->core.resmgr, (void*)arg, (void**)&r);
|
||||
|
||||
if (rc <= 0) {
|
||||
printf("[sim] info resource: no such resource '%s'\n", arg);
|
||||
return;
|
||||
}
|
||||
repl_info_resource_one(sim, r);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_summary - AC 22's "info summary": "Quick overview: N_jobs (M
|
||||
* pending, K running), N_workers (M online), N_queues, N_resources." Four
|
||||
* independent iter_make()/iter_next() walks, same idiom repl.c's own
|
||||
* repl_count_jobs() already establishes -- no persistent counters,
|
||||
* computed fresh each call.
|
||||
*/
|
||||
static void repl_info_summary(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
iterator_t it;
|
||||
int rc;
|
||||
int n_jobs = 0, n_pending = 0, n_running = 0;
|
||||
int n_workers = 0, n_online = 0;
|
||||
int n_queues = 0, n_resources = 0;
|
||||
xce_sched_job_t* job = NULL;
|
||||
|
||||
rc = iter_make(&it, sim->core.jobs);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&job);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
n_jobs++;
|
||||
if (JOB_IS_PEND(job->state)) {
|
||||
n_pending++;
|
||||
}
|
||||
if (JOB_IS_RUN(job->state)) {
|
||||
n_running++;
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
{
|
||||
xce_sched_worker_t* w = NULL;
|
||||
|
||||
rc = iter_make(&it, sim->core.workers);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
rc = iter_next(&it, (void**)&w);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
n_workers++;
|
||||
if (!IS_WORKER_UNAVAIL(w->status)) {
|
||||
n_online++;
|
||||
}
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
void* tmp = NULL;
|
||||
|
||||
rc = iter_make(&it, sim->core.queues);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
rc = iter_next(&it, &tmp);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
n_queues++;
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
|
||||
rc = iter_make(&it, sim->core.resmgr);
|
||||
if (rc >= 0) {
|
||||
for (;;) {
|
||||
rc = iter_next(&it, &tmp);
|
||||
if ((rc < 0) || (rc == RTTI_ITER_NO_DATA)) {
|
||||
break;
|
||||
}
|
||||
n_resources++;
|
||||
}
|
||||
iter_destroy(&it);
|
||||
}
|
||||
}
|
||||
|
||||
printf("[sim] summary: %d jobs (%d pending, %d running), %d workers "
|
||||
"(%d online), %d queues, %d resources\n",
|
||||
n_jobs, n_pending, n_running, n_workers, n_online, n_queues,
|
||||
n_resources);
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_info_session - AC 22's "info session": "Show current simulation
|
||||
* position: logical time (xce_core_now()), event index (Nth message
|
||||
* processed), session count, last SessionSummary if available." The last-
|
||||
* session dispatch/pend/preempt tally is this task's own stand-in for "last
|
||||
* SessionSummary" -- no in-memory SessionSummary wire struct is decoded on
|
||||
* the replay-read path (only written on the record path,
|
||||
* recording_session.c), so this reports the same live decision tally
|
||||
* repl_break.c's xce_repl_on_decision() already accumulates per session,
|
||||
* strictly more current than a stale recorded summary would be for a
|
||||
* replay in progress.
|
||||
*/
|
||||
static void repl_info_session(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
xce_sched_repl_state_t* rs = sim->repl_state;
|
||||
|
||||
printf("[sim] session: ts=%llu event_index=%llu sessions=%llu\n",
|
||||
(unsigned long long)xce_core_now(&(sim->core)),
|
||||
(unsigned long long)((rs != NULL) ? rs->event_index : 0),
|
||||
(unsigned long long)((rs != NULL) ? rs->session_count : 0));
|
||||
|
||||
if ((rs != NULL) && (rs->session_count > 0)) {
|
||||
printf("[sim] last session: round=%llu dispatch=%d pend=%d "
|
||||
"preempt=%d\n",
|
||||
(unsigned long long)rs->last_begin_round, rs->last_n_dispatch,
|
||||
rs->last_n_pend, rs->last_n_preempt);
|
||||
} else {
|
||||
printf("[sim] last session: (none yet)\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* repl_info_break - AC 22's "info break": "List all breakpoints with their
|
||||
* IDs, event types, and filters." */
|
||||
static void repl_info_break(xce_sched_sim_driver_t* sim)
|
||||
{
|
||||
xce_sched_repl_state_t* rs = sim->repl_state;
|
||||
int i, j;
|
||||
|
||||
if ((rs == NULL) || (rs->nbreakpoints == 0)) {
|
||||
printf("[sim] no breakpoints set\n");
|
||||
return;
|
||||
}
|
||||
|
||||
printf("[sim] breakpoints:\n");
|
||||
for (i = 0; i < rs->nbreakpoints; i++) {
|
||||
xce_repl_breakpoint_t* bp = rs->breakpoints[i];
|
||||
|
||||
printf("[sim] %d: %s", bp->id, bp->token);
|
||||
for (j = 0; j < bp->nfilters; j++) {
|
||||
printf(" %s=%s", bp->filters[j].key, bp->filters[j].value);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* repl_cmd_info - Req 5 AC 22's "info ..." family: dispatches on the second
|
||||
* token to repl_info_break()/repl_info_session()/repl_info_job()/
|
||||
* repl_info_queue()/repl_info_worker()/repl_info_resource()/
|
||||
* repl_info_summary() (all above). No `info` subcommand ever hands control
|
||||
* back to the driver loop -- inspecting state is never a reason to advance
|
||||
* the simulation. repl.c's repl_dispatch() calls this (repl_internal.h's
|
||||
* own extern declaration).
|
||||
*/
|
||||
bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line)
|
||||
{
|
||||
char tokens[XCE_REPL_MAX_TOKENS][XCE_REPL_TOKEN_LEN];
|
||||
int ntok = repl_split_tokens(line, tokens, XCE_REPL_MAX_TOKENS);
|
||||
|
||||
if (ntok < 2) {
|
||||
printf("[sim] info: usage: info "
|
||||
"<break|session|job|queue|worker|resource|summary> [arg]\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strcmp(tokens[1], "break") == 0) {
|
||||
repl_info_break(sim);
|
||||
} else if (strcmp(tokens[1], "session") == 0) {
|
||||
repl_info_session(sim);
|
||||
} else if (strcmp(tokens[1], "summary") == 0) {
|
||||
repl_info_summary(sim);
|
||||
} else if (strcmp(tokens[1], "job") == 0) {
|
||||
if (ntok < 3) {
|
||||
printf("[sim] info job: usage: info job <jobid>\n");
|
||||
} else {
|
||||
repl_info_job(sim, tokens[2]);
|
||||
}
|
||||
} else if (strcmp(tokens[1], "queue") == 0) {
|
||||
if (ntok < 3) {
|
||||
printf("[sim] info queue: usage: info queue <name>|all\n");
|
||||
} else {
|
||||
repl_info_queue(sim, tokens[2]);
|
||||
}
|
||||
} else if (strcmp(tokens[1], "worker") == 0) {
|
||||
if (ntok < 3) {
|
||||
printf("[sim] info worker: usage: info worker <name>|all\n");
|
||||
} else {
|
||||
repl_info_worker(sim, tokens[2]);
|
||||
}
|
||||
} else if (strcmp(tokens[1], "resource") == 0) {
|
||||
if (ntok < 3) {
|
||||
printf("[sim] info resource: usage: info resource <name>|all\n");
|
||||
} else {
|
||||
repl_info_resource(sim, tokens[2]);
|
||||
}
|
||||
} else {
|
||||
printf("[sim] info: unrecognized subcommand '%s'\n", tokens[1]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
162
src/scheduler/src/repl_internal.h
Normal file
162
src/scheduler/src/repl_internal.h
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#if !defined(_REPL_INTERNAL_H_)
|
||||
|
||||
#define _REPL_INTERNAL_H_ 1
|
||||
|
||||
#include "repl.h"
|
||||
|
||||
/*
|
||||
* repl_internal.h - private state and cross-file declarations shared by
|
||||
* repl.c/repl_break.c/repl_info.c (scheduler-core spec, task 12.2). NOT
|
||||
* installed, NOT included by anything outside src/scheduler/src/ -- the
|
||||
* SAME "private header shared between sibling .c files, moved out of a
|
||||
* single file that used to hold it alone" pattern src/lib/client_internal.h
|
||||
* already establishes for client.c/client_failover.c (see that file's own
|
||||
* header comment).
|
||||
*
|
||||
* Why a split at all: task 12.1's repl.c (execution control only) fit
|
||||
* comfortably at 370/800 lines. Task 12.2's full scope -- breakpoints on 10
|
||||
* wire event kinds plus BEGIN_ROUND/DECISION, each with key=value filter
|
||||
* matching, PLUS five separate `info` state-inspection dumps -- does not
|
||||
* fit in one file under this project's 800-line-per-file cap
|
||||
* (CLAUDE.md/coding-style.md). Split along the same domain boundaries
|
||||
* task 8.1 already used for snapshot.c/snapshot_wire.c/snapshot_load.c
|
||||
* (that file's own "Three-file split" header section is the precedent):
|
||||
*
|
||||
* - repl.c: the command loop itself (readline()/dispatch), execution
|
||||
* control (`step`/`continue`/`quit`/`help`), and the two task-12.1
|
||||
* driver-facing entry points (xce_repl_start()/
|
||||
* xce_repl_post_session_check()). Unchanged in scope from task 12.1
|
||||
* except for three new repl_dispatch() branches that hand off to the
|
||||
* other two files.
|
||||
* - repl_break.c: breakpoint kind/filter matching, the breakpoint list
|
||||
* (create/list/delete), and the three task-12.2 driver-facing hooks
|
||||
* (xce_repl_on_event()/xce_repl_on_begin_round()/xce_repl_on_decision(),
|
||||
* declared in repl.h, implemented here since they are breakpoint-domain
|
||||
* logic through and through).
|
||||
* - repl_info.c: the `info job`/`queue`/`worker`/`resource`/`summary`/
|
||||
* `session`/`break` state-inspection dumps.
|
||||
*
|
||||
* `struct xce_sched_repl_state_s` (the full definition of sim.h's opaque
|
||||
* forward declaration) moves here from repl.c for the same reason:
|
||||
* repl_break.c's breakpoint list and repl_info.c's `info session`/
|
||||
* `info break` both need direct field access, not just the opaque pointer
|
||||
* sim_loop.c gets via sim.h. This does NOT weaken the "opaque outside the
|
||||
* owning module" property that struct always had -- repl_internal.h is
|
||||
* never included outside src/scheduler/src/repl*.c, so sim_loop.c and
|
||||
* every other production consumer still see only the pointer (via sim.h),
|
||||
* exactly as before this split.
|
||||
*/
|
||||
|
||||
typedef enum {
|
||||
XCE_REPL_MODE_STEP = 0, /* pause once `step_budget` sessions complete */
|
||||
XCE_REPL_MODE_CONTINUE /* run to EOF or the next breakpoint hit */
|
||||
} xce_repl_run_mode_t;
|
||||
|
||||
/*
|
||||
* xce_repl_bp_kind_t (task 12.2, Req 5 AC 22 "Breakpoints") - the event
|
||||
* types a `break` command may name. The ten *_PUSH, *_UPDATE, *_CHANGE,
|
||||
* *_RUSAGE kinds map one-for-one onto core.h's xce_core_event_type_t
|
||||
* (repl_break.c's repl_bp_kind_for_core_event() has the mapping, and
|
||||
* documents why the token spelling sometimes differs, e.g. "JOB_PUSH" here
|
||||
* vs. XCE_CORE_EVENT_JOBS_PUSH there -- matching requirements.md's own AC
|
||||
* 22 example text verbatim). BEGIN_ROUND and DECISION are NOT
|
||||
* xce_core_event_t members -- BEGIN_ROUND is the ReplaySession boundary
|
||||
* itself, DECISION is a drained xce_decision_t -- each gets its own
|
||||
* breakpoint-kind value and its own matching path (repl_break.c).
|
||||
*/
|
||||
typedef enum {
|
||||
XCE_REPL_BP_CONFIG_PUSH = 1,
|
||||
XCE_REPL_BP_JOB_PUSH,
|
||||
XCE_REPL_BP_WORKERS_PUSH,
|
||||
XCE_REPL_BP_RESOURCES_PUSH,
|
||||
XCE_REPL_BP_RESVALS_FULL,
|
||||
XCE_REPL_BP_RESVALS_DELTA,
|
||||
XCE_REPL_BP_JOB_STATE_CHANGE,
|
||||
XCE_REPL_BP_JOB_UPDATE,
|
||||
XCE_REPL_BP_QUEUE_UPDATE,
|
||||
XCE_REPL_BP_JOB_RUSAGE,
|
||||
XCE_REPL_BP_BEGIN_ROUND,
|
||||
XCE_REPL_BP_DECISION,
|
||||
} xce_repl_bp_kind_t;
|
||||
|
||||
/* xce_repl_filter_t - one AND'd `key=value` clause of a `break` command
|
||||
* (task 12.2). Both strings are makeString()-interned xstrings (RTTI
|
||||
* reference-counted, not plain malloc'd) -- never explicitly released,
|
||||
* the same "makeString()'d fields live for the process's lifetime"
|
||||
* precedent this codebase's own kill_sched_job()/kill_sched_queue()
|
||||
* (schedobj.c) already establish for every other such field; see
|
||||
* repl_break.c's repl_breakpoint_free() for the full rationale (a bare
|
||||
* free() on one corrupts the heap allocator). */
|
||||
typedef struct {
|
||||
char* key;
|
||||
char* value;
|
||||
} xce_repl_filter_t;
|
||||
|
||||
/* xce_repl_breakpoint_t (task 12.2) - one entry set by `break`, listed by
|
||||
* `info break`, removed by `delete`. `token` is the user-typed event-type
|
||||
* text (e.g. "JOB_STATE_CHANGE"), kept verbatim for `info break`'s own
|
||||
* listing rather than re-derived from `kind`, so listing output always
|
||||
* echoes exactly what the operator typed. */
|
||||
typedef struct {
|
||||
int id;
|
||||
xce_repl_bp_kind_t kind;
|
||||
char* token;
|
||||
xce_repl_filter_t* filters;
|
||||
int nfilters;
|
||||
} xce_repl_breakpoint_t;
|
||||
|
||||
/*
|
||||
* xce_sched_repl_state_s - see this header's own "Why a split at all"
|
||||
* section above for why the full definition lives here now instead of
|
||||
* repl.c. Task 12.1 fields: mode/step_budget. Task 12.2 fields:
|
||||
* `breakpoints`/`nbreakpoints`/`next_bp_id` back `break`/`info break`/
|
||||
* `delete` (a plain realloc'd array of pointers, same growth idiom
|
||||
* sim_loop.c's own buf_events[] uses -- no new library dependency).
|
||||
* `event_index`/`session_count`/`last_begin_round`/`last_n_dispatch`/
|
||||
* `last_n_pend`/`last_n_preempt` back `info session`'s position/summary
|
||||
* reporting; updated by xce_repl_on_event()/xce_repl_on_begin_round()/
|
||||
* xce_repl_on_decision() (repl_break.c) as a side effect of the same
|
||||
* per-event/per-session/per-decision call sim_loop.c already makes for
|
||||
* breakpoint checking (repl.h's doc comment on those three functions
|
||||
* explains why no separate hook site was needed for this bookkeeping).
|
||||
*/
|
||||
struct xce_sched_repl_state_s {
|
||||
xce_repl_run_mode_t mode;
|
||||
long step_budget;
|
||||
xce_repl_breakpoint_t** breakpoints;
|
||||
int nbreakpoints;
|
||||
int next_bp_id;
|
||||
uint64_t event_index;
|
||||
uint64_t session_count;
|
||||
uint64_t last_begin_round;
|
||||
int last_n_dispatch;
|
||||
int last_n_pend;
|
||||
int last_n_preempt;
|
||||
};
|
||||
|
||||
/* repl_split_tokens/XCE_REPL_MAX_TOKENS/XCE_REPL_TOKEN_LEN (repl.c) -
|
||||
* shared whitespace tokenizer: `break`/`info`/`delete` (repl_break.c/
|
||||
* repl_info.c) all need more than repl_dispatch()'s own single-token
|
||||
* `sscanf(" %31s")`. */
|
||||
#define XCE_REPL_MAX_TOKENS 16
|
||||
#define XCE_REPL_TOKEN_LEN 64
|
||||
|
||||
extern int repl_split_tokens(const char* line,
|
||||
char tokens[][XCE_REPL_TOKEN_LEN], int max_tokens);
|
||||
|
||||
/* repl_command_loop (repl.c) - the blocking prompt loop. Breakpoint hits
|
||||
* (repl_break.c's xce_repl_on_event()/on_begin_round()/on_decision()) call
|
||||
* back into it directly, the same re-entry xce_repl_post_session_check()
|
||||
* (repl.c) already uses for a `step N` pause. */
|
||||
extern void repl_command_loop(xce_sched_sim_driver_t* sim);
|
||||
|
||||
/* repl_cmd_break/repl_cmd_delete (repl_break.c) and repl_cmd_info
|
||||
* (repl_info.c) - the three task-12.2 command handlers repl.c's
|
||||
* repl_dispatch() hands off to. Same "returns false, re-prompt" contract
|
||||
* repl_cmd_step()/repl_print_help() already follow (repl.c's own doc
|
||||
* comment on repl_dispatch()). */
|
||||
extern bool repl_cmd_break(xce_sched_sim_driver_t* sim, const char* line);
|
||||
extern bool repl_cmd_delete(xce_sched_sim_driver_t* sim, const char* line);
|
||||
extern bool repl_cmd_info(xce_sched_sim_driver_t* sim, const char* line);
|
||||
|
||||
#endif
|
||||
|
|
@ -20,41 +20,38 @@
|
|||
* (sim.c/task 11.1 implements steps 1-4) and hosts sched_sim_driver_run()
|
||||
* itself -- the design.md-literal SimDriver entry point sim.h declares.
|
||||
*
|
||||
* Task 11.5 (Req 5 AC 16/19, this revision) adds the --sim-alt-events event
|
||||
* source switch (`event_src` in sched_sim_driver_run(), chosen once before
|
||||
* the loop) and the --sim-output writer: header+snapshot (once, right
|
||||
* after startup -- sim_record.c's sim_record_write_header_and_snapshot()),
|
||||
* per-event pass-through (sim_record_write_event(), same file), and
|
||||
* per-round ReplaySession accumulation (recording_session.c's
|
||||
* scheduler_t-free sched_record_session_ctx_new()/_add_decision()/
|
||||
* _finalize(), externed directly -- no wrapper needed). sim_print_decision()
|
||||
* remains this file's own unconditional Req 5 AC 19 default; it, --sim-output
|
||||
* recording, and --sim-verify comparison are all independently gated per
|
||||
* drained decision (AC 19 does not make them mutually exclusive). Task 11.4
|
||||
* owns comparing each drained decision against the recorded
|
||||
* ReplaySession.decisions when --sim-verify is set -- see the ReplaySession
|
||||
* branch below and sim_verify.c (sibling file) for that comparison logic.
|
||||
* Task 11.5 (Req 5 AC 16/19) adds the --sim-alt-events event source switch
|
||||
* (`event_src`, chosen once before the loop) and the --sim-output writer:
|
||||
* header+snapshot (once, right after startup -- sim_record.c's
|
||||
* sim_record_write_header_and_snapshot()), per-event pass-through
|
||||
* (sim_record_write_event()), and per-round ReplaySession accumulation
|
||||
* (recording_session.c's scheduler_t-free sched_record_session_ctx_new()/
|
||||
* _add_decision()/_finalize(), externed directly). sim_print_decision()
|
||||
* remains this file's own unconditional Req 5 AC 19 default; it,
|
||||
* --sim-output recording, and --sim-verify comparison are independently
|
||||
* gated per drained decision. Task 11.4 owns comparing each drained
|
||||
* decision against the recorded ReplaySession.decisions when --sim-verify
|
||||
* is set -- see the ReplaySession branch below and sim_verify.c.
|
||||
*
|
||||
* Picker-driving thread (sim_picker_thread_fn, below): Req 5 AC 18's
|
||||
* pseudocode calls `core_go()` then blocks draining `core->decisions` until
|
||||
* the end-of-session NULL marker -- exactly like the normal-mode driver's
|
||||
* own protocol (core_session.c's doc comments on xce_core_go()/
|
||||
* xce_core_end_session()), which only works because sched_job_picker()/
|
||||
* sched_allocator() (scheduler.c) are two real, persistent threads started
|
||||
* once at sched_init() and living for the scheduler's whole lifetime. The
|
||||
* simulation driver (xce_sched_sim_driver_t, sim.h) deliberately has no
|
||||
* xce_scheduler_t* (Req 5 AC 17) and therefore cannot use either of those
|
||||
* xce_scheduler_t*-typed thread entry points -- without SOME equivalent
|
||||
* thread, the very first ReplaySession boundary in any fixture would call
|
||||
* xce_core_go() and then block forever on cbuf_get(&core->decisions), since
|
||||
* nothing would ever produce the closing NULL marker. sim_picker_thread_fn()
|
||||
* below is that equivalent: core-scoped (xce_sched_core_t*, not
|
||||
* xce_scheduler_t*), spawned once here, living for the simulation driver's
|
||||
* whole run -- see that function's own doc comment for why it also stands
|
||||
* in for the allocator role (no separate thread/jbuf hand-off), and for the
|
||||
* documented, out-of-this-task's-scope reason every candidate job it sees
|
||||
* is conservatively PENDed rather than DISPATCHed with a fabricated
|
||||
* allocation.
|
||||
* Task 12.2 (Req 5 AC 22 "Breakpoints") adds three bare hook calls below --
|
||||
* xce_repl_check_event()/xce_repl_check_begin_round()/xce_repl_check_decision()
|
||||
* (repl.h) -- at the three points design.md's REPL integration pseudocode
|
||||
* requires: per decoded event (before it joins the accumulated batch), per
|
||||
* ReplaySession boundary (before the batch is pushed), and per drained
|
||||
* decision (after sim_print_decision()). Each is a single call expression,
|
||||
* no branching -- the `sim->interactive` gate and all breakpoint-matching
|
||||
* logic live in repl.h/repl_break.c, this file having zero line-budget
|
||||
* headroom for either (see repl_internal.h's own "Why a split at all").
|
||||
*
|
||||
* Picker-driving thread (sim_picker_thread_fn, below): stands in for BOTH
|
||||
* the normal-mode picker AND allocator threads (scheduler.c), which the
|
||||
* simulation driver cannot use directly (no xce_scheduler_t*, Req 5 AC 17)
|
||||
* -- without it, the first ReplaySession would block forever on
|
||||
* cbuf_get(&core->decisions) with nothing to ever produce the closing NULL
|
||||
* marker. Conservatively PENDs every candidate job (real resource-matching
|
||||
* allocation belongs to the not-yet-designed scheduler-allocation spec) via
|
||||
* the real on_pend()/make_pend_decision()/make_pendreason() production
|
||||
* calls -- see that function's own doc comment for the full reasoning.
|
||||
*/
|
||||
|
||||
/* Shared with sim.c (task 11.1) -- see that file's own file header comment
|
||||
|
|
@ -595,6 +592,8 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
|
|||
}
|
||||
|
||||
if (translated > 0) {
|
||||
xce_repl_check_event(sim, &ev);
|
||||
|
||||
if (buf_n == buf_cap) {
|
||||
int newcap = buf_cap ? (buf_cap * 2) : 16;
|
||||
xce_core_event_t* tmp = (xce_core_event_t*)realloc(
|
||||
|
|
@ -630,6 +629,7 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
|
|||
|
||||
sp.p = root;
|
||||
begin_round = ReplaySession_get_beginRound(sp);
|
||||
xce_repl_check_begin_round(sim, begin_round);
|
||||
|
||||
/* Req 5 AC 15/18 (task 11.4): decode the FULL session --
|
||||
* including its `decisions` list -- before the capnp arena is
|
||||
|
|
@ -722,6 +722,7 @@ int sched_sim_driver_run(xce_sched_sim_driver_t* sim,
|
|||
record_ctx, xce_core_now(&(sim->core)), d);
|
||||
}
|
||||
sim_print_decision(d);
|
||||
xce_repl_check_decision(sim, d);
|
||||
}
|
||||
|
||||
if (sim->verify && (decoded_session != NULL)
|
||||
|
|
|
|||
|
|
@ -551,6 +551,15 @@ sched_sim_isolation_test = executable(
|
|||
'-DXCE_SIM_REPL_H_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'include' / 'repl.h')
|
||||
+ '"',
|
||||
'-DXCE_SIM_REPL_BREAK_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'repl_break.c')
|
||||
+ '"',
|
||||
'-DXCE_SIM_REPL_INFO_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'repl_info.c')
|
||||
+ '"',
|
||||
'-DXCE_SIM_REPL_INTERNAL_H_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'repl_internal.h')
|
||||
+ '"',
|
||||
'-DXCE_SIM_LIB_PATH="'
|
||||
+ sched_sim_isolation_lib.full_path()
|
||||
+ '"'],
|
||||
|
|
@ -660,7 +669,13 @@ sched_sim_equivalence_test = executable(
|
|||
# (core.c, core_clock.c, core_events.c, core_session.c, decision.c, plugin.c,
|
||||
# plugins/builtin_fcfs.c, recording.c, recording_session.c, sim.c, sim_loop.c,
|
||||
# sim_record.c, sim_verify.c, snapshot.c/snapshot_wire.c/snapshot_load.c) PLUS
|
||||
# the new ../src/repl.c under test. Needs libedit_dep (parent meson.build)
|
||||
# ../src/repl.c under test. Also pulls in ../src/repl_break.c/repl_info.c
|
||||
# (task 12.2's 800-line-cap split of repl.c -- repl_internal.h's own "Why a
|
||||
# split at all") -- required at link time since sim_loop.c's new breakpoint
|
||||
# hooks (repl.h) reference symbols repl_break.c defines, even though this
|
||||
# file's own tests exercise only the execution-control slice; task 12.2's
|
||||
# own breakpoint/inspection tests live in the sibling
|
||||
# test_repl_breakpoints_inspection.c. Needs libedit_dep (parent meson.build)
|
||||
# at link time -- repl.c's real readline()/add_history() calls are pulled
|
||||
# into this TU by the white-box #include.
|
||||
sched_repl_execution_control_test = executable(
|
||||
|
|
@ -670,6 +685,29 @@ sched_repl_execution_control_test = executable(
|
|||
dependencies: [scheduler_test_deps, libedit_dep],
|
||||
install: false)
|
||||
|
||||
# Contract test for the interactive simulation REPL's breakpoint and
|
||||
# state-inspection slice (scheduler-core spec, task 12.2, Req 5 AC 22
|
||||
# "Breakpoints"/"State inspection"): setting a breakpoint on a specific
|
||||
# event type and field value pauses the REPL exactly when a matching event
|
||||
# is about to be processed, and each `info` command reports state matching
|
||||
# a snapshot dump of the same moment. Same white-box #include pattern as
|
||||
# sched_repl_execution_control_test above (core.c, core_clock.c,
|
||||
# core_events.c, core_session.c, decision.c, plugin.c,
|
||||
# plugins/builtin_fcfs.c, recording.c, recording_session.c, sim.c,
|
||||
# sim_loop.c, sim_record.c, sim_verify.c,
|
||||
# snapshot.c/snapshot_wire.c/snapshot_load.c, repl.c, repl_break.c,
|
||||
# repl_info.c). Split into its own file rather than appended to
|
||||
# test_repl_execution_control.c -- that file was already 644 lines after
|
||||
# task 12.1, and this task's own fixture/assertion surface (breakpoints on
|
||||
# multiple event kinds plus five separate `info` dumps) does not fit
|
||||
# alongside it under this project's 800-line cap.
|
||||
sched_repl_breakpoints_inspection_test = executable(
|
||||
'sched_repl_breakpoints_inspection_test',
|
||||
['test_repl_breakpoints_inspection.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps, libedit_dep],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -763,3 +801,6 @@ test('scheduler sim equivalence test',
|
|||
test('scheduler repl execution control test',
|
||||
sched_repl_execution_control_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler repl breakpoints inspection test',
|
||||
sched_repl_breakpoints_inspection_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
1063
src/scheduler/testsuite/test_repl_breakpoints_inspection.c
Normal file
1063
src/scheduler/testsuite/test_repl_breakpoints_inspection.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -94,6 +94,8 @@
|
|||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
|
|
@ -46,13 +46,16 @@
|
|||
* White-box #include of the same TU set test_sim_verify.c uses (see that
|
||||
* file's own header comment) -- core.c/core_clock.c/core_events.c/
|
||||
* core_session.c/decision.c/plugin.c/plugins/builtin_fcfs.c/recording.c/
|
||||
* recording_session.c/sim.c/sim_loop.c/sim_record.c/sim_verify.c/
|
||||
* recording_session.c/repl.c/sim.c/sim_loop.c/sim_record.c/sim_verify.c/
|
||||
* snapshot.c/snapshot_load.c/snapshot_wire.c. sim_verify.c AND
|
||||
* recording_session.c/sim_record.c are all required at link time
|
||||
* recording_session.c/sim_record.c/repl.c are all required at link time
|
||||
* (sched_sim_driver_run() externs sim_decision_eq()/sim_log_mismatch() and
|
||||
* sched_record_session_ctx_new()/_finalize()/sim_record_write_header_and_
|
||||
* snapshot()/sim_record_write_event() unconditionally) even though this
|
||||
* file never sets --sim-verify or --sim-output, same reason
|
||||
* snapshot()/sim_record_write_event() unconditionally, and repl.h's task
|
||||
* 12.1 inline wrappers -- pulled in transitively by sim_loop.c's own
|
||||
* `#include "repl.h"` -- reference xce_repl_start()/
|
||||
* xce_repl_post_session_check() unconditionally too) even though this file
|
||||
* never sets --sim-verify/--sim-output/--interactive, same reason
|
||||
* test_sim_event_loop.c's own header comment documents.
|
||||
*/
|
||||
#include <errno.h>
|
||||
|
|
@ -83,6 +86,9 @@
|
|||
#include "../src/plugins/builtin_fcfs.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
|
|
@ -100,9 +100,11 @@
|
|||
* recording.c, recording_events.c, recording_session.c, snapshot.c,
|
||||
* snapshot_wire.c) and test_sim_record_output.c's own set (core.c,
|
||||
* core_clock.c, core_events.c, core_session.c, decision.c, plugin.c,
|
||||
* plugins/builtin_fcfs.c, recording.c, recording_session.c, sim.c,
|
||||
* plugins/builtin_fcfs.c, recording.c, recording_session.c, repl.c, sim.c,
|
||||
* sim_loop.c, sim_record.c, sim_verify.c, snapshot.c, snapshot_load.c,
|
||||
* snapshot_wire.c) -- no prior test file combines both the normal-mode
|
||||
* snapshot_wire.c -- repl.c added by task 12.1, required at link time by
|
||||
* sim_loop.c's own repl.h inline wrappers) -- no prior test file combines
|
||||
* both the normal-mode
|
||||
* driver stack and the full simulation stack in one TU; this task is the
|
||||
* first to genuinely need both halves of the pipeline live simultaneously.
|
||||
* Verified no static-symbol collisions exist across the combined file set
|
||||
|
|
@ -153,6 +155,9 @@
|
|||
#include "../src/recording.c"
|
||||
#include "../src/recording_events.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,12 @@
|
|||
* sched_record_session_ctx_new()/_finalize()/sim_record_write_header_and_
|
||||
* snapshot()/sim_record_write_event() as extern functions, undefined
|
||||
* references at link time otherwise, even though this file never sets
|
||||
* --sim-output either) PLUS
|
||||
* --sim-output either) PLUS ../src/repl.c (task 12.1's sibling file -- same
|
||||
* reasoning again: sim_loop.c now `#include`s repl.h, whose task 12.1
|
||||
* inline wrappers unconditionally reference xce_repl_start()/
|
||||
* xce_repl_post_session_check() as extern functions, undefined references
|
||||
* at link time otherwise, even though this file never sets --interactive)
|
||||
* PLUS
|
||||
* ../src/snapshot.c/snapshot_wire.c/snapshot_load.c (task 8.1, needed by
|
||||
* both the fixture-building and sim.c's own snapshot load).
|
||||
*/
|
||||
|
|
@ -96,6 +101,9 @@
|
|||
#include "../src/plugins/builtin_fcfs.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
|
|
@ -4,27 +4,30 @@
|
|||
* or structural check confirms no controller-facing symbol is reachable
|
||||
* from the simulation driver's compiled object" -- plus the AC's own
|
||||
* "confirmed by the struct definition itself, not runtime checks alone"
|
||||
* clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h
|
||||
* together (task 11.1 + task 11.2's sibling file + task 11.5's sibling
|
||||
* file, the --sim-output writer + task 12.1's sibling files, the
|
||||
* interactive REPL) -- NOT a re-audit of core.c (task 4.1's own territory,
|
||||
* test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared
|
||||
* normal-mode infra, out of this task's boundary). sim_verify.c (task 11.4)
|
||||
* is likewise out of scope here -- it is never compiled into
|
||||
* clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h/
|
||||
* repl_break.c/repl_info.c/repl_internal.h together (task 11.1 + task
|
||||
* 11.2's sibling file + task 11.5's sibling file, the --sim-output writer +
|
||||
* task 12.1's REPL files + task 12.2's own repl_break.c/repl_info.c/
|
||||
* repl_internal.h, its 800-line-cap split of repl.c -- see repl_internal.h's
|
||||
* own "Why a split at all" section) -- NOT a re-audit of core.c (task 4.1's
|
||||
* own territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c
|
||||
* (shared normal-mode infra, out of this task's boundary). sim_verify.c
|
||||
* (task 11.4) is likewise out of scope here -- it is never compiled into
|
||||
* sched_sim_isolation_lib below and carries no isolation-sensitive logic of
|
||||
* its own (pure struct comparison, no I/O); extending this guard to it was
|
||||
* not part of task 11.4's own text and is left as-is. repl.c/repl.h are
|
||||
* added here per task 12.1's own text (extending this exact 3-file
|
||||
* precedent to a 5-file one) -- the REPL reads/writes stdin/stdout and
|
||||
* touches xce_sched_sim_driver_t directly, the same category of
|
||||
* driver-level code this guard already covers.
|
||||
* not part of task 11.4's own text and is left as-is. repl.c/repl.h were
|
||||
* added per task 12.1's own text (extending this exact 3-file precedent to
|
||||
* a 5-file one); repl_break.c/repl_info.c/repl_internal.h extend it again
|
||||
* (task 12.2, to an 8-file one) -- all touch xce_sched_sim_driver_t
|
||||
* directly or read/write stdin/stdout, the same category of driver-level
|
||||
* code this guard already covers.
|
||||
*
|
||||
* Three layers, extending test_core_isolation.c's two-layer pattern (same
|
||||
* genre: source-text scans of the real, current on-disk files, not a frozen
|
||||
* snapshot):
|
||||
*
|
||||
* 1. Source-text scan of sim.h/sim.c/sim_loop.c/sim_record.c/repl.c/
|
||||
* repl.h: no #include of
|
||||
* repl.h/repl_break.c/repl_info.c/repl_internal.h: no #include of
|
||||
* client.h/netio.h/xceapi.h/delay.h/idgen.h/xce/cluster.h; no call-site
|
||||
* substring matching a symbol exported only by those libraries (the
|
||||
* exact FORBIDDEN_SYMBOLS list test_core_isolation.c already
|
||||
|
|
@ -97,9 +100,21 @@
|
|||
#error \
|
||||
"XCE_SIM_REPL_H_PATH must be defined by meson.build to repl.h's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SIM_REPL_BREAK_C_PATH
|
||||
#error \
|
||||
"XCE_SIM_REPL_BREAK_C_PATH must be defined by meson.build to repl_break.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SIM_REPL_INFO_C_PATH
|
||||
#error \
|
||||
"XCE_SIM_REPL_INFO_C_PATH must be defined by meson.build to repl_info.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SIM_REPL_INTERNAL_H_PATH
|
||||
#error \
|
||||
"XCE_SIM_REPL_INTERNAL_H_PATH must be defined by meson.build to repl_internal.h's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SIM_LIB_PATH
|
||||
#error \
|
||||
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c's isolated static_library() archive"
|
||||
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c+repl_break.c+repl_info.c's isolated static_library() archive"
|
||||
#endif
|
||||
|
||||
/* Forbidden headers (design.md "Allowed Dependencies", same list
|
||||
|
|
@ -369,12 +384,15 @@ void tearDown(void) { return; }
|
|||
* failure mode this whole test exists to avoid). */
|
||||
static void test_sim_source_paths_are_readable(void)
|
||||
{
|
||||
char* h = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* h = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* repl_break_c = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH);
|
||||
char* repl_info_c = read_whole_file(XCE_SIM_REPL_INFO_C_PATH);
|
||||
char* repl_int_h = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH);
|
||||
|
|
@ -382,6 +400,9 @@ static void test_sim_source_paths_are_readable(void)
|
|||
TEST_ASSERT_NOT_NULL_MESSAGE(record, XCE_SIM_RECORD_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(repl_c, XCE_SIM_REPL_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(repl_h, XCE_SIM_REPL_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(repl_break_c, XCE_SIM_REPL_BREAK_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(repl_info_c, XCE_SIM_REPL_INFO_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(repl_int_h, XCE_SIM_REPL_INTERNAL_H_PATH);
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
|
|
@ -389,22 +410,31 @@ static void test_sim_source_paths_are_readable(void)
|
|||
free(record);
|
||||
free(repl_c);
|
||||
free(repl_h);
|
||||
free(repl_break_c);
|
||||
free(repl_info_c);
|
||||
free(repl_int_h);
|
||||
}
|
||||
|
||||
static void test_sim_never_includes_forbidden_headers(void)
|
||||
{
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH);
|
||||
char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH);
|
||||
char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH);
|
||||
char* h;
|
||||
char* c;
|
||||
char* loop;
|
||||
char* record;
|
||||
char* repl_c;
|
||||
char* repl_h;
|
||||
char* repl_break_c;
|
||||
char* repl_info_c;
|
||||
char* repl_int_h;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h_raw);
|
||||
TEST_ASSERT_NOT_NULL(c_raw);
|
||||
|
|
@ -412,19 +442,28 @@ static void test_sim_never_includes_forbidden_headers(void)
|
|||
TEST_ASSERT_NOT_NULL(record_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_h_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_break_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_info_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_int_h_raw);
|
||||
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
record = strip_c_comments(record_raw);
|
||||
repl_c = strip_c_comments(repl_c_raw);
|
||||
repl_h = strip_c_comments(repl_h_raw);
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
record = strip_c_comments(record_raw);
|
||||
repl_c = strip_c_comments(repl_c_raw);
|
||||
repl_h = strip_c_comments(repl_h_raw);
|
||||
repl_break_c = strip_c_comments(repl_break_c_raw);
|
||||
repl_info_c = strip_c_comments(repl_info_c_raw);
|
||||
repl_int_h = strip_c_comments(repl_int_h_raw);
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(loop);
|
||||
TEST_ASSERT_NOT_NULL(record);
|
||||
TEST_ASSERT_NOT_NULL(repl_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_h);
|
||||
TEST_ASSERT_NOT_NULL(repl_break_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_info_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_int_h);
|
||||
|
||||
assert_no_forbidden_includes(h, "sim.h");
|
||||
assert_no_forbidden_includes(c, "sim.c");
|
||||
|
|
@ -432,6 +471,9 @@ static void test_sim_never_includes_forbidden_headers(void)
|
|||
assert_no_forbidden_includes(record, "sim_record.c");
|
||||
assert_no_forbidden_includes(repl_c, "repl.c");
|
||||
assert_no_forbidden_includes(repl_h, "repl.h");
|
||||
assert_no_forbidden_includes(repl_break_c, "repl_break.c");
|
||||
assert_no_forbidden_includes(repl_info_c, "repl_info.c");
|
||||
assert_no_forbidden_includes(repl_int_h, "repl_internal.h");
|
||||
|
||||
free(h_raw);
|
||||
free(c_raw);
|
||||
|
|
@ -439,28 +481,40 @@ static void test_sim_never_includes_forbidden_headers(void)
|
|||
free(record_raw);
|
||||
free(repl_c_raw);
|
||||
free(repl_h_raw);
|
||||
free(repl_break_c_raw);
|
||||
free(repl_info_c_raw);
|
||||
free(repl_int_h_raw);
|
||||
free(h);
|
||||
free(c);
|
||||
free(loop);
|
||||
free(record);
|
||||
free(repl_c);
|
||||
free(repl_h);
|
||||
free(repl_break_c);
|
||||
free(repl_info_c);
|
||||
free(repl_int_h);
|
||||
}
|
||||
|
||||
static void test_sim_never_calls_forbidden_symbols(void)
|
||||
{
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* h_raw = read_whole_file(XCE_SIM_H_PATH);
|
||||
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
|
||||
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
|
||||
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
|
||||
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
|
||||
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
|
||||
char* repl_break_c_raw = read_whole_file(XCE_SIM_REPL_BREAK_C_PATH);
|
||||
char* repl_info_c_raw = read_whole_file(XCE_SIM_REPL_INFO_C_PATH);
|
||||
char* repl_int_h_raw = read_whole_file(XCE_SIM_REPL_INTERNAL_H_PATH);
|
||||
char* h;
|
||||
char* c;
|
||||
char* loop;
|
||||
char* record;
|
||||
char* repl_c;
|
||||
char* repl_h;
|
||||
char* repl_break_c;
|
||||
char* repl_info_c;
|
||||
char* repl_int_h;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h_raw);
|
||||
TEST_ASSERT_NOT_NULL(c_raw);
|
||||
|
|
@ -468,19 +522,28 @@ static void test_sim_never_calls_forbidden_symbols(void)
|
|||
TEST_ASSERT_NOT_NULL(record_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_h_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_break_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_info_c_raw);
|
||||
TEST_ASSERT_NOT_NULL(repl_int_h_raw);
|
||||
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
record = strip_c_comments(record_raw);
|
||||
repl_c = strip_c_comments(repl_c_raw);
|
||||
repl_h = strip_c_comments(repl_h_raw);
|
||||
h = strip_c_comments(h_raw);
|
||||
c = strip_c_comments(c_raw);
|
||||
loop = strip_c_comments(loop_raw);
|
||||
record = strip_c_comments(record_raw);
|
||||
repl_c = strip_c_comments(repl_c_raw);
|
||||
repl_h = strip_c_comments(repl_h_raw);
|
||||
repl_break_c = strip_c_comments(repl_break_c_raw);
|
||||
repl_info_c = strip_c_comments(repl_info_c_raw);
|
||||
repl_int_h = strip_c_comments(repl_int_h_raw);
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(loop);
|
||||
TEST_ASSERT_NOT_NULL(record);
|
||||
TEST_ASSERT_NOT_NULL(repl_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_h);
|
||||
TEST_ASSERT_NOT_NULL(repl_break_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_info_c);
|
||||
TEST_ASSERT_NOT_NULL(repl_int_h);
|
||||
|
||||
assert_no_forbidden_symbols(h, "sim.h");
|
||||
assert_no_forbidden_symbols(c, "sim.c");
|
||||
|
|
@ -488,6 +551,9 @@ static void test_sim_never_calls_forbidden_symbols(void)
|
|||
assert_no_forbidden_symbols(record, "sim_record.c");
|
||||
assert_no_forbidden_symbols(repl_c, "repl.c");
|
||||
assert_no_forbidden_symbols(repl_h, "repl.h");
|
||||
assert_no_forbidden_symbols(repl_break_c, "repl_break.c");
|
||||
assert_no_forbidden_symbols(repl_info_c, "repl_info.c");
|
||||
assert_no_forbidden_symbols(repl_int_h, "repl_internal.h");
|
||||
|
||||
free(h_raw);
|
||||
free(c_raw);
|
||||
|
|
@ -495,12 +561,18 @@ static void test_sim_never_calls_forbidden_symbols(void)
|
|||
free(record_raw);
|
||||
free(repl_c_raw);
|
||||
free(repl_h_raw);
|
||||
free(repl_break_c_raw);
|
||||
free(repl_info_c_raw);
|
||||
free(repl_int_h_raw);
|
||||
free(h);
|
||||
free(c);
|
||||
free(loop);
|
||||
free(record);
|
||||
free(repl_c);
|
||||
free(repl_h);
|
||||
free(repl_break_c);
|
||||
free(repl_info_c);
|
||||
free(repl_int_h);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
@ -544,15 +616,14 @@ static void test_sim_driver_struct_has_no_forbidden_fields(void)
|
|||
|
||||
/*
|
||||
* test_sim_object_has_no_forbidden_undefined_symbols - the literal
|
||||
* build-time/link-time proof: sim.c + sim_loop.c + sim_record.c, compiled
|
||||
* together in isolation into sched_sim_isolation_lib (meson.build), must
|
||||
* not reference
|
||||
* (as an undefined external symbol in the compiled archive) anything
|
||||
* matching the forbidden prefixes above. Independent of what any header
|
||||
* transitively declares -- reflects only what this driver's compiled code
|
||||
* actually calls. Same technique and same any_undefined_seen
|
||||
* vacuous-pass guard as test_core_isolation.c's
|
||||
* test_core_object_has_no_forbidden_undefined_symbols().
|
||||
* build-time/link-time proof: sim.c + sim_loop.c + sim_record.c + repl.c +
|
||||
* repl_break.c + repl_info.c, compiled together in isolation into
|
||||
* sched_sim_isolation_lib (meson.build), must not reference (as an
|
||||
* undefined external symbol in the compiled archive) anything matching the
|
||||
* forbidden prefixes above. Independent of what any header transitively
|
||||
* declares -- reflects only what this driver's compiled code actually
|
||||
* calls. Same technique and same any_undefined_seen vacuous-pass guard as
|
||||
* test_core_isolation.c's test_core_object_has_no_forbidden_undefined_symbols().
|
||||
*/
|
||||
static void test_sim_object_has_no_forbidden_undefined_symbols(void)
|
||||
{
|
||||
|
|
@ -575,8 +646,9 @@ static void test_sim_object_has_no_forbidden_undefined_symbols(void)
|
|||
/* posix format for an undefined symbol: "<name> U" (no
|
||||
* value/size columns). Skip anything that doesn't parse as
|
||||
* exactly that -- e.g. a blank line, or an archive member header
|
||||
* some nm versions print (this archive has four member objects,
|
||||
* sim.c.o, sim_loop.c.o, sim_record.c.o, and repl.c.o). */
|
||||
* some nm versions print (this archive has six member objects,
|
||||
* sim.c.o, sim_loop.c.o, sim_record.c.o, repl.c.o, repl_break.c.o,
|
||||
* and repl_info.c.o). */
|
||||
if (sscanf(line, "%511s %c", symbol, &type) != 2) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@
|
|||
* already establish.
|
||||
*
|
||||
* White-box #include of the same TU set test_sim_verify.c uses (see that
|
||||
* file's own header comment) PLUS ../src/recording_session.c (the
|
||||
* scheduler_t-free session-accumulator entry points sim_loop.c now calls)
|
||||
* PLUS the new ../src/sim_record.c (the code under test).
|
||||
* file's own header comment, now including task 12.1's repl.c too) PLUS
|
||||
* ../src/recording_session.c (the scheduler_t-free session-accumulator
|
||||
* entry points sim_loop.c now calls) PLUS the new ../src/sim_record.c (the
|
||||
* code under test).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
|
@ -72,6 +73,9 @@
|
|||
#include "../src/plugins/builtin_fcfs.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@
|
|||
*
|
||||
* White-box #include of the same TU set test_sim_event_loop.c uses (see
|
||||
* that file's own header comment, including task 11.5's
|
||||
* recording_session.c/sim_record.c -- required at link time by
|
||||
* sched_sim_driver_run()'s unconditional externs regardless of whether
|
||||
* --sim-output is ever set in a given test) PLUS ../src/sim_verify.c (the
|
||||
* code under test).
|
||||
* recording_session.c/sim_record.c AND task 12.1's repl.c -- all required at
|
||||
* link time by sched_sim_driver_run()'s/repl.h's unconditional externs
|
||||
* regardless of whether --sim-output/--interactive is ever set in a given
|
||||
* test) PLUS ../src/sim_verify.c (the code under test).
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
|
|
@ -93,6 +93,9 @@
|
|||
#include "../src/plugins/builtin_fcfs.c"
|
||||
#include "../src/recording.c"
|
||||
#include "../src/recording_session.c"
|
||||
#include "../src/repl.c"
|
||||
#include "../src/repl_break.c"
|
||||
#include "../src/repl_info.c"
|
||||
#include "../src/sim.c"
|
||||
#include "../src/sim_loop.c"
|
||||
#include "../src/sim_record.c"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue