Skip to content

perf(build): stage IR with perl, a faster axiomcode-index, and AXIOMCODE_REINDEX=1 rebuilds; same graphs (#1835) - #1965

Open
swapnilpaliwal-sd wants to merge 4 commits into
0.2.1from
perf/perf-staging-index
Open

swapnilpaliwal-sd wants to merge 4 commits into
0.2.1from
perf/perf-staging-index

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1975

Part of #1835.

Three build-time savings. Each one leaves the graph unchanged: on a ~1,300-file Python project, a ~700-file Python project and a Java project, every table has identical rows (rowids included) before and after. Only build metadata differs (built_at, timings, engine paths).

1. Stage IR rows with perl when it is there (graph/pipeline/stage-rows.sh)

The staging filter ran under BSD awk, which took 12 CPU-s across all IR files of the ~1,300-file project. The same filter in perl produces byte-identical output: the per-file and overall md5 match for every staged file on all three projects. It falls back to the same awk program when perl is absent; Git for Windows' bash, which the win32 path uses, ships perl. AXIOM_STAGE_WITH=awk forces the fallback.

staging alone, wall / CPU (s) awk perl
~1,300-file Python 12.84 / 12.10 1.58 / 1.49
~700-file Python 3.33 / 2.94 0.97 / 0.61
Java 2.09 / 1.27 1.32 / 0.58

Test: graph/test/tools/stage-rows-test.sh, run as a preflight of the Java suite. It compares perl and awk output on 14 edge-case fixtures (47 checks), and fails if perl is present but not chosen. A mutant that drops the empty-line case fails 5 checks.

2. axiomcode-index: a faster IR reader, no needless VACUUM, no duplicate indexes

  • ir_rows() replaces csv.DictReader with a line split. It keeps DictReader's semantics exactly: line endings, skipped blank lines, short and long rows. It returns the same dicts on every row of the real IR of all three projects (1.6 M rows).
  • The VACUUM runs only when this run dropped a table or the file has free pages. A freshly bundled graph has none.
  • ce_callee, ce_caller and fa_caller are created only when the bundle did not already index those columns.

axiomcode-index alone on the ~1,300-file project: 13.5 / 10.3 s → 9.3 / 7.7 s (wall / CPU), and the file is 7 MB smaller. Test: tests/index_reader.py (21 fixtures, identical under Python 3.13 and 3.9; two mutants fail it).

3. AXIOMCODE_REINDEX=1 rebuilds a graph whose files are unchanged

It was documented to force a rebuild, but both up-to-date checks in axiomcode-build still answered "graph up to date". A new check in tests/refresh.py fails before the fix and passes after.

End to end

Cold index of the ~1,300-file Python project: CPU 118.6 → 104.7 s (−12%). Wall time was measured under load, so it is noisy.

Suites

  • tests/run.py --lang java: 372/374. --lang python: 437/437 when re-run alone. The 2 Java failures fail the same way on the base.
  • run-suite.sh python: 43/43. run-suite.sh java --no-torture: 79/0.
  • The Java --oracle leg needs JDK 24+, which this machine lacks, so CI is the first to run it.
  • tests/surfaces.py and tests/refresh.py fail only on checks that also fail on the base.
  • Changing run-souffle.sh changes CI's engine cache key, so the first CI run recompiles the engines.

@swapnilpaliwal-sd swapnilpaliwal-sd added bug Something isn't working enhancement New feature or request build Build, packaging and developer setup plugin the axiomcode plugin: skill + MCP layer labels Oct 15, 2026
Staging copies every IR CSV into a Souffle fact file and drops rows whose
tab-separated field count differs from the header's. BSD awk spends most of
that time splitting fields it never reads: on a ~1,300-file Python project
the step took 16-19 s of wall time per build, 13.6 CPU-s of it on the
largest IR file (expressions). perl counts tabs instead and stages the same
bytes (identical md5 over every staged file of three projects, two Python
and one Java) at about a tenth of the CPU.

The filter moves to graph/pipeline/stage-rows.sh. It picks perl when it runs
(macOS, mainstream Linux, and Git for Windows' bash, which is the shell the
pipeline runs under on win32) and falls back to the same awk program where
perl is missing, so no machine loses the step. AXIOM_STAGE_WITH=awk forces
the fallback. graph/test/tools/stage-rows-test.sh holds both to the same
stdout and stderr on the edge cases (trailing tabs, empty lines, CRLF, no
final newline, torn rows) and runs as a Java suite preflight.

Refs #1835
… nothing to reclaim, do not rebuild bundle indexes

On a ~1,300-file Python project axiomcode-index took 12-14 s. Three parts
of that bought nothing:

- csv.DictReader over the IR. The reader is QUOTE_NONE, so a row is one
  line and a field is the text between tabs; ir_rows() does exactly that
  with str.split and dict(zip()) at about half the cost. It keeps the
  reader's semantics where they are not obvious: \r, \n and \r\n each end
  a row, blank lines are skipped, a short row gets None for the missing
  columns and a long one keeps the rest under the key None.
  tests/index_reader.py compares the two on fixtures (quoted tabs and
  newlines, CRLF, lone CR, repeated header names, invalid UTF-8) and on
  any IR directory given to it.
- VACUUM of a database the bundle wrote seconds earlier. It now runs only
  when this run dropped a table it recreates (a re-index) or the file has
  free pages; a freshly bundled graph has neither.
- ce_caller, ce_callee and fa_caller duplicate the bundle's own
  idx_call_edges_caller_id, idx_call_edges_callee_method_id and
  idx_field_access_caller_id. They are created only when the graph has no
  index on that column, so an older graph keeps getting them.

graph.sqlite holds the same rows in every table (rowids included) on two
Python projects and one Java project; only index_meta.built_at and the
three duplicate indexes differ.

Refs #1835
The build's messages, SKILL.md and ax_fresh all describe
AXIOMCODE_REINDEX=1 as a rebuild ("rebuilds it with this one", "to
force"). It skipped the newer-build shortcut and the remembered languages,
but both up-to-date checks still answered "graph up to date" whenever the
files matched the file table, so it rebuilt nothing. Both checks now step
aside when it is set.

tests/refresh.py checks that `index` with AXIOMCODE_REINDEX=1 rebuilds
over an unchanged tree; the existing check beside it, the same `index`
without it, is the control.

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

Labels

bug Something isn't working build Build, packaging and developer setup enhancement New feature or request plugin the axiomcode plugin: skill + MCP layer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant