Skip to content

perf: faster graph export — bundle and impact facts, same answers - #1934

Open
swapnilpaliwal-sd wants to merge 2 commits into
devfrom
perf/graph-export
Open

swapnilpaliwal-sd wants to merge 2 commits into
devfrom
perf/graph-export

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Closes #1933.

Speeds up both graph exports without changing any answer: the bundle (raw relations + IR → graph.sqlite) and the impact facts export (axiomcode-impact --warm). The branch also merges origin/dev, which the integration branch was behind by 25 commits.

Before / after

Medians of interleaved runs (base, new, base, new). Each run is a full axiomcode index with AXIOMCODE_REINDEX=1. "Facts ready" means the index has returned and every background warm-up has finished. The laptop had other load during the runs (1-minute load average 2-13).

6k-file Java project: base new 8.6k-file Java project: base new
bundle stage 40.8 s 28.8 s (-29%) 17.4 s 10.9 s (-37%)
axiomcode index wall 217.3 s 193.6 s 104.2 s 92.9 s
index → impact facts ready 414.9 s 269.1 s (-35%) 134.6 s 105.2 s (-22%)
facts export alone (--warm) ~218 s ~79 s (-64%) ~58 s ~36 s (-38%)
bundle replayed alone (same raw + IR) 75.8 s 42.3 s 25.2 s 14.0 s
graph.sqlite (bundle only) 2.01 GB 1.89 GB 0.87 GB 0.79 GB

Where the time went (profiled, base)

Bundle (6k-file project):

  • The expression IR (559 MB, 1.83 M rows) was parsed one character at a time: 10.6 s.
  • Keyed tables were inserted in hash order: call_sites (510 k rows) took 5.6 s and methods 1.6 s.
  • Index builds took 12.5 s in total. 3.1 s of that was c0 indexes on ext relations, most of which no query reads.
  • Ext inserts took 8.7 s. 4.8 s of that was ext_config_affects_method, with 3.4 M rows; it is read, so it is kept.

Facts export (6k-file project, 218 s):

  • const_values ran the call_sites × call_edges join once for every const it was asked about: 45 runs, 22 M rows fetched, 82 s of SQL plus the Python that went with it.
  • The gen_table scan walked every symbol once per decorator name.
  • Impact.code() blanked comments and strings one character per loop iteration.
  • stub_sites and the route-registration walk each ran twice.
  • The ext_config_affects_method ⋈ symbols join made 3.4 M index probes.

What changed

Bundle

  • readRfc4180 splits a line with no quote on tabs. A line with a quote, and any line a quoted field continues onto, still goes through the character reader, which defines the format. An optional pick skips the split for rows the caller would drop; it is used by the call-site and expression passes.
  • Keyed (WITHOUT ROWID) tables are loaded in key order from a stable-sorted copy. Storage order does not change, and INSERT OR IGNORE still keeps the first of two rows with the same key.
  • The bundle sets page_size = 16384 before the first table. AXIOM_BUNDLE_PAGE_SIZE overrides it. Query time did not change: the 20 measured impact/path queries took the same time on the new graph.
  • The c0 index is built only for ext relations that the query layer reads (EXT_QUERIED in catalog.ts). On Java that is 12 of 39. Every manifest relation is still loaded as an ext table, so the documented schema is unchanged. bundle-test.sh now fails if a script under plugins/, hooks/ or bin/ names an ext relation that is missing from the list.
  • AXIOM_BUNDLE_PROFILE=1 prints the time of each stage to stderr.

Facts export

  • ax_registration.const_value_tables reads the join once for each _Handlers instance. It also keeps one cached copy per open connection, so Impact._export's own const_values call reuses it.
  • gen_table finds the first declaration of each decorator name in a single pass. The factory-base scan skips lines that do not contain the base name.
  • Impact.code() copies a run of characters that cannot open a comment, string or regex in one step, and blanks a span with a single regex.
  • registrations(…, with_own_lines=True) returns the route walk's own-line map as well, so route_site_lines no longer runs separately. G.stub_sites() caches the stub set per connection for both exports.
  • The config rows read ext_config_affects_method in scan order and repeat each row once per symbols row that names its method, which is what the old join returned. ckey is computed once per key.

Not changed (measured, left for a decision)

  • Ext tables that no script reads were not dropped. README and SCHEMA.md promise every manifest relation, and the large unread ones are not exact copies of a core table: call_chain_edge has a column the core table drops, and type_use/field_access turn - into NULL. Dropping them would save about 3.5 s and 280 MB on the 6k-file project.
  • No worker-thread overlap. Building ext tables in a side database and copying them with SQLite's transfer path saved about 2 s on the largest table, at the cost of writing it twice. The bundle is now bound by I/O and B-tree building.
  • The SQLite pragmas cache_size and threads were not kept. They did not help, and index builds were slower with them.

Verification

  • Bundle content: a .dump hash of every table, every row in storage order, and every core index matches base for both projects. The only differences are the ext c0 indexes, as intended, and run.created_at.
  • Facts: all 107 exported fact files are byte-identical on both projects (PYTHONHASHSEED=0; without a fixed seed, base itself writes gen_table.facts in a different order on each run).
  • Answers: impact <t> --json for 10 targets and path <a> <b> --json for 10 pairs on each project, base engine on a base-built graph against new engine on a new-built graph. All 40 stdout and stderr files are byte-identical.
  • Reader: old and new readRfc4180 return identical rows on every IR file of both projects (up to 1.83 M rows each) and on 15 synthetic edge cases: CRLF, quoted newlines, doubled quotes, unterminated quotes, a quote mid-field, and a randomized 300 k-row file with fields that cross chunk boundaries.
  • code(): old and new output is identical on 50,559 files, including Java files read as TS and Python. The two raise on the same files.
  • Case suites (tests/run.py --lang <l> --jobs 3): typescript 352/352, python 445/445, javascript 354/354 and csharp 319/319 pass. Java passes 372/374. The 2 failures are in library-auto-discovers-imported-dependencies, which needs a decompiler that this machine lacks; base fails the same 2 checks.
  • Bundle tests: bundle-test.sh, csv-limit-test.sh and empty-relation-test.sh pass. tsc --noEmit is clean.

swapnilpaliwal-sd and others added 2 commits October 11, 2026 10:57
)

Bundle (raw relations + IR -> graph.sqlite):
- the IR reader splits a quote-free line on tabs and keeps the character
  reader for lines a quote touches; rows the caller drops are skipped before
  they are split (call-site and expression passes)
- keyed (WITHOUT ROWID) tables are loaded in key order (stable sort of a copy)
- 16 KiB pages (AXIOM_BUNDLE_PAGE_SIZE overrides)
- the c0 index only on ext relations the query layer reads (EXT_QUERIED);
  bundle-test fails when a script names one missing from it
- AXIOM_BUNDLE_PROFILE=1 prints per-stage times

Facts export (axiomcode-impact --warm):
- const_values reads its whole-graph tables once, not once per const
- gen_table finds each decorator's first declaration in one pass
- Impact.code() copies runs of plain code whole
- the route walk and stub_sites run once for both exports
- config rows are filtered by a count of symbols rows instead of a join

Every table row, every exported fact and every impact/path --json answer
is byte-identical before and after on both measured projects.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd swapnilpaliwal-sd added the build Build, packaging and developer setup label Oct 11, 2026
@swapnilpaliwal-sd swapnilpaliwal-sd self-assigned this Oct 11, 2026
Base automatically changed from apps/integration-0.1.10 to 0.1.10 October 13, 2026 08:03
Base automatically changed from 0.1.10 to dev October 13, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build, packaging and developer setup

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant