Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bcec42b

Browse files
tacaswellgreglucas
andcommitted
FIX: move the font lock higher up the call and class tree
We have for a long time (~2012) had an RLock on `RenderAgg` that was used in `FigureCanvasAgg.draw` to protect the shared cache of ft2font objects (the underlying c++ is very stateful and not thread safe). This lock was also implicitly protecting the mathtext cache when using the Agg backend. However, given the recent improvements to the layout code there are now ways to call call `Figure.draw(renderer)` without acquiring this lock which leads to exceptions when using mathtext and a layout manager to save independent figures on different threads. This bug also exists for the other backendends which use both the mathtext parser and the ft2font cache in the case of rendering texts as paths (in the vector backends). The fix is to: - pull the lock up to `Figure` so all renderer instances effectively share a single lock - acquire the lock in `Figure.draw` which is always the top entry point to rendering a Figure. Closes matplotlib#26289 Co-authored-by: Greg Lucas <[email protected]>
1 parent 02d2e13 commit bcec42b

File tree

3 files changed

+36
-38
lines changed

3 files changed

+36
-38
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ class RendererBase:
170170
* `draw_path_collection`
171171
* `draw_quad_mesh`
172172
"""
173-
174173
def __init__(self):
175174
super().__init__()
176175
self._texmanager = None
@@ -2147,9 +2146,10 @@ def print_figure(
21472146
functools.partial(
21482147
print_method, orientation=orientation)
21492148
)
2149+
# we do this instead of `self.figure.draw_without_rendering`
2150+
# so that we can inject the orientation
21502151
with getattr(renderer, "_draw_disabled", nullcontext)():
21512152
self.figure.draw(renderer)
2152-
21532153
if bbox_inches:
21542154
if bbox_inches == "tight":
21552155
bbox_inches = self.figure.get_tightbbox(

lib/matplotlib/backends/backend_agg.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323

2424
from contextlib import nullcontext
2525
from math import radians, cos, sin
26-
import threading
2726

2827
import numpy as np
2928

@@ -62,19 +61,6 @@ class RendererAgg(RendererBase):
6261
context instance that controls the colors/styles
6362
"""
6463

65-
# we want to cache the fonts at the class level so that when
66-
# multiple figures are created we can reuse them. This helps with
67-
# a bug on windows where the creation of too many figures leads to
68-
# too many open file handles. However, storing them at the class
69-
# level is not thread safe. The solution here is to let the
70-
# FigureCanvas acquire a lock on the fontd at the start of the
71-
# draw, and release it when it is done. This allows multiple
72-
# renderers to share the cached fonts, but only one figure can
73-
# draw at time and so the font cache is used by only one
74-
# renderer at a time.
75-
76-
lock = threading.RLock()
77-
7864
def __init__(self, width, height, dpi):
7965
super().__init__()
8066

@@ -395,8 +381,7 @@ def draw(self):
395381
self.renderer = self.get_renderer()
396382
self.renderer.clear()
397383
# Acquire a lock on the shared font cache.
398-
with RendererAgg.lock, \
399-
(self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
384+
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
400385
else nullcontext()):
401386
self.figure.draw(self.renderer)
402387
# A GUI class may be need to update a window using this draw, so

lib/matplotlib/figure.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import itertools
3636
import logging
3737
from numbers import Integral
38+
import threading
3839

3940
import numpy as np
4041

@@ -2365,6 +2366,18 @@ class Figure(FigureBase):
23652366
*suppressComposite* is a boolean, this will override the renderer.
23662367
"""
23672368

2369+
# we want to cache the fonts and mathtext at a global level so that when
2370+
# multiple figures are created we can reuse them. This helps with a bug on
2371+
# windows where the creation of too many figures leads to too many open
2372+
# file handles and improves the performance of parsing mathtext. However,
2373+
# these global caches are not thread safe. The solution here is to let the
2374+
# Figure acquire a shared lock at the start of the draw, and release it when it
2375+
# is done. This allows multiple renderers to share the cached fonts and
2376+
# parsed text, but only one figure can draw at a time and so the font cache
2377+
# and mathtext cache are used by only one renderer at a time.
2378+
2379+
_render_lock = threading.RLock()
2380+
23682381
def __str__(self):
23692382
return "Figure(%gx%g)" % tuple(self.bbox.size)
23702383

@@ -3124,33 +3137,33 @@ def clear(self, keep_observers=False):
31243137
@allow_rasterization
31253138
def draw(self, renderer):
31263139
# docstring inherited
3127-
3128-
# draw the figure bounding box, perhaps none for white figure
31293140
if not self.get_visible():
31303141
return
31313142

3132-
artists = self._get_draw_artists(renderer)
3133-
try:
3134-
renderer.open_group('figure', gid=self.get_gid())
3135-
if self.axes and self.get_layout_engine() is not None:
3136-
try:
3137-
self.get_layout_engine().execute(self)
3138-
except ValueError:
3139-
pass
3140-
# ValueError can occur when resizing a window.
3143+
with self._render_lock:
31413144

3142-
self.patch.draw(renderer)
3143-
mimage._draw_list_compositing_images(
3144-
renderer, self, artists, self.suppressComposite)
3145+
artists = self._get_draw_artists(renderer)
3146+
try:
3147+
renderer.open_group('figure', gid=self.get_gid())
3148+
if self.axes and self.get_layout_engine() is not None:
3149+
try:
3150+
self.get_layout_engine().execute(self)
3151+
except ValueError:
3152+
pass
3153+
# ValueError can occur when resizing a window.
31453154

3146-
for sfig in self.subfigs:
3147-
sfig.draw(renderer)
3155+
self.patch.draw(renderer)
3156+
mimage._draw_list_compositing_images(
3157+
renderer, self, artists, self.suppressComposite)
31483158

3149-
renderer.close_group('figure')
3150-
finally:
3151-
self.stale = False
3159+
for sfig in self.subfigs:
3160+
sfig.draw(renderer)
3161+
3162+
renderer.close_group('figure')
3163+
finally:
3164+
self.stale = False
31523165

3153-
DrawEvent("draw_event", self.canvas, renderer)._process()
3166+
DrawEvent("draw_event", self.canvas, renderer)._process()
31543167

31553168
def draw_without_rendering(self):
31563169
"""

0 commit comments

Comments
 (0)