From 6dde0de526b3ce290b15f5e83c2ffe643fe9db97 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:50:06 -0700 Subject: [PATCH] =?UTF-8?q?impact/path:=20the=20facts=20export=20stops=20p?= =?UTF-8?q?aying=20per-row=20SQL=20=E2=80=94=20171=20s=20to=2055=20s=20on?= =?UTF-8?q?=208,619=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three wastes, found by timing each exported relation and profiling the poles standalone: - site_file() ran one has() and one paths lookup PER CALL, and the export calls it for every call edge: 638k round-trips over 319k edges. The paths map is now read once per graph (171 s -> 89 s). - via_base_rows probes field_access per single-target call site, and the table had no caller_id index: a 65,797-row scan 5,162 times. fa_caller lands with the other build-time indexes (89 s -> 63 s). - registrations() was computed twice in one export, once for the registration relation and again inside reg_key_fact (63 s -> 55 s). Every fact file is content-identical before and after (two differ in row order only, written from unsorted sets before this change too). Tests: java 329/329, python 287/287, facts_cache, latency, freshness, export_singleflight, front_door. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../skills/axiomcode/scripts/axiomcode-impact | 5 +++-- .../skills/axiomcode/scripts/axiomcode-index | 3 +++ .../skills/axiomcode/scripts/axiomcode-path | 14 ++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact index 191cb9f4..609a53b7 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact @@ -1517,7 +1517,8 @@ class Impact: L = self.code(r['file']); text = L[r['line'] - 1] if r['line'] <= len(L) else '' for qn in re.findall(rf'([A-Za-z_$][\w$]*)\s*\.\s*{re.escape(r["name"])}\b', text): quals.append((r['file'], r['line'], r['name'], qn)) W('ref', refs); W('qualifier', sorted(set(quals))) - W('registration', ax_registration.registrations(g.q, g.site_file)) + regs = ax_registration.registrations(g.q, g.site_file) # computed once: reg_key_fact below reads it too + W('registration', regs) trs = []; trf = set() # a reference on a module-level type alias's own lines belongs to the alias, not the module (#784) import graph_sql @@ -1599,7 +1600,7 @@ class Impact: regk = [] # `rd` and not `decl`: `decl` is the dec_literal list above for rd, _f, _l, kind, key, _why in (ax_registration.decoration_keys(g.q, g.site_file) + ax_registration.value_route_registrations(g.q, g.site_file) - + ax_registration.registrations(g.q, g.site_file) + tregs): + + regs + tregs): if rd in g.sym and key: regk.append((rd, kind, key)) W('reg_key_fact', sorted(set(regk))) # the callables in test files: a test that publishes a handler table's key drives the handler, and is not diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index index 3b6fa191..19c50a44 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-index @@ -702,6 +702,9 @@ CREATE INDEX IF NOT EXISTS ce_callee ON call_edges(callee_method_id); CREATE IND -- lookups by id and by file + line: without them each per-site or per-test query scans the whole table CREATE INDEX IF NOT EXISTS symbols_id ON symbols(id); CREATE INDEX IF NOT EXISTS refs_file_line ON refs(file, line); CREATE INDEX IF NOT EXISTS literals_file_line ON literals(file, line); +-- the fields a caller reads: via_base_rows asks per single-target call site, and without the index each ask +-- scanned the whole table (65,797 rows × 5,162 sites on an 8,619-file repository = 24 s of a 171 s warm → 1.4 s) +CREATE INDEX IF NOT EXISTS fa_caller ON field_access(caller_id); -- who calls a method: one row per (site, target), with the engine's tier. caller_id is symbols.id: a -- Java call written in a field initializer has the TYPE as its caller, and that row is kept, not dropped CREATE VIEW callers AS diff --git a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path index 997e29fe..55b02956 100755 --- a/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path +++ b/plugins/axiomcode/skills/axiomcode/scripts/axiomcode-path @@ -969,10 +969,16 @@ class G: unfoll = [(r['lib'], r['n']) for r in rows if r['from_site']][:cap] return named, unfoll def site_file(self, raw): - """a call site's file as the repo sees it: Java bundles store absolute paths; the index's paths table maps them""" - if self.has('paths'): - r = self.q("SELECT rel FROM paths WHERE raw = ?", raw) - if r: return r[0]['rel'] + """a call site's file as the repo sees it: Java bundles store absolute paths; the index's paths table maps them. + The map is read ONCE PER GRAPH, not once per row: this ran a query per call (one `has`, one lookup), and the + facts export calls it for every call edge — 319k edges on an 8,619-file repository made 638k round-trips + through sqlite, which was most of the export's time (reg_key_fact 36 s, calls 34 s, registration 31 s, + via_base 25 s of a 171 s warm).""" + m = getattr(self, '_site_files', None) + if m is None: + m = self._site_files = {r['raw']: r['rel'] for r in self.q("SELECT raw, rel FROM paths")} if self.has('paths') else {} + got = m.get(raw) + if got is not None: return got return os.path.relpath(raw, self.repo).replace(os.sep, '/') if os.path.isabs(raw) and raw.startswith(self.repo) else raw # ── facts: exported once per graph, reused while graph.sqlite is unchanged ───────────────────────────────