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)

@codecov

codecov Bot commented May 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.45%. Comparing base (2ef6d50) to head (56387a3).

Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                        @@
##           codspeed-wizard-1774989768270    #3069      +/-   ##
=================================================================
- Coverage                          93.56%   93.45%   -0.11%     
=================================================================
  Files                                 92       92              
  Lines                              11345    11324      -21     
=================================================================
- Hits                               10615    10583      -32     
- Misses                               730      741      +11     
Flag Coverage Δ
linux 93.32% <100.00%> (-0.11%) ⬇️
pypy 93.45% <100.00%> (-0.11%) ⬇️
windows 93.42% <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 13 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
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the perf/lazy-brain-loading branch from adf138a to aee742f Compare May 24, 2026 12:07
@Pierre-Sassoulas Pierre-Sassoulas changed the base branch from main to codspeed-wizard-1774989768270 May 24, 2026 12:07
@codspeed-hq

codspeed-hq Bot commented May 24, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 2.59%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 2 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_bench_endtoend_parse_flask 2.2 s 2.3 s -2.59%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/lazy-brain-loading (9ee39c6) with main (c50a1f4)

Open in CodSpeed

@Pierre-Sassoulas Pierre-Sassoulas force-pushed the codspeed-wizard-1774989768270 branch from f8520dd to 2d391a6 Compare May 24, 2026 12:42
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the perf/lazy-brain-loading branch from aee742f to b5bba4c Compare May 24, 2026 12:43
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the codspeed-wizard-1774989768270 branch from 2d391a6 to 2ef6d50 Compare May 24, 2026 13:23
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the perf/lazy-brain-loading branch from b5bba4c to 56387a3 Compare May 24, 2026 13:25
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the codspeed-wizard-1774989768270 branch 2 times, most recently from 2fb0a52 to 7e482f9 Compare June 6, 2026 05:51
Base automatically changed from codspeed-wizard-1774989768270 to main June 6, 2026 21:41
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.
``_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.
@Pierre-Sassoulas Pierre-Sassoulas force-pushed the perf/lazy-brain-loading branch from 56387a3 to 9ee39c6 Compare June 6, 2026 21:53
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