Skip to content

Commit

Permalink
perf: cache missing XBlock mappings in load_class
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ormsbee committed Jan 19, 2025
1 parent cd8e19a commit 2096337
Showing 1 changed file with 35 additions and 21 deletions.
56 changes: 35 additions & 21 deletions xblock/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,28 +142,42 @@ def select(identifier, all_entry_points):
"""
identifier = identifier.lower()
key = (cls.entry_point, identifier)
if key not in PLUGIN_CACHE:

if select is None:
select = default_select

all_entry_points = [
*importlib.metadata.entry_points(group=f'{cls.entry_point}.overrides', name=identifier),
*importlib.metadata.entry_points(group=cls.entry_point, name=identifier)
]

for extra_identifier, extra_entry_point in iter(cls.extra_entry_points):
if identifier == extra_identifier:
all_entry_points.append(extra_entry_point)

try:
selected_entry_point = select(identifier, all_entry_points)
except PluginMissingError:
if default is not None:
return default
raise

PLUGIN_CACHE[key] = cls._load_class_entry_point(selected_entry_point)
if key in PLUGIN_CACHE:
xblock_cls = PLUGIN_CACHE[key] or default
if xblock_cls:
return xblock_cls

# If the key was in PLUGIN_CACHE, but the value stored was None, and
# there was no default class to fall back to, we give up and raise
# PluginMissingError. This is for performance reasons. We've cached
# the fact that there is no XBlock class that maps to this
# identifier, and it is very expensive to check through the entry
# points each time. This assumes that XBlock class mappings do not
# change during the life of the process.
raise PluginMissingError(identifier)

if select is None:
select = default_select

all_entry_points = [
*importlib.metadata.entry_points(group=f'{cls.entry_point}.overrides', name=identifier),
*importlib.metadata.entry_points(group=cls.entry_point, name=identifier)
]

for extra_identifier, extra_entry_point in iter(cls.extra_entry_points):
if identifier == extra_identifier:
all_entry_points.append(extra_entry_point)

try:
selected_entry_point = select(identifier, all_entry_points)
except PluginMissingError:
PLUGIN_CACHE[key] = None
if default is not None:
return default
raise

PLUGIN_CACHE[key] = cls._load_class_entry_point(selected_entry_point)

return PLUGIN_CACHE[key]

Expand Down

0 comments on commit 2096337

Please sign in to comment.