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
39 changes: 32 additions & 7 deletions qpageview/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ 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
oldsize = self.maxsize
self.maxsize = max(type(self).maxsize, 2 * tileBytes)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, the cache size was fixed at 200MB, which was too small for the tile images at higher zoom levels. This code dynamically adjusts the size to the larger of 200MB or two pages' visible tiles. We do two pages because otherwise each would purge the other from cache when scrolling over a page break, forcing frequent re-renders (you can see this now scrolling over a page break at 800% zoom).

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."""
d = self._cache.setdefault(key.group, {}).setdefault(key.ident, {}).setdefault(key[2:], {})
Expand All @@ -77,17 +99,20 @@ 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:
def purge(self, reservedsize=0):
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously part of addtile() and done after the new tile was added. I made it a separate method so it could be called before, which is more efficient.

"""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()
Expand All @@ -96,11 +121,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:
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing to >= fixes a bug in the original code where an extra image could be added when the cache was exactly at its size limit.

break
self.currentsize = currentsize
# ... and delete the remaining images, deleting empty dicts as well
Expand Down
1 change: 1 addition & 0 deletions qpageview/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ def schedule(self, page, key, tiles, callback):
pending job.

"""
self.cache.prepare(tiles)
Copy link
Copy Markdown
Member Author

@bmjcode bmjcode May 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls our new code to adjust the maximum cache size and purge old tiles just before creating the jobs to render new ones. Previously, the maximum cache size was fixed, and old tiles were purged after rendering and caching new ones.

for tile in tiles:
try:
job = _jobs[(key, tile)]
Expand Down
Loading