feat(scheduler-sim-decision-trace): trace entry sequence and line budget

Task 1.4: requirement path push/pop with balanced begin/end entries
carrying the full nesting path, one entry per traced event kind, and a
line budget that stops storing at the bound and counts omitted lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-08 08:05:44 +00:00
parent 383e2a90e4
commit 4f951f2b52
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
5 changed files with 1567 additions and 2 deletions

View file

@ -38,8 +38,12 @@
* Produced by xce_alloc_plan_iter_t.next(); the returned pointer is valid
* only until the following next() or fini() call on that same iterator
* (it is not independently heap-owned by the caller).
*
* Tagged so sched/trace.h can forward-declare it without including this
* header (which pulls in scheduler.h -> core.h, and core.h will carry the
* trace record pointer).
*/
typedef struct {
typedef struct xce_alloc_plan_s {
int nbuckets; /* number of hosts this plan needs (==
* len(bucket_slots)) */
int* bucket_slots; /* partition of maxTasks; bucket_slots[i] = slots

View file

@ -26,6 +26,13 @@
#include <stddef.h>
#include <stdint.h>
#include "resreq.h"
#include "sched/types.h"
/* Defined in sched/alloc_iter.h, which is not included here: it pulls in
* scheduler.h and so core.h. */
typedef struct xce_alloc_plan_s xce_alloc_plan_t;
/* Requirement 1.8: two detail levels; summary is the default. */
typedef enum {
XCE_TRACE_LEVEL_SUMMARY = 0,
@ -130,6 +137,140 @@ typedef struct {
int omitted_lines;
} sched_trace_counters_t;
/*
* Entry types (design.md "Logical Data Model"). Every stored entry renders
* as exactly one line in the budgeted sections (requirement, cluster-wide
* check, plan and host), so the number of stored entries is the number of
* budgeted lines. The check rows, host rows, search-end, position-summary
* and rollback entries are added by the tasks that capture them.
*/
typedef enum {
SCHED_TRACE_E_RESREQ_BEGIN = 0, /* requirement named by the path */
SCHED_TRACE_E_RESREQ_END, /* its outcome; one per begin */
SCHED_TRACE_E_ALT_SKIPPED, /* alternative with no content (3.6) */
SCHED_TRACE_E_ALT_RESULT, /* chosen alternative, or none (3.4) */
SCHED_TRACE_E_MEMBER_FAILED, /* merge failed after member j/m (3.3) */
SCHED_TRACE_E_ATTEMPT_REASONS, /* failure reasons of an attempt (3.5) */
SCHED_TRACE_E_CHECK_END, /* cluster-wide check result (4.8) */
SCHED_TRACE_E_NO_PLAN, /* no plan generated, and why (4.7) */
SCHED_TRACE_E_PLAN_BEGIN, /* plan tried (4.3) */
SCHED_TRACE_E_PLAN_DISCARDED, /* plan generated, discarded (4.9) */
SCHED_TRACE_E_PLAN_NOT_TRIED, /* plan generated, internal error (4.4)*/
SCHED_TRACE_E_PLAN_END, /* plan result (4.4) */
SCHED_TRACE_E_POS_BEGIN, /* plan position (4.4) */
SCHED_TRACE_E_POS_END, /* filled / unfilled / scan error /
* internal (4.4) */
SCHED_TRACE_E_HOST, /* host verdict (5.1-5.7, 5.10) */
SCHED_TRACE_E_COMMIT, /* committed plan (4.5) */
SCHED_TRACE_E_FINALIZE_FAILED, /* allocation build failed (4.5) */
SCHED_TRACE_E_NTYPES
} sched_trace_entry_type_t;
/* Where a host verdict came from (5.2). Only the selection, shortfall and
* limit kinds carry a phase; the other kinds use NONE. */
typedef enum {
SCHED_TRACE_PHASE_NONE = 0,
SCHED_TRACE_PHASE_PREFILTER, /* host-selection pre-filter */
SCHED_TRACE_PHASE_PERHOST /* per-host resource and limit check */
} sched_trace_phase_t;
/* One requested resource of a single requirement or member (3.2). */
typedef struct {
const char* name; /* arena copy */
float value;
int per; /* rusage_per_type_t */
} sched_trace_req_t;
/* One pend-reason code of an attempt's failure reasons and how often it
* occurred (3.5). */
typedef struct {
int code;
int count;
} sched_trace_reason_count_t;
/*
* One stored entry. All pointers point into the record's arena. `depth` is
* the nesting level the renderer indents by: a requirement, plan or
* position begin is stored at the current level and opens the next one;
* its end closes it and is stored at the begin's level. `path` is the full
* requirement path in effect when the entry was made (3.1).
*/
typedef struct sched_trace_entry_s {
struct sched_trace_entry_s* next;
sched_trace_entry_type_t type;
int depth;
sched_trace_path_t path;
union {
struct { /* RESREQ_BEGIN */
bool container; /* COMPOUND or ALT: no primary */
int nmembers; /* container only */
int min_tasks;
int max_tasks;
int nreq;
sched_trace_req_t* req;
} resreq_begin;
struct { /* RESREQ_END */
int rc; /* > 0 satisfied, 0 not, < 0 error */
int not_tried_after; /* members after this one, not tried */
} resreq_end;
struct { /* ALT_SKIPPED, ALT_RESULT (i == 0: none chosen) */
int i;
int n;
} alt;
struct { /* MEMBER_FAILED */
int j;
int m;
int err;
} member_failed;
struct { /* ATTEMPT_REASONS */
int ncodes;
sched_trace_reason_count_t* codes;
int nhosts;
const char** hosts;
int nres;
const char** res;
} reasons;
struct { /* CHECK_END */
int rc;
bool plans_follow;
} check_end;
struct { /* NO_PLAN */
xce_sched_trace_noplan_t why;
} no_plan;
struct { /* PLAN_BEGIN, PLAN_DISCARDED, PLAN_NOT_TRIED */
int plan_no; /* 1-based, in generation order per requirement */
int nbuckets; /* hosts the plan needs */
int* slots; /* tasks per position, nbuckets of them */
int err; /* PLAN_NOT_TRIED only */
} plan;
struct { /* PLAN_END */
int plan_no;
int rc;
int failed_pos; /* the position that could not be filled */
} plan_end;
struct { /* POS_BEGIN */
int pos;
int slots;
} pos_begin;
struct { /* POS_END */
int pos;
xce_sched_trace_pos_end_t how;
const char* host; /* filled: the accepted host */
int err; /* scan error */
} pos_end;
struct { /* HOST */
const char* host;
unsigned verdicts; /* xce_sched_trace_verdict_t bits */
sched_trace_phase_t phase;
int err;
} host;
struct { /* COMMIT, FINALIZE_FAILED */
int plan_no; /* the last plan begun */
int err; /* FINALIZE_FAILED only */
} commit;
} u;
} sched_trace_entry_t;
/* One arena chunk; the record frees the whole chain at once. */
typedef struct sched_trace_chunk_s {
struct sched_trace_chunk_s* next;
@ -162,6 +303,27 @@ struct xce_sched_trace_rec_s {
sched_trace_counters_t counters;
/* stored entries, in the order made; at most line_bound of them */
sched_trace_entry_t* entries;
sched_trace_entry_t* entries_tail;
int nentries;
/* builder state (allocator thread only, while the record is built) */
int depth; /* nesting level of the next entry */
int path_overflow; /* pushes beyond the two path levels, not kept */
int resreq_open; /* requirement begins without their end yet */
int plan_no; /* plans generated so far in this requirement */
int cur_plan_no; /* the last plan begun */
int cur_pos; /* the position begun last */
int scan_err; /* pos_scan_error() for the current position */
bool scan_err_set;
unsigned host_row_verdicts; /* SHORTFALL/LIMIT from the current host's
* unmet rows (set by the row capture) */
/* scope pointers, kept only until the matching scope ends and never
* dereferenced after it (TraceCapture's limit lookup) */
const xce_sched_obj_t* scope_obj;
const xce_sched_worker_t* scope_w;
/* per-record arena */
sched_trace_chunk_t* arena;
size_t arena_bytes; /* sum of chunk sizes, headers included */
@ -203,4 +365,93 @@ extern void* sched_trace_arena_memdup(xce_sched_trace_rec_t* rec,
extern char* sched_trace_arena_strdup(xce_sched_trace_rec_t* rec,
const char* s);
/*
* Entry sequence (design.md "TraceModel"). `rec` is never NULL (the
* SCHED_TRACE gate guarantees it). Calls nest in the order the allocator
* makes them: every path_push has one path_pop and every *_begin one
* *_end. Entries past rec->line_bound are not stored and each adds one to
* counters.omitted_lines (9.1, 9.2); the builder state (path, nesting,
* plan numbers, scope pointers) is kept up to date either way.
*/
/* Requirement path: SINGLE 1/1, COMPOUND 1/1 or ALT i/n as the container
* level, MEMBER j/m below it. Not an entry. */
extern void sched_trace_path_push(xce_sched_trace_rec_t* rec,
xce_sched_trace_path_kind_t kind, int i,
int n);
extern void sched_trace_path_pop(xce_sched_trace_rec_t* rec);
/* Begin and end of the requirement the current path names. `primary` is
* NULL for a container, whose `nmembers` is its member count. */
extern void sched_trace_resreq_begin(xce_sched_trace_rec_t* rec,
const xce_primary_resreq_t* primary,
int nmembers);
extern void sched_trace_resreq_end(xce_sched_trace_rec_t* rec, int rc,
int not_tried_after);
/* Merge failure after member j of m: members j+1..m not tried (3.3). */
extern void sched_trace_member_failed(xce_sched_trace_rec_t* rec, int j,
int m, int err);
/* Alternative i/n chosen, later ones not tried (3.4); 0: all failed. */
extern void sched_trace_alt_result(xce_sched_trace_rec_t* rec, int chosen_i,
int n);
/* Alternative i/n skipped: it has no requirement content (3.6). */
extern void sched_trace_alt_skipped(xce_sched_trace_rec_t* rec, int i,
int n);
/* Failure reasons of a compound attempt: per-code counts and the hosts and
* resources they name, all copied (3.5). TraceCapture builds the arrays
* from the attempt's pend-reason set. */
extern void
sched_trace_attempt_reasons(xce_sched_trace_rec_t* rec,
const sched_trace_reason_count_t* codes,
int ncodes, const char* const* hosts, int nhosts,
const char* const* res, int nres);
/* Cluster-wide check scope. check_begin stores no entry; check_end stores
* the result, which on failure says no placement plans were tried (4.8). */
extern void sched_trace_check_begin(xce_sched_trace_rec_t* rec,
const xce_sched_obj_t* obj);
extern void sched_trace_check_end(xce_sched_trace_rec_t* rec, int rc,
bool plans_follow);
extern void sched_trace_no_plan(xce_sched_trace_rec_t* rec,
xce_sched_trace_noplan_t why);
/* Plans, numbered in generation order within the current requirement. */
extern void sched_trace_plan_begin(xce_sched_trace_rec_t* rec,
const xce_alloc_plan_t* plan);
extern void sched_trace_plan_discarded(xce_sched_trace_rec_t* rec,
const xce_alloc_plan_t* plan);
extern void sched_trace_plan_not_tried(xce_sched_trace_rec_t* rec,
const xce_alloc_plan_t* plan, int err);
extern void sched_trace_plan_end(xce_sched_trace_rec_t* rec, int rc,
int failed_pos);
/* Plan positions. A pos_scan_error() stores no entry: it turns the
* position's following UNFILLED end into SCAN_ERROR with that error. */
extern void sched_trace_pos_begin(xce_sched_trace_rec_t* rec, int pos,
int slots);
extern void sched_trace_pos_end(xce_sched_trace_rec_t* rec,
xce_sched_trace_pos_end_t how,
const char* host);
extern void sched_trace_pos_scan_error(xce_sched_trace_rec_t* rec, int err);
/*
* Per-host scope: host_begin stores no entry; host_verdict stores the
* host's verdict and ends the scope. An explicit SELECT verdict comes from
* the host-selection pre-filter. `verdicts == 0` means "from the rows": the
* SHORTFALL/LIMIT bits the current host's unmet rows set, or SELECT when
* there were none (the per-host check's own selection branch); either way
* the phase is the per-host check (5.2).
*/
extern void sched_trace_host_begin(xce_sched_trace_rec_t* rec,
const xce_sched_obj_t* obj,
const xce_sched_worker_t* w);
extern void sched_trace_host_verdict(xce_sched_trace_rec_t* rec,
unsigned verdicts, int err);
/* The plan begun last was committed, or building its allocation failed. */
extern void sched_trace_commit(xce_sched_trace_rec_t* rec);
extern void sched_trace_finalize_failed(xce_sched_trace_rec_t* rec, int err);
#endif