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

Skip to content

Commit bd5812b

Browse files
committed
Reduce number of font files opened
This should hopefully address the long-reported "Too many open files" error message (Fix #3315). To reproduce: On a Mac or Windows box with starvation for file handles (Linux has a much higher file handle limit by default), build the docs, then immediately build again. This will trigger the caching bug. The font cache in the mathtext renderer was broken. It was caching a font file once for every *combination* of font properties, including things like size. Therefore, in a complex math expression containing many different sizes of the same font, the font file was opened once for each of those sizes. Font files are opened and kept open (rather than opened, read, and closed) so that FreeType only needs to load the actual glyphs that are used, rather than the entire font. In an era of cheap memory and fast disk, it probably doesn't matter for our current fonts, but once #5214 is merged, we will have larger font files with many more glyphs and this loading time will matter more. The solution here is to do all font file loading in one place and to use `lru_cache` (available since Python 3.2) to do the caching, and to use only the file name and hinting parameters as a cache key. For earlier versions of Python, the functools32 backport package is required. (Or we can discuss whether we want to vendor it).
1 parent d0ca019 commit bd5812b

File tree

12 files changed

+67
-79
lines changed

12 files changed

+67
-79
lines changed

lib/matplotlib/backends/backend_agg.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
FigureManagerBase, FigureCanvasBase
3333
from matplotlib.cbook import is_string_like, maxdict, restrict_dict
3434
from matplotlib.figure import Figure
35-
from matplotlib.font_manager import findfont
36-
from matplotlib.ft2font import FT2Font, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, \
35+
from matplotlib.font_manager import findfont, get_font
36+
from matplotlib.ft2font import LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, \
3737
LOAD_DEFAULT, LOAD_NO_AUTOHINT
3838
from matplotlib.mathtext import MathTextParser
3939
from matplotlib.path import Path
@@ -81,7 +81,6 @@ class RendererAgg(RendererBase):
8181
# renderer at a time
8282

8383
lock = threading.RLock()
84-
_fontd = maxdict(50)
8584
def __init__(self, width, height, dpi):
8685
if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
8786
RendererBase.__init__(self)
@@ -191,6 +190,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
191190

192191
flags = get_hinting_flag()
193192
font = self._get_agg_font(prop)
193+
194194
if font is None: return None
195195
if len(s) == 1 and ord(s) > 127:
196196
font.load_char(ord(s), flags=flags)
@@ -272,18 +272,10 @@ def _get_agg_font(self, prop):
272272
if __debug__: verbose.report('RendererAgg._get_agg_font',
273273
'debug-annoying')
274274

275-
key = hash(prop)
276-
font = RendererAgg._fontd.get(key)
277-
278-
if font is None:
279-
fname = findfont(prop)
280-
font = RendererAgg._fontd.get(fname)
281-
if font is None:
282-
font = FT2Font(
283-
fname,
284-
hinting_factor=rcParams['text.hinting_factor'])
285-
RendererAgg._fontd[fname] = font
286-
RendererAgg._fontd[key] = font
275+
fname = findfont(prop)
276+
font = get_font(
277+
fname,
278+
hinting_factor=rcParams['text.hinting_factor'])
287279

288280
font.clear()
289281
size = prop.get_size_in_points()

lib/matplotlib/backends/backend_pdf.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@
3535
from matplotlib.cbook import Bunch, is_string_like, \
3636
get_realpath_and_stat, is_writable_file_like, maxdict
3737
from matplotlib.figure import Figure
38-
from matplotlib.font_manager import findfont, is_opentype_cff_font
38+
from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font
3939
from matplotlib.afm import AFM
4040
import matplotlib.type1font as type1font
4141
import matplotlib.dviread as dviread
42-
from matplotlib.ft2font import FT2Font, FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, \
42+
from matplotlib.ft2font import FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, \
4343
LOAD_NO_HINTING, KERNING_UNFITTED
4444
from matplotlib.mathtext import MathTextParser
4545
from matplotlib.transforms import Affine2D, BboxBase
@@ -757,7 +757,7 @@ def createType1Descriptor(self, t1font, fontfile):
757757
if 0:
758758
flags |= 1 << 18
759759

760-
ft2font = FT2Font(fontfile)
760+
ft2font = get_font(fontfile)
761761

762762
descriptor = {
763763
'Type': Name('FontDescriptor'),
@@ -817,7 +817,7 @@ def _get_xobject_symbol_name(self, filename, symbol_name):
817817
def embedTTF(self, filename, characters):
818818
"""Embed the TTF font from the named file into the document."""
819819

820-
font = FT2Font(filename)
820+
font = get_font(filename)
821821
fonttype = rcParams['pdf.fonttype']
822822

823823
def cvt(length, upe=font.units_per_EM, nearest=True):
@@ -1526,7 +1526,6 @@ def writeTrailer(self):
15261526

15271527

15281528
class RendererPdf(RendererBase):
1529-
truetype_font_cache = maxdict(50)
15301529
afm_font_cache = maxdict(50)
15311530

15321531
def __init__(self, file, image_dpi):
@@ -2126,15 +2125,8 @@ def _get_font_afm(self, prop):
21262125
return font
21272126

21282127
def _get_font_ttf(self, prop):
2129-
key = hash(prop)
2130-
font = self.truetype_font_cache.get(key)
2131-
if font is None:
2132-
filename = findfont(prop)
2133-
font = self.truetype_font_cache.get(filename)
2134-
if font is None:
2135-
font = FT2Font(filename)
2136-
self.truetype_font_cache[filename] = font
2137-
self.truetype_font_cache[key] = font
2128+
filename = findfont(prop)
2129+
font = get_font(filename)
21382130
font.clear()
21392131
font.set_size(prop.get_size_in_points(), 72)
21402132
return font

lib/matplotlib/backends/backend_pgf.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,9 @@
3636
system_fonts = []
3737
if sys.platform.startswith('win'):
3838
from matplotlib import font_manager
39-
from matplotlib.ft2font import FT2Font
4039
for f in font_manager.win32InstalledFonts():
4140
try:
42-
system_fonts.append(FT2Font(str(f)).family_name)
41+
system_fonts.append(font_manager.get_font(str(f)).family_name)
4342
except:
4443
pass # unknown error, skip this font
4544
else:

lib/matplotlib/backends/backend_ps.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ def _fn_name(): return sys._getframe(1).f_code.co_name
2828
is_writable_file_like, maxdict, file_requires_unicode
2929
from matplotlib.figure import Figure
3030

31-
from matplotlib.font_manager import findfont, is_opentype_cff_font
32-
from matplotlib.ft2font import FT2Font, KERNING_DEFAULT, LOAD_NO_HINTING
31+
from matplotlib.font_manager import findfont, is_opentype_cff_font, get_font
32+
from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
3333
from matplotlib.ttconv import convert_ttf_to_ps
3434
from matplotlib.mathtext import MathTextParser
3535
from matplotlib._mathtext_data import uni2type1
@@ -199,7 +199,6 @@ class RendererPS(RendererBase):
199199
context instance that controls the colors/styles.
200200
"""
201201

202-
fontd = maxdict(50)
203202
afmfontd = maxdict(50)
204203

205204
def __init__(self, width, height, pswriter, imagedpi=72):
@@ -393,15 +392,8 @@ def _get_font_afm(self, prop):
393392
return font
394393

395394
def _get_font_ttf(self, prop):
396-
key = hash(prop)
397-
font = self.fontd.get(key)
398-
if font is None:
399-
fname = findfont(prop)
400-
font = self.fontd.get(fname)
401-
if font is None:
402-
font = FT2Font(fname)
403-
self.fontd[fname] = font
404-
self.fontd[key] = font
395+
fname = findfont(prop)
396+
font = get_font(fname)
405397
font.clear()
406398
size = prop.get_size_in_points()
407399
font.set_size(size, 72.0)
@@ -1145,7 +1137,7 @@ def print_figure_impl():
11451137
if not rcParams['ps.useafm']:
11461138
for font_filename, chars in six.itervalues(ps_renderer.used_characters):
11471139
if len(chars):
1148-
font = FT2Font(font_filename)
1140+
font = get_font(font_filename)
11491141
cmap = font.get_charmap()
11501142
glyph_ids = []
11511143
for c in chars:

lib/matplotlib/backends/backend_svg.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
from matplotlib.cbook import is_string_like, is_writable_file_like, maxdict
2020
from matplotlib.colors import rgb2hex
2121
from matplotlib.figure import Figure
22-
from matplotlib.font_manager import findfont, FontProperties
23-
from matplotlib.ft2font import FT2Font, KERNING_DEFAULT, LOAD_NO_HINTING
22+
from matplotlib.font_manager import findfont, FontProperties, get_font
23+
from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
2424
from matplotlib.mathtext import MathTextParser
2525
from matplotlib.path import Path
2626
from matplotlib import _path
@@ -326,15 +326,8 @@ def _make_flip_transform(self, transform):
326326
.translate(0.0, self.height))
327327

328328
def _get_font(self, prop):
329-
key = hash(prop)
330-
font = self.fontd.get(key)
331-
if font is None:
332-
fname = findfont(prop)
333-
font = self.fontd.get(fname)
334-
if font is None:
335-
font = FT2Font(fname)
336-
self.fontd[fname] = font
337-
self.fontd[key] = font
329+
fname = findfont(prop)
330+
font = get_font(fname)
338331
font.clear()
339332
size = prop.get_size_in_points()
340333
font.set_size(size, 72.0)
@@ -495,7 +488,7 @@ def _write_svgfonts(self):
495488
writer = self.writer
496489
writer.start('defs')
497490
for font_fname, chars in six.iteritems(self._fonts):
498-
font = FT2Font(font_fname)
491+
font = get_font(font_fname)
499492
font.set_size(72, 72)
500493
sfnt = font.get_sfnt()
501494
writer.start('font', id=sfnt[(1, 0, 0, 4)])

lib/matplotlib/font_manager.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@
6363
from matplotlib.fontconfig_pattern import \
6464
parse_fontconfig_pattern, generate_fontconfig_pattern
6565

66+
try:
67+
from functools import lru_cache
68+
except ImportError:
69+
from functools32 import lru_cache
70+
71+
6672
USE_FONTCONFIG = False
6773
verbose = matplotlib.verbose
6874

@@ -733,7 +739,7 @@ def get_name(self):
733739
Return the name of the font that best matches the font
734740
properties.
735741
"""
736-
return ft2font.FT2Font(findfont(self)).family_name
742+
return get_font(findfont(self)).family_name
737743

738744
def get_style(self):
739745
"""
@@ -1336,7 +1342,6 @@ def findfont(self, prop, fontext='ttf', directory=None,
13361342
_lookup_cache[fontext].set(prop, result)
13371343
return result
13381344

1339-
13401345
_is_opentype_cff_font_cache = {}
13411346
def is_opentype_cff_font(filename):
13421347
"""
@@ -1357,6 +1362,10 @@ def is_opentype_cff_font(filename):
13571362
fontManager = None
13581363
_fmcache = None
13591364

1365+
1366+
get_font = lru_cache(64)(ft2font.FT2Font)
1367+
1368+
13601369
# The experimental fontconfig-based backend.
13611370
if USE_FONTCONFIG and sys.platform != 'win32':
13621371
import re

lib/matplotlib/mathtext.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
from matplotlib.afm import AFM
5151
from matplotlib.cbook import Bunch, get_realpath_and_stat, \
5252
is_string_like, maxdict
53-
from matplotlib.ft2font import FT2Font, FT2Image, KERNING_DEFAULT, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING
54-
from matplotlib.font_manager import findfont, FontProperties
53+
from matplotlib.ft2font import FT2Image, KERNING_DEFAULT, LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING
54+
from matplotlib.font_manager import findfont, FontProperties, get_font
5555
from matplotlib._mathtext_data import latex_to_bakoma, \
5656
latex_to_standard, tex2uni, latex_to_cmex, stix_virtual_fonts
5757
from matplotlib import get_data_path, rcParams
@@ -563,7 +563,7 @@ def __init__(self, default_font_prop, mathtext_backend):
563563
self._fonts = {}
564564

565565
filename = findfont(default_font_prop)
566-
default_font = self.CachedFont(FT2Font(filename))
566+
default_font = self.CachedFont(get_font(filename))
567567
self._fonts['default'] = default_font
568568
self._fonts['regular'] = default_font
569569

@@ -576,10 +576,9 @@ def _get_font(self, font):
576576
basename = self.fontmap[font]
577577
else:
578578
basename = font
579-
580579
cached_font = self._fonts.get(basename)
581580
if cached_font is None and os.path.exists(basename):
582-
font = FT2Font(basename)
581+
font = get_font(basename)
583582
cached_font = self.CachedFont(font)
584583
self._fonts[basename] = cached_font
585584
self._fonts[font.postscript_name] = cached_font

lib/matplotlib/tests/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ def setup():
4949
rcParams['text.hinting'] = False
5050
rcParams['text.hinting_factor'] = 8
5151

52-
# Clear the font caches. Otherwise, the hinting mode can travel
53-
# from one test to another.
54-
backend_agg.RendererAgg._fontd.clear()
55-
backend_pdf.RendererPdf.truetype_font_cache.clear()
56-
backend_svg.RendererSVG.fontd.clear()
57-
5852

5953
def assert_str_equal(reference_str, test_str,
6054
format_str=('String {str1} and {str2} do not '

lib/matplotlib/textpath.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
from matplotlib.path import Path
1414
from matplotlib import rcParams
1515
import matplotlib.font_manager as font_manager
16-
from matplotlib.ft2font import FT2Font, KERNING_DEFAULT, LOAD_NO_HINTING
16+
from matplotlib.ft2font import KERNING_DEFAULT, LOAD_NO_HINTING
1717
from matplotlib.ft2font import LOAD_TARGET_LIGHT
1818
from matplotlib.mathtext import MathTextParser
1919
import matplotlib.dviread as dviread
20-
from matplotlib.font_manager import FontProperties
20+
from matplotlib.font_manager import FontProperties, get_font
2121
from matplotlib.transforms import Affine2D
2222
from matplotlib.externals.six.moves.urllib.parse import quote as urllib_quote
2323

@@ -54,7 +54,7 @@ def _get_font(self, prop):
5454
find a ttf font.
5555
"""
5656
fname = font_manager.findfont(prop)
57-
font = FT2Font(fname)
57+
font = get_font(fname)
5858
font.set_size(self.FONT_SCALE, self.DPI)
5959

6060
return font
@@ -334,7 +334,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None,
334334
font_bunch = self.tex_font_map[dvifont.texname]
335335

336336
if font_and_encoding is None:
337-
font = FT2Font(font_bunch.filename)
337+
font = get_font(font_bunch.filename)
338338

339339
for charmap_name, charmap_code in [("ADOBE_CUSTOM",
340340
1094992451),

lib/mpl_toolkits/tests/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,6 @@ def setup():
4949
rcParams['text.hinting'] = False
5050
rcParams['text.hinting_factor'] = 8
5151

52-
# Clear the font caches. Otherwise, the hinting mode can travel
53-
# from one test to another.
54-
backend_agg.RendererAgg._fontd.clear()
55-
backend_pdf.RendererPdf.truetype_font_cache.clear()
56-
backend_svg.RendererSVG.fontd.clear()
57-
5852

5953
def assert_str_equal(reference_str, test_str,
6054
format_str=('String {str1} and {str2} do not '

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
'Required dependencies and extensions',
6868
setupext.Numpy(),
6969
setupext.Dateutil(),
70+
setupext.FuncTools32(),
7071
setupext.Pytz(),
7172
setupext.Cycler(),
7273
setupext.Tornado(),

setupext.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,29 @@ def get_install_requires(self):
12211221
return [dateutil]
12221222

12231223

1224+
class FuncTools32(SetupPackage):
1225+
name = "functools32"
1226+
1227+
def check(self):
1228+
if sys.version_info[:2] < (3, 2):
1229+
try:
1230+
import functools32
1231+
except ImportError:
1232+
return (
1233+
"functools32 was not found. It is required for for"
1234+
"python versions prior to 3.2")
1235+
1236+
return "using functools32 version %s" % functools32.__version__
1237+
else:
1238+
return "Not required"
1239+
1240+
def get_install_requires(self):
1241+
if sys.version_info[:2] < (3, 2):
1242+
return ['functools32']
1243+
else:
1244+
return []
1245+
1246+
12241247
class Tornado(OptionalPackage):
12251248
name = "tornado"
12261249

0 commit comments

Comments
 (0)