Merge pull request #2079 from DeusData/fix/complexity-pass-deterministic-order

fix(pipeline): make the complexity pass independent of worker id order
This commit is contained in:
Martin Vogel
2026-09-08 00:53:07 +02:00
committed by GitHub
68 changed files with 773 additions and 56 deletions
+156 -56
View File
@@ -99,61 +99,166 @@ static void append_complexity_props(cbm_gbuf_node_t *node, int tld, bool recursi
node->properties_json = neu;
}
/* Content-only node order: qualified_name, then file path and start line.
* Never the temp id: extract workers draw ids from one shared counter, so id
* order is worker-scheduling order and differs run to run. The cycle guard
* flags whichever member the DFS ENTERS first, so both the seed order and the
* callee order must be a function of the inputs alone, or `recursive` flips
* between otherwise identical multi-worker runs. A dangling target (no node
* for the id) has no edges of its own, so where it sorts cannot move a flag;
* it keys as empty strings. */
enum { TLD_CMP_LESS = -1, TLD_CMP_GREATER = 1 };
static const char *str_or_empty(const char *s) {
return s ? s : "";
}
static int cmp_node_canonical(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b) {
int r = strcmp(str_or_empty(a ? a->qualified_name : NULL),
str_or_empty(b ? b->qualified_name : NULL));
if (r != 0) {
return r;
}
r = strcmp(str_or_empty(a ? a->file_path : NULL), str_or_empty(b ? b->file_path : NULL));
if (r != 0) {
return r;
}
int la = a ? a->start_line : 0;
int lb = b ? b->start_line : 0;
if (la != lb) {
return la < lb ? TLD_CMP_LESS : TLD_CMP_GREATER;
}
return 0;
}
static int cmp_seed_canonical(const void *pa, const void *pb) {
return cmp_node_canonical(*(const cbm_gbuf_node_t *const *)pa,
*(const cbm_gbuf_node_t *const *)pb);
}
typedef struct {
const cbm_gbuf_node_t *node; /* NULL for a dangling target id */
int64_t id;
} tld_callee_t;
static int cmp_callee_canonical(const void *pa, const void *pb) {
return cmp_node_canonical(((const tld_callee_t *)pa)->node, ((const tld_callee_t *)pb)->node);
}
/* Traversal state. `callees` is one bump stack shared by every DFS frame: a
* frame takes its out-degree worth of slots, sorts them, recurses, then
* releases them. Nodes on the recursion path are distinct (state 1 blocks
* re-entry), so the live slots never exceed the CALLS edge count the stack is
* sized for. */
typedef struct {
const cbm_gbuf_t *gb;
int64_t maxid;
int *loop_depth;
int *tld;
char *state;
bool *recursive;
cbm_gbuf_node_t **seeds; /* Function/Method nodes, canonical order */
int seed_count;
int seed_cap;
tld_callee_t *callees;
int callee_top;
int callee_cap;
} tld_ctx_t;
static void tld_ctx_free(tld_ctx_t *cx) {
free(cx->loop_depth);
free(cx->tld);
free(cx->state);
free(cx->recursive);
free(cx->seeds);
free(cx->callees);
}
/* Memoized DFS: tld(id) = loop_depth(id) + max over CALLS-callees of tld(callee).
* state: 0=unvisited, 1=in-progress (back-edge → cycle), 2=done. */
static int tld_dfs(const cbm_gbuf_t *gb, int64_t id, const int *loop_depth, int *tld, char *state,
bool *recursive, int64_t maxid, int depth) {
if (id < 1 || id > maxid) {
* state: 0=unvisited, 1=in-progress (back-edge -> cycle), 2=done. */
static int tld_dfs(tld_ctx_t *cx, int64_t id, int depth) {
if (id < 1 || id > cx->maxid) {
return 0;
}
if (state[id] == 2) {
return tld[id];
if (cx->state[id] == 2) {
return cx->tld[id];
}
if (state[id] == 1) {
recursive[id] = true; /* back edge → call-graph cycle */
if (cx->state[id] == 1) {
cx->recursive[id] = true; /* back edge -> call-graph cycle */
return 0;
}
if (depth > CBM_TLD_MAX_DEPTH) {
return loop_depth[id];
return cx->loop_depth[id];
}
state[id] = 1;
int best = 0;
const cbm_gbuf_edge_t **edges = NULL;
int ne = 0;
cbm_gbuf_find_edges_by_source_type(gb, id, "CALLS", &edges, &ne);
cbm_gbuf_find_edges_by_source_type(cx->gb, id, "CALLS", &edges, &ne);
if (ne > cx->callee_cap - cx->callee_top) {
return cx->loop_depth[id]; /* unreachable by construction; same as the depth cap */
}
cx->state[id] = 1;
tld_callee_t *callees = cx->callees + cx->callee_top;
int nc = 0;
for (int i = 0; i < ne; i++) {
int64_t c = edges[i]->target_id;
if (c == id) {
recursive[id] = true; /* direct self-recursion */
cx->recursive[id] = true; /* direct self-recursion */
continue;
}
int ct = tld_dfs(gb, c, loop_depth, tld, state, recursive, maxid, depth + 1);
callees[nc].id = c;
callees[nc].node = cbm_gbuf_find_by_id(cx->gb, c);
nc++;
}
cx->callee_top += nc;
qsort(callees, (size_t)nc, sizeof(*callees), cmp_callee_canonical);
int best = 0;
for (int i = 0; i < nc; i++) {
int ct = tld_dfs(cx, callees[i].id, depth + 1);
if (ct > best) {
best = ct;
}
}
tld[id] = loop_depth[id] + best;
state[id] = 2;
return tld[id];
cx->callee_top -= nc;
cx->tld[id] = cx->loop_depth[id] + best;
cx->state[id] = 2;
return cx->tld[id];
}
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
* remember the node pointer for write-back. The self_recursive seed (set at
* extraction) feeds the final recursive flag; tld_dfs additionally ORs in
* mutual recursion discovered as a call-graph cycle. */
static void seed_loop_depths(const cbm_gbuf_t *gb, const char *label, int *loop_depth,
bool *recursive, cbm_gbuf_node_t **nptr, int64_t maxid) {
static int label_count(const cbm_gbuf_t *gb, const char *label) {
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_label(gb, label, &nodes, &count) != 0) {
return 0;
}
return count;
}
static int calls_edge_count(const cbm_gbuf_t *gb) {
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count) != 0) {
return 0;
}
return count;
}
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
* collect the node as a traversal seed (also the write-back target). The
* self_recursive seed (set at extraction) feeds the final recursive flag;
* tld_dfs additionally ORs in mutual recursion discovered as a call-graph
* cycle. */
static void seed_loop_depths(tld_ctx_t *cx, const char *label) {
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_label(cx->gb, label, &nodes, &count) != 0) {
return;
}
for (int i = 0; i < count; i++) {
for (int i = 0; i < count && cx->seed_count < cx->seed_cap; i++) {
const cbm_gbuf_node_t *n = nodes[i];
if (n->id >= 1 && n->id <= maxid) {
loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
nptr[n->id] = (cbm_gbuf_node_t *)n;
if (n->id >= 1 && n->id <= cx->maxid) {
cx->loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
cx->recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
cx->seeds[cx->seed_count++] = (cbm_gbuf_node_t *)n;
}
}
}
@@ -168,40 +273,35 @@ void cbm_pipeline_pass_complexity(cbm_pipeline_ctx_t *ctx) {
return;
}
size_t sz = (size_t)maxid + 1;
int *loop_depth = calloc(sz, sizeof(int));
int *tld = calloc(sz, sizeof(int));
char *state = calloc(sz, sizeof(char));
bool *recursive = calloc(sz, sizeof(bool));
cbm_gbuf_node_t **nptr = calloc(sz, sizeof(cbm_gbuf_node_t *));
if (!loop_depth || !tld || !state || !recursive || !nptr) {
free(loop_depth);
free(tld);
free(state);
free(recursive);
free(nptr);
tld_ctx_t cx = {0};
cx.gb = gb;
cx.maxid = maxid;
cx.seed_cap = label_count(gb, "Function") + label_count(gb, "Method");
cx.callee_cap = calls_edge_count(gb);
cx.loop_depth = calloc(sz, sizeof(int));
cx.tld = calloc(sz, sizeof(int));
cx.state = calloc(sz, sizeof(char));
cx.recursive = calloc(sz, sizeof(bool));
cx.seeds = calloc((size_t)cx.seed_cap + 1, sizeof(cbm_gbuf_node_t *));
cx.callees = calloc((size_t)cx.callee_cap + 1, sizeof(tld_callee_t));
if (!cx.loop_depth || !cx.tld || !cx.state || !cx.recursive || !cx.seeds || !cx.callees) {
tld_ctx_free(&cx);
return;
}
seed_loop_depths(gb, "Function", loop_depth, recursive, nptr, maxid);
seed_loop_depths(gb, "Method", loop_depth, recursive, nptr, maxid);
seed_loop_depths(&cx, "Function");
seed_loop_depths(&cx, "Method");
qsort(cx.seeds, (size_t)cx.seed_count, sizeof(*cx.seeds), cmp_seed_canonical);
int updated = 0;
for (int64_t id = 1; id <= maxid; id++) {
if (!nptr[id]) {
continue; /* only Function/Method nodes */
for (int i = 0; i < cx.seed_count; i++) {
cbm_gbuf_node_t *n = cx.seeds[i];
if (cx.state[n->id] != 2) {
tld_dfs(&cx, n->id, 0);
}
if (state[id] != 2) {
tld_dfs(gb, id, loop_depth, tld, state, recursive, maxid, 0);
}
append_complexity_props(nptr[id], tld[id], recursive[id]);
updated++;
append_complexity_props(n, cx.tld[n->id], cx.recursive[n->id]);
}
cbm_log_info("pass.complexity", "functions", itoa_cx(updated));
cbm_log_info("pass.complexity", "functions", itoa_cx(cx.seed_count));
free(loop_depth);
free(tld);
free(state);
free(recursive);
free(nptr);
tld_ctx_free(&cx);
}
+6
View File
@@ -0,0 +1,6 @@
from p00_b import pair00_b
def pair00_a(n):
for _ in range(n):
pass
return pair00_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p00_a import pair00_a
def pair00_b(n):
if n <= 0:
return 0
return pair00_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p01_b import pair01_b
def pair01_a(n):
for _ in range(n):
pass
return pair01_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p01_a import pair01_a
def pair01_b(n):
if n <= 0:
return 0
return pair01_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p02_b import pair02_b
def pair02_a(n):
for _ in range(n):
pass
return pair02_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p02_a import pair02_a
def pair02_b(n):
if n <= 0:
return 0
return pair02_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p03_b import pair03_b
def pair03_a(n):
for _ in range(n):
pass
return pair03_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p03_a import pair03_a
def pair03_b(n):
if n <= 0:
return 0
return pair03_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p04_b import pair04_b
def pair04_a(n):
for _ in range(n):
pass
return pair04_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p04_a import pair04_a
def pair04_b(n):
if n <= 0:
return 0
return pair04_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p05_b import pair05_b
def pair05_a(n):
for _ in range(n):
pass
return pair05_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p05_a import pair05_a
def pair05_b(n):
if n <= 0:
return 0
return pair05_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p06_b import pair06_b
def pair06_a(n):
for _ in range(n):
pass
return pair06_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p06_a import pair06_a
def pair06_b(n):
if n <= 0:
return 0
return pair06_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p07_b import pair07_b
def pair07_a(n):
for _ in range(n):
pass
return pair07_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p07_a import pair07_a
def pair07_b(n):
if n <= 0:
return 0
return pair07_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p08_b import pair08_b
def pair08_a(n):
for _ in range(n):
pass
return pair08_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p08_a import pair08_a
def pair08_b(n):
if n <= 0:
return 0
return pair08_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p09_b import pair09_b
def pair09_a(n):
for _ in range(n):
pass
return pair09_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p09_a import pair09_a
def pair09_b(n):
if n <= 0:
return 0
return pair09_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p10_b import pair10_b
def pair10_a(n):
for _ in range(n):
pass
return pair10_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p10_a import pair10_a
def pair10_b(n):
if n <= 0:
return 0
return pair10_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p11_b import pair11_b
def pair11_a(n):
for _ in range(n):
pass
return pair11_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p11_a import pair11_a
def pair11_b(n):
if n <= 0:
return 0
return pair11_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p12_b import pair12_b
def pair12_a(n):
for _ in range(n):
pass
return pair12_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p12_a import pair12_a
def pair12_b(n):
if n <= 0:
return 0
return pair12_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p13_b import pair13_b
def pair13_a(n):
for _ in range(n):
pass
return pair13_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p13_a import pair13_a
def pair13_b(n):
if n <= 0:
return 0
return pair13_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p14_b import pair14_b
def pair14_a(n):
for _ in range(n):
pass
return pair14_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p14_a import pair14_a
def pair14_b(n):
if n <= 0:
return 0
return pair14_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p15_b import pair15_b
def pair15_a(n):
for _ in range(n):
pass
return pair15_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p15_a import pair15_a
def pair15_b(n):
if n <= 0:
return 0
return pair15_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p16_b import pair16_b
def pair16_a(n):
for _ in range(n):
pass
return pair16_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p16_a import pair16_a
def pair16_b(n):
if n <= 0:
return 0
return pair16_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p17_b import pair17_b
def pair17_a(n):
for _ in range(n):
pass
return pair17_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p17_a import pair17_a
def pair17_b(n):
if n <= 0:
return 0
return pair17_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p18_b import pair18_b
def pair18_a(n):
for _ in range(n):
pass
return pair18_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p18_a import pair18_a
def pair18_b(n):
if n <= 0:
return 0
return pair18_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p19_b import pair19_b
def pair19_a(n):
for _ in range(n):
pass
return pair19_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p19_a import pair19_a
def pair19_b(n):
if n <= 0:
return 0
return pair19_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p20_b import pair20_b
def pair20_a(n):
for _ in range(n):
pass
return pair20_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p20_a import pair20_a
def pair20_b(n):
if n <= 0:
return 0
return pair20_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p21_b import pair21_b
def pair21_a(n):
for _ in range(n):
pass
return pair21_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p21_a import pair21_a
def pair21_b(n):
if n <= 0:
return 0
return pair21_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p22_b import pair22_b
def pair22_a(n):
for _ in range(n):
pass
return pair22_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p22_a import pair22_a
def pair22_b(n):
if n <= 0:
return 0
return pair22_a(n)
+6
View File
@@ -0,0 +1,6 @@
from p23_b import pair23_b
def pair23_a(n):
for _ in range(n):
pass
return pair23_b(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from p23_a import pair23_a
def pair23_b(n):
if n <= 0:
return 0
return pair23_a(n)
+6
View File
@@ -0,0 +1,6 @@
from t00_y import tri00_y
def tri00_x(n):
for _ in range(n):
pass
return tri00_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t00_z import tri00_z
def tri00_y(n):
for _ in range(n):
pass
return tri00_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t00_x import tri00_x
def tri00_z(n):
for _ in range(n):
pass
return tri00_x(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t01_y import tri01_y
def tri01_x(n):
for _ in range(n):
pass
return tri01_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t01_z import tri01_z
def tri01_y(n):
for _ in range(n):
pass
return tri01_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t01_x import tri01_x
def tri01_z(n):
for _ in range(n):
pass
return tri01_x(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t02_y import tri02_y
def tri02_x(n):
for _ in range(n):
pass
return tri02_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t02_z import tri02_z
def tri02_y(n):
for _ in range(n):
pass
return tri02_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t02_x import tri02_x
def tri02_z(n):
for _ in range(n):
pass
return tri02_x(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t03_y import tri03_y
def tri03_x(n):
for _ in range(n):
pass
return tri03_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t03_z import tri03_z
def tri03_y(n):
for _ in range(n):
pass
return tri03_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t03_x import tri03_x
def tri03_z(n):
for _ in range(n):
pass
return tri03_x(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t04_y import tri04_y
def tri04_x(n):
for _ in range(n):
pass
return tri04_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t04_z import tri04_z
def tri04_y(n):
for _ in range(n):
pass
return tri04_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t04_x import tri04_x
def tri04_z(n):
for _ in range(n):
pass
return tri04_x(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t05_y import tri05_y
def tri05_x(n):
for _ in range(n):
pass
return tri05_y(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t05_z import tri05_z
def tri05_y(n):
for _ in range(n):
pass
return tri05_z(n - 1)
+6
View File
@@ -0,0 +1,6 @@
from t05_x import tri05_x
def tri05_z(n):
for _ in range(n):
pass
return tri05_x(n - 1)
+221
View File
@@ -2066,6 +2066,226 @@ TEST(pipeline_call_reference_sequential_parallel_edge_set_parity) {
PASS();
}
/* Reproduce-first for the multi-worker determinism gap distilled from #1925:
* the complexity pass walked Function/Method nodes and their CALLS targets in
* temp-id order. Extract workers draw ids from one shared counter, so that
* order is worker-scheduling order, and the cycle guard flags whichever member
* of a mutual-recursion cycle the DFS enters first. Run to run, `recursive`
* therefore flipped between the members of a cycle while the CALLS edge set
* stayed identical -- a violation of the MT-byte-identical invariant. The
* fixture holds 24 two-member and 6 three-member cycles, one function per
* file, whose members are equal-sized (adjacent in the size-ordered work
* queue), which is what makes the flip likely. The
* fixture exceeds MIN_FILES_FOR_PARALLEL; CBM_INDEX_SINGLE_THREAD forces the
* reference run through the sequential path and CBM_WORKERS forces the
* repeated runs through pass_parallel.c, exactly like the parity test above.
* Every multi-worker run must match the sequential one byte for byte. */
enum { CX_ORDER_MT_RUNS = 6, CX_ORDER_LINE_MAX = 512 };
static const char *cx_order_fixture_dir(void) {
return "tests/fixtures/complexity_pass_cycle_order";
}
/* Copy the regular files of a flat fixture directory into dst_dir. Returns the
* number of files copied, or -1 when one could not be read whole. */
static int cx_order_copy_fixture(const char *src_dir, const char *dst_dir) {
cbm_dir_t *d = cbm_opendir(src_dir);
if (!d) {
return -1;
}
int copied = 0;
cbm_dirent_t *entry;
while ((entry = cbm_readdir(d)) != NULL) {
if (entry->name[0] == '.' || entry->is_dir) {
continue;
}
char src[CBM_SZ_1K];
snprintf(src, sizeof(src), "%s/%s", src_dir, entry->name);
FILE *in = cbm_fopen(src, "rb");
if (!in) {
copied = -1;
break;
}
char buf[CBM_SZ_4K];
size_t n = fread(buf, 1, sizeof(buf) - 1, in);
fclose(in);
if (n == sizeof(buf) - 1) {
copied = -1; /* fixture files are tiny; a full buffer means truncation */
break;
}
buf[n] = '\0';
write_temp_file(dst_dir, entry->name, buf);
copied++;
}
cbm_closedir(d);
return copied;
}
static int cx_order_cmp_node_qn(const void *pa, const void *pb) {
const cbm_node_t *a = *(const cbm_node_t *const *)pa;
const cbm_node_t *b = *(const cbm_node_t *const *)pb;
return strcmp(a->qualified_name ? a->qualified_name : "",
b->qualified_name ? b->qualified_name : "");
}
/* One line per Function in qualified-name order: "<qn> <tld> <recursive>".
* Sorting by name keeps DB ids and row order out of the comparison. Returns a
* heap string, or NULL when the store cannot be read. */
static char *cx_order_signature(const char *db_path, const char *project, int *func_count) {
cbm_store_t *s = cbm_store_open_path(db_path);
if (!s) {
return NULL;
}
cbm_node_t *funcs = NULL;
int count = 0;
if (cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &count) != CBM_STORE_OK) {
cbm_store_close(s);
return NULL;
}
const cbm_node_t **sorted = calloc((size_t)count + 1, sizeof(*sorted));
char *sig = calloc((size_t)count + 1, CX_ORDER_LINE_MAX);
if (sorted && sig) {
for (int i = 0; i < count; i++) {
sorted[i] = &funcs[i];
}
qsort(sorted, (size_t)count, sizeof(*sorted), cx_order_cmp_node_qn);
size_t used = 0;
for (int i = 0; i < count; i++) {
const char *props = sorted[i]->properties_json ? sorted[i]->properties_json : "{}";
const char *tld = strstr(props, "\"transitive_loop_depth\":");
const char *rec = strstr(props, "\"recursive\":");
int w = snprintf(sig + used, CX_ORDER_LINE_MAX, "%s %.*s %.*s\n",
sorted[i]->qualified_name ? sorted[i]->qualified_name : "",
tld ? (int)strcspn(tld, ",}") : 0, tld ? tld : "",
rec ? (int)strcspn(rec, ",}") : 0, rec ? rec : "");
if (w < 0) {
w = 0;
} else if (w >= CX_ORDER_LINE_MAX) {
w = CX_ORDER_LINE_MAX - 1;
}
used += (size_t)w;
}
} else {
free(sig);
sig = NULL;
}
free(sorted);
cbm_store_free_nodes(funcs, count);
cbm_store_close(s);
*func_count = count;
return sig;
}
/* First line of `got` that differs from `want`, copied into out (for the
* failure diagnostic). Returns false when the strings are identical. */
static bool cx_order_first_diff(const char *want, const char *got, char *out, size_t cap) {
if (strcmp(want, got) == 0) {
return false;
}
while (*want && *got) {
size_t lw = strcspn(want, "\n");
size_t lg = strcspn(got, "\n");
if (lw != lg || memcmp(want, got, lw) != 0) {
snprintf(out, cap, "want '%.*s' got '%.*s'", (int)lw, want, (int)lg, got);
return true;
}
want += lw + (want[lw] == '\n');
got += lg + (got[lg] == '\n');
}
snprintf(out, cap, "line count differs");
return true;
}
TEST(pipeline_complexity_props_independent_of_worker_order) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_cx_order_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
int copied = cx_order_copy_fixture(cx_order_fixture_dir(), tmp);
if (copied <= 0) {
th_rmtree(tmp);
FAIL("fixture copy");
}
char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/cx_sequential.db", tmp);
cbm_pipeline_t *sequential = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int sequential_rc = sequential ? cbm_pipeline_run(sequential) : -1;
int sequential_funcs = 0;
char *sequential_sig = NULL;
if (sequential && sequential_rc == 0) {
sequential_sig =
cx_order_signature(db_path, cbm_pipeline_project_name(sequential), &sequential_funcs);
}
cbm_pipeline_free(sequential);
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_setenv("CBM_WORKERS", "4", 1);
int parallel_rc[CX_ORDER_MT_RUNS];
char *parallel_sig[CX_ORDER_MT_RUNS];
for (int r = 0; r < CX_ORDER_MT_RUNS; r++) {
snprintf(db_path, sizeof(db_path), "%s/cx_parallel_%d.db", tmp, r);
cbm_pipeline_t *parallel = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
parallel_rc[r] = parallel ? cbm_pipeline_run(parallel) : -1;
parallel_sig[r] = NULL;
if (parallel && parallel_rc[r] == 0) {
int funcs = 0;
parallel_sig[r] =
cx_order_signature(db_path, cbm_pipeline_project_name(parallel), &funcs);
}
cbm_pipeline_free(parallel);
}
if (saved_workers) {
cbm_setenv("CBM_WORKERS", saved_workers, 1);
free(saved_workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
if (saved_single) {
cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1);
free(saved_single);
} else {
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
}
th_rmtree(tmp);
/* Verdicts first, then release, then assert: the assertions must not leak. */
bool cycles_detected = sequential_sig && strstr(sequential_sig, "\"recursive\":true") != NULL;
int mismatch_run = -1;
char diff[CBM_SZ_1K] = "";
for (int r = 0; r < CX_ORDER_MT_RUNS && mismatch_run < 0; r++) {
if (parallel_rc[r] != 0 || !parallel_sig[r]) {
mismatch_run = r;
snprintf(diff, sizeof(diff), "run %d rc=%d", r, parallel_rc[r]);
} else if (sequential_sig &&
cx_order_first_diff(sequential_sig, parallel_sig[r], diff, sizeof(diff))) {
mismatch_run = r;
}
}
free(sequential_sig);
for (int r = 0; r < CX_ORDER_MT_RUNS; r++) {
free(parallel_sig[r]);
}
ASSERT_EQ(sequential_rc, 0);
ASSERT_NOT_NULL(sequential_sig);
ASSERT_GTE(sequential_funcs, copied); /* at least the one function per fixture file */
ASSERT_TRUE(cycles_detected); /* the cycles must reach the pass at all */
if (mismatch_run >= 0) {
printf("\n parallel run %d diverges from sequential: %s\n", mismatch_run, diff);
FAIL("complexity props depend on worker id order");
}
PASS();
}
#ifdef _WIN32
/* utimensat/AT_FDCWD do not exist on Windows. Set the same instant through
* SetFileTime: FILETIME is 100ns ticks since 1601, the same representation
@@ -13587,6 +13807,7 @@ SUITE(pipeline) {
#endif
RUN_TEST(pipeline_env_access_configures_sequential_parallel_parity);
RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity);
RUN_TEST(pipeline_complexity_props_independent_of_worker_order);
RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full);
RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference);
RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids);