From 57e5e3bd378f59a5856a40341ee5aefd421a7d10 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson <42893476+bmjcode@users.noreply.github.com> Date: Sat, 9 May 2026 10:08:00 -0400 Subject: [PATCH 1/4] Optimize the cache size to avoid unnecessary re-rendering At higher zoom levels, the hard-coded limit of 200MB may be too small to hold all the displayed tiles, forcing them to be re-rendered each time the widget is repainted. This in turn can cause flickering as the tiles are erased, re-rendered, and repainted. This is especially noticeable when scrolling over a page break; rendering each page purges the other's tiles from cache, causing both to be re-rendered at each increment. This patch scales the cache size dynamically with the page size to prevent re-rendering in this scenario while still limiting memory usage as much as possible. --- qpageview/render.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qpageview/render.py b/qpageview/render.py index 89d4b7a..301846d 100644 --- a/qpageview/render.py +++ b/qpageview/render.py @@ -380,6 +380,11 @@ def schedule(self, page, key, tiles, callback): pending job. """ + # adjust the cache size to fit at least two pages' visible tiles + # to avoid re-rendering either page when both are displayed + # derivation: tile area * (32 bits == 4 bytes / pixel) * 2 pages + self.cache.maxsize = max(type(self.cache).maxsize, + 8 * sum(tile.w * tile.h for tile in tiles)) for tile in tiles: try: job = _jobs[(key, tile)] From 8fb666db72430dc72eea4cc9c03f5a6fc15b167f Mon Sep 17 00:00:00 2001 From: Benjamin Johnson <42893476+bmjcode@users.noreply.github.com> Date: Sat, 9 May 2026 13:30:30 -0400 Subject: [PATCH 2/4] Fix a couple bugs in the cache-purging logic These were allowing images to stay in cache longer than needed when the cache was at or near its maximum size, which was wasting memory at higher zoom levels. --- qpageview/cache.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/qpageview/cache.py b/qpageview/cache.py index eefb8af..3a8a7d4 100644 --- a/qpageview/cache.py +++ b/qpageview/cache.py @@ -77,12 +77,10 @@ def addtile(self, key, tile, image): except KeyError: pass - purgeneeded = self.currentsize > self.maxsize - e = d[tile] = ImageEntry(image) self.currentsize += e.bcount - if not purgeneeded: + if self.currentsize <= self.maxsize: return # purge old images is needed, @@ -100,7 +98,7 @@ def addtile(self, key, tile, image): currentsize = 0 for time, bcount, group, ident, key, tile in entries: currentsize += bcount - if currentsize > self.maxsize: + if currentsize >= self.maxsize: break self.currentsize = currentsize # ... and delete the remaining images, deleting empty dicts as well From b7fe9886da0046644baa459d8bbe6dc44f44a220 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson <42893476+bmjcode@users.noreply.github.com> Date: Sat, 9 May 2026 14:32:10 -0400 Subject: [PATCH 3/4] Purge old images before rendering new tiles This is a fairly self-explanatory memory optimization. --- qpageview/cache.py | 32 +++++++++++++++++++++++++++----- qpageview/render.py | 6 +----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/qpageview/cache.py b/qpageview/cache.py index 3a8a7d4..95d2e8d 100644 --- a/qpageview/cache.py +++ b/qpageview/cache.py @@ -69,6 +69,23 @@ def tileset(self, key): except KeyError: return {} + def prepare(self, tiles): + """Make room before rendering new tiles. + + This ensures there is space for the new tiles by adjusting + the cache size and/or purging older images as needed. + + """ + # approximate size of the new tiles at 32 bits (== 4 bytes) per pixel + tileBytes = sum(tile.w * tile.h for tile in tiles) * 4 + + # adjust the cache size to fit at least two pages' visible tiles + # to avoid re-rendering either page when both are displayed + self.maxsize = max(type(self).maxsize, 2 * tileBytes) + + # free memory from unneeded tiles before rendering the new ones + self.purge(tileBytes) + def addtile(self, key, tile, image): """Add image for the specified key and tile.""" d = self._cache.setdefault(key.group, {}).setdefault(key.ident, {}).setdefault(key[2:], {}) @@ -80,12 +97,17 @@ def addtile(self, key, tile, image): e = d[tile] = ImageEntry(image) self.currentsize += e.bcount - if self.currentsize <= self.maxsize: + def purge(self, reservedsize=0): + """Purge old images if needed. + + At least `reservedsize` bytes will remain available. + + """ + limit = self.maxsize - reservedsize + if self.currentsize <= limit: return - # purge old images is needed, # cache groups may have disappeared so count all images - entries = iter(sorted( ((entry.time, entry.bcount, group, ident, key, tile) for group, identd in self._cache.items() @@ -94,11 +116,11 @@ def addtile(self, key, tile, image): for tile, entry in tiled.items()), key=(lambda item: item[:2]), reverse=True)) - # now count the newest images until maxsize ... + # now count the newest images until our limit ... currentsize = 0 for time, bcount, group, ident, key, tile in entries: currentsize += bcount - if currentsize >= self.maxsize: + if currentsize >= limit: break self.currentsize = currentsize # ... and delete the remaining images, deleting empty dicts as well diff --git a/qpageview/render.py b/qpageview/render.py index 301846d..9ce5117 100644 --- a/qpageview/render.py +++ b/qpageview/render.py @@ -380,11 +380,7 @@ def schedule(self, page, key, tiles, callback): pending job. """ - # adjust the cache size to fit at least two pages' visible tiles - # to avoid re-rendering either page when both are displayed - # derivation: tile area * (32 bits == 4 bytes / pixel) * 2 pages - self.cache.maxsize = max(type(self.cache).maxsize, - 8 * sum(tile.w * tile.h for tile in tiles)) + self.cache.prepare(tiles) for tile in tiles: try: job = _jobs[(key, tile)] From e4ab3d09a91e44714f57b3b9c292a68bf779eb05 Mon Sep 17 00:00:00 2001 From: Benjamin Johnson <42893476+bmjcode@users.noreply.github.com> Date: Sat, 9 May 2026 14:54:56 -0400 Subject: [PATCH 4/4] Clear everything when shrinking the cache This is mainly a workaround for large tiles surviving purge() when reducing the zoom level, but honestly it seems like a reasonable thing to do anyway in its own right. --- qpageview/cache.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/qpageview/cache.py b/qpageview/cache.py index 95d2e8d..9e7411e 100644 --- a/qpageview/cache.py +++ b/qpageview/cache.py @@ -81,10 +81,15 @@ def prepare(self, tiles): # adjust the cache size to fit at least two pages' visible tiles # to avoid re-rendering either page when both are displayed + oldsize = self.maxsize self.maxsize = max(type(self).maxsize, 2 * tileBytes) - - # free memory from unneeded tiles before rendering the new ones - self.purge(tileBytes) + if self.maxsize < oldsize: + # clear everything when shrinking the cache because otherwise + # larger tiles might prove difficult to purge() + self.clear() + else: + # free memory from unneeded tiles before rendering the new ones + self.purge(tileBytes) def addtile(self, key, tile, image): """Add image for the specified key and tile."""