perf(tools): text prefilter before AST parse in tool discovery

`_module_registers_tools()` reads each `tools/*.py` file and fully
AST-parses it to check for a top-level `registry.register()` call.
90 files are scanned on every process start — but only 32 actually
register tools.

Add a cheap text prefilter: after reading the file (which we need to
do anyway for AST), check that both `"registry"` and `"register"`
appear in the source before calling `ast.parse`. A file with a
top-level `registry.register()` call must contain both strings, so
this is a perfect superset — zero false negatives. 50 of 90 files
skip the AST parse entirely.

The `source=` parameter is not threaded through `discover_builtin_tools`;
the prefilter lives entirely inside `_module_registers_tools`, keeping
the public API unchanged.

Benchmark (median of 10 runs, scanning 90 files):

  before (read + ast.parse all):  305.9ms
  after  (text prefilter + ast):   187.8ms
  speedup: 1.6x  (118ms saved)

Identical module set: 32 modules, same names, same order.
This commit is contained in:
ethernet 2026-07-13 15:35:53 -04:00
parent 658c011266
commit 2d71e2f1e4

View file

@ -45,11 +45,20 @@ def _module_registers_tools(module_path: Path) -> bool:
Only inspects module-body statements so that helper modules which happen
to call ``registry.register()`` inside a function are not picked up.
A cheap text prefilter avoids the ``ast.parse`` cost for files that do not
mention both ``registry`` and ``register`` a necessary condition for a
top-level ``registry.register()`` call to exist.
"""
try:
source = module_path.read_text(encoding="utf-8")
except OSError:
return False
if "registry" not in source or "register" not in source:
return False
try:
tree = ast.parse(source, filename=str(module_path))
except (OSError, SyntaxError):
except SyntaxError:
return False
return any(_is_registry_register_call(stmt) for stmt in tree.body)