Commit e99cf1fa authored by Daniel Hosseinian's avatar Daniel Hosseinian Committed by Commit Bot

PDF Viewer Update: Clear thumbnails that are far out of view

Discard thumbnail image data for thumbnails that are far from view to
save memory.

The top margin of 500% was selected with the assumption that every
100% height of the thumbnail bar can fit ~4 thumbnails on an average
screen. Along with the 100% bottom margin and the visible area, around
30 thumbnails worth of image data would be kept in memory at any given
time. Metrics also show that ~90% of PDFs are less than 30 pages.

Bug: 652400
Change-Id: I92126374ad715b502fd6e052246e537d65c407e2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2439645Reviewed-by: default avatardpapad <dpapad@chromium.org>
Commit-Queue: Daniel Hosseinian <dhoss@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812752}
parent c1012ef4
......@@ -49,26 +49,29 @@ export class ViewerThumbnailBarElement extends PolymerElement {
/** @private {!IntersectionObserver} */
this.intersectionObserver_ = new IntersectionObserver(entries => {
entries.forEach(entry => {
const thumbnail = /** @type {!ViewerThumbnailElement} */ (entry.target);
if (!entry.isIntersecting) {
// TODO(crbug.com/652400): Unpaint thumbnails.
thumbnail.clearImage();
return;
}
const thumbnail = /** @type {!ViewerThumbnailElement} */ (entry.target);
if (thumbnail.isPending()) {
if (thumbnail.isPainted()) {
return;
}
thumbnail.setPending();
thumbnail.setPainted();
this.dispatchEvent(new CustomEvent(
'paint-thumbnail',
{detail: thumbnail, bubbles: true, composed: true}));
});
}, {
root: thumbnailsDiv,
// The vertical root margin is set to 100% to also track thumbnails that
// are one standard finger swipe away.
rootMargin: '100% 0%',
// The root margin is set to 100% on the bottom to prepare thumbnails that
// are one standard scroll finger swipe away.
// The root margin is set to 500% on the top to discard thumbnails that
// far from view, but to avoid regenerating thumbnails that are close.
rootMargin: '500% 0% 100%',
});
FocusOutlineManager.forDocument(document);
......
......@@ -16,6 +16,9 @@ const PORTRAIT_WIDTH = 108;
/** @type {number} */
const LANDSCAPE_WIDTH = 140;
/** @type {string} */
const PAINTED_ATTRIBUTE = 'painted';
export class ViewerThumbnailElement extends PolymerElement {
static get is() {
return 'viewer-thumbnail';
......@@ -66,7 +69,20 @@ export class ViewerThumbnailElement extends PolymerElement {
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
this.removeAttribute('pending');
}
clearImage() {
if (!this.isPainted()) {
return;
}
const canvas = this.getCanvas_();
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.removeAttribute(PAINTED_ATTRIBUTE);
// For tests
this.dispatchEvent(new CustomEvent('clear-thumbnail-for-testing'));
}
/** @return {!HTMLElement} */
......@@ -122,12 +138,12 @@ export class ViewerThumbnailElement extends PolymerElement {
}
/** @return {boolean} */
isPending() {
return this.hasAttribute('pending');
isPainted() {
return this.hasAttribute(PAINTED_ATTRIBUTE);
}
setPending() {
this.toggleAttribute('pending', true);
setPainted() {
this.toggleAttribute(PAINTED_ATTRIBUTE, true);
}
/** @private */
......
......@@ -18,6 +18,18 @@ function createThumbnailBar() {
return thumbnailBar;
}
/** @return {number} */
function getTestThumbnailBarHeight() {
// Create a viewer-thumbnail element to get the standard height.
document.body.innerHTML = '';
const sizerThumbnail = document.createElement('viewer-thumbnail');
sizerThumbnail.pageNumber = 1;
document.body.appendChild(sizerThumbnail);
// Add 24 to cover padding between thumbnails.
const thumbnailBarHeight = sizerThumbnail.offsetHeight + 24;
return thumbnailBarHeight;
}
// Unit tests for the viewer-thumbnail-bar element.
const tests = [
// Test that the thumbnail bar has the correct number of thumbnails and
......@@ -60,15 +72,10 @@ const tests = [
});
},
function testTriggerPaint() {
// Create a viewer-thumbnail element to get the standard height.
document.body.innerHTML = '';
const sizerThumbnail = document.createElement('viewer-thumbnail');
document.body.appendChild(sizerThumbnail);
// Add 24 to cover padding between thumbnails.
const thumbnailBarHeight = sizerThumbnail.offsetHeight + 24;
const thumbnailBarHeight = getTestThumbnailBarHeight();
// Clear HTML for just the thumbnail bar.
const testDocLength = 4;
const testDocLength = 8;
const thumbnailBar = createThumbnailBar();
thumbnailBar.docLength = testDocLength;
......@@ -77,6 +84,10 @@ const tests = [
thumbnailBar.style.height = `${thumbnailBarHeight}px`;
thumbnailBar.style.display = 'block';
// Remove any padding from the scroller.
const scroller = thumbnailBar.shadowRoot.querySelector('#thumbnails');
scroller.style.padding = '';
flush();
const thumbnails =
......@@ -99,35 +110,52 @@ const tests = [
});
}
const whenRequestedPaintingFirst = [
paintThumbnailToPromise(thumbnails[0]),
paintThumbnailToPromise(thumbnails[1]),
];
testAsync(async () => {
// Only two thumbnails should be "painted" upon load.
const whenRequestedPaintingFirst = [
paintThumbnailToPromise(thumbnails[0]),
paintThumbnailToPromise(thumbnails[1]),
];
await Promise.all(whenRequestedPaintingFirst);
// Only two thumbnails should be pending.
chrome.test.assertEq(testDocLength, thumbnails.length);
for (let i = 0; i < thumbnails.length; i++) {
chrome.test.assertEq(i < 2, thumbnails[i].isPainted());
}
// Test that scrolling to the sixth thumbnail triggers 'paint-thumbnail'
// for thumbnails 3 through 7. When on the sixth thumbnail, five
// thumbnails above and one thumbnail below should also be painted because
// of the 500% top and 100% bottom root margins.
const whenRequestedPaintingNext = [];
for (let i = 2; i < 7; i++) {
whenRequestedPaintingNext.push(paintThumbnailToPromise(thumbnails[i]));
}
scroller.scrollTop = 5 * thumbnailBarHeight;
await Promise.all(whenRequestedPaintingNext);
// First seven thumbnails should be painted.
for (let i = 0; i < thumbnails.length; i++) {
chrome.test.assertEq(i < 2, thumbnails[i].hasAttribute('pending'));
chrome.test.assertEq(i < 7, thumbnails[i].isPainted());
}
// Test that scrolling to the bottom triggers 'paint-thumbnail' events for
// the remaining thumbnails.
// Test that scrolling down to the eighth thumbnail will clear the
// thumbnails outside the root margin, namely the first two. A paint
// should also be triggered for the eighth thumbnail.
const whenRequestedPaintingLast = [
paintThumbnailToPromise(thumbnails[2]),
paintThumbnailToPromise(thumbnails[3]),
paintThumbnailToPromise(thumbnails[7]),
eventToPromise('clear-thumbnail-for-testing', thumbnails[0]),
eventToPromise('clear-thumbnail-for-testing', thumbnails[1]),
];
thumbnailBar.shadowRoot.querySelector('#thumbnails').scrollTop =
2 * thumbnailBarHeight;
scroller.scrollTop = 7 * thumbnailBarHeight;
await Promise.all(whenRequestedPaintingLast);
for (const thumbnail of thumbnails) {
chrome.test.assertTrue(thumbnail.hasAttribute('pending'));
// Only first two thumbnails should not be painted.
for (let i = 0; i < thumbnails.length; i++) {
chrome.test.assertEq(i > 1, thumbnails[i].isPainted());
}
});
}
},
];
chrome.test.runTests(tests);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment