Use a LRU cache to avoid extra HTTP requests when destroying/re-building the books list page

This commit is contained in:
Kovid Goyal 2017-02-09 10:52:47 +05:30
parent 4875060c50
commit 9c156f9ba2
3 changed files with 125 additions and 16 deletions

View File

@ -6,7 +6,7 @@ from dom import clear, set_css, build_rule
from elementmaker import E from elementmaker import E
from gettext import gettext as _ from gettext import gettext as _
from book_list.library_data import cover_url, book_metadata from book_list.library_data import thumbnail_cache, book_metadata
COVER_GRID_CLASS = 'book-list-cover-grid' COVER_GRID_CLASS = 'book-list-cover-grid'
THUMBNAIL_MAX_WIDTH = 300 THUMBNAIL_MAX_WIDTH = 300
@ -41,26 +41,26 @@ def init(container):
for i in range(12): for i in range(12):
container.lastChild.appendChild(E.div(class_='cover-grid-filler')) container.lastChild.appendChild(E.div(class_='cover-grid-filler'))
def on_img_load_error(err): def on_img_load(img, load_type):
img = err.target
div = img.parentNode div = img.parentNode
if not div: if not div:
return return
clear(div) if load_type is not 'load':
div.appendChild(E.div( metadata = book_metadata(int(img.dataset.bookId))
E.h2(img.getAttribute('data-title'), style='text-align:center; font-size:larger; font-weight: bold'), if metadata:
E.div(_('by'), style='text-align: center'), clear(div)
E.h2(img.getAttribute('data-authors'), style='text-align:center; font-size:larger; font-weight: bold') div.appendChild(E.div(
)) E.h2(metadata.title, style='text-align:center; font-size:larger; font-weight: bold'),
set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px') E.div(_('by'), style='text-align: center'),
E.h2(metadata.authors.join(' & '), style='text-align:center; font-size:larger; font-weight: bold')
))
set_css(div, border='dashed 1px currentColor', border_radius=BORDER_RADIUS+'px')
def create_item(book_id, show_book_details): def create_item(book_id, show_book_details):
curl = cover_url(book_id, THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT)
metadata = book_metadata(book_id) metadata = book_metadata(book_id)
alt = _('{} by {}').format(metadata.title, metadata.authors.join(' & ')) authors = metadata.authors.join(' & ')
img = E.img(src=curl, alt=alt, title=alt, data_title=metadata.title, data_authors=metadata.authors.join(' & ')) img = thumbnail_cache.get(book_id, THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT, on_img_load)
img.onerror = on_img_load_error img.setAttribute('alt', _('{} by {}').format(metadata.title, authors))
ans = E.div(img, data_book_id=str(book_id), onclick=show_book_details) ans = E.div(img, data_book_id=str(book_id), onclick=show_book_details)
return ans return ans

View File

@ -3,6 +3,7 @@
from __python__ import hash_literals, bound_methods from __python__ import hash_literals, bound_methods
from ajax import ajax from ajax import ajax
from lru_cache import LRUCache
from session import get_interface_data from session import get_interface_data
from utils import parse_url_params from utils import parse_url_params
@ -77,7 +78,7 @@ def fetch_init_data():
load_status.current_fetch.send() load_status.current_fetch.send()
def cover_url(book_id, width, height): def thumbnail_url(book_id, width, height):
return 'get/thumb/{}/{}?sz={}x{}'.format(book_id, loaded_books_query().library_id, Math.ceil(width * window.devicePixelRatio), Math.ceil(height * window.devicePixelRatio)) return 'get/thumb/{}/{}?sz={}x{}'.format(book_id, loaded_books_query().library_id, Math.ceil(width * window.devicePixelRatio), Math.ceil(height * window.devicePixelRatio))
@ -95,3 +96,41 @@ def ensure_current_library_data():
break break
if not matches: if not matches:
fetch_init_data() fetch_init_data()
class ThumbnailCache:
# Cache to prevent browser from issuing HTTP requests when thumbnails pages
# are destroyed/rebuilt.
def __init__(self, size=250):
self.cache = LRUCache(size)
def get(self, book_id, width, height, callback):
url = thumbnail_url(book_id, width, height)
item = self.cache.get(url)
if item is None:
img = new Image()
item = {'img':img, 'load_type':None, 'callbacks':v'[callback]'}
img.onerror = self.load_finished.bind(None, item, 'error')
img.onload = self.load_finished.bind(None, item, 'load')
img.onabort = self.load_finished.bind(None, item, 'abort')
img.dataset.bookId = str(book_id)
img.src = url
self.cache.set(url, item)
return img
if item.load_type is None:
if item.callbacks.indexOf(callback) < 0:
item.callbacks.push(callback)
else:
callback(item.img, item.load_type)
return item.img
def load_finished(self, item, load_type):
item.load_type = load_type
img = item.img
img.onload = img.onerror = img.onabort = None
for callback in item.callbacks:
callback(img, load_type)
thumbnail_cache = ThumbnailCache()

70
src/pyj/lru_cache.pyj Normal file
View File

@ -0,0 +1,70 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import hash_literals, bound_methods
def lru_node(key, val):
return {'key': key, 'val':val, 'prev':None, 'next':None}
class LRUCache:
def __init__(self, size):
self.limit = 200
self.clear(size)
def set_head(self, node):
node.next = self.head
node.prev = None
if self.head is not None:
self.head.prev = node
self.head = node
if self.tail is None:
self.tail = node
self.size += 1
self.map[node.key] = node
def pop(self, key):
node = self.map[key]
if not node:
return
if node.prev is not None:
node.prev.next = node.next
else:
self.head = node.next
if node.next is not None:
node.next.prev = node.prev
else:
self.tail = node.prev
v'delete self.map[key]'
self.size -= 1
def set(self, key, val):
node = lru_node(key, val)
existing = self.map[key]
if existing:
existing.value = node.value
self.pop(node.key)
elif self.size > self.limit:
v'delete self.map[self.tail.key]'
self.size -= 1
self.tail = self.tail.prev
self.tail.next = None
self.set_head(node)
def get(self, key, defval):
existing = self.map[key]
if not existing:
return None if defval is undefined else defval
val = existing.value
node = lru_node(key, val)
self.pop(key)
self.set_head(node)
return val
def clear(self, size):
self.size = 0
self.map = {}
self.head = self.tail = None
if jstype(size) is 'number':
self.limit = size