summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Haigh <hello@nelf.in>2021-06-08 01:05:59 +1000
committerGitHub <noreply@github.com>2021-06-07 17:05:59 +0200
commitcf6528cbc158097c4903f0cab68242ff14bb591b (patch)
tree8cdc8beab6af265bddb13039c8a5e44bd47cd6db
parent85791635a24cd300434ab2fa31acf21835315227 (diff)
downloadastroid-git-cf6528cbc158097c4903f0cab68242ff14bb591b.tar.gz
Performance improvements to counter context.clone slowdown (#1009)
* Add limit to the total number of nodes inferred per context This change abuses mutable references to create a sort of interior mutable cell shared between a context and all of its clones. The idea is that when a node is inferred at the toplevel, it is called with context = None, creating a new InferenceContext and starting a count from zero. However, when a context is cloned we re-use the cell and cause the count in the "parent" context to be incremented when nodes are inferred in the "child" context. * Add global inference cache * Update safe_infer to catch StopIteration
-rw-r--r--ChangeLog5
-rw-r--r--astroid/context.py60
-rw-r--r--astroid/helpers.py2
-rw-r--r--astroid/node_classes.py9
-rw-r--r--astroid/transforms.py3
5 files changed, 64 insertions, 15 deletions
diff --git a/ChangeLog b/ChangeLog
index 1286a980..019dbb91 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -16,6 +16,11 @@ Release Date: TBA
* Add lineno and col_offset for ``Keyword`` nodes and Python 3.9+
+* Add global inference cache to speed up inference of long statement blocks
+
+* Add a limit to the total number of nodes inferred indirectly as a result
+ of inferring some node
+
What's New in astroid 2.5.7?
============================
diff --git a/astroid/context.py b/astroid/context.py
index 440d1ceb..08985841 100644
--- a/astroid/context.py
+++ b/astroid/context.py
@@ -13,7 +13,17 @@
"""Various context related utilities, including inference and call contexts."""
import contextlib
import pprint
-from typing import Optional
+from typing import TYPE_CHECKING, MutableMapping, Optional, Sequence, Tuple
+
+if TYPE_CHECKING:
+ from astroid.node_classes import NodeNG
+
+
+_INFERENCE_CACHE = {}
+
+
+def _invalidate_cache():
+ _INFERENCE_CACHE.clear()
class InferenceContext:
@@ -28,11 +38,17 @@ class InferenceContext:
"lookupname",
"callcontext",
"boundnode",
- "inferred",
"extra_context",
+ "_nodes_inferred",
)
- def __init__(self, path=None, inferred=None):
+ max_inferred = 100
+
+ def __init__(self, path=None, nodes_inferred=None):
+ if nodes_inferred is None:
+ self._nodes_inferred = [0]
+ else:
+ self._nodes_inferred = nodes_inferred
self.path = path or set()
"""
:type: set(tuple(NodeNG, optional(str)))
@@ -65,14 +81,6 @@ class InferenceContext:
e.g. the bound node of object.__new__(cls) is the object node
"""
- self.inferred = inferred or {}
- """
- :type: dict(seq, seq)
-
- Inferred node contexts to their mapped results
- Currently the key is ``(node, lookupname, callcontext, boundnode)``
- and the value is tuple of the inferred results
- """
self.extra_context = {}
"""
:type: dict(NodeNG, Context)
@@ -81,6 +89,34 @@ class InferenceContext:
for call arguments
"""
+ @property
+ def nodes_inferred(self):
+ """
+ Number of nodes inferred in this context and all its clones/decendents
+
+ Wrap inner value in a mutable cell to allow for mutating a class
+ variable in the presence of __slots__
+ """
+ return self._nodes_inferred[0]
+
+ @nodes_inferred.setter
+ def nodes_inferred(self, value):
+ self._nodes_inferred[0] = value
+
+ @property
+ def inferred(
+ self,
+ ) -> MutableMapping[
+ Tuple["NodeNG", Optional[str], Optional[str], Optional[str]], Sequence["NodeNG"]
+ ]:
+ """
+ Inferred node contexts to their mapped results
+
+ Currently the key is ``(node, lookupname, callcontext, boundnode)``
+ and the value is tuple of the inferred results
+ """
+ return _INFERENCE_CACHE
+
def push(self, node):
"""Push node into inference path
@@ -103,7 +139,7 @@ class InferenceContext:
starts with the same context but diverge as each side is inferred
so the InferenceContext will need be cloned"""
# XXX copy lookupname/callcontext ?
- clone = InferenceContext(self.path.copy(), inferred=self.inferred.copy())
+ clone = InferenceContext(self.path.copy(), nodes_inferred=self._nodes_inferred)
clone.callcontext = self.callcontext
clone.boundnode = self.boundnode
clone.extra_context = self.extra_context
diff --git a/astroid/helpers.py b/astroid/helpers.py
index cb16ecdc..db86606e 100644
--- a/astroid/helpers.py
+++ b/astroid/helpers.py
@@ -150,7 +150,7 @@ def safe_infer(node, context=None):
try:
inferit = node.infer(context=context)
value = next(inferit)
- except exceptions.InferenceError:
+ except (exceptions.InferenceError, StopIteration):
return None
try:
next(inferit)
diff --git a/astroid/node_classes.py b/astroid/node_classes.py
index ff74fa21..85e3cea6 100644
--- a/astroid/node_classes.py
+++ b/astroid/node_classes.py
@@ -357,12 +357,16 @@ class NodeNG:
# explicit_inference is not bound, give it self explicitly
try:
# pylint: disable=not-callable
- yield from self._explicit_inference(self, context, **kwargs)
+ results = tuple(self._explicit_inference(self, context, **kwargs))
+ if context is not None:
+ context.nodes_inferred += len(results)
+ yield from results
return
except exceptions.UseInferenceDefault:
pass
if not context:
+ # nodes_inferred?
yield from self._infer(context, **kwargs)
return
@@ -378,11 +382,12 @@ class NodeNG:
# exponentially exploding possible results.
limit = MANAGER.max_inferable_values
for i, result in enumerate(generator):
- if i >= limit:
+ if i >= limit or (context.nodes_inferred > context.max_inferred):
yield util.Uninferable
break
results.append(result)
yield result
+ context.nodes_inferred += 1
# Cache generated results for subsequent inferences of the
# same node using the same context
diff --git a/astroid/transforms.py b/astroid/transforms.py
index 1c4081cb..5314fcb2 100644
--- a/astroid/transforms.py
+++ b/astroid/transforms.py
@@ -10,6 +10,8 @@
import collections
from functools import lru_cache
+from astroid import context as contextmod
+
class TransformVisitor:
"""A visitor for handling transforms.
@@ -42,6 +44,7 @@ class TransformVisitor:
# if the transformation function returns something, it's
# expected to be a replacement for the node
if ret is not None:
+ contextmod._invalidate_cache()
node = ret
if ret.__class__ != cls:
# Can no longer apply the rest of the transforms.