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 { ...@@ -49,26 +49,29 @@ export class ViewerThumbnailBarElement extends PolymerElement {
/** @private {!IntersectionObserver} */ /** @private {!IntersectionObserver} */
this.intersectionObserver_ = new IntersectionObserver(entries => { this.intersectionObserver_ = new IntersectionObserver(entries => {
entries.forEach(entry => { entries.forEach(entry => {
const thumbnail = /** @type {!ViewerThumbnailElement} */ (entry.target);
if (!entry.isIntersecting) { if (!entry.isIntersecting) {
// TODO(crbug.com/652400): Unpaint thumbnails. thumbnail.clearImage();
return; return;
} }
const thumbnail = /** @type {!ViewerThumbnailElement} */ (entry.target); if (thumbnail.isPainted()) {
if (thumbnail.isPending()) {
return; return;
} }
thumbnail.setPending(); thumbnail.setPainted();
this.dispatchEvent(new CustomEvent( this.dispatchEvent(new CustomEvent(
'paint-thumbnail', 'paint-thumbnail',
{detail: thumbnail, bubbles: true, composed: true})); {detail: thumbnail, bubbles: true, composed: true}));
}); });
}, { }, {
root: thumbnailsDiv, root: thumbnailsDiv,
// The vertical root margin is set to 100% to also track thumbnails that // The root margin is set to 100% on the bottom to prepare thumbnails that
// are one standard finger swipe away. // are one standard scroll finger swipe away.
rootMargin: '100% 0%', // 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); FocusOutlineManager.forDocument(document);
......
...@@ -16,6 +16,9 @@ const PORTRAIT_WIDTH = 108; ...@@ -16,6 +16,9 @@ const PORTRAIT_WIDTH = 108;
/** @type {number} */ /** @type {number} */
const LANDSCAPE_WIDTH = 140; const LANDSCAPE_WIDTH = 140;
/** @type {string} */
const PAINTED_ATTRIBUTE = 'painted';
export class ViewerThumbnailElement extends PolymerElement { export class ViewerThumbnailElement extends PolymerElement {
static get is() { static get is() {
return 'viewer-thumbnail'; return 'viewer-thumbnail';
...@@ -66,7 +69,20 @@ export class ViewerThumbnailElement extends PolymerElement { ...@@ -66,7 +69,20 @@ export class ViewerThumbnailElement extends PolymerElement {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0); 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} */ /** @return {!HTMLElement} */
...@@ -122,12 +138,12 @@ export class ViewerThumbnailElement extends PolymerElement { ...@@ -122,12 +138,12 @@ export class ViewerThumbnailElement extends PolymerElement {
} }
/** @return {boolean} */ /** @return {boolean} */
isPending() { isPainted() {
return this.hasAttribute('pending'); return this.hasAttribute(PAINTED_ATTRIBUTE);
} }
setPending() { setPainted() {
this.toggleAttribute('pending', true); this.toggleAttribute(PAINTED_ATTRIBUTE, true);
} }
/** @private */ /** @private */
......
...@@ -18,6 +18,18 @@ function createThumbnailBar() { ...@@ -18,6 +18,18 @@ function createThumbnailBar() {
return thumbnailBar; 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. // Unit tests for the viewer-thumbnail-bar element.
const tests = [ const tests = [
// Test that the thumbnail bar has the correct number of thumbnails and // Test that the thumbnail bar has the correct number of thumbnails and
...@@ -60,15 +72,10 @@ const tests = [ ...@@ -60,15 +72,10 @@ const tests = [
}); });
}, },
function testTriggerPaint() { function testTriggerPaint() {
// Create a viewer-thumbnail element to get the standard height. const thumbnailBarHeight = getTestThumbnailBarHeight();
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;
// Clear HTML for just the thumbnail bar. // Clear HTML for just the thumbnail bar.
const testDocLength = 4; const testDocLength = 8;
const thumbnailBar = createThumbnailBar(); const thumbnailBar = createThumbnailBar();
thumbnailBar.docLength = testDocLength; thumbnailBar.docLength = testDocLength;
...@@ -77,6 +84,10 @@ const tests = [ ...@@ -77,6 +84,10 @@ const tests = [
thumbnailBar.style.height = `${thumbnailBarHeight}px`; thumbnailBar.style.height = `${thumbnailBarHeight}px`;
thumbnailBar.style.display = 'block'; thumbnailBar.style.display = 'block';
// Remove any padding from the scroller.
const scroller = thumbnailBar.shadowRoot.querySelector('#thumbnails');
scroller.style.padding = '';
flush(); flush();
const thumbnails = const thumbnails =
...@@ -99,35 +110,52 @@ const tests = [ ...@@ -99,35 +110,52 @@ const tests = [
}); });
} }
testAsync(async () => {
// Only two thumbnails should be "painted" upon load.
const whenRequestedPaintingFirst = [ const whenRequestedPaintingFirst = [
paintThumbnailToPromise(thumbnails[0]), paintThumbnailToPromise(thumbnails[0]),
paintThumbnailToPromise(thumbnails[1]), paintThumbnailToPromise(thumbnails[1]),
]; ];
testAsync(async () => {
await Promise.all(whenRequestedPaintingFirst); 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++) { 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 // Test that scrolling down to the eighth thumbnail will clear the
// the remaining thumbnails. // thumbnails outside the root margin, namely the first two. A paint
// should also be triggered for the eighth thumbnail.
const whenRequestedPaintingLast = [ const whenRequestedPaintingLast = [
paintThumbnailToPromise(thumbnails[2]), paintThumbnailToPromise(thumbnails[7]),
paintThumbnailToPromise(thumbnails[3]), eventToPromise('clear-thumbnail-for-testing', thumbnails[0]),
eventToPromise('clear-thumbnail-for-testing', thumbnails[1]),
]; ];
scroller.scrollTop = 7 * thumbnailBarHeight;
thumbnailBar.shadowRoot.querySelector('#thumbnails').scrollTop =
2 * thumbnailBarHeight;
await Promise.all(whenRequestedPaintingLast); await Promise.all(whenRequestedPaintingLast);
for (const thumbnail of thumbnails) { // Only first two thumbnails should not be painted.
chrome.test.assertTrue(thumbnail.hasAttribute('pending')); for (let i = 0; i < thumbnails.length; i++) {
chrome.test.assertEq(i > 1, thumbnails[i].isPainted());
} }
}); });
} },
]; ];
chrome.test.runTests(tests); 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