@@ -64,17 +64,89 @@ def _get_textbox(text, renderer):
6464
6565def _get_text_metrics_with_cache (renderer , text , fontprop , ismath , dpi ):
6666 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67- # Cached based on a copy of fontprop so that later in-place mutations of
68- # the passed-in argument do not mess up the cache.
69- return _get_text_metrics_with_cache_impl (
70- weakref .ref (renderer ), text , fontprop .copy (), ismath , dpi )
7167
68+ # hit the outer cache layer and get the function to compute the metrics
69+ # for this renderer instance
70+ get_text_metrics = _get_text_metrics_function (renderer )
71+ # call the function to compute the metrics and return
72+ #
73+ # We pass a copy of the fontprop because FontProperties is both mutable and
74+ # has a `__hash__` that depends on that mutable state. This is not ideal
75+ # as it means the hash of an object is not stable over time which leads to
76+ # very confusing behavior when used as keys in dictionaries or hashes.
77+ return get_text_metrics (text , fontprop .copy (), ismath , dpi )
7278
73- @functools .lru_cache (4096 )
74- def _get_text_metrics_with_cache_impl (
75- renderer_ref , text , fontprop , ismath , dpi ):
76- # dpi is unused, but participates in cache invalidation (via the renderer).
77- return renderer_ref ().get_text_width_height_descent (text , fontprop , ismath )
79+
80+ def _get_text_metrics_function (input_renderer , _cache = weakref .WeakKeyDictionary ()):
81+ """
82+ Helper function to provide a two-layered cache for font metrics
83+
84+
85+ To get the rendered size of a size of string we need to know:
86+ - what renderer we are using
87+ - the current dpi of the renderer
88+ - the string
89+ - the font properties
90+ - is it math text or not
91+
92+ We do this as a two-layer cache with the outer layer being tied to a
93+ renderer instance and the inner layer handling everything else.
94+
95+ The outer layer is implemented as `.WeakKeyDictionary` keyed on the
96+ renderer. As long as someone else is holding a hard ref to the renderer
97+ we will keep the cache alive, but it will be automatically dropped when
98+ the renderer is garbage collected.
99+
100+ The inner layer is provided by an lru_cache with a large maximum size (such
101+ that we expect very few cache misses in actual use cases). As the
102+ dpi is mutable on the renderer, we need to explicitly include it as part of
103+ the cache key on the inner layer even though we do not directly use it (it is
104+ used in the method call on the renderer).
105+
106+ This function takes a renderer and returns a function that can be used to
107+ get the font metrics.
108+
109+ Parameters
110+ ----------
111+ input_renderer : maplotlib.backend_bases.RendererBase
112+ The renderer to set the cache up for.
113+
114+ _cache : dict, optional
115+ We are using the mutable default value to attach the cache to the function.
116+
117+ In principle you could pass a different dict-like to this function to inject
118+ a different cache, but please don't. This is an internal function not meant to
119+ be reused outside of the narrow context we need it for.
120+
121+ There is a possible race condition here between threads, we may need to drop the
122+ mutable default and switch to a threadlocal variable in the future.
123+
124+ """
125+ if (_text_metrics := _cache .get (input_renderer , None )) is None :
126+ # We are going to include this in the closure we put as values in the
127+ # cache. Closing over a hard-ref would create an unbreakable reference
128+ # cycle.
129+ renderer_ref = weakref .ref (input_renderer )
130+
131+ # define the function locally to get a new lru_cache per renderer
132+ @functools .lru_cache (4096 )
133+ # dpi is unused, but participates in cache invalidation (via the renderer).
134+ def _text_metrics (text , fontprop , ismath , dpi ):
135+ # this should never happen under normal use, but this is a better error to
136+ # raise than an AttributeError on `None`
137+ if (local_renderer := renderer_ref ()) is None :
138+ raise RuntimeError (
139+ "Trying to get text metrics for a renderer that no longer exists. "
140+ "This should never happen and is evidence of a bug elsewhere."
141+ )
142+ # do the actual method call we need and return the result
143+ return local_renderer .get_text_width_height_descent (text , fontprop , ismath )
144+
145+ # stash the function for later use.
146+ _cache [input_renderer ] = _text_metrics
147+
148+ # return the inner function
149+ return _text_metrics
78150
79151
80152@_docstring .interpd
0 commit comments