Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# PythonQwt Releases

## Version 0.16.1

### Bug fixes

- Fixed [Issue #107](https://github.com/PlotPyStack/PythonQwt/issues/107): corrected a Windows crash/access violation that could occur in long-running sessions creating and rendering large amounts of data, due to GDI handle exhaustion caused by object lifetime and cache growth
- Preserved the rendering performance improvements introduced in 0.16.0 while restoring safer Qt object ownership for internal text/scale-draw private data and adding defensive limits to font-metrics caches


## Version 0.16.0

### Performance
Expand Down
2 changes: 1 addition & 1 deletion qwt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
from qwt.text import QwtText # noqa: F401
from qwt.toqimage import array_to_qimage as toQImage # noqa: F401

__version__ = "0.16.0"
__version__ = "0.16.1"
QWT_VERSION_STR = "6.1.5"


Expand Down
33 changes: 7 additions & 26 deletions qwt/scale_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

from qtpy.QtCore import (
QLineF,
QObject,
QPoint,
QPointF,
QRect,
Expand All @@ -50,22 +51,11 @@
_ALIGN_BOTTOM = int(Qt.AlignBottom)


class QwtAbstractScaleDraw_PrivateData(object):
# See QwtText_PrivateData: ``QObject`` inheritance is unused and the
# base class' ``__init__`` is a measurable cost in tick-heavy renders.
__slots__ = (
"spacing",
"penWidth",
"minExtent",
"components",
"tick_length",
"tick_lighter_factor",
"map",
"scaleDiv",
"labelCache",
)
class QwtAbstractScaleDraw_PrivateData(QObject):
# QObject base class restored for Qt parent/child ownership semantics.

def __init__(self):
QObject.__init__(self)
self.spacing = 4
self.penWidth = 0
self.minExtent = 0.0
Expand Down Expand Up @@ -492,20 +482,11 @@ def invalidateCache(self):
self.__data.labelCache.clear()


class QwtScaleDraw_PrivateData(object):
# See QwtText_PrivateData: ``QObject`` inheritance is unused and the
# base class' ``__init__`` is a measurable cost in tick-heavy renders.
__slots__ = (
"len",
"alignment",
"orientation",
"labelAlignment",
"labelRotation",
"labelAutoSize",
"pos",
)
class QwtScaleDraw_PrivateData(QObject):
# QObject base class restored for Qt parent/child ownership semantics.

def __init__(self):
QObject.__init__(self)
self.len = 0
self.alignment = QwtScaleDraw.BottomScale
# Cached orientation - kept in sync by ``QwtScaleDraw.setAlignment``
Expand Down
30 changes: 12 additions & 18 deletions qwt/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ def draw(self, painter, rect, flags, text):
_FONT_TUPLE_CACHE: dict = {} # tuple_key -> tuple_key (interning, also acts
# as the "have we seen this logical font" set)
_FONT_KEY_CACHE_LIMIT = 1024
_FM_CACHE_LIMIT = 256 # max QFontMetrics/QFontMetricsF entries per engine


def _font_tuple_key(font):
Expand Down Expand Up @@ -317,13 +318,17 @@ def fontmetrics(self, font):
try:
return self._fm_cache[fid]
except KeyError:
if len(self._fm_cache) >= _FM_CACHE_LIMIT:
self._fm_cache.clear()
return self._fm_cache.setdefault(fid, QFontMetrics(font))

def fontmetrics_f(self, font):
fid = font_key_cached(font)
try:
return self._fm_cache_f[fid]
except KeyError:
if len(self._fm_cache_f) >= _FM_CACHE_LIMIT:
self._fm_cache_f.clear()
return self._fm_cache_f.setdefault(fid, QFontMetricsF(font))

def heightForWidth(self, font, flags, text, width):
Expand Down Expand Up @@ -359,6 +364,8 @@ def effectiveAscent(self, font):
ascent = ASCENTCACHE.get(fontKey)
if ascent is not None:
return ascent
if len(ASCENTCACHE) >= _FM_CACHE_LIMIT:
ASCENTCACHE.clear()
return ASCENTCACHE.setdefault(fontKey, self.findAscent(font))

def findAscent(self, font):
Expand Down Expand Up @@ -411,6 +418,8 @@ def textMargins(self, font):
if cached is None:
fm = self.fontmetrics(font)
cached = (0, 0, fm.ascent() - self.effectiveAscent(font), fm.descent())
if len(self._margins_cache) >= _FM_CACHE_LIMIT:
self._margins_cache.clear()
self._margins_cache[fkey] = cached
self._margins_last_id = font_id
self._margins_last_value = cached
Expand Down Expand Up @@ -557,26 +566,11 @@ def textMargins(self, font):
return 0, 0, 0, 0


class QwtText_PrivateData(object):
# ``QObject`` was previously used as the base class but no Qt signals
# or events are ever emitted from ``_PrivateData`` containers and the
# ``QObject.__init__`` call dominates ``QwtText.__init__`` (it is the
# single most expensive line for tick-label-heavy renders, see
# https://github.com/PlotPyStack/PythonQwt/issues/93).
__slots__ = (
"renderFlags",
"borderRadius",
"borderPen",
"backgroundBrush",
"paintAttributes",
"layoutAttributes",
"textEngine",
"text",
"font",
"color",
)
class QwtText_PrivateData(QObject):
# QObject base class restored for Qt parent/child ownership semantics.

def __init__(self):
QObject.__init__(self)
self.renderFlags = Qt.AlignCenter
self.borderRadius = 0
self.borderPen = Qt.NoPen
Expand Down