diff --git a/.kiro/specs/scheduler-sim-decision-trace/tasks.md b/.kiro/specs/scheduler-sim-decision-trace/tasks.md index 9e484358..acb73005 100644 --- a/.kiro/specs/scheduler-sim-decision-trace/tasks.md +++ b/.kiro/specs/scheduler-sim-decision-trace/tasks.md @@ -90,7 +90,7 @@ - Done when capture tests over a fixture core and job pass under ASAN, showing exact copied values that do not change when the source structures change later. - _Requirements: 2.2, 2.3, 3.5_ -- [ ] 2.2 Capture shortfall and limit rows from the satisfaction check, test first +- [x] 2.2 Capture shortfall and limit rows from the satisfaction check, test first - Each checked row carries the resource name, flags, value type, total, in-use, provisionally reserved, requested and met. - A row whose resource is flagged as a limit is a limit row. It is resolved to its limit through the scope's matched limits and carries the limit name, the scope instance (plus the host for host scope) and the limited resource's name. - The cluster-wide scope keeps every row. A host scope keeps only unmet rows, and these set the host's shortfall and/or limit verdict. @@ -347,3 +347,5 @@ - 1.7 OPEN for 5.3: the sim decision-free sites at sim_loop.c ~1327 (reconstructed dispatch) and ~1709 (drain loop) don't yet call `sched_trace_rec_free(d->trace)`, and the TRACE op still falls to the sim's switch defaults. 5.3 must add the early TRACE branch before verify/record/breakpoint, or sim_verify reports false mismatches and breakpoints count it as "NONE". decision.h forward-declares `struct xce_sched_trace_rec_s` rather than including sched/trace.h. - 1.8: `core->trace_ctl` and `core->trace_rec` are plain forward-declared pointers (core.h does not include sched/trace.h), and `xce_sched_core_fini()` clears them without freeing because core.c can't link the trace sources. test_trace_hooks_gate.c counts `sched_trace_rec_new` with a link-time wrap. Its "full allocation creates no record" check only becomes meaningful once 3.1 adds the per-job hook. The 11.3 check (non-interactive sim never publishes trace_ctl) is in 5.3. - 2.1: capture reads struct fields only and calls no scheduler functions, so the white-box tests need no extra sources. Policy name = ctl policy_name, else core->job_pick_plugin; the queue picker override is ignored because election always uses the core picker. Cluster-wide limit scope is stored as "u=..,q=..,p=.." ("" when unscoped); 2.2 should reuse split_vres_name/copy_scope and append "h=". 4.1 must render selection_set/glimits_set false as "unavailable" and an ATTEMPT_REASONS entry with ncodes==0. +- 2.2: rows go through `sched_trace_row()`, a model addition not named in the design, as CHECK_ROW (cluster-wide check, all rows) or HOST_ROW (host scope, unmet rows only). Host rows are held until the host verdict is stored and then stored one level deeper, each counting against the budget; they are dropped at summary level, where the verdict bits and limit tally are still set. Rows arriving outside an open check (`rec->check_open`) or a host scope are dropped, so 3.3/3.4 must open a host scope before host_iter's pre-filter calls `sched_check_host_satisfy` and must call `sched_trace_check_end` on every exit path of the cluster-wide check. 4.1 must render a NULL row name by its index, `limited_res == NULL` as a raw amount, and HOST_ROW at host depth + 1. Capture now calls librtti `vget()`, which every test already links. +- 2.2 OPEN for 6.2: if `limit_row()` runs out of memory, `sched_trace_check_rows` returns early, so that row's LIMIT bit and later rows' SHORTFALL bits are lost. The verdict derived from rows then becomes SELECT and the Req 9.3 counts are off. This needs fault injection to test, so fix it in 6.2: keep setting the verdict bits and continue. diff --git a/src/scheduler/include/sched/trace.h b/src/scheduler/include/sched/trace.h index 475edf38..76099ddf 100644 --- a/src/scheduler/include/sched/trace.h +++ b/src/scheduler/include/sched/trace.h @@ -157,8 +157,7 @@ typedef struct { * 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 and host rows are added by the task that - * captures them (TraceCapture). + * budgeted lines. */ typedef enum { SCHED_TRACE_E_RESREQ_BEGIN = 0, /* requirement named by the path */ @@ -184,6 +183,8 @@ typedef enum { SCHED_TRACE_E_POS_SUMMARY, /* summary level: the position end with * its verdict counts (5.9) */ SCHED_TRACE_E_ROLLBACK, /* released hosts, merged (5.8) */ + SCHED_TRACE_E_CHECK_ROW, /* cluster-wide check row (4.1, 4.2) */ + SCHED_TRACE_E_HOST_ROW, /* unmet row under a host (5.4, 5.5) */ SCHED_TRACE_E_NTYPES } sched_trace_entry_type_t; @@ -222,6 +223,32 @@ typedef struct { const char* scope; /* arena copy */ } sched_trace_limit_ref_t; +/* + * One row of the satisfaction check (resource.c unsatisfy_()): one + * requested resource compared against its total. `met` is the check's own + * verdict, !(total < inuse + reserved + requested). A row whose resource + * has XCE_RESF_LIMIT is a limit row: `limit`, `scope` and `limited_res` + * name the limit, the scope instance it applies to ("u=..,q=..,p=..", with + * ",h=" or "h=" appended in a host scope) and the resource it + * limits (NULL when no matched limit names it). Every other row is a + * resource row, its limit fields NULL. Stored strings are arena copies. + */ +typedef struct { + const char* res; /* resource name; NULL when the index is unknown */ + int idx; /* resource index */ + int flags; /* XCE_RESF_* */ + int valtype; + float total; + float inuse; + float reserved; /* provisionally reserved by earlier parts of + * this job's evaluation (uncommitted) */ + float requested; + bool met; + const char* limit; /* limit rows only */ + const char* scope; /* limit rows only */ + const char* limited_res; /* limit rows only; NULL: unknown */ +} sched_trace_row_t; + /* One requested resource of a single requirement or member (3.2). */ typedef struct { const char* name; /* arena copy */ @@ -335,6 +362,7 @@ typedef struct sched_trace_entry_s { sched_trace_limit_tally_t* limits; /* hits per limit, in the * order first hit */ } pos_summary; + sched_trace_row_t row; /* CHECK_ROW, HOST_ROW */ struct { /* ROLLBACK: consecutive released hosts, in call order */ int nhosts; int cap; @@ -404,6 +432,13 @@ struct xce_sched_trace_rec_s { bool scan_err_set; unsigned host_row_verdicts; /* SHORTFALL/LIMIT from the current host's * unmet rows (set by the row capture) */ + bool check_open; /* between check_begin and check_end */ + /* the current host's unmet rows (full level), held in builder memory + * (malloc'd, strings strdup'd) until its verdict line is stored, then + * stored after it; dropped when the host scope ends any other way */ + sched_trace_row_t* host_rows; + int host_nrows; + int host_rows_cap; /* plans of the current requirement's search (reset by resreq_begin * and after search_end) */ int scope_tried; @@ -564,6 +599,17 @@ extern void sched_trace_host_verdict(xce_sched_trace_rec_t* rec, extern void sched_trace_host_limit_hit(xce_sched_trace_rec_t* rec, const char* limit, const char* scope); +/* + * One satisfaction check row (2.2), copied. In a host scope (between + * host_begin and host_verdict) it is a HOST_ROW, held until the host's + * verdict line and stored right after it, one level deeper; at summary + * level it is not stored at all. Otherwise it is a CHECK_ROW, stored now. + * Which rows to keep, and the host verdict bits they set, are the caller's + * (TraceCapture's) decision. + */ +extern void sched_trace_row(xce_sched_trace_rec_t* rec, + const sched_trace_row_t* row); + /* One host whose claim a failed plan released. Consecutive calls merge * into one ROLLBACK entry, "released hosts: n01, n02" (5.8). */ extern void sched_trace_rollback_host(xce_sched_trace_rec_t* rec, @@ -602,6 +648,27 @@ extern void sched_trace_capture_selection(xce_sched_trace_rec_t* rec, extern void sched_trace_capture_glimits(xce_sched_trace_rec_t* rec, const xce_sched_obj_t* obj); +/* + * The satisfaction check's rows (4.1, 4.2, 5.4, 5.5), called from + * resource.c unsatisfy_() with its inputs: `nres` rows of resource index, + * total, in-use and requested, and the dense uncommitted vector keyed by + * resource index. Each row's resource is resolved through core->resmgr. + * A limit row (XCE_RESF_LIMIT) is resolved to its limit through the + * scope's matched limits: obj->glimits in the cluster-wide check scope, + * obj->hlimits[w->idx] in a host scope; without a match, the limit name + * and scope come from the resource's own name. The cluster-wide scope + * keeps every row. A host scope keeps only unmet rows: an unmet resource + * row sets the host's SHORTFALL verdict, an unmet limit row its LIMIT + * verdict (via sched_trace_host_limit_hit), never SHORTFALL. Outside both + * scopes nothing is recorded. `core` is only read. + */ +extern void sched_trace_check_rows(xce_sched_trace_rec_t* rec, + xce_sched_core_t* core, int nres, + const int* idxs, const float* total, + const float* inuse, + const xvec_t* uncomitted, + const float* wanted); + /* A failed compound attempt's reasons (3.5), from its pend-reason set before * the scheduler replaces obj->reason: one ATTEMPT_REASONS entry with a * count per pend-reason code (main reason, host and resource reasons, in diff --git a/src/scheduler/src/sched_trace.c b/src/scheduler/src/sched_trace.c index 561cb697..af03ec9b 100644 --- a/src/scheduler/src/sched_trace.c +++ b/src/scheduler/src/sched_trace.c @@ -78,6 +78,20 @@ static void pos_limits_clear(xce_sched_trace_rec_t* rec) rec->pos_nlimits = 0; } +/* Drops the current host's held rows (keeps the array). */ +static void host_rows_clear(xce_sched_trace_rec_t* rec) +{ + int k; + + for (k = 0; k < rec->host_nrows; k++) { + free((void*)rec->host_rows[k].res); + free((void*)rec->host_rows[k].limit); + free((void*)rec->host_rows[k].scope); + free((void*)rec->host_rows[k].limited_res); + } + rec->host_nrows = 0; +} + /* Starts the current position's tallies over. */ static void pos_tally_reset(xce_sched_trace_rec_t* rec) { @@ -234,6 +248,8 @@ void sched_trace_rec_free(xce_sched_trace_rec_t* rec) pos_limits_clear(rec); free(rec->pos_limits); + host_rows_clear(rec); + free(rec->host_rows); c = rec->arena; while (c != NULL) { @@ -545,8 +561,9 @@ void sched_trace_check_begin(xce_sched_trace_rec_t* rec, const xce_sched_obj_t* obj) { rollback_run_end(rec); - rec->scope_obj = obj; - rec->scope_w = NULL; + rec->scope_obj = obj; + rec->scope_w = NULL; + rec->check_open = true; } void sched_trace_check_end(xce_sched_trace_rec_t* rec, int rc, @@ -554,7 +571,8 @@ void sched_trace_check_end(xce_sched_trace_rec_t* rec, int rc, { sched_trace_entry_t* e; - rec->scope_obj = NULL; + rec->scope_obj = NULL; + rec->check_open = false; e = entry_add(rec, SCHED_TRACE_E_CHECK_END); if (e == NULL) { @@ -761,6 +779,7 @@ void sched_trace_host_begin(xce_sched_trace_rec_t* rec, rec->scope_obj = obj; rec->scope_w = w; rec->host_row_verdicts = 0; + host_rows_clear(rec); /* a scope that ended without its verdict */ } /* Counts one host evaluation: decision-wide per verdict kind, and at @@ -786,6 +805,103 @@ static void host_count(xce_sched_trace_rec_t* rec, unsigned verdicts) } } +/* Copies a row's strings into the arena (NULL stays NULL). */ +static void row_to_arena(xce_sched_trace_rec_t* rec, sched_trace_row_t* dst, + const sched_trace_row_t* src) +{ + *dst = *src; + dst->res = sched_trace_arena_strdup(rec, src->res); + dst->limit = sched_trace_arena_strdup(rec, src->limit); + dst->scope = sched_trace_arena_strdup(rec, src->scope); + dst->limited_res = sched_trace_arena_strdup(rec, src->limited_res); +} + +/* Stores the current host's held rows after its verdict line, one level + * below it, then drops them. Each counts against the budget. */ +static void host_rows_flush(xce_sched_trace_rec_t* rec) +{ + int k; + + for (k = 0; k < rec->host_nrows; k++) { + sched_trace_entry_t* e = entry_add(rec, SCHED_TRACE_E_HOST_ROW); + + if (e == NULL) { + continue; + } + e->depth++; + row_to_arena(rec, &e->u.row, &rec->host_rows[k]); + } + host_rows_clear(rec); +} + +/* NULL-preserving heap copy for held rows; sets *failed on failure. */ +static const char* row_strdup(const char* s, bool* failed) +{ + char* d; + + if (s == NULL) { + return NULL; + } + d = strdup(s); + if (d == NULL) { + *failed = true; + } + return d; +} + +void sched_trace_row(xce_sched_trace_rec_t* rec, const sched_trace_row_t* row) +{ + sched_trace_entry_t* e; + sched_trace_row_t* h; + bool failed = false; + + rollback_run_end(rec); + if (row == NULL) { + return; + } + + if (rec->scope_w == NULL) { + /* the cluster-wide check: stored now, before its check-end line */ + e = entry_add(rec, SCHED_TRACE_E_CHECK_ROW); + if (e != NULL) { + row_to_arena(rec, &e->u.row, row); + } + return; + } + + /* a host scope: no host lines at summary level; at full level the row + * waits for the host's verdict line, which it belongs under */ + if ((rec->level == XCE_TRACE_LEVEL_SUMMARY) || rec->incomplete) { + return; + } + if (rec->host_nrows == rec->host_rows_cap) { + int cap = (rec->host_rows_cap > 0) ? rec->host_rows_cap * 2 : 4; + void* p = realloc(rec->host_rows, sizeof(*h) * (size_t)cap); + + if (p == NULL) { + rec->incomplete = true; /* design "Error Strategy" */ + return; + } + rec->host_rows = (sched_trace_row_t*)p; + rec->host_rows_cap = cap; + } + h = &rec->host_rows[rec->host_nrows]; + *h = *row; + h->res = row_strdup(row->res, &failed); + h->limit = row_strdup(row->limit, &failed); + h->scope = row_strdup(row->scope, &failed); + h->limited_res = row_strdup(row->limited_res, &failed); + if (failed) { + free((void*)h->res); + free((void*)h->limit); + free((void*)h->scope); + free((void*)h->limited_res); + rec->incomplete = true; + return; + } + rec->host_nrows++; +} + void sched_trace_host_verdict(xce_sched_trace_rec_t* rec, unsigned verdicts, int err) { @@ -820,17 +936,19 @@ void sched_trace_host_verdict(xce_sched_trace_rec_t* rec, unsigned verdicts, rollback_run_end(rec); if (rec->level == XCE_TRACE_LEVEL_SUMMARY) { + host_rows_clear(rec); return; /* tallied for the position summary, no host line */ } e = entry_add(rec, SCHED_TRACE_E_HOST); - if (e == NULL) { - return; + if (e != NULL) { + e->u.host.host = sched_trace_arena_strdup(rec, name); + e->u.host.verdicts = verdicts; + e->u.host.phase = phase; + e->u.host.err = err; } - e->u.host.host = sched_trace_arena_strdup(rec, name); - e->u.host.verdicts = verdicts; - e->u.host.phase = phase; - e->u.host.err = err; + /* the host's unmet rows, under its verdict line (5.4, 5.5) */ + host_rows_flush(rec); } void sched_trace_commit(xce_sched_trace_rec_t* rec) diff --git a/src/scheduler/src/sched_trace_capture.c b/src/scheduler/src/sched_trace_capture.c index 41c90d81..27606e27 100644 --- a/src/scheduler/src/sched_trace_capture.c +++ b/src/scheduler/src/sched_trace_capture.c @@ -15,9 +15,11 @@ * formatting them is the renderer's job. * * Task 2.1: selection facts (2.2), cluster-wide limits (2.3) and attempt - * failure reasons (3.5). These read struct fields only and call no - * scheduler function, so every white-box test that #includes this file - * links without extra sources. + * failure reasons (3.5). Task 2.2: the satisfaction check's rows (4.1, + * 4.2, 5.4, 5.5). These read struct fields and call no scheduler function + * (the row capture resolves resources with librtti's vget(), which every + * scheduler target links), so every white-box test that #includes this + * file links without extra sources. */ #include #include @@ -200,6 +202,202 @@ void sched_trace_capture_glimits(xce_sched_trace_rec_t* rec, } } +/* ---- satisfaction check rows (4.1, 4.2, 5.4, 5.5) --------------------- */ + +/* The uncommitted amount of resource index `idx`: the same read as + * resource.c uncomit_at_() (the vector is dense, keyed by resource index; + * past its end or NULL means nothing is uncommitted). */ +static float row_uncomit_at(const xvec_t* uncomitted, int idx) +{ + if ((uncomitted == NULL) || (uncomitted->data == NULL) || (idx < 0) + || (idx >= uncomitted->count)) { + return 0.0f; + } + return ((const float*)uncomitted->data)[idx]; +} + +/* The current scope's matched limit list: obj->glimits in the cluster-wide + * check, obj->hlimits[w->idx] in a host scope (sparse: NULL when the + * worker has none). Read directly: zarray_get()/xlist_get() take + * non-const containers. */ +static const xlist_t* scope_limits(const xce_sched_trace_rec_t* rec) +{ + const zarray_t* hl; + int idx; + + if (rec->scope_obj == NULL) { + return NULL; + } + if (rec->scope_w == NULL) { + return &rec->scope_obj->glimits; + } + hl = &rec->scope_obj->hlimits; + idx = rec->scope_w->idx; + if ((hl->data == NULL) || (idx < 0) || (idx >= hl->caps)) { + return NULL; + } + return (const xlist_t*)hl->data[idx]; +} + +/* The matched limit whose vres resource is `r`, or NULL. */ +static const xce_vres_t* scope_vres(const xce_sched_trace_rec_t* rec, + const xce_resource_t* r) +{ + const xlist_t* list = scope_limits(rec); + int i; + + if ((list == NULL) || (list->data == NULL)) { + return NULL; + } + for (i = 0; i < list->count; i++) { + const xce_vres_t* v = (const xce_vres_t*)list->data[i]; + + if ((v == NULL) || (v->vres == NULL)) { + continue; + } + if ((v->vres == r) + || ((v->vres->name != NULL) && (r->name != NULL) + && (strcmp(v->vres->name, r->name) == 0))) { + return v; + } + } + return NULL; +} + +/* + * Fills a limit row's limit name, scope instance and limited resource for + * the limit vres resource `r`, composing the strings in a scratch buffer + * returned through *buf (the caller frees it once the model has copied the + * row). Returns false on allocation failure. + */ +static bool limit_row(const xce_sched_trace_rec_t* rec, const xce_resource_t* r, + sched_trace_row_t* row, char** buf) +{ + const xce_vres_t* v = scope_vres(rec, r); + const char* known = NULL; + const char* host = NULL; + const char* name; + size_t name_len; + const char* scope; + size_t scope_len; + size_t host_len = 0; + char* p; + size_t k; + + if ((v != NULL) && (v->limit != NULL)) { + known = v->limit->name; + } + if (!split_vres_name(r->name, known, &name, &name_len, &scope)) { + /* not a "limit=" name: the resource names the limit itself */ + name = (known != NULL) ? known : r->name; + name_len = (name != NULL) ? strlen(name) : 0; + scope = ""; + } else if (known != NULL) { + name = known; + name_len = strlen(known); + } + if (rec->scope_w != NULL) { + /* host-limit vres names do not encode the host */ + host = (rec->scope_w->name != NULL) ? rec->scope_w->name : ""; + host_len = strlen(host); + } + scope_len = strlen(scope); + + /* "\0[,]h=\0" */ + *buf = (char*)malloc(name_len + scope_len + host_len + 5); + if (*buf == NULL) { + return false; + } + p = *buf; + memcpy(p, (name != NULL) ? name : "", name_len); + p[name_len] = '\0'; + row->limit = p; + p += name_len + 1; + row->scope = p; + for (k = 0; k < scope_len; k++) { + *p++ = (scope[k] == '/') ? ',' : scope[k]; + } + if (host != NULL) { + if (scope_len > 0) { + *p++ = ','; + } + memcpy(p, "h=", 2); + p += 2; + memcpy(p, host, host_len); + p += host_len; + } + *p = '\0'; + + row->limited_res = NULL; + if ((v != NULL) && (v->res != NULL)) { + row->limited_res = v->res->name; + } + return true; +} + +void sched_trace_check_rows(xce_sched_trace_rec_t* rec, xce_sched_core_t* core, + int nres, const int* idxs, const float* total, + const float* inuse, const xvec_t* uncomitted, + const float* wanted) +{ + bool host_scope = (rec->scope_w != NULL); + int i; + + /* a row belongs to the cluster-wide check or to one host's check; + * outside both there is nothing to attribute it to */ + if ((!host_scope && !rec->check_open) || (nres <= 0) || (idxs == NULL) + || (total == NULL) || (inuse == NULL) || (wanted == NULL)) { + return; + } + + for (i = 0; i < nres; i++) { + xce_resource_t* r = NULL; + sched_trace_row_t row; + char* buf = NULL; + float uncomit = row_uncomit_at(uncomitted, idxs[i]); + + memset(&row, 0, sizeof(row)); + row.idx = idxs[i]; + row.total = total[i]; + row.inuse = inuse[i]; + row.reserved = uncomit; + row.requested = wanted[i]; + /* the check's own comparison, resource.c unsatisfy_() */ + row.met = !(total[i] < inuse[i] + uncomit + wanted[i]); + + /* a host scope keeps only the rows that rejected the host */ + if (host_scope && row.met) { + continue; + } + + if ((core != NULL) && (idxs[i] >= 0) + && (vget(core->resmgr, idxs[i], (void**)&r) < 0)) { + r = NULL; + } + if (r != NULL) { + row.res = r->name; + row.flags = r->flags; + row.valtype = r->valtype; + } + + if ((r != NULL) && ((r->flags & XCE_RESF_LIMIT) != 0)) { + if (!limit_row(rec, r, &row, &buf)) { + rec->incomplete = true; /* design "Error Strategy" */ + return; + } + /* a limit rejection, never a shortfall (4.2, 5.5) */ + if (host_scope) { + sched_trace_host_limit_hit(rec, row.limit, row.scope); + } + } else if (host_scope) { + rec->host_row_verdicts |= XCE_TRACE_V_SHORTFALL; /* 5.4 */ + } + + sched_trace_row(rec, &row); + free(buf); + } +} + /* ---- attempt failure reasons (3.5) ------------------------------------ */ static void count_code(sched_trace_reason_count_t* codes, int* ncodes, diff --git a/src/scheduler/testsuite/test_trace_capture.c b/src/scheduler/testsuite/test_trace_capture.c index 276874c0..6fa45523 100644 --- a/src/scheduler/testsuite/test_trace_capture.c +++ b/src/scheduler/testsuite/test_trace_capture.c @@ -13,8 +13,22 @@ * structures afterwards leaves the record unchanged (design "Invariants": * a record never references scheduler-owned memory). * + * Task 2.2 covers the satisfaction check's rows (Requirements 4.1, 4.2, + * 5.4, 5.5), over a fixture core whose resmgr registers plain resources and + * limit vres resources, with a cluster-wide limit in the schedobj's + * glimits and a host-scoped limit in its hlimits for the test worker: + * - every row copies the resource name, flags, value type, total, + * in-use, provisionally reserved, requested and met; + * - a row whose resource has XCE_RESF_LIMIT is a limit row, resolved + * through the scope's matched limits to (limit name, scope instance, + * limited resource), "h=" appended in a host scope; it is never + * reported as a shortfall; + * - the cluster-wide scope keeps every row; a host scope keeps only the + * unmet rows, stored after the host's verdict line, and they set its + * SHORTFALL and/or LIMIT verdict. + * * Links src/sched_trace.c and src/sched_trace_capture.c, plus libxce for - * the pend-reason helpers. + * the pend-reason, resmgr and container helpers. */ #include #include @@ -23,9 +37,13 @@ #include #include "core.h" +#include "kvmgr.h" #include "preason.h" +#include "resobj.h" #include "sched/trace.h" #include "xlist.h" +#include "xstring.h" +#include "zarray.h" void setUp(void) { return; } void tearDown(void) { return; } @@ -486,8 +504,528 @@ static void test_attempt_reasons_counts_against_the_budget(void) sched_trace_rec_free(rec); } +/* ---- satisfaction check rows (4.1, 4.2, 5.4, 5.5) -------------------- */ + +typedef struct { + xce_sched_core_t core; + xce_sched_obj_t obj; + xce_sched_worker_t w; + xlist_t hl; /* the worker's matched host limits */ + /* registered resources (resmgr copies) */ + xce_resource_t* mem; + xce_resource_t* lic; + xce_resource_t* slots; + xce_resource_t* lim_lic; /* cluster-wide limit vres, scoped */ + xce_resource_t* lim_orphan; /* limit vres matched by no limit */ + xce_resource_t* lim_host; /* host limit vres, scoped */ + xce_resource_t* lim_hcap; /* host limit vres, no consumer scope */ + xce_limit_t limit_lic; + xce_limit_t limit_host; + xce_limit_t limit_hcap; + xce_vres_t vres_lic; + xce_vres_t vres_host; + xce_vres_t vres_hcap; +} rows_fx_t; + +#define ROWS_HOST_IDX 2 + +static xce_resource_t* reg_res(var resmgr, const char* name, int flags, + int valtype) +{ + xce_resource_t res; + xce_resource_t* r; + + memset(&res, 0, sizeof(res)); + res.name = (char*)name; + res.flags = flags; + res.valtype = valtype; + r = makeResource(resmgr, &res); + TEST_ASSERT_NOT_NULL(r); + return r; +} + +static rows_fx_t* rows_fx_new(void) +{ + kvmgr_params_t res_params = { .key_type = KVMGR_KEY_STRING, + .nslots = 16, + .delta = 8, + .datakiller = NULL }; + rows_fx_t* fx = (rows_fx_t*)calloc(1, sizeof(*fx)); + + TEST_ASSERT_NOT_NULL(fx); + fx->core.resmgr = makeInstance(&xkvmgr_type, &res_params); + TEST_ASSERT_NOT_NULL(fx->core.resmgr); + + fx->mem = reg_res(fx->core.resmgr, "mem", XCE_RESF_CONSUMABLE, 1); + fx->lic = reg_res(fx->core.resmgr, "lic", + XCE_RESF_SHARED | XCE_RESF_CONSUMABLE, 1); + fx->slots = reg_res(fx->core.resmgr, "slots", XCE_RESF_CONSUMABLE, 2); + fx->lim_lic = reg_res(fx->core.resmgr, "limit=lic_cap/u=alice/q=normal", + XCE_RESF_LIMIT | XCE_RESF_SHARED, 1); + fx->lim_orphan = reg_res(fx->core.resmgr, "limit=site_total/p=proj1", + XCE_RESF_LIMIT | XCE_RESF_SHARED, 1); + fx->lim_host = reg_res(fx->core.resmgr, "limit=host_slots/u=alice", + XCE_RESF_LIMIT, 2); + fx->lim_hcap = reg_res(fx->core.resmgr, "limit=hcap", XCE_RESF_LIMIT, + 2); + + fx->limit_lic.name = "lic_cap"; + fx->limit_host.name = "host_slots"; + fx->limit_hcap.name = "hcap"; + fx->vres_lic.vres = fx->lim_lic; + fx->vres_lic.res = fx->lic; + fx->vres_lic.limit = &fx->limit_lic; + fx->vres_host.vres = fx->lim_host; + fx->vres_host.res = fx->slots; + fx->vres_host.limit = &fx->limit_host; + fx->vres_hcap.vres = fx->lim_hcap; + fx->vres_hcap.res = fx->slots; + fx->vres_hcap.limit = &fx->limit_hcap; + + xlist_init(&fx->obj.glimits, 4); + xlist_store(&fx->obj.glimits, &fx->vres_lic); + xlist_init(&fx->hl, 4); + xlist_store(&fx->hl, &fx->vres_host); + xlist_store(&fx->hl, &fx->vres_hcap); + zarray_init(&fx->obj.hlimits, 8, NULL); + TEST_ASSERT_EQUAL_INT(0, zarray_put(&fx->obj.hlimits, ROWS_HOST_IDX, + &fx->hl)); + + fx->w.name = "n01"; + fx->w.idx = ROWS_HOST_IDX; + return fx; +} + +static void rows_fx_free(rows_fx_t* fx) +{ + zarray_fini(&fx->obj.hlimits); + xlist_fini(&fx->hl); + xlist_fini(&fx->obj.glimits); + free(fx); +} + +/* A dense uncommitted vector keyed by resource index (as schedobj.c fills + * the context's), all zero except `idx` = `val`. */ +static void make_uncomit(xvec_t* v, float* buf, int n, int idx, float val) +{ + memset(buf, 0, sizeof(float) * (size_t)n); + if ((idx >= 0) && (idx < n)) { + buf[idx] = val; + } + v->count = n; + v->elsize = sizeof(float); + v->data = buf; +} + +static sched_trace_entry_t* entry_n(xce_sched_trace_rec_t* rec, int k) +{ + sched_trace_entry_t* e = rec->entries; + + while ((e != NULL) && (k-- > 0)) { + e = e->next; + } + TEST_ASSERT_NOT_NULL(e); + return e; +} + +static void assert_resource_row(const sched_trace_entry_t* e, int type, + const char* res, int flags, int valtype, + float total, float inuse, float reserved, + float requested, bool met) +{ + TEST_ASSERT_EQUAL_INT(type, e->type); + TEST_ASSERT_EQUAL_STRING(res, e->u.row.res); + TEST_ASSERT_EQUAL_INT(flags, e->u.row.flags); + TEST_ASSERT_EQUAL_INT(valtype, e->u.row.valtype); + TEST_ASSERT_EQUAL_FLOAT(total, e->u.row.total); + TEST_ASSERT_EQUAL_FLOAT(inuse, e->u.row.inuse); + TEST_ASSERT_EQUAL_FLOAT(reserved, e->u.row.reserved); + TEST_ASSERT_EQUAL_FLOAT(requested, e->u.row.requested); + TEST_ASSERT_EQUAL(met, e->u.row.met); +} + +static void assert_limit_row(const sched_trace_entry_t* e, const char* limit, + const char* scope, const char* limited) +{ + TEST_ASSERT_TRUE((e->u.row.flags & XCE_RESF_LIMIT) != 0); + TEST_ASSERT_EQUAL_STRING(limit, e->u.row.limit); + TEST_ASSERT_EQUAL_STRING(scope, e->u.row.scope); + if (limited == NULL) { + TEST_ASSERT_NULL(e->u.row.limited_res); + } else { + TEST_ASSERT_EQUAL_STRING(limited, e->u.row.limited_res); + } +} + +/* Cluster-wide scope: every row is kept, met or not, and limit rows are + * resolved through obj->glimits (4.1, 4.2). */ +static void test_check_rows_cluster_scope_keeps_every_row(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + float ubuf[16]; + xvec_t uncomit; + int idxs[4] = { fx->mem->idx, fx->lic->idx, fx->lim_lic->idx, + fx->lim_orphan->idx }; + float total[4] = { 64, 10, 5, 3 }; + float inuse[4] = { 60, 9, 4, 0 }; + float wanted[4] = { 8, 1, 2, 1 }; + sched_trace_entry_t* e; + int k; + + make_uncomit(&uncomit, ubuf, 16, fx->lim_lic->idx, 1); + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_check_begin(rec, &fx->obj); + sched_trace_check_rows(rec, &fx->core, 4, idxs, total, inuse, &uncomit, + wanted); + sched_trace_check_end(rec, 0, false); + + TEST_ASSERT_EQUAL_INT(5, rec->nentries); + /* mem: 64 < 60 + 0 + 8: not met, a resource row */ + e = entry_n(rec, 0); + assert_resource_row(e, SCHED_TRACE_E_CHECK_ROW, "mem", XCE_RESF_CONSUMABLE, + 1, 64, 60, 0, 8, false); + TEST_ASSERT_NULL(e->u.row.limit); + TEST_ASSERT_EQUAL_INT(fx->mem->idx, e->u.row.idx); + /* lic: 10 < 9 + 0 + 1 is false: met, kept anyway */ + e = entry_n(rec, 1); + assert_resource_row(e, SCHED_TRACE_E_CHECK_ROW, "lic", + XCE_RESF_SHARED | XCE_RESF_CONSUMABLE, 1, 10, 9, 0, 1, + true); + TEST_ASSERT_NULL(e->u.row.limit); + /* the cluster-wide limit: 5 < 4 + 1 + 2, reserved from the dense + * vector at its own index; a limit row, not a shortfall */ + e = entry_n(rec, 2); + assert_resource_row(e, SCHED_TRACE_E_CHECK_ROW, + "limit=lic_cap/u=alice/q=normal", + XCE_RESF_LIMIT | XCE_RESF_SHARED, 1, 5, 4, 1, 2, false); + assert_limit_row(e, "lic_cap", "u=alice,q=normal", "lic"); + /* a limit vres in no matched limit: name and scope parsed from its + * resource name, limited resource unknown */ + e = entry_n(rec, 3); + assert_resource_row(e, SCHED_TRACE_E_CHECK_ROW, "limit=site_total/p=proj1", + XCE_RESF_LIMIT | XCE_RESF_SHARED, 1, 3, 0, 0, 1, true); + assert_limit_row(e, "site_total", "p=proj1", NULL); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_CHECK_END, entry_n(rec, 4)->type); + /* rows sit at the check's level, with the check-end line */ + TEST_ASSERT_EQUAL_INT(entry_n(rec, 4)->depth, entry_n(rec, 0)->depth); + + /* no host verdict and no limit tally from the cluster-wide check */ + TEST_ASSERT_EQUAL_UINT(0, rec->host_row_verdicts); + TEST_ASSERT_EQUAL_INT(0, rec->pos_nlimits); + + /* copies: the record survives its sources */ + for (k = 0; k < 4; k++) { + e = entry_n(rec, k); + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.res)); + if (e->u.row.limit != NULL) { + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.limit)); + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.scope)); + } + } + TEST_ASSERT_TRUE(in_arena(rec, entry_n(rec, 2)->u.row.limited_res)); + total[0] = 1; + inuse[0] = 1; + ubuf[fx->lim_lic->idx] = 9; + TEST_ASSERT_EQUAL_FLOAT(64, entry_n(rec, 0)->u.row.total); + TEST_ASSERT_EQUAL_FLOAT(1, entry_n(rec, 2)->u.row.reserved); + rows_fx_free(fx); + TEST_ASSERT_EQUAL_STRING("lic_cap", entry_n(rec, 2)->u.row.limit); + TEST_ASSERT_FALSE(rec->incomplete); + sched_trace_rec_free(rec); +} + +/* Host scope: only unmet rows are kept, after the host's verdict line, + * one level below it; the limit row resolves through obj->hlimits with + * the host appended to its scope, and sets LIMIT, never SHORTFALL (5.4, + * 5.5). */ +static void test_check_rows_host_scope_keeps_unmet_rows(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + float ubuf[16]; + xvec_t uncomit; + int idxs[4] = { fx->mem->idx, fx->slots->idx, fx->lim_host->idx, + fx->lim_hcap->idx }; + float total[4] = { 64, 8, 10, 100 }; + float inuse[4] = { 60, 2, 9, 3 }; + float wanted[4] = { 8, 4, 2, 4 }; + sched_trace_entry_t* e; + + make_uncomit(&uncomit, ubuf, 16, fx->mem->idx, 0.5f); + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 4, idxs, total, inuse, &uncomit, + wanted); + /* the rows wait for the verdict: nothing stored yet */ + TEST_ASSERT_EQUAL_INT(0, rec->nentries); + TEST_ASSERT_EQUAL_UINT(XCE_TRACE_V_SHORTFALL | XCE_TRACE_V_LIMIT, + rec->host_row_verdicts); + sched_trace_host_verdict(rec, 0, 0); + + TEST_ASSERT_EQUAL_INT(3, rec->nentries); + e = entry_n(rec, 0); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_HOST, e->type); + TEST_ASSERT_EQUAL_STRING("n01", e->u.host.host); + TEST_ASSERT_EQUAL_UINT(XCE_TRACE_V_SHORTFALL | XCE_TRACE_V_LIMIT, + e->u.host.verdicts); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_PHASE_PERHOST, e->u.host.phase); + + /* mem: 64 < 60 + 0.5 + 8 */ + e = entry_n(rec, 1); + assert_resource_row(e, SCHED_TRACE_E_HOST_ROW, "mem", XCE_RESF_CONSUMABLE, + 1, 64, 60, 0.5f, 8, false); + TEST_ASSERT_NULL(e->u.row.limit); + TEST_ASSERT_EQUAL_INT(entry_n(rec, 0)->depth + 1, e->depth); + /* slots (met) and hcap (met) are not kept; host_slots is */ + e = entry_n(rec, 2); + assert_resource_row(e, SCHED_TRACE_E_HOST_ROW, "limit=host_slots/u=alice", + XCE_RESF_LIMIT, 2, 10, 9, 0, 2, false); + assert_limit_row(e, "host_slots", "u=alice,h=n01", "slots"); + TEST_ASSERT_EQUAL_INT(entry_n(rec, 0)->depth + 1, e->depth); + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.res)); + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.scope)); + TEST_ASSERT_TRUE(in_arena(rec, e->u.row.limited_res)); + + /* one evaluation, counted under each kind it matched */ + TEST_ASSERT_EQUAL_INT(1, rec->counters.host_evals[2]); /* SHORTFALL */ + TEST_ASSERT_EQUAL_INT(1, rec->counters.host_evals[3]); /* LIMIT */ + rows_fx_free(fx); + TEST_ASSERT_EQUAL_STRING("u=alice,h=n01", entry_n(rec, 2)->u.row.scope); + sched_trace_rec_free(rec); +} + +/* A host whose only unmet row is a limit is a limit rejection alone: the + * limit is never also reported as a resource shortfall (5.5). A limit with + * no consumer scope gets the host as its whole scope. */ +static void test_check_rows_host_limit_only_is_not_a_shortfall(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + int idxs[2] = { fx->slots->idx, fx->lim_hcap->idx }; + float total[2] = { 8, 4 }; + float inuse[2] = { 2, 3 }; + float wanted[2] = { 4, 4 }; + sched_trace_entry_t* e; + + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + sched_trace_host_verdict(rec, 0, 0); + + TEST_ASSERT_EQUAL_INT(2, rec->nentries); + TEST_ASSERT_EQUAL_UINT(XCE_TRACE_V_LIMIT, entry_n(rec, 0)->u.host.verdicts); + e = entry_n(rec, 1); + assert_resource_row(e, SCHED_TRACE_E_HOST_ROW, "limit=hcap", + XCE_RESF_LIMIT, 2, 4, 3, 0, 4, false); + assert_limit_row(e, "hcap", "h=n01", "slots"); + TEST_ASSERT_EQUAL_INT(0, rec->counters.host_evals[2]); /* SHORTFALL */ + TEST_ASSERT_EQUAL_INT(1, rec->counters.host_evals[3]); /* LIMIT */ + sched_trace_rec_free(rec); + + /* a shortfall alone is a shortfall alone */ + { + int i2[1] = { fx->mem->idx }; + float t2[1] = { 4 }; + float u2[1] = { 3 }; + float w2[1] = { 2 }; + + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 1, i2, t2, u2, NULL, w2); + sched_trace_host_verdict(rec, 0, 0); + TEST_ASSERT_EQUAL_INT(2, rec->nentries); + TEST_ASSERT_EQUAL_UINT(XCE_TRACE_V_SHORTFALL, + entry_n(rec, 0)->u.host.verdicts); + TEST_ASSERT_NULL(entry_n(rec, 1)->u.row.limit); + sched_trace_rec_free(rec); + } + rows_fx_free(fx); +} + +/* Every row met: nothing kept, and the verdict falls back to the per-host + * check's own selection branch (the model's "from rows" rule). */ +static void test_check_rows_host_all_met_keeps_nothing(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + int idxs[2] = { fx->mem->idx, fx->lim_host->idx }; + float total[2] = { 64, 10 }; + float inuse[2] = { 0, 0 }; + float wanted[2] = { 8, 2 }; + + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + TEST_ASSERT_EQUAL_UINT(0, rec->host_row_verdicts); + sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0); + TEST_ASSERT_EQUAL_INT(1, rec->nentries); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_HOST, entry_n(rec, 0)->type); + sched_trace_rec_free(rec); + rows_fx_free(fx); +} + +/* A worker with no matched host limits: the limit row still resolves its + * name and scope from the vres resource name, host appended. */ +static void test_check_rows_host_limit_without_matched_limits(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_worker_t w2; + xce_sched_trace_rec_t* rec; + int idxs[1] = { fx->lim_host->idx }; + float total[1] = { 1 }; + float inuse[1] = { 1 }; + float wanted[1] = { 1 }; + sched_trace_entry_t* e; + + memset(&w2, 0, sizeof(w2)); + w2.name = "n77"; + w2.idx = 7; /* no hlimits list at this index */ + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &w2); + sched_trace_check_rows(rec, &fx->core, 1, idxs, total, inuse, NULL, + wanted); + sched_trace_host_verdict(rec, 0, 0); + TEST_ASSERT_EQUAL_INT(2, rec->nentries); + TEST_ASSERT_EQUAL_UINT(XCE_TRACE_V_LIMIT, entry_n(rec, 0)->u.host.verdicts); + e = entry_n(rec, 1); + assert_limit_row(e, "host_slots", "u=alice,h=n77", NULL); + sched_trace_rec_free(rec); + rows_fx_free(fx); +} + +/* Summary level: no host lines and no rows, but each limit a host hit is + * tallied with its scope instance for the position summary (5.9). */ +static void test_check_rows_host_summary_tallies_limit(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + int idxs[2] = { fx->mem->idx, fx->lim_host->idx }; + float total[2] = { 64, 10 }; + float inuse[2] = { 60, 9 }; + float wanted[2] = { 8, 2 }; + sched_trace_entry_t* e; + + ctl.level = XCE_TRACE_LEVEL_SUMMARY; + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_pos_begin(rec, 1, 1); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + sched_trace_host_verdict(rec, 0, 0); + sched_trace_pos_end(rec, XCE_TRACE_POS_UNFILLED, NULL); + + TEST_ASSERT_EQUAL_INT(2, rec->nentries); + e = entry_n(rec, 1); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_POS_SUMMARY, e->type); + TEST_ASSERT_EQUAL_INT(1, e->u.pos_summary.counts[2]); /* SHORTFALL */ + TEST_ASSERT_EQUAL_INT(1, e->u.pos_summary.counts[3]); /* LIMIT */ + TEST_ASSERT_EQUAL_INT(1, e->u.pos_summary.nlimits); + TEST_ASSERT_EQUAL_STRING("host_slots", e->u.pos_summary.limits[0].limit); + TEST_ASSERT_EQUAL_STRING("u=alice,h=n01", e->u.pos_summary.limits[0].scope); + TEST_ASSERT_EQUAL_INT(1, e->u.pos_summary.limits[0].count); + sched_trace_rec_free(rec); + rows_fx_free(fx); +} + +/* Rows outside any check scope are not attributed to anything; a resource + * index the resmgr does not know is kept as an unnamed resource row. */ +static void test_check_rows_scope_edges(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + int idxs[1] = { fx->mem->idx }; + int unknown[1] = { 99 }; + float total[1] = { 1 }; + float inuse[1] = { 1 }; + float wanted[1] = { 1 }; + sched_trace_entry_t* e; + + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_check_rows(rec, &fx->core, 1, idxs, total, inuse, NULL, + wanted); + TEST_ASSERT_EQUAL_INT(0, rec->nentries); + TEST_ASSERT_EQUAL_UINT(0, rec->host_row_verdicts); + + sched_trace_check_begin(rec, &fx->obj); + sched_trace_check_end(rec, 1, true); + sched_trace_check_rows(rec, &fx->core, 1, idxs, total, inuse, NULL, + wanted); + TEST_ASSERT_EQUAL_INT(1, rec->nentries); /* the check end only */ + + sched_trace_check_begin(rec, &fx->obj); + sched_trace_check_rows(rec, &fx->core, 1, unknown, total, inuse, NULL, + wanted); + TEST_ASSERT_EQUAL_INT(2, rec->nentries); + e = entry_n(rec, 1); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_CHECK_ROW, e->type); + TEST_ASSERT_NULL(e->u.row.res); + TEST_ASSERT_EQUAL_INT(99, e->u.row.idx); + TEST_ASSERT_EQUAL_INT(0, e->u.row.flags); + TEST_ASSERT_NULL(e->u.row.limit); + TEST_ASSERT_FALSE(e->u.row.met); + sched_trace_check_end(rec, 0, false); + sched_trace_rec_free(rec); + rows_fx_free(fx); +} + +/* Host rows count against the line budget like every other line. */ +static void test_check_rows_host_rows_count_against_budget(void) +{ + rows_fx_t* fx = rows_fx_new(); + xce_sched_trace_ctl_t ctl = make_ctl("fcfs"); + xce_sched_trace_rec_t* rec; + int idxs[2] = { fx->mem->idx, fx->lim_host->idx }; + float total[2] = { 1, 1 }; + float inuse[2] = { 1, 1 }; + float wanted[2] = { 1, 1 }; + int i; + + ctl.line_bound = XCE_TRACE_LINE_BOUND_MIN; + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + for (i = 0; i < XCE_TRACE_LINE_BOUND_MIN - 2; i++) { + sched_trace_alt_skipped(rec, 1, 1); + } + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + sched_trace_host_verdict(rec, 0, 0); + TEST_ASSERT_EQUAL_INT(XCE_TRACE_LINE_BOUND_MIN, rec->nentries); + TEST_ASSERT_EQUAL_INT(1, rec->counters.omitted_lines); + TEST_ASSERT_EQUAL_INT(SCHED_TRACE_E_HOST_ROW, rec->entries_tail->type); + TEST_ASSERT_EQUAL_STRING("mem", rec->entries_tail->u.row.res); + + /* a host scope that ends without its verdict leaves no rows behind */ + sched_trace_rec_free(rec); + ctl = make_ctl("fcfs"); + rec = sched_trace_rec_new(&ctl, 0x77, XCE_TRACE_REC_DECISION); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_host_verdict(rec, XCE_TRACE_V_CLAIMED, 0); + TEST_ASSERT_EQUAL_INT(1, rec->nentries); + /* and a record freed with rows still pending frees them (checked only + * under ASAN with detect_leaks=1; the suite runs with leaks off) */ + sched_trace_host_begin(rec, &fx->obj, &fx->w); + sched_trace_check_rows(rec, &fx->core, 2, idxs, total, inuse, NULL, + wanted); + sched_trace_rec_free(rec); + rows_fx_free(fx); +} + int main(void) { + xstring_table_init(1024); UNITY_BEGIN(); RUN_TEST(test_selection_copies_ordering_keys); RUN_TEST(test_selection_policy_ignores_queue_override); @@ -502,5 +1040,13 @@ int main(void) RUN_TEST(test_attempt_reasons_main_reason_only); RUN_TEST(test_attempt_reasons_none_recorded); RUN_TEST(test_attempt_reasons_counts_against_the_budget); + RUN_TEST(test_check_rows_cluster_scope_keeps_every_row); + RUN_TEST(test_check_rows_host_scope_keeps_unmet_rows); + RUN_TEST(test_check_rows_host_limit_only_is_not_a_shortfall); + RUN_TEST(test_check_rows_host_all_met_keeps_nothing); + RUN_TEST(test_check_rows_host_limit_without_matched_limits); + RUN_TEST(test_check_rows_host_summary_tallies_limit); + RUN_TEST(test_check_rows_scope_edges); + RUN_TEST(test_check_rows_host_rows_count_against_budget); return UNITY_END(); } diff --git a/src/scheduler/testsuite/test_trace_model.c b/src/scheduler/testsuite/test_trace_model.c index e825a52a..04153b00 100644 --- a/src/scheduler/testsuite/test_trace_model.c +++ b/src/scheduler/testsuite/test_trace_model.c @@ -1092,8 +1092,12 @@ static int emit_every_kind(xce_sched_trace_rec_t* rec) xce_alloc_plan_t plan = make_plan(slots, 1); xce_sched_worker_t w = make_worker(hname, 0); sched_trace_reason_count_t rc = { 1, 1 }; + sched_trace_row_t row; make_primary(&fp, 1, 1); + memset(&row, 0, sizeof(row)); + row.res = "mem"; + row.total = 1; sched_trace_path_push(rec, XCE_TRACE_PATH_ALT, 1, 2); sched_trace_alt_skipped(rec, 1, 2); /* 1 */ sched_trace_path_pop(rec); @@ -1102,29 +1106,31 @@ static int emit_every_kind(xce_sched_trace_rec_t* rec) sched_trace_path_push(rec, XCE_TRACE_PATH_MEMBER, 1, 1); sched_trace_resreq_begin(rec, &fp.p, 0); /* 3 */ sched_trace_check_begin(rec, NULL); - sched_trace_check_end(rec, 1, true); /* 4 */ - sched_trace_no_plan(rec, XCE_TRACE_NOPLAN_DECLINED); /* 5 */ - sched_trace_plan_discarded(rec, &plan); /* 6 */ - sched_trace_plan_not_tried(rec, &plan, -12); /* 7 */ - sched_trace_plan_begin(rec, &plan); /* 8 */ - sched_trace_pos_begin(rec, 1, 1); /* 9 */ + sched_trace_row(rec, &row); /* 4 */ + sched_trace_check_end(rec, 1, true); /* 5 */ + sched_trace_no_plan(rec, XCE_TRACE_NOPLAN_DECLINED); /* 6 */ + sched_trace_plan_discarded(rec, &plan); /* 7 */ + sched_trace_plan_not_tried(rec, &plan, -12); /* 8 */ + sched_trace_plan_begin(rec, &plan); /* 9 */ + sched_trace_pos_begin(rec, 1, 1); /* 10 */ sched_trace_host_begin(rec, NULL, &w); - sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0); /* 10 */ - sched_trace_pos_end(rec, XCE_TRACE_POS_FILLED, hname); /* 11 */ - sched_trace_plan_end(rec, 1, 0); /* 12 */ - sched_trace_rollback_host(rec, hname); /* 13 */ + sched_trace_row(rec, &row); /* 11, held */ + sched_trace_host_verdict(rec, XCE_TRACE_V_ACCEPTED, 0); /* 12 */ + sched_trace_pos_end(rec, XCE_TRACE_POS_FILLED, hname); /* 13 */ + sched_trace_plan_end(rec, 1, 0); /* 14 */ + sched_trace_rollback_host(rec, hname); /* 15 */ sched_trace_rollback_host(rec, hname); /* merged */ - sched_trace_commit(rec); /* 14 */ - sched_trace_finalize_failed(rec, -5); /* 15 */ - sched_trace_search_end(rec, 1); /* 16 */ - sched_trace_resreq_end(rec, 1, 0); /* 17 */ + sched_trace_commit(rec); /* 16 */ + sched_trace_finalize_failed(rec, -5); /* 17 */ + sched_trace_search_end(rec, 1); /* 18 */ + sched_trace_resreq_end(rec, 1, 0); /* 19 */ sched_trace_path_pop(rec); - sched_trace_member_failed(rec, 1, 1, -12); /* 18 */ - sched_trace_attempt_reasons(rec, &rc, 1, NULL, 0, NULL, 0); /* 19 */ - sched_trace_resreq_end(rec, 0, 0); /* 20 */ + sched_trace_member_failed(rec, 1, 1, -12); /* 20 */ + sched_trace_attempt_reasons(rec, &rc, 1, NULL, 0, NULL, 0); /* 21 */ + sched_trace_resreq_end(rec, 0, 0); /* 22 */ sched_trace_path_pop(rec); - sched_trace_alt_result(rec, 0, 2); /* 21 */ - return 21; + sched_trace_alt_result(rec, 0, 2); /* 23 */ + return 23; } static void test_every_entry_kind_is_one_entry(void) @@ -1152,14 +1158,16 @@ static void test_every_entry_kind_is_one_entry(void) TEST_ASSERT_EQUAL_INT(0, seen[SCHED_TRACE_E_POS_SUMMARY]); sched_trace_rec_free(rec); - /* summary level: the position summary replaces the host lines and the - * position end, so the same calls make one entry fewer */ + /* summary level: the position summary replaces the host lines (the + * host verdict and its row) and the position end, so the same calls + * make two entries fewer */ rec = new_rec_level(XCE_TRACE_LINE_BOUND_DEFAULT, XCE_TRACE_LEVEL_SUMMARY); TEST_ASSERT_NOT_NULL(rec); TEST_ASSERT_EQUAL_INT(n, emit_every_kind(rec)); - TEST_ASSERT_EQUAL_INT(n - 1, rec->nentries); + TEST_ASSERT_EQUAL_INT(n - 2, rec->nentries); for (e = rec->entries; e != NULL; e = e->next) { TEST_ASSERT_TRUE(e->type != SCHED_TRACE_E_HOST); + TEST_ASSERT_TRUE(e->type != SCHED_TRACE_E_HOST_ROW); TEST_ASSERT_TRUE(e->type != SCHED_TRACE_E_POS_END); seen[e->type]++; }