Skip to content

Commit 2096337

Browse files
committed
perf: cache missing XBlock mappings in load_class
Checking the entrypoints for the XBlock class we want for a given tag name is relatively expensive. We mitigate this by caching the mapping of tags to classes in Plugin.load_class(). But before this commit, we weren't caching failed lookups, i.e. the situation where there is no XBlock that maps to a given tag. This meant that if a course had many instances of a type of XBlock that is not installed on the given server, we would be constantly scanning entrypoints to look for our non-existent XBlock subclass. This really slows down things like the Studio course outline page. The fix here is to store failed lookups as None values in the PLUGIN_CACHE.
1 parent cd8e19a commit 2096337

File tree

1 file changed

+35
-21
lines changed

1 file changed

+35
-21
lines changed

xblock/plugin.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -142,28 +142,42 @@ def select(identifier, all_entry_points):
142142
"""
143143
identifier = identifier.lower()
144144
key = (cls.entry_point, identifier)
145-
if key not in PLUGIN_CACHE:
146145

147-
if select is None:
148-
select = default_select
149-
150-
all_entry_points = [
151-
*importlib.metadata.entry_points(group=f'{cls.entry_point}.overrides', name=identifier),
152-
*importlib.metadata.entry_points(group=cls.entry_point, name=identifier)
153-
]
154-
155-
for extra_identifier, extra_entry_point in iter(cls.extra_entry_points):
156-
if identifier == extra_identifier:
157-
all_entry_points.append(extra_entry_point)
158-
159-
try:
160-
selected_entry_point = select(identifier, all_entry_points)
161-
except PluginMissingError:
162-
if default is not None:
163-
return default
164-
raise
165-
166-
PLUGIN_CACHE[key] = cls._load_class_entry_point(selected_entry_point)
146+
if key in PLUGIN_CACHE:
147+
xblock_cls = PLUGIN_CACHE[key] or default
148+
if xblock_cls:
149+
return xblock_cls
150+
151+
# If the key was in PLUGIN_CACHE, but the value stored was None, and
152+
# there was no default class to fall back to, we give up and raise
153+
# PluginMissingError. This is for performance reasons. We've cached
154+
# the fact that there is no XBlock class that maps to this
155+
# identifier, and it is very expensive to check through the entry
156+
# points each time. This assumes that XBlock class mappings do not
157+
# change during the life of the process.
158+
raise PluginMissingError(identifier)
159+
160+
if select is None:
161+
select = default_select
162+
163+
all_entry_points = [
164+
*importlib.metadata.entry_points(group=f'{cls.entry_point}.overrides', name=identifier),
165+
*importlib.metadata.entry_points(group=cls.entry_point, name=identifier)
166+
]
167+
168+
for extra_identifier, extra_entry_point in iter(cls.extra_entry_points):
169+
if identifier == extra_identifier:
170+
all_entry_points.append(extra_entry_point)
171+
172+
try:
173+
selected_entry_point = select(identifier, all_entry_points)
174+
except PluginMissingError:
175+
PLUGIN_CACHE[key] = None
176+
if default is not None:
177+
return default
178+
raise
179+
180+
PLUGIN_CACHE[key] = cls._load_class_entry_point(selected_entry_point)
167181

168182
return PLUGIN_CACHE[key]
169183

0 commit comments

Comments
 (0)