Skip to content

perf: load brain plugins lazily, only when their target module is imported#3069

Draft
Pierre-Sassoulas wants to merge 4 commits into
mainfrom
perf/lazy-brain-loading
Draft

perf: load brain plugins lazily, only when their target module is imported#3069
Pierre-Sassoulas wants to merge 4 commits into
mainfrom
perf/lazy-brain-loading

Conversation

@Pierre-Sassoulas
Copy link
Copy Markdown
Member

Summary

astroid_manager eagerly loaded every brain plugin (~49 modules) at
import astroid so their transforms were registered up front. Most of
that work is wasted — brain_numpy_* does nothing for code that never
imports numpy, brain_pytest nothing without pytest, and so on.

  • Data-driven brain registry (first commit, behavior-preserving
    refactor): replace the hand-maintained register_all_brains — a
    50-line import block plus 50 .register() calls — with an
    _EAGER_BRAINS tuple and a _LAZY_BRAINS mapping from a module-name
    trigger to the brain modules it needs. Brains load via importlib
    and register at most once per process.
  • Lazy loading (second commit): register only the two universal
    brains (brain_builtin_inference and brain_type, which target
    import-less builtins) at startup, and load the rest on demand.
    AstroidBuilder._post_build scans a freshly built module's Import
    / ImportFrom nodes and AstroidManager.ast_from_module_name keys
    on the module being resolved; each calls load_brains_for_modname
    so a brain is registered before the transforms that need it run.

import astroid goes from 165 to 109 loaded modules (brain modules
51 → 2). Combined with #3062, 165 → 100.

test_recursion_error_trapped relied on the brain inference
predicates running on every Call node and exhausting the stack; with
lazy loading those predicates aren't registered, so it now force-loads
every brain to keep verifying the trap (trap behaviour unchanged).

Test plan

  • Full test suite passes locally (1956 passed, 53 skipped, 16 xfailed)

Replace the hand-maintained ``register_all_brains`` -- a 50-line
import block followed by 50 ``.register()`` calls -- with two data
structures: an ``_EAGER_BRAINS`` tuple and a ``_LAZY_BRAINS`` mapping
from a module-name trigger to the brain modules it needs. A brain is
loaded via ``importlib`` and registered by ``_load_brain``, tracked in
``_loaded_brain_names`` so it registers at most once per process.

``register_all_brains`` keeps its behaviour -- it still eagerly loads
every brain -- so this commit changes nothing observable. It adds
``register_brains`` (eager brains only) and ``load_brains_for_modname``,
which the next commit wires up for lazy loading.
``astroid.astroid_manager`` eagerly loaded every brain plugin (~49
modules) so their transforms were registered up front. Most of that
work is wasted: ``brain_numpy_*`` does nothing for code that never
imports ``numpy``, ``brain_pytest`` nothing without ``pytest``, etc.

Using the brain registry, register only the two universal brains
(``brain_builtin_inference`` and ``brain_type``, which target
import-less builtins) at startup, and load the rest on demand.
``AstroidBuilder._post_build`` scans a freshly built module's
``Import`` / ``ImportFrom`` nodes and ``AstroidManager.ast_from_module_name``
keys on the module being resolved; each calls ``load_brains_for_modname``
so a brain is registered before the transforms that need it run.

``test_recursion_error_trapped`` depended on the brain inference
predicates running on every ``Call`` node and exhausting the stack via
their internal ``safe_infer`` calls. With lazy loading those
predicates are not registered, so the recursion no longer fires. The
test now force-loads every brain to keep verifying the trap; the trap
behaviour itself is unchanged.

``import astroid`` now loads 4 brain-related modules (the package,
``helpers``, and the 2 eager brains) instead of 53.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.42%. Comparing base (649ec7d) to head (adf138a).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #3069      +/-   ##
==========================================
- Coverage   93.52%   93.42%   -0.11%     
==========================================
  Files          92       92              
  Lines       11329    11313      -16     
==========================================
- Hits        10596    10569      -27     
- Misses        733      744      +11     
Flag Coverage Δ
linux 93.29% <100.00%> (-0.11%) ⬇️
pypy 93.42% <100.00%> (-0.11%) ⬇️
windows 93.39% <100.00%> (-0.11%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
astroid/astroid_manager.py 100.00% <100.00%> (ø)
astroid/brain/helpers.py 100.00% <100.00%> (ø)
astroid/builder.py 96.21% <100.00%> (+0.09%) ⬆️
astroid/manager.py 90.24% <100.00%> (+0.07%) ⬆️

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Pierre-Sassoulas Pierre-Sassoulas marked this pull request as draft May 20, 2026 08:45
``_load_brain`` early-returns for a brain that is already registered,
which keeps a brain reachable through several module triggers from
registering its transforms more than once. Add a direct test for that
guard so the path is exercised.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant