You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
skill/impact: a flat 40 s per query on a large project, and the fast path declines an edit's target shapes #1033
On one open-source TypeScript project of 3,302 code files (37,767 symbols, 267,876 references,
98 MB of exported facts) every impact query costs about 40 seconds, whatever is asked.
$ axiomcode index . --lang typescript
$ time axiomcode impact <any source file> . --limit 3 --depth 1
real 0m36.070s
$ time axiomcode impact Owner.method . --limit 3
real 0m42.744s
It is not the answer size and not the closure walk: --depth 1 and --limit 3 do not move it,
a file with 12 methods and 50 dependents costs the same as one with 216, and a second run of the
same target is no faster, so it is not a cold cache either. It is not repository size, because
every other project measured is two orders of magnitude cheaper.
code files
symbols
facts
impact
3,302
37,767
98 MB
46.9 s mean over 12 targets, 36.1 to 58.9 s
2,316
26,089
48 MB
1.2 s
1,686
22,293
37 MB
2.8 s
783
10,709
20 MB
0.9 s
Root cause, for the half of it that is localised. plugins/axiomcode/hooks/changes.py:55 runs axiomcode-impact with timeout=14, as a fallback for when the SQL fast path declines. That
fast path is graph_sql.impact_shaped (plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py:178), and on this same tree it answers a qualified target in
1.37 s but returns None for a file target and for a bare unqualified method name. Those are the
shapes an edit produces, so on a project this size the hook takes the 40 second path into a 14
second budget and prints "(impact unavailable)" after waiting the full timeout, which is the
behaviour the comment above that line says the fast path was added to prevent.
What is not localised here is why the CLI path itself has a flat 40 second floor on this graph
and 1 to 3 seconds on graphs half its size. The cost does not scale with the fact volume ratio,
so it is a cliff rather than a gradient. It has not been profiled.
The fast path declining a target shape is the part that turns a slow query into no answer.
Narrowed. The cost is the solve, and three plausible causes are ruled out with measurements.
Phase breakdown (AXIOMCODE_PROFILE=1, the 3,300-file project from the report):
[ 0.5s] query facts written
[ 32.8s] souffle returned
32.3 s of 32.8 s is the Soufflé process. Export, target resolution and linking together are 0.5 s. On a 2,300-file project the same phases are 0.2 s and 0.6 s.
Not the closure depth.impact.dl hard-codes d < 40 in five places (lines 527, 528, 548 and the pair below them). Rebuilt with d < 6: 32.2 s against 32.8 s. This also explains the report's observation that --depth 1 does not help. --depth and --limit are applied when printing and never reach the program, so neither narrows the solve.
Not the input volume.ref is the largest relation at 267,876 rows and is read for this target kind (17 of 57 relations are blanked by KIND_SKIPS, ref is not one of them). Emptying it: 33.4 s against 33.3 s. Same for the next five relations by size.
Not the fact export. A cProfile run appears to put 19 s in export and shows 595,391 SQLite calls, 297,668 of them has() re-reading sqlite_master and 297,640 site_file() doing a single-row lookup against a 3,299-row table. Those call counts are real, and both are worth tidying, but the wall-clock is not: measured with a cold export and no profiler attached the whole phase is 0.5 to 0.8 s. The 19 s was the profiler's own per-call overhead on 59 M instrumented calls. Memoising both changed the total by about 0.3 s and perturbed which entries survive the --limit cap, so it is not the fix and I have not proposed it as one.
Where it is not yet narrowed. Something in the program has a fixed cost on this graph that does not scale with any input relation. The cached binary is built without the profiler (-p is unrecognised; it accepts only -F and -D), so the next step is a build with profiling enabled in dl_program.py to get per-rule timings, rather than more guessing from the outside.
Partly addressed on skill/query-frontend; leaving this open for the rest.
Fixed: the fast path declined a retyped parameter.graph_sql.impact resolves its target
by an exact match on display, which no parenthesised target can equal — and Owner.m(p) is
the shape changed emits for a retyped parameter. So that edit was declined, fell through to
the Datalog path inside the hook's timeout=14, and printed (impact unavailable).
Reproduced end to end on a two-file graph:
before: signature formatAmount — cents: number → string (impact unavailable)
after : signature formatAmount — cents: number → string
reads / uses it — resolved callers: [resolved] formatAmount, subtotalLabel, totalLabel
[fast path] reaches 2 more callable(s) …; 0 test(s) reach the change
The parameter is checked to exist rather than assumed, so a name that is not a parameter of
that method is still declined and still belongs to the rules. tests/fastpath.py compares the
two paths as RELATIONS rather than rendered output — a relation empty on both sides reads as a
match — and parity is exact on all four shapes.
Not fixed, and why this stays open.
The flat ~40 s floor is unprofiled and needs that tree to profile.
The larger reason the fast path is silent on real repositories is not the target shape. _has_framework_hops is graph-wide: one registration key edge anywhere, one conftest.py,
or one rebinding decorator declines every target on that graph. Of four indexed
repositories checked here, all four decline wholesale — two through ax_registration.key_edges, two through a conftest plus a rebinding decorator. On those,
a parameter target is declined twice over and the fix above changes nothing.
Narrowing that guard is a separate change whose failure direction is under-reporting a blast
radius, so it is measured and reported here rather than attempted.
Merged in #1096 (ae568a3) on skill/query-frontend. Not auto-closed: the closing keyword only fires on a merge to the default branch, and this surface does not exist there.
tests/run.py: 61 cases, 121 of 123 checks, 0 failures, 2 PENDING (both the known #967 gap).
Partly fixed by #1099, merged into the skills branch: the flat floor is root-caused and cut 7.58s to 0.44s on a 366-file graph, and the file and bare-name shapes now resolve instead of declining. Left open for the remaining half — the command-line path has a separate, still-unprofiled cost on the project in the report, and no corpus here reproduces a project of that size.
Activity
swapnilpaliwal-sd commentedon Sep 19, 2026
Narrowed. The cost is the solve, and three plausible causes are ruled out with measurements.
Phase breakdown (
AXIOMCODE_PROFILE=1, the 3,300-file project from the report):32.3 s of 32.8 s is the Soufflé process. Export, target resolution and linking together are 0.5 s. On a 2,300-file project the same phases are 0.2 s and 0.6 s.
Not the closure depth.
impact.dlhard-codesd < 40in five places (lines 527, 528, 548 and the pair below them). Rebuilt withd < 6: 32.2 s against 32.8 s. This also explains the report's observation that--depth 1does not help.--depthand--limitare applied when printing and never reach the program, so neither narrows the solve.Not the input volume.
refis the largest relation at 267,876 rows and is read for this target kind (17 of 57 relations are blanked byKIND_SKIPS,refis not one of them). Emptying it: 33.4 s against 33.3 s. Same for the next five relations by size.Not the fact export. A cProfile run appears to put 19 s in
exportand shows 595,391 SQLite calls, 297,668 of themhas()re-readingsqlite_masterand 297,640site_file()doing a single-row lookup against a 3,299-row table. Those call counts are real, and both are worth tidying, but the wall-clock is not: measured with a cold export and no profiler attached the whole phase is 0.5 to 0.8 s. The 19 s was the profiler's own per-call overhead on 59 M instrumented calls. Memoising both changed the total by about 0.3 s and perturbed which entries survive the--limitcap, so it is not the fix and I have not proposed it as one.Where it is not yet narrowed. Something in the program has a fixed cost on this graph that does not scale with any input relation. The cached binary is built without the profiler (
-pis unrecognised; it accepts only-Fand-D), so the next step is a build with profiling enabled indl_program.pyto get per-rule timings, rather than more guessing from the outside.Merge pull request #1096 from AxiomCodeAI/fix/1029-1035-skill
swapnilpaliwal-sd commentedon Sep 19, 2026
Partly addressed on
skill/query-frontend; leaving this open for the rest.Fixed: the fast path declined a retyped parameter.
graph_sql.impactresolves its targetby an exact match on
display, which no parenthesised target can equal — andOwner.m(p)isthe shape
changedemits for a retyped parameter. So that edit was declined, fell through tothe Datalog path inside the hook's
timeout=14, and printed(impact unavailable).Reproduced end to end on a two-file graph:
The parameter is checked to exist rather than assumed, so a name that is not a parameter of
that method is still declined and still belongs to the rules.
tests/fastpath.pycompares thetwo paths as RELATIONS rather than rendered output — a relation empty on both sides reads as a
match — and parity is exact on all four shapes.
Not fixed, and why this stays open.
The flat ~40 s floor is unprofiled and needs that tree to profile.
The larger reason the fast path is silent on real repositories is not the target shape.
_has_framework_hopsis graph-wide: one registration key edge anywhere, oneconftest.py,or one rebinding decorator declines every target on that graph. Of four indexed
repositories checked here, all four decline wholesale — two through
ax_registration.key_edges, two through a conftest plus a rebinding decorator. On those,a parameter target is declined twice over and the fix above changes nothing.
Narrowing that guard is a separate change whose failure direction is under-reporting a blast
radius, so it is measured and reported here rather than attempted.
Merged in #1096 (ae568a3) on
skill/query-frontend. Not auto-closed: the closing keyword only fires on a merge to the default branch, and this surface does not exist there.tests/run.py: 61 cases, 121 of 123 checks, 0 failures, 2 PENDING (both the known #967 gap).Merge pull request #1099 from AxiomCodeAI/fix/1033-fastpath-floor-and…
swapnilpaliwal-sd commentedon Sep 20, 2026
Partly fixed by #1099, merged into the skills branch: the flat floor is root-caused and cut 7.58s to 0.44s on a 366-file graph, and the file and bare-name shapes now resolve instead of declining. Left open for the remaining half — the command-line path has a separate, still-unprofiled cost on the project in the report, and no corpus here reproduces a project of that size.
Merge pull request #1786 from AxiomCodeAI/loop/ty-is1033
swapnilpaliwal-sd commentedon Sep 29, 2026
Fixed by #1786 (merged into 0.1.9).
swapnilpaliwal-sd commentedon Sep 29, 2026
Fixed by #1786 (merged into 0.1.9).
impact: walk the import closure back from the reached modules, not ov…