Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitcfccd8a

Browse files
authored
Merge pull request#28831 from tacaswell/fix/better_fm_cache
Improve the cache when getting font metrics
2 parentsea40d72 +94d6fc8 commitcfccd8a

File tree

2 files changed

+119
-11
lines changed

2 files changed

+119
-11
lines changed

‎lib/matplotlib/tests/test_text.py‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
fromdatetimeimportdatetime
2+
importgc
3+
importinspect
24
importio
35
importwarnings
46

@@ -874,14 +876,21 @@ def test_pdf_chars_beyond_bmp():
874876

875877
@needs_usetex
876878
deftest_metrics_cache():
877-
mpl.text._get_text_metrics_with_cache_impl.cache_clear()
879+
# dig into the signature to get the mutable default used as a cache
880+
renderer_cache=inspect.signature(
881+
mpl.text._get_text_metrics_function
882+
).parameters['_cache'].default
883+
884+
renderer_cache.clear()
878885

879886
fig=plt.figure()
880887
fig.text(.3,.5,"foo\nbar")
881888
fig.text(.3,.5,"foo\nbar",usetex=True)
882889
fig.text(.5,.5,"foo\nbar",usetex=True)
883890
fig.canvas.draw()
884891
renderer=fig._get_renderer()
892+
assertrendererinrenderer_cache
893+
885894
ys= {}# mapping of strings to where they were drawn in y with draw_tex.
886895

887896
defcall(*args,**kwargs):
@@ -897,12 +906,39 @@ def call(*args, **kwargs):
897906
# get incorrectly reused by the first TeX string.
898907
assertlen(ys["foo"])==len(ys["bar"])==1
899908

900-
info=mpl.text._get_text_metrics_with_cache_impl.cache_info()
909+
info=renderer_cache[renderer].cache_info()
901910
# Every string gets a miss for the first layouting (extents), then a hit
902911
# when drawing, but "foo\nbar" gets two hits as it's drawn twice.
903912
assertinfo.hits>info.misses
904913

905914

915+
deftest_metrics_cache2():
916+
# dig into the signature to get the mutable default used as a cache
917+
renderer_cache=inspect.signature(
918+
mpl.text._get_text_metrics_function
919+
).parameters['_cache'].default
920+
gc.collect()
921+
renderer_cache.clear()
922+
923+
defhelper():
924+
fig,ax=plt.subplots()
925+
fig.draw_without_rendering()
926+
# show we hit the outer cache
927+
assertlen(renderer_cache)==1
928+
func=renderer_cache[fig.canvas.get_renderer()]
929+
cache_info=func.cache_info()
930+
# show we hit the inner cache
931+
assertcache_info.currsize>0
932+
assertcache_info.currsize==cache_info.misses
933+
assertcache_info.hits>cache_info.misses
934+
plt.close(fig)
935+
936+
helper()
937+
gc.collect()
938+
# show the outer cache has a lifetime tied to the renderer (via the figure)
939+
assertlen(renderer_cache)==0
940+
941+
906942
deftest_annotate_offset_fontsize():
907943
# Test that offset_fontsize parameter works and uses accurate values
908944
fig,ax=plt.subplots()

‎lib/matplotlib/text.py‎

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,89 @@ def _get_textbox(text, renderer):
6464

6565
def_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+
returnget_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-
returnrenderer_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))isNone:
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())isNone:
138+
raiseRuntimeError(
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+
returnlocal_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

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp