feat(scheduler-sim-decision-trace): trace record model foundation
Task 1.3: trace types and constants, per-record arena, job selection matching, no-decision record finishing; empty capture module; both sources scanned by the core isolation test with a no-stdio check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7f40ff27dc
commit
7d3c71fc34
8 changed files with 1234 additions and 1 deletions
|
|
@ -22,7 +22,7 @@
|
|||
- Done when every existing REPL test and the 1.1 baseline test pass unchanged under ASAN, and the helpers can be called from other REPL files.
|
||||
- _Requirements: 7.7_
|
||||
|
||||
- [ ] 1.3 Build the trace record model foundation, test first
|
||||
- [x] 1.3 Build the trace record model foundation, test first
|
||||
- Define these types: record, settings snapshot, verdict kinds, record kinds, path, no-plan and position-end. Define these constants: line bound (default 1000, range 50 to 100000) and outcome hosts (50).
|
||||
- The per-record arena copies every string and number, so a record never points into scheduler memory. The record reports its own byte size, and freeing a NULL record is a no-op.
|
||||
- Job selection matching covers all jobs, a plain job, an array element, and every element of an array (including elements created later).
|
||||
|
|
@ -337,3 +337,6 @@
|
|||
|
||||
## Implementation Notes
|
||||
- 1.1: build dir is src/xce.build. Baseline test sched_repl_trace_baseline_test runs the built xce_scheduler (depends: scheduler); regenerate fixtures with XCE_REPL_BASELINE_REGEN=1 (script.txt is hand-written). Fixture sends the license value as a RESVALS_FULL event because shared resvals in the snapshot are dropped on load (bug-1923). sim_output.replay has no session decisions (set output switches files before session 1 is finalized); decisions are covered by set_output.replay and sim_verify.txt.
|
||||
- 1.3: `sched_trace_ctl_wants` whole-array selectors match elements only, not the bare key. A bare id in `trace on job <id>` can't be told apart from an array base for a job that doesn't exist yet, so 4.3's parser must emit both `{key,false}` and `{key,true}`, or the matcher must also accept `jobid == key`.
|
||||
- 1.3: design's record header lists a "file tag", but `xce_sched_trace_ctl_t` has no such field; the 4.2/4.6 store/emit tasks must set it or drop it explicitly.
|
||||
- 1.3: trace sources (sched_trace.c, sched_trace_capture.c) may not include <stdio.h> (core isolation test); capture copies raw strings/numbers and leaves formatting to the renderer.
|
||||
|
|
|
|||
206
src/scheduler/include/sched/trace.h
Normal file
206
src/scheduler/include/sched/trace.h
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#if !defined(_SCHED_TRACE_H_)
|
||||
|
||||
#define _SCHED_TRACE_H_ 1
|
||||
|
||||
/*
|
||||
* trace.h - scheduling decision trace model (scheduler-sim-decision-trace
|
||||
* spec, design.md "TraceModel").
|
||||
*
|
||||
* A trace record explains one allocator decision for one traced job. It is
|
||||
* built on the allocator thread while the job is allocated, handed to the
|
||||
* simulator REPL on the decision, and rendered there. The record is
|
||||
* self-contained: every string and number it holds is copied into its own
|
||||
* per-record arena, so it never points into scheduler memory and stays
|
||||
* valid after the job, its schedobj or the core state it was captured from
|
||||
* change or go away.
|
||||
*
|
||||
* Only the simulator REPL ever publishes a control block (`trace_ctl`), so
|
||||
* the live scheduler never builds a record (Requirement 11.2).
|
||||
*
|
||||
* The model (src/sched_trace.c) and the capture helpers
|
||||
* (src/sched_trace_capture.c) do no I/O and no logging; test_core_isolation.c
|
||||
* scans both files for stdio and file I/O.
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Requirement 1.8: two detail levels; summary is the default. */
|
||||
typedef enum {
|
||||
XCE_TRACE_LEVEL_SUMMARY = 0,
|
||||
XCE_TRACE_LEVEL_FULL
|
||||
} xce_sched_trace_level_t;
|
||||
|
||||
/* Requirement 5.10 verdict kinds, bit flags (a host may carry several). */
|
||||
typedef enum {
|
||||
XCE_TRACE_V_ACCEPTED = 1u << 0,
|
||||
XCE_TRACE_V_SELECT = 1u << 1, /* selection mismatch (pre-filter) */
|
||||
XCE_TRACE_V_SHORTFALL = 1u << 2, /* resource shortfall (per-host check) */
|
||||
XCE_TRACE_V_LIMIT = 1u << 3, /* limit exceeded (per-host check) */
|
||||
XCE_TRACE_V_CLAIMED = 1u << 4, /* already claimed by earlier position */
|
||||
XCE_TRACE_V_INTERNAL = 1u << 5 /* not evaluated: internal error */
|
||||
} xce_sched_trace_verdict_t;
|
||||
|
||||
/* Number of verdict kinds above: the size of every per-kind counter array,
|
||||
* indexed by the kind's bit position. */
|
||||
#define XCE_TRACE_V_NKINDS 6
|
||||
|
||||
/* Requirement 2.4 / 2.5 record kinds. */
|
||||
typedef enum {
|
||||
XCE_TRACE_REC_DECISION = 0,
|
||||
XCE_TRACE_REC_NOT_FOUND,
|
||||
XCE_TRACE_REC_NO_SCHEDOBJ,
|
||||
XCE_TRACE_REC_INTERNAL_ERROR, /* alloc_rc < 0 or decision build failure */
|
||||
XCE_TRACE_REC_PEND_NO_REASON,
|
||||
XCE_TRACE_REC_RECONSTRUCTED
|
||||
} xce_sched_trace_rec_kind_t;
|
||||
|
||||
/* One selector from `trace on job ...` (Requirements 1.2, 1.3). */
|
||||
typedef struct xce_sched_trace_sel_s {
|
||||
uint64_t key; /* plain jobid, array base id, or element id */
|
||||
bool whole_array; /* true: key is an array base, matches every element */
|
||||
} xce_sched_trace_sel_t;
|
||||
|
||||
/*
|
||||
* Settings snapshot published to the core for one session. Immutable during
|
||||
* the session except `position`, which only the allocator thread writes.
|
||||
* REPL-owned; a record copies what it needs and never keeps a pointer here.
|
||||
*/
|
||||
typedef struct xce_sched_trace_ctl_s {
|
||||
bool all;
|
||||
int nsel;
|
||||
xce_sched_trace_sel_t* sel;
|
||||
xce_sched_trace_level_t level;
|
||||
int line_bound; /* 50..100000 */
|
||||
uint64_t session_no; /* rs->session_count of this session */
|
||||
const char* policy_name; /* picker name, owned by the ctl */
|
||||
int position; /* allocator-owned, reset at session begin */
|
||||
} xce_sched_trace_ctl_t;
|
||||
|
||||
typedef struct xce_sched_trace_rec_s xce_sched_trace_rec_t;
|
||||
|
||||
#define XCE_TRACE_LINE_BOUND_DEFAULT 1000 /* Requirement 9.1 */
|
||||
#define XCE_TRACE_LINE_BOUND_MIN 50
|
||||
#define XCE_TRACE_LINE_BOUND_MAX 100000
|
||||
#define XCE_TRACE_OUTCOME_HOSTS 50 /* Requirement 9.4 */
|
||||
|
||||
typedef enum {
|
||||
XCE_TRACE_PATH_SINGLE, /* PRIMARY job: the single requirement */
|
||||
XCE_TRACE_PATH_COMPOUND, /* top-level COMPOUND container */
|
||||
XCE_TRACE_PATH_ALT, /* alternative i/n of an ALTER (a compound) */
|
||||
XCE_TRACE_PATH_MEMBER /* member j/m of the enclosing compound */
|
||||
} xce_sched_trace_path_kind_t;
|
||||
|
||||
/* Kept inside the record, not passed by hooks. */
|
||||
typedef struct {
|
||||
int depth; /* 0..2: container (COMPOUND|ALT) > MEMBER, or SINGLE */
|
||||
struct {
|
||||
xce_sched_trace_path_kind_t kind;
|
||||
int i;
|
||||
int n;
|
||||
} lvl[2];
|
||||
} sched_trace_path_t;
|
||||
|
||||
/* Requirement 4.7 reasons. */
|
||||
typedef enum {
|
||||
XCE_TRACE_NOPLAN_DECLINED, /* allocator accepts() declined */
|
||||
XCE_TRACE_NOPLAN_GENERATOR_FAILED, /* plan_init failed */
|
||||
XCE_TRACE_NOPLAN_NONE_GENERATED /* generator produced no plan */
|
||||
} xce_sched_trace_noplan_t;
|
||||
|
||||
typedef enum {
|
||||
XCE_TRACE_POS_FILLED,
|
||||
XCE_TRACE_POS_UNFILLED,
|
||||
XCE_TRACE_POS_SCAN_ERROR, /* host scan could not start or stopped (4.4) */
|
||||
XCE_TRACE_POS_INTERNAL /* host accepted but claim snapshot failed */
|
||||
} xce_sched_trace_pos_end_t;
|
||||
|
||||
/*
|
||||
* Decision-wide counters (Requirement 9.3). They stay exact when the line
|
||||
* budget truncates the stored entries.
|
||||
*/
|
||||
typedef struct {
|
||||
int plans_tried;
|
||||
int plans_discarded;
|
||||
int reqs_none_generated;
|
||||
int reqs_all_discarded;
|
||||
int reqs_none_placed;
|
||||
int host_evals[XCE_TRACE_V_NKINDS]; /* indexed by verdict bit position */
|
||||
int omitted_lines;
|
||||
} sched_trace_counters_t;
|
||||
|
||||
/* One arena chunk; the record frees the whole chain at once. */
|
||||
typedef struct sched_trace_chunk_s {
|
||||
struct sched_trace_chunk_s* next;
|
||||
size_t cap;
|
||||
size_t used;
|
||||
/* followed by `cap` bytes of storage */
|
||||
} sched_trace_chunk_t;
|
||||
|
||||
/*
|
||||
* Record layout. Read only by sched_trace*.c and the REPL renderer; every
|
||||
* other file treats the record as opaque. All pointers point into the
|
||||
* record's own arena.
|
||||
*/
|
||||
struct xce_sched_trace_rec_s {
|
||||
/* header */
|
||||
uint64_t jobid;
|
||||
xce_sched_trace_rec_kind_t kind;
|
||||
int err; /* no-decision kinds: the error, else 0 */
|
||||
xce_sched_trace_level_t level;
|
||||
uint64_t session_no;
|
||||
int position;
|
||||
int line_bound;
|
||||
bool incomplete; /* arena growth failed: appending stopped */
|
||||
|
||||
/* selection block */
|
||||
const char* policy_name; /* arena copy of ctl->policy_name, or NULL */
|
||||
|
||||
/* current requirement path (pushed and popped by the allocation chain) */
|
||||
sched_trace_path_t path;
|
||||
|
||||
sched_trace_counters_t counters;
|
||||
|
||||
/* per-record arena */
|
||||
sched_trace_chunk_t* arena;
|
||||
size_t arena_bytes; /* sum of chunk sizes, headers included */
|
||||
};
|
||||
|
||||
/* True iff `jobid` is selected by `ctl`: `all`, an exact plain-job or
|
||||
* element selector, or a whole-array selector whose array the element
|
||||
* belongs to (also for elements created after the command). NULL ctl:
|
||||
* false. */
|
||||
extern bool sched_trace_ctl_wants(const xce_sched_trace_ctl_t* ctl,
|
||||
uint64_t jobid);
|
||||
|
||||
/* New record for `jobid` with the header copied from `ctl`. NULL when `ctl`
|
||||
* is NULL or allocation fails (the job is then not traced). */
|
||||
extern xce_sched_trace_rec_t*
|
||||
sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl, uint64_t jobid,
|
||||
xce_sched_trace_rec_kind_t kind);
|
||||
|
||||
/* Frees the record and its arena. NULL-safe. */
|
||||
extern void sched_trace_rec_free(xce_sched_trace_rec_t* rec);
|
||||
|
||||
/* Finish a record as a no-decision kind (NOT_FOUND, NO_SCHEDOBJ,
|
||||
* INTERNAL_ERROR, PEND_NO_REASON) before it is pushed via
|
||||
* make_trace_decision (2.4). Any other kind leaves the record unchanged. */
|
||||
extern void sched_trace_rec_finish(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_rec_kind_t kind, int err);
|
||||
|
||||
/* Bytes owned by the record: the record block plus its arena. 0 for NULL. */
|
||||
extern size_t sched_trace_rec_bytes(const xce_sched_trace_rec_t* rec);
|
||||
|
||||
/*
|
||||
* Arena copies. Each returns storage owned by the record, freed with it.
|
||||
* On allocation failure they set rec->incomplete and return NULL. A NULL
|
||||
* source (or rec) returns NULL without marking the record incomplete.
|
||||
*/
|
||||
extern void* sched_trace_arena_alloc(xce_sched_trace_rec_t* rec, size_t n);
|
||||
extern void* sched_trace_arena_memdup(xce_sched_trace_rec_t* rec,
|
||||
const void* src, size_t n);
|
||||
extern char* sched_trace_arena_strdup(xce_sched_trace_rec_t* rec,
|
||||
const char* s);
|
||||
|
||||
#endif
|
||||
|
|
@ -35,6 +35,8 @@ scheduler_srcs = [
|
|||
'src/alloc_cache.c',
|
||||
'src/host_iter.c',
|
||||
'src/alloc_runner.c',
|
||||
'src/sched_trace.c',
|
||||
'src/sched_trace_capture.c',
|
||||
'src/plugins/fcfs.c',
|
||||
'src/plugins/builtin_fcfs.c',
|
||||
'src/plugins/seqjob_allocator.c',
|
||||
|
|
|
|||
231
src/scheduler/src/sched_trace.c
Normal file
231
src/scheduler/src/sched_trace.c
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* sched_trace.c - decision trace record model (scheduler-sim-decision-trace
|
||||
* spec, design.md "TraceModel").
|
||||
*
|
||||
* Owns the trace record: its header (copied from the session's settings
|
||||
* snapshot), the job selection predicate and the per-record arena that
|
||||
* holds every string and number the record keeps, so a record never
|
||||
* points into scheduler memory (design "Invariants").
|
||||
*
|
||||
* A record is built by exactly one thread at a time (the allocator, or the
|
||||
* sim thread for a reconstructed record) and is immutable once attached to
|
||||
* a decision, so nothing here locks.
|
||||
*
|
||||
* No I/O and no logging: test_core_isolation.c scans this file for stdio
|
||||
* and file I/O.
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sched/trace.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Smallest chunk the arena allocates; a larger request gets a chunk of its
|
||||
* own size. Small enough that an untruncated short record stays cheap. */
|
||||
#define SCHED_TRACE_CHUNK_MIN 4096
|
||||
|
||||
#define SCHED_TRACE_ALIGN _Alignof(max_align_t)
|
||||
|
||||
static size_t align_up(size_t n)
|
||||
{
|
||||
return (n + (SCHED_TRACE_ALIGN - 1)) & ~(SCHED_TRACE_ALIGN - 1);
|
||||
}
|
||||
|
||||
/* Header size rounded so the storage that follows is max-aligned. */
|
||||
static size_t chunk_hdr_size(void)
|
||||
{
|
||||
return align_up(sizeof(sched_trace_chunk_t));
|
||||
}
|
||||
|
||||
static unsigned char* chunk_data(sched_trace_chunk_t* c)
|
||||
{
|
||||
return ((unsigned char*)c) + chunk_hdr_size();
|
||||
}
|
||||
|
||||
bool sched_trace_ctl_wants(const xce_sched_trace_ctl_t* ctl, uint64_t jobid)
|
||||
{
|
||||
int i;
|
||||
|
||||
if (ctl == NULL) {
|
||||
return false;
|
||||
}
|
||||
if (ctl->all) {
|
||||
return true;
|
||||
}
|
||||
if (ctl->sel == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (i = 0; i < ctl->nsel; i++) {
|
||||
const xce_sched_trace_sel_t* s = &ctl->sel[i];
|
||||
|
||||
if (s->whole_array) {
|
||||
/* every element of the array, including elements created
|
||||
* after the selector was made (1.2, 1.3) */
|
||||
if (is_array_tag(jobid)
|
||||
&& (array_jobid(jobid) == array_jobid(s->key))) {
|
||||
return true;
|
||||
}
|
||||
} else if (jobid == s->key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void* sched_trace_arena_alloc(xce_sched_trace_rec_t* rec, size_t n)
|
||||
{
|
||||
sched_trace_chunk_t* c;
|
||||
size_t need;
|
||||
unsigned char* p;
|
||||
|
||||
if (rec == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
need = align_up((n == 0) ? 1 : n);
|
||||
if (need < n) { /* overflow */
|
||||
rec->incomplete = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
c = rec->arena;
|
||||
if ((c == NULL) || ((c->cap - c->used) < need)) {
|
||||
size_t cap
|
||||
= (need > SCHED_TRACE_CHUNK_MIN) ? need : SCHED_TRACE_CHUNK_MIN;
|
||||
size_t total = chunk_hdr_size() + cap;
|
||||
|
||||
if (total < cap) { /* overflow */
|
||||
rec->incomplete = true;
|
||||
return NULL;
|
||||
}
|
||||
c = (sched_trace_chunk_t*)malloc(total);
|
||||
if (c == NULL) {
|
||||
rec->incomplete = true;
|
||||
return NULL;
|
||||
}
|
||||
c->cap = cap;
|
||||
c->used = 0;
|
||||
c->next = rec->arena;
|
||||
rec->arena = c;
|
||||
rec->arena_bytes += total;
|
||||
}
|
||||
|
||||
p = chunk_data(c) + c->used;
|
||||
c->used += need;
|
||||
memset(p, 0, need);
|
||||
return p;
|
||||
}
|
||||
|
||||
void* sched_trace_arena_memdup(xce_sched_trace_rec_t* rec, const void* src,
|
||||
size_t n)
|
||||
{
|
||||
void* p;
|
||||
|
||||
if ((rec == NULL) || (src == NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
p = sched_trace_arena_alloc(rec, n);
|
||||
if (p == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
memcpy(p, src, n);
|
||||
return p;
|
||||
}
|
||||
|
||||
char* sched_trace_arena_strdup(xce_sched_trace_rec_t* rec, const char* s)
|
||||
{
|
||||
if ((rec == NULL) || (s == NULL)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return (char*)sched_trace_arena_memdup(rec, s, strlen(s) + 1);
|
||||
}
|
||||
|
||||
xce_sched_trace_rec_t* sched_trace_rec_new(const xce_sched_trace_ctl_t* ctl,
|
||||
uint64_t jobid,
|
||||
xce_sched_trace_rec_kind_t kind)
|
||||
{
|
||||
xce_sched_trace_rec_t* rec;
|
||||
|
||||
if (ctl == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rec = (xce_sched_trace_rec_t*)calloc(1, sizeof(*rec));
|
||||
if (rec == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rec->jobid = jobid;
|
||||
rec->kind = kind;
|
||||
rec->err = 0;
|
||||
rec->level = ctl->level;
|
||||
rec->session_no = ctl->session_no;
|
||||
rec->position = ctl->position;
|
||||
rec->line_bound = ctl->line_bound;
|
||||
|
||||
if (ctl->policy_name != NULL) {
|
||||
rec->policy_name = sched_trace_arena_strdup(rec, ctl->policy_name);
|
||||
if (rec->policy_name == NULL) {
|
||||
/* the record itself could not be built: the job is not traced
|
||||
* (design "Error Strategy") */
|
||||
sched_trace_rec_free(rec);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
void sched_trace_rec_free(xce_sched_trace_rec_t* rec)
|
||||
{
|
||||
sched_trace_chunk_t* c;
|
||||
|
||||
if (rec == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
c = rec->arena;
|
||||
while (c != NULL) {
|
||||
sched_trace_chunk_t* next = c->next;
|
||||
|
||||
free(c);
|
||||
c = next;
|
||||
}
|
||||
free(rec);
|
||||
}
|
||||
|
||||
void sched_trace_rec_finish(xce_sched_trace_rec_t* rec,
|
||||
xce_sched_trace_rec_kind_t kind, int err)
|
||||
{
|
||||
if (rec == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case XCE_TRACE_REC_NOT_FOUND:
|
||||
case XCE_TRACE_REC_NO_SCHEDOBJ:
|
||||
case XCE_TRACE_REC_INTERNAL_ERROR:
|
||||
case XCE_TRACE_REC_PEND_NO_REASON:
|
||||
rec->kind = kind;
|
||||
rec->err = err;
|
||||
break;
|
||||
case XCE_TRACE_REC_DECISION:
|
||||
case XCE_TRACE_REC_RECONSTRUCTED:
|
||||
default:
|
||||
/* not a no-decision kind (2.4): the record is left unchanged */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t sched_trace_rec_bytes(const xce_sched_trace_rec_t* rec)
|
||||
{
|
||||
if (rec == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return sizeof(*rec) + rec->arena_bytes;
|
||||
}
|
||||
16
src/scheduler/src/sched_trace_capture.c
Normal file
16
src/scheduler/src/sched_trace_capture.c
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* sched_trace_capture.c - allocator-side trace capture helpers
|
||||
* (scheduler-sim-decision-trace spec, design.md "TraceCapture").
|
||||
*
|
||||
* Will read scheduler state (job and queue keys, cluster-wide and host
|
||||
* limits, resource flags, allocation and pend outcomes) on the thread that
|
||||
* owns it and write it into a trace record through the model
|
||||
* (sched_trace.c). Inputs are read-only: nothing here may call a scheduler
|
||||
* API that mutates state (Requirement 10).
|
||||
*
|
||||
* The helpers are added by the capture tasks; this module is registered in
|
||||
* the scheduler build and in test_core_isolation.c's source scan from the
|
||||
* start, so the no-I/O rule (no stdio, no file I/O, no logging) applies to
|
||||
* every line that lands here.
|
||||
*/
|
||||
#include "sched/trace.h"
|
||||
|
|
@ -234,6 +234,13 @@ sched_core_isolation_test = executable(
|
|||
'-DXCE_CORE_CLOCK_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'core_clock.c')
|
||||
+ '"',
|
||||
'-DXCE_SCHED_TRACE_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src' / 'sched_trace.c')
|
||||
+ '"',
|
||||
'-DXCE_SCHED_TRACE_CAPTURE_C_PATH="'
|
||||
+ (meson.current_source_dir() / '..' / 'src'
|
||||
/ 'sched_trace_capture.c')
|
||||
+ '"',
|
||||
'-DXCE_CORE_LIB_PATH="'
|
||||
+ sched_core_isolation_lib.full_path()
|
||||
+ '"'],
|
||||
|
|
@ -2509,3 +2516,18 @@ test('scheduler repl trace baseline test',
|
|||
sched_repl_trace_baseline_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'],
|
||||
depends: scheduler)
|
||||
|
||||
# scheduler-sim-decision-trace task 1.3: decision trace record model
|
||||
# (src/sched_trace.c) unit tests -- selection matching, record header and
|
||||
# per-record arena copying and size, record finishing. Links only the model
|
||||
# source; libxce supplies the jobid helpers.
|
||||
sched_trace_model_test = executable(
|
||||
'sched_trace_model_test',
|
||||
['test_trace_model.c', '../src/sched_trace.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
test('scheduler trace model test',
|
||||
sched_trace_model_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
|
|
@ -63,6 +63,17 @@
|
|||
* transitive include, or have to special-case it; the nm check
|
||||
* sidesteps that entirely by checking *usage*, not *visibility*).
|
||||
*
|
||||
* Decision trace sources (scheduler-sim-decision-trace spec, task 1.3,
|
||||
* design.md "Invariant Cross-Check" dimension 2): sched_trace.c (record
|
||||
* model) and sched_trace_capture.c (capture helpers) run on the allocator
|
||||
* thread inside the core's allocation chain, so they join layer 1's
|
||||
* forbidden-headers and forbidden-symbols scans, plus a check that applies
|
||||
* to these two files only: no stdio, no file I/O and no logging (a trace
|
||||
* record is pure data; only the simulator REPL renders or writes it). They
|
||||
* are NOT part of layer 2: sched_core_isolation_lib covers the four core
|
||||
* state-machine files, and the capture file references scheduler symbols
|
||||
* outside that archive's scope.
|
||||
*
|
||||
* Why nm on a static_library() rather than the full xce_scheduler
|
||||
* executable: the executable links the whole monolithic libxce_dep, so
|
||||
* every symbol from netio/xceapi/client/delay/idgen would appear in ITS
|
||||
|
|
@ -98,6 +109,14 @@
|
|||
#error \
|
||||
"XCE_CORE_CLOCK_C_PATH must be defined by meson.build to core_clock.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SCHED_TRACE_C_PATH
|
||||
#error \
|
||||
"XCE_SCHED_TRACE_C_PATH must be defined by meson.build to sched_trace.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_SCHED_TRACE_CAPTURE_C_PATH
|
||||
#error \
|
||||
"XCE_SCHED_TRACE_CAPTURE_C_PATH must be defined by meson.build to sched_trace_capture.c's absolute path"
|
||||
#endif
|
||||
#ifndef XCE_CORE_LIB_PATH
|
||||
#error \
|
||||
"XCE_CORE_LIB_PATH must be defined by meson.build to the absolute path of core.c's isolated static_library() archive"
|
||||
|
|
@ -196,18 +215,24 @@ static void test_core_source_paths_are_readable(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_CORE_H_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_CORE_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(ce, XCE_CORE_EVENTS_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(cs, XCE_CORE_SESSION_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(cc, XCE_CORE_CLOCK_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(st, XCE_SCHED_TRACE_C_PATH);
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(sc, XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* Scans one file's text for every FORBIDDEN_INCLUDES entry, tagging each
|
||||
|
|
@ -234,24 +259,32 @@ static void test_core_never_includes_forbidden_headers(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(ce);
|
||||
TEST_ASSERT_NOT_NULL(cs);
|
||||
TEST_ASSERT_NOT_NULL(cc);
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_no_forbidden_includes(h, "core.h");
|
||||
assert_no_forbidden_includes(c, "core.c");
|
||||
assert_no_forbidden_includes(ce, "core_events.c");
|
||||
assert_no_forbidden_includes(cs, "core_session.c");
|
||||
assert_no_forbidden_includes(cc, "core_clock.c");
|
||||
assert_no_forbidden_includes(st, "sched_trace.c");
|
||||
assert_no_forbidden_includes(sc, "sched_trace_capture.c");
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* Shared by test_core_never_calls_forbidden_symbols() -- see
|
||||
|
|
@ -278,24 +311,32 @@ static void test_core_never_calls_forbidden_symbols(void)
|
|||
char* ce = read_whole_file(XCE_CORE_EVENTS_C_PATH);
|
||||
char* cs = read_whole_file(XCE_CORE_SESSION_C_PATH);
|
||||
char* cc = read_whole_file(XCE_CORE_CLOCK_C_PATH);
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(h);
|
||||
TEST_ASSERT_NOT_NULL(c);
|
||||
TEST_ASSERT_NOT_NULL(ce);
|
||||
TEST_ASSERT_NOT_NULL(cs);
|
||||
TEST_ASSERT_NOT_NULL(cc);
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_no_forbidden_symbols(h, "core.h");
|
||||
assert_no_forbidden_symbols(c, "core.c");
|
||||
assert_no_forbidden_symbols(ce, "core_events.c");
|
||||
assert_no_forbidden_symbols(cs, "core_session.c");
|
||||
assert_no_forbidden_symbols(cc, "core_clock.c");
|
||||
assert_no_forbidden_symbols(st, "sched_trace.c");
|
||||
assert_no_forbidden_symbols(sc, "sched_trace_capture.c");
|
||||
|
||||
free(h);
|
||||
free(c);
|
||||
free(ce);
|
||||
free(cs);
|
||||
free(cc);
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/* core_clock.c is deliberately NOT scanned here -- it is the one
|
||||
|
|
@ -342,6 +383,284 @@ static void test_core_never_calls_wallclock_functions(void)
|
|||
free(cs);
|
||||
}
|
||||
|
||||
/*
|
||||
* No stdio, no file I/O, no logging in the decision trace sources
|
||||
* (scheduler-sim-decision-trace design.md, TraceModel / TraceCapture: "No
|
||||
* I/O and no tlog"). Applies to sched_trace.c and sched_trace_capture.c
|
||||
* only.
|
||||
*
|
||||
* Comments, string literals and character literals are blanked before the
|
||||
* scan, so prose such as "never calls fopen" in a comment is not a hit.
|
||||
* Call names match on identifier boundaries followed by '(' (spaces
|
||||
* allowed), and not as a member call (`x.read(` / `x->read(`), so
|
||||
* `sched_trace_rec_free(` or `SCHED_TRACE(` never match `free(`/`TRACE(`.
|
||||
*/
|
||||
static const char* const TRACE_FORBIDDEN_INCLUDES[] = {
|
||||
"<stdio.h>", "<fcntl.h>", "<unistd.h>", "<sys/stat.h>",
|
||||
"<sys/file.h>", "<tlog.h>", "\"xcelog.h\"",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_INCLUDES \
|
||||
(sizeof(TRACE_FORBIDDEN_INCLUDES) / sizeof(TRACE_FORBIDDEN_INCLUDES[0]))
|
||||
|
||||
/* Identifiers that must not appear at all (stdio stream objects). */
|
||||
static const char* const TRACE_FORBIDDEN_IDENTS[] = {
|
||||
"FILE",
|
||||
"stdin",
|
||||
"stdout",
|
||||
"stderr",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_IDENTS \
|
||||
(sizeof(TRACE_FORBIDDEN_IDENTS) / sizeof(TRACE_FORBIDDEN_IDENTS[0]))
|
||||
|
||||
/* Function-like names that must not be called. */
|
||||
static const char* const TRACE_FORBIDDEN_CALLS[] = {
|
||||
/* stdio */
|
||||
"printf",
|
||||
"fprintf",
|
||||
"vprintf",
|
||||
"vfprintf",
|
||||
"dprintf",
|
||||
"vdprintf",
|
||||
"puts",
|
||||
"fputs",
|
||||
"putchar",
|
||||
"fputc",
|
||||
"putc",
|
||||
"getchar",
|
||||
"fgetc",
|
||||
"getc",
|
||||
"fgets",
|
||||
"scanf",
|
||||
"fscanf",
|
||||
"perror",
|
||||
"fopen",
|
||||
"fdopen",
|
||||
"freopen",
|
||||
"fmemopen",
|
||||
"open_memstream",
|
||||
"fclose",
|
||||
"fflush",
|
||||
"fwrite",
|
||||
"fread",
|
||||
"fseek",
|
||||
"fseeko",
|
||||
"ftell",
|
||||
"ftello",
|
||||
"rewind",
|
||||
"setvbuf",
|
||||
"popen",
|
||||
"pclose",
|
||||
"tmpfile",
|
||||
/* POSIX file I/O */
|
||||
"open",
|
||||
"openat",
|
||||
"creat",
|
||||
"close",
|
||||
"read",
|
||||
"write",
|
||||
"pread",
|
||||
"pwrite",
|
||||
"readv",
|
||||
"writev",
|
||||
"lseek",
|
||||
"fsync",
|
||||
"fdatasync",
|
||||
"ftruncate",
|
||||
"truncate",
|
||||
"unlink",
|
||||
"rename",
|
||||
"mkdir",
|
||||
"stat",
|
||||
"fstat",
|
||||
"lstat",
|
||||
"mmap",
|
||||
"syslog",
|
||||
/* project logging (tlog macros and SCHEDULER_LOG) */
|
||||
"TRACE",
|
||||
"DEBUG",
|
||||
"INFO",
|
||||
"WARN",
|
||||
"ERROR",
|
||||
"FATAL",
|
||||
"SCHEDULER_LOG",
|
||||
};
|
||||
#define NUM_TRACE_FORBIDDEN_CALLS \
|
||||
(sizeof(TRACE_FORBIDDEN_CALLS) / sizeof(TRACE_FORBIDDEN_CALLS[0]))
|
||||
|
||||
static int is_ident_char(char c)
|
||||
{
|
||||
return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'))
|
||||
|| ((c >= '0') && (c <= '9')) || (c == '_');
|
||||
}
|
||||
|
||||
/* Blanks comments and string/char literals in place (newlines kept). A
|
||||
* line that is an #include directive keeps its header name, so the
|
||||
* include scan above can still see it. */
|
||||
static void blank_comments_and_literals(char* t)
|
||||
{
|
||||
size_t i = 0;
|
||||
int in_include = 0;
|
||||
|
||||
while (t[i] != '\0') {
|
||||
if ((t[i] == '#') && (strncmp(t + i, "#include", 8) == 0)) {
|
||||
in_include = 1;
|
||||
} else if (t[i] == '\n') {
|
||||
in_include = 0;
|
||||
}
|
||||
|
||||
if ((t[i] == '/') && (t[i + 1] == '*')) {
|
||||
while ((t[i] != '\0') && !((t[i] == '*') && (t[i + 1] == '/'))) {
|
||||
if (t[i] != '\n') {
|
||||
t[i] = ' ';
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (t[i] != '\0') {
|
||||
t[i] = ' ';
|
||||
t[i + 1] = ' ';
|
||||
i += 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ((t[i] == '/') && (t[i + 1] == '/')) {
|
||||
while ((t[i] != '\0') && (t[i] != '\n')) {
|
||||
t[i++] = ' ';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (((t[i] == '"') || (t[i] == '\'')) && !in_include) {
|
||||
char q = t[i++];
|
||||
|
||||
while ((t[i] != '\0') && (t[i] != q) && (t[i] != '\n')) {
|
||||
if ((t[i] == '\\') && (t[i + 1] != '\0')) {
|
||||
t[i++] = ' ';
|
||||
}
|
||||
t[i++] = ' ';
|
||||
}
|
||||
if (t[i] == q) {
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Offset of the first identifier-bounded occurrence of `name` in `t`
|
||||
* (followed by '(' when `call`), or -1. */
|
||||
static long find_ident(const char* t, const char* name, int call)
|
||||
{
|
||||
size_t n = strlen(name);
|
||||
const char* p = t;
|
||||
|
||||
while ((p = strstr(p, name)) != NULL) {
|
||||
const char* e = p + n;
|
||||
int ok = 1;
|
||||
|
||||
if (p > t) {
|
||||
char b = p[-1];
|
||||
|
||||
if (is_ident_char(b) || (b == '.')
|
||||
|| ((b == '>') && (p - t >= 2) && (p[-2] == '-'))) {
|
||||
ok = 0;
|
||||
}
|
||||
}
|
||||
if (is_ident_char(*e)) {
|
||||
ok = 0;
|
||||
}
|
||||
if (ok && call) {
|
||||
while ((*e == ' ') || (*e == '\t')) {
|
||||
e++;
|
||||
}
|
||||
ok = (*e == '(');
|
||||
}
|
||||
if (ok) {
|
||||
return (long)(p - t);
|
||||
}
|
||||
p += n;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void assert_trace_source_has_no_io(char* text, const char* label)
|
||||
{
|
||||
char msg[256];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_INCLUDES; i++) {
|
||||
snprintf(msg, sizeof(msg), "%s includes stdio/file-I/O/log header %s",
|
||||
label, TRACE_FORBIDDEN_INCLUDES[i]);
|
||||
TEST_ASSERT_NULL_MESSAGE(strstr(text, TRACE_FORBIDDEN_INCLUDES[i]),
|
||||
msg);
|
||||
}
|
||||
|
||||
blank_comments_and_literals(text);
|
||||
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_IDENTS; i++) {
|
||||
snprintf(msg, sizeof(msg), "%s references stdio stream '%s'", label,
|
||||
TRACE_FORBIDDEN_IDENTS[i]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
-1, find_ident(text, TRACE_FORBIDDEN_IDENTS[i], 0), msg);
|
||||
}
|
||||
for (i = 0; i < NUM_TRACE_FORBIDDEN_CALLS; i++) {
|
||||
snprintf(msg, sizeof(msg),
|
||||
"%s calls stdio/file-I/O/log function '%s' -- trace model "
|
||||
"and capture code must do no I/O",
|
||||
label, TRACE_FORBIDDEN_CALLS[i]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(
|
||||
-1, find_ident(text, TRACE_FORBIDDEN_CALLS[i], 1), msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* The scan itself must be able to fail: a probe text with one hit of each
|
||||
* class is rejected, and the same names inside comments, literals and
|
||||
* longer identifiers are not. */
|
||||
static void test_trace_io_scan_detects_and_ignores(void)
|
||||
{
|
||||
char ok_text[] = "/* never fopen( or printf( here */\n"
|
||||
"// stdout FILE write(\n"
|
||||
"static const char* s = \"fprintf(stderr\";\n"
|
||||
"void f(void) { sched_trace_rec_free(0); "
|
||||
"SCHED_TRACE(core, x); p->read(1); q.write(2); "
|
||||
"readonly(3); my_FILE_t z; }\n";
|
||||
char hit_call[] = "void f(void) { fwrite (b, 1, 1, 0); }\n";
|
||||
char hit_ident[] = "static void* g = (void*)stderr;\n";
|
||||
char hit_log[] = "void f(void) { WARN(\"x\"); }\n";
|
||||
char hit_posix[] = "void f(void) { (void)write(1, 0, 0); }\n";
|
||||
|
||||
blank_comments_and_literals(ok_text);
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fopen", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "printf", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "fprintf", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stdout", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "stderr", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "FILE", 0));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "free", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "TRACE", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "read", 1));
|
||||
TEST_ASSERT_EQUAL_INT(-1, find_ident(ok_text, "write", 1));
|
||||
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_call, "fwrite", 1));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_ident, "stderr", 0));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_log, "WARN", 1));
|
||||
TEST_ASSERT_NOT_EQUAL(-1, find_ident(hit_posix, "write", 1));
|
||||
}
|
||||
|
||||
static void test_trace_sources_do_no_stdio_or_file_io(void)
|
||||
{
|
||||
char* st = read_whole_file(XCE_SCHED_TRACE_C_PATH);
|
||||
char* sc = read_whole_file(XCE_SCHED_TRACE_CAPTURE_C_PATH);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(st);
|
||||
TEST_ASSERT_NOT_NULL(sc);
|
||||
|
||||
assert_trace_source_has_no_io(st, "sched_trace.c");
|
||||
assert_trace_source_has_no_io(sc, "sched_trace_capture.c");
|
||||
|
||||
free(st);
|
||||
free(sc);
|
||||
}
|
||||
|
||||
/*
|
||||
* test_core_object_has_no_forbidden_undefined_symbols - the literal
|
||||
* build-time/link-time proof: core.c/core_events.c/core_session.c/
|
||||
|
|
@ -422,6 +741,8 @@ int main(void)
|
|||
RUN_TEST(test_core_never_includes_forbidden_headers);
|
||||
RUN_TEST(test_core_never_calls_forbidden_symbols);
|
||||
RUN_TEST(test_core_never_calls_wallclock_functions);
|
||||
RUN_TEST(test_trace_io_scan_detects_and_ignores);
|
||||
RUN_TEST(test_trace_sources_do_no_stdio_or_file_io);
|
||||
RUN_TEST(test_core_object_has_no_forbidden_undefined_symbols);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
432
src/scheduler/testsuite/test_trace_model.c
Normal file
432
src/scheduler/testsuite/test_trace_model.c
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
/*
|
||||
* test_trace_model.c - unit tests for the decision trace record model
|
||||
* (scheduler-sim-decision-trace spec, task 1.3; design.md "TraceModel").
|
||||
*
|
||||
* Covers the model foundation:
|
||||
* - job selection matching: all jobs, a plain job, an array element, and
|
||||
* a whole array including elements that did not exist when the
|
||||
* selector was made (Requirements 1.2, 1.3);
|
||||
* - the record header copied from the settings snapshot and the
|
||||
* per-record arena: every string and number is copied, so a record
|
||||
* never points into scheduler (or REPL) memory, and the record reports
|
||||
* its own byte size (Requirement 9.1, design "Invariants");
|
||||
* - finishing a record as a no-decision kind (Requirement 2.4);
|
||||
* - freeing a NULL record is a no-op.
|
||||
*
|
||||
* Links only src/sched_trace.c (plus libxce for the jobid helpers).
|
||||
*/
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "sched/trace.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* A plain job, and an array block whose elements are tagged TAG_JOB_ARRAY
|
||||
* and numbered from 1 within the block (job-array-one-based-index). */
|
||||
#define PLAIN_ID(seq) MAKE_JOBID(1, 2, 1000, 0x0, (uint64_t)(seq))
|
||||
#define ARRAY_BASE(blk) \
|
||||
MAKE_JOBID(1, 2, 1000, 0x0, ((uint64_t)(blk)) << N_ARRAY_IDX_BITS)
|
||||
#define ARRAY_ELEM(blk, k) \
|
||||
MAKE_JOBID(1, 2, 1000, TAG_JOB_ARRAY, \
|
||||
(((uint64_t)(blk)) << N_ARRAY_IDX_BITS) | (uint64_t)(k))
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
static xce_sched_trace_ctl_t make_ctl(xce_sched_trace_sel_t* sel, int nsel)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl;
|
||||
|
||||
memset(&ctl, 0, sizeof(ctl));
|
||||
ctl.all = false;
|
||||
ctl.nsel = nsel;
|
||||
ctl.sel = sel;
|
||||
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
|
||||
ctl.line_bound = XCE_TRACE_LINE_BOUND_DEFAULT;
|
||||
ctl.session_no = 7;
|
||||
ctl.policy_name = "fcfs";
|
||||
ctl.position = 3;
|
||||
return ctl;
|
||||
}
|
||||
|
||||
/* ---- constants ------------------------------------------------------- */
|
||||
|
||||
static void test_constants_match_requirements(void)
|
||||
{
|
||||
TEST_ASSERT_EQUAL_INT(1000, XCE_TRACE_LINE_BOUND_DEFAULT);
|
||||
TEST_ASSERT_EQUAL_INT(50, XCE_TRACE_LINE_BOUND_MIN);
|
||||
TEST_ASSERT_EQUAL_INT(100000, XCE_TRACE_LINE_BOUND_MAX);
|
||||
TEST_ASSERT_EQUAL_INT(50, XCE_TRACE_OUTCOME_HOSTS);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_SUMMARY, 0);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, 0);
|
||||
/* verdict kinds are distinct single bits, one per counter slot */
|
||||
TEST_ASSERT_EQUAL_HEX(0x3f, XCE_TRACE_V_ACCEPTED | XCE_TRACE_V_SELECT
|
||||
| XCE_TRACE_V_SHORTFALL | XCE_TRACE_V_LIMIT
|
||||
| XCE_TRACE_V_CLAIMED
|
||||
| XCE_TRACE_V_INTERNAL);
|
||||
TEST_ASSERT_EQUAL_INT(6, XCE_TRACE_V_NKINDS);
|
||||
}
|
||||
|
||||
/* ---- selection matching (1.2, 1.3) ----------------------------------- */
|
||||
|
||||
static void test_wants_null_ctl_is_false(void)
|
||||
{
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(NULL, PLAIN_ID(5)));
|
||||
}
|
||||
|
||||
static void test_wants_all_matches_every_job(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
|
||||
ctl.all = true;
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 400)));
|
||||
}
|
||||
|
||||
static void test_wants_empty_selection_matches_nothing(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
|
||||
}
|
||||
|
||||
static void test_wants_plain_job_matches_only_that_job(void)
|
||||
{
|
||||
xce_sched_trace_sel_t sel[] = {
|
||||
{ PLAIN_ID(5), false }
|
||||
};
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
|
||||
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(5)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(6)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 5)));
|
||||
}
|
||||
|
||||
static void test_wants_element_matches_only_that_element(void)
|
||||
{
|
||||
xce_sched_trace_sel_t sel[] = {
|
||||
{ ARRAY_ELEM(3, 2), false }
|
||||
};
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
|
||||
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 2)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 3)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(4, 2)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_BASE(3)));
|
||||
}
|
||||
|
||||
static void test_wants_whole_array_matches_every_element(void)
|
||||
{
|
||||
xce_sched_trace_sel_t sel[] = {
|
||||
{ ARRAY_BASE(3), true }
|
||||
};
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(sel, 1);
|
||||
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 1)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, 2)));
|
||||
/* elements created later (any index in the block) also match (1.3) */
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(3, MAX_ARRAY_IDX)));
|
||||
/* another array, and a plain job that happens to share the base id's
|
||||
* bit pattern, do not */
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(4, 1)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_BASE(3)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(7)));
|
||||
}
|
||||
|
||||
static void test_wants_unknown_yet_future_ids(void)
|
||||
{
|
||||
/* Selectors naming jobs that do not exist yet are kept as plain keys:
|
||||
* the predicate is a pure function of the id, so the job matches once
|
||||
* it appears (1.3). */
|
||||
xce_sched_trace_sel_t sel[] = {
|
||||
{ PLAIN_ID(900), false },
|
||||
{ ARRAY_ELEM(8, 4), false },
|
||||
{ ARRAY_BASE(9), true }
|
||||
};
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(sel, 3);
|
||||
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, PLAIN_ID(900)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(8, 4)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(9, 1)));
|
||||
TEST_ASSERT_TRUE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(9, 77)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, ARRAY_ELEM(8, 5)));
|
||||
TEST_ASSERT_FALSE(sched_trace_ctl_wants(&ctl, PLAIN_ID(901)));
|
||||
}
|
||||
|
||||
/* ---- record header and arena ----------------------------------------- */
|
||||
|
||||
static void test_rec_new_null_ctl_returns_null(void)
|
||||
{
|
||||
TEST_ASSERT_NULL(
|
||||
sched_trace_rec_new(NULL, PLAIN_ID(5), XCE_TRACE_REC_DECISION));
|
||||
}
|
||||
|
||||
static void test_rec_new_copies_header_from_ctl(void)
|
||||
{
|
||||
char policy[] = "priority-fcfs";
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec;
|
||||
|
||||
ctl.level = XCE_TRACE_LEVEL_FULL;
|
||||
ctl.line_bound = 75;
|
||||
ctl.policy_name = policy;
|
||||
|
||||
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
|
||||
/* change every source the record was built from */
|
||||
ctl.level = XCE_TRACE_LEVEL_SUMMARY;
|
||||
ctl.line_bound = 9999;
|
||||
ctl.session_no = 99;
|
||||
ctl.position = 42;
|
||||
memset(policy, 'x', sizeof(policy) - 1);
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT64(PLAIN_ID(5), rec->jobid);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->err);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_LEVEL_FULL, rec->level);
|
||||
TEST_ASSERT_EQUAL_INT(75, rec->line_bound);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, rec->session_no);
|
||||
TEST_ASSERT_EQUAL_INT(3, rec->position);
|
||||
TEST_ASSERT_FALSE(rec->incomplete);
|
||||
TEST_ASSERT_NOT_NULL(rec->policy_name);
|
||||
TEST_ASSERT_TRUE(rec->policy_name != policy);
|
||||
TEST_ASSERT_EQUAL_STRING("priority-fcfs", rec->policy_name);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->path.depth);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->counters.plans_tried);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->counters.omitted_lines);
|
||||
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_rec_new_null_policy_stays_null(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec;
|
||||
|
||||
ctl.policy_name = NULL;
|
||||
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_RECONSTRUCTED);
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
TEST_ASSERT_NULL(rec->policy_name);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_RECONSTRUCTED, rec->kind);
|
||||
TEST_ASSERT_FALSE(rec->incomplete);
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_rec_new_does_not_keep_ctl_selection(void)
|
||||
{
|
||||
/* The record keeps nothing that points into the ctl: freeing the
|
||||
* ctl's selector array and policy string must leave it intact. */
|
||||
xce_sched_trace_sel_t* sel = calloc(1, sizeof(*sel));
|
||||
char* policy = strdup("fcfs");
|
||||
xce_sched_trace_ctl_t ctl;
|
||||
xce_sched_trace_rec_t* rec;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(sel);
|
||||
TEST_ASSERT_NOT_NULL(policy);
|
||||
sel[0].key = PLAIN_ID(5);
|
||||
ctl = make_ctl(sel, 1);
|
||||
ctl.policy_name = policy;
|
||||
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
free(sel);
|
||||
free(policy);
|
||||
/* ASAN would flag a read of either freed block here */
|
||||
TEST_ASSERT_EQUAL_STRING("fcfs", rec->policy_name);
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_arena_strdup_copies_string(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec
|
||||
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
char* src = strdup("host-n01");
|
||||
char* cp;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
TEST_ASSERT_NOT_NULL(src);
|
||||
cp = sched_trace_arena_strdup(rec, src);
|
||||
TEST_ASSERT_NOT_NULL(cp);
|
||||
TEST_ASSERT_TRUE(cp != src);
|
||||
free(src);
|
||||
TEST_ASSERT_EQUAL_STRING("host-n01", cp);
|
||||
|
||||
/* empty string is a real copy, NULL is NULL without marking the
|
||||
* record incomplete */
|
||||
cp = sched_trace_arena_strdup(rec, "");
|
||||
TEST_ASSERT_NOT_NULL(cp);
|
||||
TEST_ASSERT_EQUAL_STRING("", cp);
|
||||
TEST_ASSERT_NULL(sched_trace_arena_strdup(rec, NULL));
|
||||
TEST_ASSERT_FALSE(rec->incomplete);
|
||||
TEST_ASSERT_NULL(sched_trace_arena_strdup(NULL, "x"));
|
||||
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_arena_memdup_copies_numbers(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec
|
||||
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
float* src = malloc(4 * sizeof(float));
|
||||
float* cp;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
TEST_ASSERT_NOT_NULL(src);
|
||||
src[0] = 1.5f;
|
||||
src[1] = 2.0f;
|
||||
src[2] = -3.25f;
|
||||
src[3] = 1e9f;
|
||||
cp = sched_trace_arena_memdup(rec, src, 4 * sizeof(float));
|
||||
TEST_ASSERT_NOT_NULL(cp);
|
||||
TEST_ASSERT_TRUE(cp != src);
|
||||
src[0] = 0.0f;
|
||||
free(src);
|
||||
TEST_ASSERT_EQUAL_FLOAT(1.5f, cp[0]);
|
||||
TEST_ASSERT_EQUAL_FLOAT(2.0f, cp[1]);
|
||||
TEST_ASSERT_EQUAL_FLOAT(-3.25f, cp[2]);
|
||||
TEST_ASSERT_EQUAL_FLOAT(1e9f, cp[3]);
|
||||
/* storage is suitably aligned for any scalar */
|
||||
TEST_ASSERT_EQUAL_UINT(0, ((uintptr_t)cp) % _Alignof(max_align_t));
|
||||
|
||||
TEST_ASSERT_NULL(sched_trace_arena_memdup(rec, NULL, 8));
|
||||
TEST_ASSERT_FALSE(rec->incomplete);
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_arena_alloc_is_zeroed_and_aligned(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec
|
||||
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
unsigned char* p;
|
||||
size_t i;
|
||||
int k;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
for (k = 0; k < 50; k++) {
|
||||
size_t n = (size_t)(k * 37 + 1);
|
||||
|
||||
p = sched_trace_arena_alloc(rec, n);
|
||||
TEST_ASSERT_NOT_NULL(p);
|
||||
TEST_ASSERT_EQUAL_UINT(0, ((uintptr_t)p) % _Alignof(max_align_t));
|
||||
for (i = 0; i < n; i++) {
|
||||
TEST_ASSERT_EQUAL_UINT8(0, p[i]);
|
||||
}
|
||||
memset(p, 0xa5, n); /* ASAN flags any overlap with a neighbour */
|
||||
}
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_rec_bytes_reports_record_and_arena(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec;
|
||||
size_t b0, b1, b2;
|
||||
int k;
|
||||
|
||||
TEST_ASSERT_EQUAL_UINT(0, sched_trace_rec_bytes(NULL));
|
||||
|
||||
rec = sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
b0 = sched_trace_rec_bytes(rec);
|
||||
TEST_ASSERT_TRUE(b0 >= sizeof(*rec) + strlen("fcfs") + 1);
|
||||
TEST_ASSERT_EQUAL_UINT(sizeof(*rec) + rec->arena_bytes, b0);
|
||||
|
||||
/* a large copy grows the arena by at least its own size */
|
||||
TEST_ASSERT_NOT_NULL(sched_trace_arena_alloc(rec, 1 << 20));
|
||||
b1 = sched_trace_rec_bytes(rec);
|
||||
TEST_ASSERT_TRUE(b1 >= b0 + (1 << 20));
|
||||
|
||||
/* many small copies: size never shrinks and covers everything copied */
|
||||
for (k = 0; k < 1000; k++) {
|
||||
TEST_ASSERT_NOT_NULL(sched_trace_arena_strdup(rec, "0123456789"));
|
||||
}
|
||||
b2 = sched_trace_rec_bytes(rec);
|
||||
TEST_ASSERT_TRUE(b2 >= b1 + 1000 * 11);
|
||||
TEST_ASSERT_EQUAL_UINT(sizeof(*rec) + rec->arena_bytes, b2);
|
||||
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_rec_free_null_is_noop(void)
|
||||
{
|
||||
sched_trace_rec_free(NULL);
|
||||
TEST_PASS();
|
||||
}
|
||||
|
||||
/* ---- record finishing (2.4) ------------------------------------------ */
|
||||
|
||||
static void finish_and_check(xce_sched_trace_rec_kind_t kind, int err)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec
|
||||
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
sched_trace_rec_finish(rec, kind, err);
|
||||
TEST_ASSERT_EQUAL_INT(kind, rec->kind);
|
||||
TEST_ASSERT_EQUAL_INT(err, rec->err);
|
||||
/* the header is otherwise untouched */
|
||||
TEST_ASSERT_EQUAL_UINT64(PLAIN_ID(5), rec->jobid);
|
||||
TEST_ASSERT_EQUAL_INT(3, rec->position);
|
||||
TEST_ASSERT_EQUAL_UINT64(7, rec->session_no);
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
static void test_finish_records_kind_and_error(void)
|
||||
{
|
||||
finish_and_check(XCE_TRACE_REC_NOT_FOUND, -2 /* -ENOENT */);
|
||||
finish_and_check(XCE_TRACE_REC_NO_SCHEDOBJ, 0);
|
||||
finish_and_check(XCE_TRACE_REC_INTERNAL_ERROR, -5 /* -EIO */);
|
||||
finish_and_check(XCE_TRACE_REC_INTERNAL_ERROR, -12 /* -ENOMEM */);
|
||||
finish_and_check(XCE_TRACE_REC_PEND_NO_REASON, 0);
|
||||
}
|
||||
|
||||
static void test_finish_rejects_non_no_decision_kinds(void)
|
||||
{
|
||||
xce_sched_trace_ctl_t ctl = make_ctl(NULL, 0);
|
||||
xce_sched_trace_rec_t* rec
|
||||
= sched_trace_rec_new(&ctl, PLAIN_ID(5), XCE_TRACE_REC_DECISION);
|
||||
|
||||
TEST_ASSERT_NOT_NULL(rec);
|
||||
sched_trace_rec_finish(rec, XCE_TRACE_REC_RECONSTRUCTED, -5);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->err);
|
||||
sched_trace_rec_finish(rec, XCE_TRACE_REC_DECISION, -5);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_TRACE_REC_DECISION, rec->kind);
|
||||
TEST_ASSERT_EQUAL_INT(0, rec->err);
|
||||
sched_trace_rec_finish(NULL, XCE_TRACE_REC_NOT_FOUND, -2); /* no crash */
|
||||
sched_trace_rec_free(rec);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_constants_match_requirements);
|
||||
RUN_TEST(test_wants_null_ctl_is_false);
|
||||
RUN_TEST(test_wants_all_matches_every_job);
|
||||
RUN_TEST(test_wants_empty_selection_matches_nothing);
|
||||
RUN_TEST(test_wants_plain_job_matches_only_that_job);
|
||||
RUN_TEST(test_wants_element_matches_only_that_element);
|
||||
RUN_TEST(test_wants_whole_array_matches_every_element);
|
||||
RUN_TEST(test_wants_unknown_yet_future_ids);
|
||||
RUN_TEST(test_rec_new_null_ctl_returns_null);
|
||||
RUN_TEST(test_rec_new_copies_header_from_ctl);
|
||||
RUN_TEST(test_rec_new_null_policy_stays_null);
|
||||
RUN_TEST(test_rec_new_does_not_keep_ctl_selection);
|
||||
RUN_TEST(test_arena_strdup_copies_string);
|
||||
RUN_TEST(test_arena_memdup_copies_numbers);
|
||||
RUN_TEST(test_arena_alloc_is_zeroed_and_aligned);
|
||||
RUN_TEST(test_rec_bytes_reports_record_and_arena);
|
||||
RUN_TEST(test_rec_free_null_is_noop);
|
||||
RUN_TEST(test_finish_records_kind_and_error);
|
||||
RUN_TEST(test_finish_rejects_non_no_decision_kinds);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue