Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

use class level rather than instance level caching of fontd #798

Merged
merged 3 commits into from

4 participants

@jdh2358
Owner

windows users are reporting too many open file handles exceptions which appear to be triggered by too many font files being opened and parsed. I think we may be able to dramatically reduce these for normal use cases by caching the agg font dictionary at the renderer class level rather than instance level.

This needs testing and confirmation by a windows user before merging.

@mdboom
Owner

Ah. Good solution.

@jdh2358
Owner

Because this problem seems to clearly stem from too many font file openings, I am surprised that the call to FT_Done_Face in the FT2Font destructor is not enough to take care of this (in conjunction with garbage collection). This makes me wonder if we need to be triggering garbage collecting during the tests. When pyplot.close is called on a figure, it triggers a gc collect via _pylab_helpers.Gcf.destroy, and the CleanupTest does call close('all') on every test, so we should be getting a gc.collect on every test. So i don't understand why these font openings are creating too many open handles during the test. This leads me to one other possibility: we have been building and packaging freetype with the mpl windows release since before the dawn of time. If we haven't updated our version of freetype in a long time that we a re bundling, could we be seeing a bug in that version. @cgohlke , what version are you compiling mpl against, and if it's ancient could you consider upgrading? It would interesting to know if this fixes the problem even w/o this PR.

Interestingly, backend_ps and backend_pdf already do the caching at the renderer class level. Unsure, why agg was doing it at the renderer level, I went to check git blame to see if maybe it had been moved in and we could get some insight into why. Unfortunately, I found a commit by me entitled "moved agg caches to instance namespace for thread safety" 175e3ec so this was done for a reason. So we probably can't just merge this as is: the question is, can we get a smarter class level font cache that implements some sort of thread safe locking? I am a thread novice, so don't feel like I am the best person to address this.

@cgohlke , because there is a problem with this PR and thread safety, it doubles my interest in the question above: can we fix this by upgrading freetype? And does anyone else have any ideas why these handles are not getting freed? To rehash the above, in v1.1.x the font file handles are in the FT2Font instances, these font objects are in a Renderer instance level dict associated with the figure, the figure is getting closed by the test cleanup, this triggers gc.collect, and this should trigger the destruction of the figure which should lead to the call to the FT2Font destructor which in turn calls FT_Done_Face. In that pipeline, the file handle should be freed. So it looks like the logic is right on the mpl end.

@cgohlke

I am using freetype 2.4.9, the latest version available.

@jdh2358
Owner

OK, scratch the second commit. I think the right approach here is to use a threading Lock right before drawing and release it right after drawing. Now we have a single class level cache that is locked during a single renderer draw.

@ruidc

OK, scratch the second commit.
I'd like to test but am unclear as to what to test given the above line.

@jdh2358
Owner

@mdboom the PR is good to test now. I had tried something more involved in the second commit, and ended up pulling it all for the simpler lock in the third commit. I had also written a long comment after the second commit which I subsequently deleted. So "scratch the second" commit is just me talking to myself and anyone reading the history to say that that line wasn't fruitful. I think what we have in there now is good.

@ruidc

Thanks, replacing the rc version of backend_agg.py with the one in a00e510 resolves this issue for me on win32 and 64 and passes all the tests.

@cgohlke

Works for me on win-amd64-py2.7

@mdboom
Owner

This looks good to me, also. I say we merge. Still agreed?

@jdh2358
Owner

Yep, go ahead. Sorry I've been out of the loop recently. Just getting back from vacation and will have some time to close loose ends and finish this release

@mdboom mdboom merged commit fe1e48a into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 24, 2012
  1. @jdh2358

    use class level rather than instance level caching of fonts to delay …

    jdh2358 authored
    …the too many open file handles windows bug
  2. @jdh2358

    slurp down a class level cache into the instance level at the start o…

    jdh2358 authored
    …f draw, and push it back up when done
  3. @jdh2358
This page is out of date. Refresh to see the latest.
Showing with 32 additions and 8 deletions.
  1. +32 −8 lib/matplotlib/backends/backend_agg.py
View
40 lib/matplotlib/backends/backend_agg.py
@@ -21,7 +21,7 @@
* integrate screen dpi w/ ppi and text
"""
from __future__ import division
-
+import threading
import numpy as np
from matplotlib import verbose, rcParams
@@ -46,11 +46,24 @@ class RendererAgg(RendererBase):
context instance that controls the colors/styles
"""
debug=1
+
+ # we want to cache the fonts at the class level so that when
+ # multiple figures are created we can reuse them. This helps with
+ # a bug on windows where the creation of too many figures leads to
+ # too many open file handles. However, storing them at the class
+ # level is not thread safe. The solution here is to let the
+ # FigureCanvas acquire a lock on the fontd at the start of the
+ # draw, and release it when it is done. This allows multiple
+ # renderers to share the cached fonts, but only one figure can
+ # draw at at time and so the font cache is used by only one
+ # renderer at a time
+
+ lock = threading.Lock()
+ _fontd = maxdict(50)
def __init__(self, width, height, dpi):
if __debug__: verbose.report('RendererAgg.__init__', 'debug-annoying')
RendererBase.__init__(self)
self.texd = maxdict(50) # a cache of tex image rasters
- self._fontd = maxdict(50)
self.dpi = dpi
self.width = width
@@ -69,6 +82,7 @@ def __init__(self, width, height, dpi):
if __debug__: verbose.report('RendererAgg.__init__ done',
'debug-annoying')
+
def _get_hinting_flag(self):
if rcParams['text.hinting']:
return LOAD_FORCE_AUTOHINT
@@ -82,7 +96,7 @@ def draw_markers(self, *kl, **kw):
def draw_path_collection(self, *kl, **kw):
return self._renderer.draw_path_collection(*kl, **kw)
-
+
def _update_methods(self):
#self.draw_path = self._renderer.draw_path # see below
#self.draw_markers = self._renderer.draw_markers
@@ -215,15 +229,16 @@ def _get_agg_font(self, prop):
'debug-annoying')
key = hash(prop)
- font = self._fontd.get(key)
+ font = RendererAgg._fontd.get(key)
if font is None:
fname = findfont(prop)
- font = self._fontd.get(fname)
+ font = RendererAgg._fontd.get(fname)
if font is None:
font = FT2Font(str(fname))
- self._fontd[fname] = font
- self._fontd[key] = font
+ RendererAgg._fontd[fname] = font
+
+ RendererAgg._fontd[key] = font
font.clear()
size = prop.get_size_in_points()
@@ -358,6 +373,7 @@ def post_processing(image, dpi):
image)
+
def new_figure_manager(num, *args, **kwargs):
"""
Create a new figure manager instance
@@ -398,7 +414,15 @@ def draw(self):
if __debug__: verbose.report('FigureCanvasAgg.draw', 'debug-annoying')
self.renderer = self.get_renderer()
- self.figure.draw(self.renderer)
+ # acquire a lock on the shared font cache
+ RendererAgg.lock.acquire()
+
+ try:
+ self.figure.draw(self.renderer)
+ finally:
+ RendererAgg.lock.release()
+
+
def get_renderer(self):
l, b, w, h = self.figure.bbox.bounds
Something went wrong with that request. Please try again.