Skip to content

Commit 29dbf0b

Browse files
authored
Merge pull request #2079 from DeusData/fix/complexity-pass-deterministic-order
fix(pipeline): make the complexity pass independent of worker id order
2 parents 760c08f + dab58c0 commit 29dbf0b

68 files changed

Lines changed: 773 additions & 56 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/pipeline/pass_complexity.c‎

Lines changed: 156 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -99,61 +99,166 @@ static void append_complexity_props(cbm_gbuf_node_t *node, int tld, bool recursi
9999
node->properties_json = neu;
100100
}
101101

102+
/* Content-only node order: qualified_name, then file path and start line.
103+
* Never the temp id: extract workers draw ids from one shared counter, so id
104+
* order is worker-scheduling order and differs run to run. The cycle guard
105+
* flags whichever member the DFS ENTERS first, so both the seed order and the
106+
* callee order must be a function of the inputs alone, or `recursive` flips
107+
* between otherwise identical multi-worker runs. A dangling target (no node
108+
* for the id) has no edges of its own, so where it sorts cannot move a flag;
109+
* it keys as empty strings. */
110+
enum { TLD_CMP_LESS = -1, TLD_CMP_GREATER = 1 };
111+
112+
static const char *str_or_empty(const char *s) {
113+
return s ? s : "";
114+
}
115+
116+
static int cmp_node_canonical(const cbm_gbuf_node_t *a, const cbm_gbuf_node_t *b) {
117+
int r = strcmp(str_or_empty(a ? a->qualified_name : NULL),
118+
str_or_empty(b ? b->qualified_name : NULL));
119+
if (r != 0) {
120+
return r;
121+
}
122+
r = strcmp(str_or_empty(a ? a->file_path : NULL), str_or_empty(b ? b->file_path : NULL));
123+
if (r != 0) {
124+
return r;
125+
}
126+
int la = a ? a->start_line : 0;
127+
int lb = b ? b->start_line : 0;
128+
if (la != lb) {
129+
return la < lb ? TLD_CMP_LESS : TLD_CMP_GREATER;
130+
}
131+
return 0;
132+
}
133+
134+
static int cmp_seed_canonical(const void *pa, const void *pb) {
135+
return cmp_node_canonical(*(const cbm_gbuf_node_t *const *)pa,
136+
*(const cbm_gbuf_node_t *const *)pb);
137+
}
138+
139+
typedef struct {
140+
const cbm_gbuf_node_t *node; /* NULL for a dangling target id */
141+
int64_t id;
142+
} tld_callee_t;
143+
144+
static int cmp_callee_canonical(const void *pa, const void *pb) {
145+
return cmp_node_canonical(((const tld_callee_t *)pa)->node, ((const tld_callee_t *)pb)->node);
146+
}
147+
148+
/* Traversal state. `callees` is one bump stack shared by every DFS frame: a
149+
* frame takes its out-degree worth of slots, sorts them, recurses, then
150+
* releases them. Nodes on the recursion path are distinct (state 1 blocks
151+
* re-entry), so the live slots never exceed the CALLS edge count the stack is
152+
* sized for. */
153+
typedef struct {
154+
const cbm_gbuf_t *gb;
155+
int64_t maxid;
156+
int *loop_depth;
157+
int *tld;
158+
char *state;
159+
bool *recursive;
160+
cbm_gbuf_node_t **seeds; /* Function/Method nodes, canonical order */
161+
int seed_count;
162+
int seed_cap;
163+
tld_callee_t *callees;
164+
int callee_top;
165+
int callee_cap;
166+
} tld_ctx_t;
167+
168+
static void tld_ctx_free(tld_ctx_t *cx) {
169+
free(cx->loop_depth);
170+
free(cx->tld);
171+
free(cx->state);
172+
free(cx->recursive);
173+
free(cx->seeds);
174+
free(cx->callees);
175+
}
176+
102177
/* Memoized DFS: tld(id) = loop_depth(id) + max over CALLS-callees of tld(callee).
103-
* state: 0=unvisited, 1=in-progress (back-edge → cycle), 2=done. */
104-
static int tld_dfs(const cbm_gbuf_t *gb, int64_t id, const int *loop_depth, int *tld, char *state,
105-
bool *recursive, int64_t maxid, int depth) {
106-
if (id < 1 || id > maxid) {
178+
* state: 0=unvisited, 1=in-progress (back-edge -> cycle), 2=done. */
179+
static int tld_dfs(tld_ctx_t *cx, int64_t id, int depth) {
180+
if (id < 1 || id > cx->maxid) {
107181
return 0;
108182
}
109-
if (state[id] == 2) {
110-
return tld[id];
183+
if (cx->state[id] == 2) {
184+
return cx->tld[id];
111185
}
112-
if (state[id] == 1) {
113-
recursive[id] = true; /* back edge → call-graph cycle */
186+
if (cx->state[id] == 1) {
187+
cx->recursive[id] = true; /* back edge -> call-graph cycle */
114188
return 0;
115189
}
116190
if (depth > CBM_TLD_MAX_DEPTH) {
117-
return loop_depth[id];
191+
return cx->loop_depth[id];
118192
}
119-
state[id] = 1;
120-
int best = 0;
121193
const cbm_gbuf_edge_t **edges = NULL;
122194
int ne = 0;
123-
cbm_gbuf_find_edges_by_source_type(gb, id, "CALLS", &edges, &ne);
195+
cbm_gbuf_find_edges_by_source_type(cx->gb, id, "CALLS", &edges, &ne);
196+
if (ne > cx->callee_cap - cx->callee_top) {
197+
return cx->loop_depth[id]; /* unreachable by construction; same as the depth cap */
198+
}
199+
cx->state[id] = 1;
200+
tld_callee_t *callees = cx->callees + cx->callee_top;
201+
int nc = 0;
124202
for (int i = 0; i < ne; i++) {
125203
int64_t c = edges[i]->target_id;
126204
if (c == id) {
127-
recursive[id] = true; /* direct self-recursion */
205+
cx->recursive[id] = true; /* direct self-recursion */
128206
continue;
129207
}
130-
int ct = tld_dfs(gb, c, loop_depth, tld, state, recursive, maxid, depth + 1);
208+
callees[nc].id = c;
209+
callees[nc].node = cbm_gbuf_find_by_id(cx->gb, c);
210+
nc++;
211+
}
212+
cx->callee_top += nc;
213+
qsort(callees, (size_t)nc, sizeof(*callees), cmp_callee_canonical);
214+
int best = 0;
215+
for (int i = 0; i < nc; i++) {
216+
int ct = tld_dfs(cx, callees[i].id, depth + 1);
131217
if (ct > best) {
132218
best = ct;
133219
}
134220
}
135-
tld[id] = loop_depth[id] + best;
136-
state[id] = 2;
137-
return tld[id];
221+
cx->callee_top -= nc;
222+
cx->tld[id] = cx->loop_depth[id] + best;
223+
cx->state[id] = 2;
224+
return cx->tld[id];
138225
}
139226

140-
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
141-
* remember the node pointer for write-back. The self_recursive seed (set at
142-
* extraction) feeds the final recursive flag; tld_dfs additionally ORs in
143-
* mutual recursion discovered as a call-graph cycle. */
144-
static void seed_loop_depths(const cbm_gbuf_t *gb, const char *label, int *loop_depth,
145-
bool *recursive, cbm_gbuf_node_t **nptr, int64_t maxid) {
227+
static int label_count(const cbm_gbuf_t *gb, const char *label) {
146228
const cbm_gbuf_node_t **nodes = NULL;
147229
int count = 0;
148230
if (cbm_gbuf_find_by_label(gb, label, &nodes, &count) != 0) {
231+
return 0;
232+
}
233+
return count;
234+
}
235+
236+
static int calls_edge_count(const cbm_gbuf_t *gb) {
237+
const cbm_gbuf_edge_t **edges = NULL;
238+
int count = 0;
239+
if (cbm_gbuf_find_edges_by_type(gb, "CALLS", &edges, &count) != 0) {
240+
return 0;
241+
}
242+
return count;
243+
}
244+
245+
/* Seed each Function/Method node's loop_depth and self_recursive flag, and
246+
* collect the node as a traversal seed (also the write-back target). The
247+
* self_recursive seed (set at extraction) feeds the final recursive flag;
248+
* tld_dfs additionally ORs in mutual recursion discovered as a call-graph
249+
* cycle. */
250+
static void seed_loop_depths(tld_ctx_t *cx, const char *label) {
251+
const cbm_gbuf_node_t **nodes = NULL;
252+
int count = 0;
253+
if (cbm_gbuf_find_by_label(cx->gb, label, &nodes, &count) != 0) {
149254
return;
150255
}
151-
for (int i = 0; i < count; i++) {
256+
for (int i = 0; i < count && cx->seed_count < cx->seed_cap; i++) {
152257
const cbm_gbuf_node_t *n = nodes[i];
153-
if (n->id >= 1 && n->id <= maxid) {
154-
loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
155-
recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
156-
nptr[n->id] = (cbm_gbuf_node_t *)n;
258+
if (n->id >= 1 && n->id <= cx->maxid) {
259+
cx->loop_depth[n->id] = json_get_int(n->properties_json, "loop_depth", 0);
260+
cx->recursive[n->id] = json_get_bool(n->properties_json, "self_recursive");
261+
cx->seeds[cx->seed_count++] = (cbm_gbuf_node_t *)n;
157262
}
158263
}
159264
}
@@ -168,40 +273,35 @@ void cbm_pipeline_pass_complexity(cbm_pipeline_ctx_t *ctx) {
168273
return;
169274
}
170275
size_t sz = (size_t)maxid + 1;
171-
int *loop_depth = calloc(sz, sizeof(int));
172-
int *tld = calloc(sz, sizeof(int));
173-
char *state = calloc(sz, sizeof(char));
174-
bool *recursive = calloc(sz, sizeof(bool));
175-
cbm_gbuf_node_t **nptr = calloc(sz, sizeof(cbm_gbuf_node_t *));
176-
if (!loop_depth || !tld || !state || !recursive || !nptr) {
177-
free(loop_depth);
178-
free(tld);
179-
free(state);
180-
free(recursive);
181-
free(nptr);
276+
tld_ctx_t cx = {0};
277+
cx.gb = gb;
278+
cx.maxid = maxid;
279+
cx.seed_cap = label_count(gb, "Function") + label_count(gb, "Method");
280+
cx.callee_cap = calls_edge_count(gb);
281+
cx.loop_depth = calloc(sz, sizeof(int));
282+
cx.tld = calloc(sz, sizeof(int));
283+
cx.state = calloc(sz, sizeof(char));
284+
cx.recursive = calloc(sz, sizeof(bool));
285+
cx.seeds = calloc((size_t)cx.seed_cap + 1, sizeof(cbm_gbuf_node_t *));
286+
cx.callees = calloc((size_t)cx.callee_cap + 1, sizeof(tld_callee_t));
287+
if (!cx.loop_depth || !cx.tld || !cx.state || !cx.recursive || !cx.seeds || !cx.callees) {
288+
tld_ctx_free(&cx);
182289
return;
183290
}
184291

185-
seed_loop_depths(gb, "Function", loop_depth, recursive, nptr, maxid);
186-
seed_loop_depths(gb, "Method", loop_depth, recursive, nptr, maxid);
292+
seed_loop_depths(&cx, "Function");
293+
seed_loop_depths(&cx, "Method");
294+
qsort(cx.seeds, (size_t)cx.seed_count, sizeof(*cx.seeds), cmp_seed_canonical);
187295

188-
int updated = 0;
189-
for (int64_t id = 1; id <= maxid; id++) {
190-
if (!nptr[id]) {
191-
continue; /* only Function/Method nodes */
192-
}
193-
if (state[id] != 2) {
194-
tld_dfs(gb, id, loop_depth, tld, state, recursive, maxid, 0);
296+
for (int i = 0; i < cx.seed_count; i++) {
297+
cbm_gbuf_node_t *n = cx.seeds[i];
298+
if (cx.state[n->id] != 2) {
299+
tld_dfs(&cx, n->id, 0);
195300
}
196-
append_complexity_props(nptr[id], tld[id], recursive[id]);
197-
updated++;
301+
append_complexity_props(n, cx.tld[n->id], cx.recursive[n->id]);
198302
}
199303

200-
cbm_log_info("pass.complexity", "functions", itoa_cx(updated));
304+
cbm_log_info("pass.complexity", "functions", itoa_cx(cx.seed_count));
201305

202-
free(loop_depth);
203-
free(tld);
204-
free(state);
205-
free(recursive);
206-
free(nptr);
306+
tld_ctx_free(&cx);
207307
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p00_b import pair00_b
2+
3+
def pair00_a(n):
4+
for _ in range(n):
5+
pass
6+
return pair00_b(n - 1)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p00_a import pair00_a
2+
3+
def pair00_b(n):
4+
if n <= 0:
5+
return 0
6+
return pair00_a(n)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p01_b import pair01_b
2+
3+
def pair01_a(n):
4+
for _ in range(n):
5+
pass
6+
return pair01_b(n - 1)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p01_a import pair01_a
2+
3+
def pair01_b(n):
4+
if n <= 0:
5+
return 0
6+
return pair01_a(n)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p02_b import pair02_b
2+
3+
def pair02_a(n):
4+
for _ in range(n):
5+
pass
6+
return pair02_b(n - 1)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p02_a import pair02_a
2+
3+
def pair02_b(n):
4+
if n <= 0:
5+
return 0
6+
return pair02_a(n)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p03_b import pair03_b
2+
3+
def pair03_a(n):
4+
for _ in range(n):
5+
pass
6+
return pair03_b(n - 1)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p03_a import pair03_a
2+
3+
def pair03_b(n):
4+
if n <= 0:
5+
return 0
6+
return pair03_a(n)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from p04_b import pair04_b
2+
3+
def pair04_a(n):
4+
for _ in range(n):
5+
pass
6+
return pair04_b(n - 1)

0 commit comments

Comments
 (0)