Problem
Roughly 48% of the L3/L4 dataflow layer is serial whole-program work — compute_summaries (33.8s) plus assemble_sdg (28.8s) on erpnext L4, out of a 228.6s run. #154 established that parallelising the other half is a dead end (measured 3.1× slower), so this is where the remaining time is.
Measurement says the serial tail is not fundamentally expensive — it is doing the same work three times.
The redundancy, measured
Instrumenting solve_function (the DDG re-derivation + reaching-defs + flow derivation that both phases call) on the flask fixture, 386 functions:
| phase |
solve_function calls |
compute_summaries |
772 (exactly 2× per function) |
assemble_sdg |
386 (1× per function) |
| total |
1,158 = 3.0× the 386 actually needed |
solve_function accounts for 95% of the serial tail's wall time, so the call count is very nearly the cost.
Two independent causes:
-
Singleton SCCs are solved twice. compute_summaries runs while changed: for sig in members: .... For a non-recursive function the first pass computes the summary and sets changed=True; the second pass recomputes an identical summary purely to observe convergence. Singleton SCCs are the overwhelming majority of functions.
-
assemble_sdg re-solves from scratch. compute_summaries discards its own intermediate results (new, _, _ = solve_function(...)), then assemble_sdg calls solve_function(info, summaries) again for every signature to recover the facts and DDG it needs.
Proposed fix
- Solve singleton SCCs once. An SCC of one member with no self-edge cannot change on a second pass — nothing it depends on is still moving, because the condensation DAG is processed bottom-up. Guard the re-iteration on
len(members) > 1 or has_self_edge.
- Reuse the converged solve. Have
compute_summaries retain the (facts, ddg) from each function's final solve and hand it to assemble_sdg, which currently recomputes it. This is sound because at convergence every member was solved against the final summaries: the last pass is the one in which nothing changed, and lower SCCs were already final.
Expected: 3 solves per function → 1, i.e. up to a 3× reduction of the serial tail, ~18% off L4 wall on erpnext and proportionally more at odoo-full scale, where the tail is larger.
CAVEATS
DEFINITION OF DONE
Problem
Roughly 48% of the L3/L4 dataflow layer is serial whole-program work —
compute_summaries(33.8s) plusassemble_sdg(28.8s) on erpnext L4, out of a 228.6s run. #154 established that parallelising the other half is a dead end (measured 3.1× slower), so this is where the remaining time is.Measurement says the serial tail is not fundamentally expensive — it is doing the same work three times.
The redundancy, measured
Instrumenting
solve_function(the DDG re-derivation + reaching-defs + flow derivation that both phases call) on the flask fixture, 386 functions:compute_summariesassemble_sdgsolve_functionaccounts for 95% of the serial tail's wall time, so the call count is very nearly the cost.Two independent causes:
Singleton SCCs are solved twice.
compute_summariesrunswhile changed: for sig in members: .... For a non-recursive function the first pass computes the summary and setschanged=True; the second pass recomputes an identical summary purely to observe convergence. Singleton SCCs are the overwhelming majority of functions.assemble_sdgre-solves from scratch.compute_summariesdiscards its own intermediate results (new, _, _ = solve_function(...)), thenassemble_sdgcallssolve_function(info, summaries)again for every signature to recover the facts and DDG it needs.Proposed fix
len(members) > 1 or has_self_edge.compute_summariesretain the(facts, ddg)from each function's final solve and hand it toassemble_sdg, which currently recomputes it. This is sound because at convergence every member was solved against the final summaries: the last pass is the one in which nothing changed, and lower SCCs were already final.Expected: 3 solves per function → 1, i.e. up to a 3× reduction of the serial tail, ~18% off L4 wall on erpnext and proportionally more at odoo-full scale, where the tail is larger.
CAVEATS
cfg/cdg/ddg/summarysizes and content,param_in/param_out, andprovdistribution, with a serial-vs-serial control run to establish the noise floor.assemble_sdgis ever called with summaries that were not produced by the immediately precedingcompute_summariesover the sameinfos, the cache must be bypassed. Keep the recompute path as the default and take the cache only when explicitly supplied.DEFINITION OF DONE
solve_functioncall count drops to 1 per function on the flask fixture (assert in a test, so the redundancy cannot silently return).