@@ -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