Skip to content

Performance: cache ClassDef._find_metaclass() per node - #3166

Open
Pierre-Sassoulas wants to merge 2 commits into
mainfrom
perf/cache-find-metaclass
Open

Performance: cache ClassDef._find_metaclass() per node#3166
Pierre-Sassoulas wants to merge 2 commits into
mainfrom
perf/cache-find-metaclass

Conversation

@Pierre-Sassoulas

Copy link
Copy Markdown
Member

Type of Changes

Type
🔨 Refactoring

Description

Split out of #3048, as requested — one optimization, one commit, tests included.

The recursive metaclass lookup walked the MRO on every call. Cache the result per node so the walk runs once, short-circuiting re-entry through a _COMPUTING_METACLASS sentinel to preserve the cycle protection that seen provides in the slow path.

Only the no-context call is cached: the cache key is context is None, so an explicit context still forces a fresh walk.

TestFindMetaclassCaching covers the four branches: a cached ClassDef result is reused, a None result is cached explicitly so the MRO walk does not repeat, an explicit context neither reads nor writes the cache, and re-entry while the sentinel is parked returns None to break the cycle.

Refs #1115

The recursive metaclass lookup walked the MRO on every call. Cache the
result per node so the walk runs once, short-circuiting re-entry through
a ``_COMPUTING_METACLASS`` sentinel to preserve cycle protection.

Only the no-context call is cached: the cache key is ``context is None``,
so an explicit ``context`` still forces a fresh walk.

``TestFindMetaclassCaching`` covers the four branches: a cached
``ClassDef`` result is reused, a ``None`` result is cached explicitly so
the MRO walk does not repeat, an explicit ``context`` neither reads nor
writes the cache, and re-entry while the sentinel is parked returns
``None`` to break the cycle.

Refs #1115
@codspeed-hq

codspeed-hq Bot commented Jul 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 3 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing perf/cache-find-metaclass (72f38fa) with main (da4a8cf)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.67%. Comparing base (da4a8cf) to head (72f38fa).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #3166   +/-   ##
=======================================
  Coverage   93.67%   93.67%           
=======================================
  Files          93       93           
  Lines       11645    11657   +12     
=======================================
+ Hits        10908    10920   +12     
  Misses        737      737           
Flag Coverage Δ
linux 93.54% <100.00%> (+<0.01%) ⬆️
pypy 93.67% <100.00%> (+<0.01%) ⬆️
windows 93.65% <100.00%> (+<0.01%) ⬆️

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

Files with missing lines Coverage Δ
astroid/nodes/scoped_nodes/scoped_nodes.py 93.42% <100.00%> (+0.06%) ⬆️

... and 1 file 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 added this to the 4.2.0 milestone Jul 30, 2026
@Pierre-Sassoulas

Copy link
Copy Markdown
Member Author

No visible result in the CI benchmark but total function calls for declared_metaclass() and ancestors() goes down as expected.

@DanielNoord DanielNoord left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the additional complexity worth it if we don't see a meaningful impact on benchmarks? I understand we're trying to improve performance but this will hurt maintainability in the long run and without clear indications of benchmark improvements that doesn't necessarily weigh up.

The inference code base is already quite hard to maintain...

@Pierre-Sassoulas

Copy link
Copy Markdown
Member Author

The benchmark is pretty rough, it's not often we're going to see 5+% shaved or added from a normal non problematic run of 20s on flake8. (We did or fixed something pretty terrible when that happen). We still need to think about performance implication and look at the cprofile to optimize problematic code with big codebase and deep recursion.

Inference is where the performances gains are all going to happen now imo.

@DanielNoord

Copy link
Copy Markdown
Collaborator

We still need to think about performance implication and look at the cprofile to optimize problematic code with big codebase and deep recursion.

Agreed, but in this case I am wondering if the added complexity really weighs up against the maintainability costs. Is this recursion really an issue?

@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 4.2.0, 4.4.0 Aug 3, 2026
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.

2 participants