-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Improve the cache when getting font metrics #28831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tacaswell
wants to merge
2
commits into
matplotlib:main
Choose a base branch
from
tacaswell:fix/better_fm_cache
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -64,17 +64,89 @@ | |||
|
||||
def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): | ||||
"""Call ``renderer.get_text_width_height_descent``, caching the results.""" | ||||
# Cached based on a copy of fontprop so that later in-place mutations of | ||||
# the passed-in argument do not mess up the cache. | ||||
return _get_text_metrics_with_cache_impl( | ||||
weakref.ref(renderer), text, fontprop.copy(), ismath, dpi) | ||||
|
||||
# hit the outer cache layer and get the function to compute the metrics | ||||
# for this renderer instance | ||||
get_text_metrics = _get_text_metrics_function(renderer) | ||||
# call the function to compute the metrics and return | ||||
# | ||||
# We pass a copy of the fontprop because FontProperties is both mutable and | ||||
# has a `__hash__` that depends on that mutable state. This is not ideal | ||||
# as it means the hash of an object is not stable over time which leads to | ||||
# very confusing behavior when used as keys in dictionaries or hashes. | ||||
return get_text_metrics(text, fontprop.copy(), ismath, dpi) | ||||
|
||||
@functools.lru_cache(4096) | ||||
def _get_text_metrics_with_cache_impl( | ||||
renderer_ref, text, fontprop, ismath, dpi): | ||||
# dpi is unused, but participates in cache invalidation (via the renderer). | ||||
return renderer_ref().get_text_width_height_descent(text, fontprop, ismath) | ||||
|
||||
def _get_text_metrics_function(input_renderer, _cache=weakref.WeakKeyDictionary()): | ||||
""" | ||||
Helper function to provide a two-layered cache for font metrics | ||||
|
||||
|
||||
To get the rendered size of a size of string we need to know: | ||||
- what renderer we are using | ||||
- the current dpi of the renderer | ||||
- the string | ||||
- the font properties | ||||
- is it math text or not | ||||
|
||||
We do this as a two-layer cache with the outer layer being tied to a | ||||
renderer instance and the inner layer handling everything else. | ||||
|
||||
The outer layer is implemented as `.WeakKeyDictionary` keyed on the | ||||
renderer. As long as someone else is holding a hard ref to the renderer | ||||
we will keep the cache alive, but it will be automatically dropped when | ||||
the renderer is garbage collected. | ||||
|
||||
The inner layer is provided by an lru_cache with a large maximum size (such | ||||
that we do not expect it to be hit in very few actual use cases). As the | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wording was a bit confusing.
Suggested change
|
||||
dpi is mutable on the renderer, we need to explicitly include it as part of | ||||
the cache key on the inner layer even though we do not directly use it (it is | ||||
used in the method call on the renderer). | ||||
|
||||
This function takes a renderer and returns a function that can be used to | ||||
get the font metrics. | ||||
|
||||
Parameters | ||||
---------- | ||||
input_renderer : maplotlib.backend_bases.RendererBase | ||||
The renderer to set the cache up for. | ||||
|
||||
_cache : dict, optional | ||||
We are using the mutable default value to attach the cache to the function. | ||||
|
||||
In principle you could pass a different dict-like to this function to inject | ||||
a different cache, but please don't. This is an internal function not meant to | ||||
be reused outside of the narrow context we need it for. | ||||
|
||||
There is a possible race condition here between threads, we may need to drop the | ||||
mutable default and switch to a threadlocal variable in the future. | ||||
|
||||
""" | ||||
if (_text_metrics := _cache.get(input_renderer, None)) is None: | ||||
# We are going to include this in the closure we put as values in the | ||||
# cache. Closing over a hard-ref would create an unbreakable reference | ||||
# cycle. | ||||
renderer_ref = weakref.ref(input_renderer) | ||||
|
||||
# define the function locally to get a new lru_cache per renderer | ||||
@functools.lru_cache(4096) | ||||
# dpi is unused, but participates in cache invalidation (via the renderer). | ||||
def _text_metrics(text, fontprop, ismath, dpi): | ||||
# this should never happen under normal use, but this is a better error to | ||||
# raise than an AttributeError on `None` | ||||
if (local_renderer := renderer_ref()) is None: | ||||
raise RuntimeError( | ||||
"Trying to get text metrics for a renderer that no longer exists. " | ||||
"This should never happen and is evidence of a bug elsewhere." | ||||
) | ||||
# do the actual method call we need and return the result | ||||
return local_renderer.get_text_width_height_descent(text, fontprop, ismath) | ||||
|
||||
# stash the function for later use. | ||||
_cache[input_renderer] = _text_metrics | ||||
|
||||
# return the inner function | ||||
return _text_metrics | ||||
|
||||
|
||||
@_docstring.interpd | ||||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, see also #22495 (immutable FontProperties).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I'm glad there is an issue for that.