Skip to content

plugin: compile the query rules once per machine, never while a query waits (#1606) - #1634

Merged
swapnilpaliwal-sd merged 1 commit into
0.1.5from
fix/1606-query-rule-cache
Sep 28, 2026
Merged

swapnilpaliwal-sd merged 1 commit into
0.1.5from
fix/1606-query-rule-cache

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1606

What was wrong

  • The compiled query programs (dl/impact.dl, dl/path*.dl) were cached inside the plugin, in dl/.cache/. Every plugin copy recompiled byte-identical rules: a plugin update installs a new versioned directory, and a second checkout or a worktree is another copy. On this machine impact.dl alone takes 43-56 s to compile.
  • axiomcode-build compiled them at the very end, after the graph was published. Since index: publish each language's graph as soon as it is solved (#1555) #1618 publishes the main graph as soon as it is solved, the first query arrives while that compile has not started, compiles the rules itself, and waits. Measured: 68-79 s for the first impact on a fresh index.
  • The compile note said "~20 s" and was printed only by the query that compiled.
  • C# (and every language): impact's sampled list of callers on an untyped receiver was ordered by symbol id. Ids are not stable between two builds of the same tree, so the sample changed from build to build.

The change

  • dl_program.py: compiled programs go to ${XDG_CACHE_HOME:-~/.cache}/axiomcode/queries/, beside the engine's Soufflé cache. AXIOMCODE_QUERY_CACHE overrides the location, and the plugin directory is the fallback when there is no writable HOME. The key is the rules hash, as before (CI's --print-id is unchanged), plus the platform, because the binary is built with -march=native. A binary an older plugin left in dl/.cache/ is still used.
  • A query never waits for a compile. With no binary, program() answers from the Soufflé interpreter, which runs the same rules and gives the same answer, and it starts one detached background compile. The compile runs in its own session, under a per-program lock in the cache, so a hook's timeout=14 cannot kill it half-way. A failed compile is recorded once, as before (.nocompile). A binary shipped in the engine package still wins over everything else.
  • axiomcode-build starts that compile when the build starts (dl_program.py --background), in parallel with the engine. After publishing, it only reports the state (--no-wait, e.g. impact.dl=compiling) and never blocks. This builds directly on index: publish each language's graph as soon as it is solved (#1555) #1618's publish path: index: publish each language's graph as soon as it is solved (#1555) #1618 made the graph available early, and this makes the rules available by then, or makes their absence cost nothing. The facts export (impact --warm) after publish is unchanged.
  • axiomcode-impact: the untyped-receiver sample (text and --json byname_callers) is ordered by file, line, then name.
  • reference/impact.md (both copies) describes the new cache and the interpreter fallback.

Tests

  • New tests/query_rules.py (13 checks, no engine, needs soufflé and c++). Checks: a first query answers from the interpreter in 0.1 s and says so; one background compile lands in the user cache and nothing is written in the plugin; the next query runs the binary; a second plugin copy reuses it; control: rules one line apart do not; a legacy dl/.cache binary is used; three simultaneous queries leave exactly one binary and no lock, .cpp or .tmp; the interpreter and the binary derive the same rows; warm() reports.
  • New case python/untyped-receiver-sample-in-source-order: the sample lists the first four callers in source order and leaves out the fifth. Control: every by-name caller is still a row. Before this change the case fails (its sample is zeta, gamma, beta, alpha).
  • tests/run.py on 0.1.5: python 103/103, csharp 26/26, java 137/138. The one Java miss is bean-registered-or-defined-by-a-class, from java: Spring Boot registrations make beans, and impact names who registers or is injected with them (#1392, #1396, #1414) #1629: the local parser build predates java: Spring Boot registrations make beans, and impact names who registers or is injected with them (#1392, #1396, #1414) #1629's parser change, and CI builds its own. Before the rebase: java 134/134. publish_order.py all passed, latency.py --no-engine 18/18, surfaces.py ok, manifests.py ok.
  • refresh_races.py builds a TypeScript fixture, so it was not run locally (JS/TS runs are left to CI).

Smoke (real copies, fresh index, cold rules cache, solve cache off)

A 182-file Python web app, a 403-file Java server and a 254-file C# shop, one impact target each. "Before" is origin/0.1.4 with #1618. The dl_program/axiomcode-build code is identical at 0.1.5. Machine load average ~30.

First query the moment the graph is published, then the second query:

graph at 1st query before → after index done before → after 2nd query before → after
Python 101 s / 100 s 68.5 s → 12.9 s 195 s → 113 s 1.9 s → 1.7 s
Java 55 s / 55 s 78.6 s → 9.9 s 160 s → 65 s 0.9 s → 1.5 s
C# 17 s / 16 s 73.4 s → 2.8 s 118 s → 19 s 0.5 s → 0.9 s

The remaining 10-13 s on Python and Java is the graph's facts export, which runs right after publish. The rules were already compiled (Python) or ran in the interpreter (Java, C#).

An existing graph with no compiled rules (a plugin update or a new copy):

1st query before → after 2nd query before → after after the background compile (40-56 s later)
Python 49.4 s → 1.5 s 1.0 s → 1.4 s 1.3 s
Java 43.3 s → 1.4 s 0.7 s → 2.0 s 2.3 s
C# 48.7 s → 2.4 s 1.0 s → 3.0 s 1.0 s

Controls:

  • Answers unchanged: impact --page all on 4 targets across the 3 projects is identical before and after, except the sample line. On every project the interpreter's answer is byte-identical to the compiled binary's.
  • Determinism: C# impact of a JSON helper with 7 untyped-receiver callers, on two builds of the same tree gives two different samples before and the same sample after.

Not yet measured on the full corpus or on held-out projects: no held-out or regression-gate run for this change.

… waits (#1606)

The compiled query programs (impact.dl, path*.dl) were cached inside the
plugin's own dl/.cache, so every plugin copy recompiled identical rules,
and the build compiled them only after publishing the graph, so the
first query raced the compile and paid it (43-79 s).

- dl_program: cache in ${XDG_CACHE_HOME:-~/.cache}/axiomcode/queries,
  keyed by the rules and the platform; an older dl/.cache binary is still
  used. A query with no binary answers from the Souffle interpreter and
  starts one detached background compile under a lock.
- axiomcode-build starts that compile when the build starts, beside the
  engine, and only reports its state after publishing.
- impact: the untyped-receiver caller sample is ordered by file and line,
  not by symbol id, so two builds of one tree show the same sample.
- tests/query_rules.py and a python case with a control.
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit df85476 into 0.1.5 Sep 28, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the fix/1606-query-rule-cache branch September 28, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant