From aa4bb8aa08e06fb3faa464f4f056ecd7faa7d0f5 Mon Sep 17 00:00:00 2001 From: John Hunter Date: Fri, 23 Mar 2012 23:42:27 -0500 Subject: [PATCH 1/3] use class level rather than instance level caching of fonts to delay the too many open file handles windows bug --- lib/matplotlib/backends/backend_agg.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 03331f5ed22d..42d510596dbe 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -46,11 +46,13 @@ class RendererAgg(RendererBase): context instance that controls the colors/styles """ debug=1 + _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 @@ -82,7 +84,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 +217,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() From cc628ceb492d756194310788e3aaba5364b491d0 Mon Sep 17 00:00:00 2001 From: John Hunter Date: Sat, 24 Mar 2012 16:20:57 -0500 Subject: [PATCH 2/3] slurp down a class level cache into the instance level at the start of draw, and push it back up when done --- lib/matplotlib/backends/backend_agg.py | 36 +++++++++++++++++++++----- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 42d510596dbe..ae10cd06daad 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -46,12 +46,23 @@ class RendererAgg(RendererBase): context instance that controls the colors/styles """ debug=1 - _fontd = maxdict(50) + + # 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 cache the + # fonts at class level, but for a given renderer to slurp them + # down into the instance cache "_fontd_instance" from the + # "_fontd_class" in a call to pre_draw_hook (managed by the + # FigureCanvas) and to restore them to the fontd_class in the + # post_draw_hook. + _fontd_class = 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_instance = maxdict(50) self.dpi = dpi @@ -71,6 +82,16 @@ def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') + def pre_draw_hook(self): + 'called by FigureCanvas right before draw; slurp in the class level cache' + self._fontd_instance = RendererAgg._fontd_class + RendererAgg._fontd_class = {} + + def post_draw_hook(self): + 'called by FigureCanvas right after draw; restore the class level cache' + RendererAgg._fontd_class = self._fontd_instance + self._fontd_instance = {} + def _get_hinting_flag(self): if rcParams['text.hinting']: return LOAD_FORCE_AUTOHINT @@ -217,16 +238,16 @@ def _get_agg_font(self, prop): 'debug-annoying') key = hash(prop) - font = RendererAgg._fontd.get(key) + font = self._fontd_instance.get(key) if font is None: fname = findfont(prop) - font = RendererAgg._fontd.get(fname) + font = self._fontd_instance.get(fname) if font is None: font = FT2Font(str(fname)) - RendererAgg._fontd[fname] = font + self._fontd_instance[fname] = font - RendererAgg._fontd[key] = font + self._fontd_instance[key] = font font.clear() size = prop.get_size_in_points() @@ -361,6 +382,7 @@ def post_processing(image, dpi): image) + def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance @@ -401,7 +423,9 @@ def draw(self): if __debug__: verbose.report('FigureCanvasAgg.draw', 'debug-annoying') self.renderer = self.get_renderer() + self.renderer.pre_draw_hook() self.figure.draw(self.renderer) + self.renderer.post_draw_hook() def get_renderer(self): l, b, w, h = self.figure.bbox.bounds From a00e510e7f3bcb113a55b47469a68f35b6b7c16b Mon Sep 17 00:00:00 2001 From: John Hunter Date: Sat, 24 Mar 2012 16:46:01 -0500 Subject: [PATCH 3/3] use threading Lock to protect class level font cache --- lib/matplotlib/backends/backend_agg.py | 49 ++++++++++++-------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index ae10cd06daad..fea9cb80c9bc 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/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 @@ -51,19 +51,19 @@ class RendererAgg(RendererBase): # 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 cache the - # fonts at class level, but for a given renderer to slurp them - # down into the instance cache "_fontd_instance" from the - # "_fontd_class" in a call to pre_draw_hook (managed by the - # FigureCanvas) and to restore them to the fontd_class in the - # post_draw_hook. - _fontd_class = maxdict(50) + # 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_instance = maxdict(50) - self.dpi = dpi self.width = width @@ -82,15 +82,6 @@ def __init__(self, width, height, dpi): if __debug__: verbose.report('RendererAgg.__init__ done', 'debug-annoying') - def pre_draw_hook(self): - 'called by FigureCanvas right before draw; slurp in the class level cache' - self._fontd_instance = RendererAgg._fontd_class - RendererAgg._fontd_class = {} - - def post_draw_hook(self): - 'called by FigureCanvas right after draw; restore the class level cache' - RendererAgg._fontd_class = self._fontd_instance - self._fontd_instance = {} def _get_hinting_flag(self): if rcParams['text.hinting']: @@ -238,16 +229,16 @@ def _get_agg_font(self, prop): 'debug-annoying') key = hash(prop) - font = self._fontd_instance.get(key) + font = RendererAgg._fontd.get(key) if font is None: fname = findfont(prop) - font = self._fontd_instance.get(fname) + font = RendererAgg._fontd.get(fname) if font is None: font = FT2Font(str(fname)) - self._fontd_instance[fname] = font + RendererAgg._fontd[fname] = font - self._fontd_instance[key] = font + RendererAgg._fontd[key] = font font.clear() size = prop.get_size_in_points() @@ -423,9 +414,15 @@ def draw(self): if __debug__: verbose.report('FigureCanvasAgg.draw', 'debug-annoying') self.renderer = self.get_renderer() - self.renderer.pre_draw_hook() - self.figure.draw(self.renderer) - self.renderer.post_draw_hook() + # 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