From 76a3fca58e2470dedb8f05bc0961d99a6daef30e Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Mon, 5 Oct 2026 07:08:20 +0100 Subject: [PATCH] perf(python): index importlib alias rebind checks instead of rescanning importlibAliases checked every distinct alias against every statement in the file, building regexes per alias and statement, so the cost grew with aliases times statements. Repeated `import importlib as ilN` took 102ms at 100 aliases, 1.5s at 500 and 5.3s at 1000 (about 26KB, far under the size cap). A statement can only rebind a name it mentions, so index identifiers to statements once and check each alias against only the statements that contain it. The same checks run on those statements, so which aliases are dropped is unchanged. 1000 aliases now take about 20ms. The new test fails before this change (5.8s against a 1.5s budget) and also checks that a rebound alias is still dropped. Fixes #847 --- packages/adapters/python/src/usage/imports.ts | 19 ++++++++++++++++++- .../adapters/python/src/usage/usage.test.ts | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/adapters/python/src/usage/imports.ts b/packages/adapters/python/src/usage/imports.ts index 6079306f..65b7d1b8 100644 --- a/packages/adapters/python/src/usage/imports.ts +++ b/packages/adapters/python/src/usage/imports.ts @@ -388,9 +388,26 @@ function importlibAliases( for (let s = scope; s >= 0; s = parent[s]!) if (s === of) return true; return false; }; + // Identifier -> statements that mention it. A statement can only rebind a + // name it contains, so each alias checks just those statements instead of + // rescanning the whole file once per alias (quadratic on many aliases). + const mentions = new Map(); + const identifier = /[\p{XID_Start}_][\p{XID_Continue}]*/gu; + texts.forEach((raw, index) => { + const seenHere = new Set(); + for (const form of new Set([raw, raw.normalize("NFKC")])) { + for (const m of form.matchAll(identifier)) seenHere.add(m[0]); + } + for (const word of seenHere) { + const list = mentions.get(word); + if (list) list.push(index); + else mentions.set(word, [index]); + } + }); for (const [scope, names] of bound) { for (const name of [...names.keys()]) { - const dropped = texts.some((raw, index) => { + const dropped = (mentions.get(name) ?? []).some((index) => { + const raw = texts[index]!; if (!inside(scopeOf[index]!, scope)) return false; const own = defs.get(index)?.filter((d) => d.name === name) ?? []; const parsed = statementParts(raw); diff --git a/packages/adapters/python/src/usage/usage.test.ts b/packages/adapters/python/src/usage/usage.test.ts index a76ca59a..680e4afb 100644 --- a/packages/adapters/python/src/usage/usage.test.ts +++ b/packages/adapters/python/src/usage/usage.test.ts @@ -124,6 +124,23 @@ describe("extractPythonImports", () => { ); }); + it("scans many importlib aliases in linear time", () => { + const count = 1000; + const source = + Array.from({ length: count }, (_, i) => `import importlib as il${i}`).join("\n") + + '\nil0.import_module("yaml")\nil999.import_module("toml")\n' + + 'il5 = other\nil5.import_module("dropped")\n'; + const started = performance.now(); + const dynamic = extractPythonImports(source) + .imports.filter((i) => i.form === "dynamic") + .map((i) => i.module); + const elapsed = performance.now() - started; + // Correctness first: a rebound alias is still dropped, the others credited. + assert.deepEqual(dynamic, ["yaml", "toml"]); + // 1000 aliases took ~5s when every alias rescanned every statement. + assert.ok(elapsed < 1500, `took ${Math.round(elapsed)}ms`); + }); + it("credits aliased importlib, import_module and __import__ calls", () => { const dyn = (src: string) => extractPythonImports(src)