Skip to content

skill/impact: a flat 40 s per query on a large project, and the fast path declines an edit's target shapes #1033

Closed

Description

@swapnilpaliwal-sd

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.

Activity

  1. added
    bugSomething isn't working
    skillAgent-facing query skill (skills/axiomcode)
    on Sep 19, 2026
  2. swapnilpaliwal-sd commented on Sep 19, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    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.

  3. added a commit that references this issue on Sep 19, 2026
  4. swapnilpaliwal-sd commented on Sep 19, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    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.

    1. The flat ~40 s floor is unprofiled and needs that tree to profile.

    2. 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).

  5. added a commit that references this issue on Sep 20, 2026
  6. swapnilpaliwal-sd commented on Sep 20, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    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.

  7. added a commit that references this issue on Sep 29, 2026
  8. swapnilpaliwal-sd commented on Sep 29, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    Fixed by #1786 (merged into 0.1.9).

  9. swapnilpaliwal-sd commented on Sep 29, 2026

    @swapnilpaliwal-sd
    ContributorAuthor

    Fixed by #1786 (merged into 0.1.9).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingskillAgent-facing query skill (skills/axiomcode)typescriptTypeScript

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions