From 983b25f86347c9614706683f65437c494b010f8a Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 14 Mar 2019 14:01:55 +0100 Subject: [PATCH] Ensure that `blob:` URLs will be revoked when pages are cleaned-up/destroyed Natively supported JPEG images are sent as-is, using a `blob:` or possibly a `data` URL, to the main-thread for loading/decoding. However there's currently no attempt at releasing these resources, which are held alive by `blob:` URLs, which seems unfortunately given that images can be arbitrarily large. As mentioned in https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL the lifetime of these URLs are tied to the document, hence they are not being removed when a page is cleaned-up/destroyed (e.g. when being removed from the `PDFPageViewBuffer` in the viewer). This is easy to test with the help of `about:memory` (in Firefox), which clearly shows the number of `blob:` URLs becomming arbitrarily large *without* this patch. With this patch however the `blob:` URLs are immediately release upon clean-up as expected, and the memory consumption should thus be considerably reduced for long documents with (simple) JPEG images. --- src/display/api.js | 21 +++++++++++++++++++-- src/display/display_utils.js | 11 +++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/display/api.js b/src/display/api.js index b5ad2851c..9c0a9ba0e 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -23,7 +23,8 @@ import { } from '../shared/util'; import { deprecated, DOMCanvasFactory, DOMCMapReaderFactory, DummyStatTimer, - loadScript, PageViewport, RenderingCancelledException, StatTimer + loadScript, PageViewport, releaseImageResources, RenderingCancelledException, + StatTimer } from './display_utils'; import { FontFaceObject, FontLoader } from './font_loader'; import { apiCompatibilityParams } from './api_compatibility'; @@ -1988,11 +1989,14 @@ class WorkerTransport { resolve(img); }; img.onerror = function() { - reject(new Error('Error during JPEG image loading')); // Note that when the browser image loading/decoding fails, // we'll fallback to the built-in PDF.js JPEG decoder; see // `PartialEvaluator.buildPaintImageXObject` in the // `src/core/evaluator.js` file. + reject(new Error('Error during JPEG image loading')); + + // Always remember to release the image data if errors occurred. + releaseImageResources(img); }; img.src = imageData; }).then((img) => { @@ -2095,6 +2099,8 @@ class WorkerTransport { } resolve({ data: buf, width, height, }); + // Immediately release the image data once decoding has finished. + releaseImageResources(img); // Zeroing the width and height cause Firefox to release graphics // resources immediately, which can greatly reduce memory consumption. tmpCanvas.width = 0; @@ -2104,6 +2110,9 @@ class WorkerTransport { }; img.onerror = function() { reject(new Error('JpegDecode failed to load image')); + + // Always remember to release the image data if errors occurred. + releaseImageResources(img); }; img.src = imageUrl; }); @@ -2323,6 +2332,14 @@ class PDFObjects { } clear() { + for (const objId in this._objs) { + const { data, } = this._objs[objId]; + + if (typeof Image !== 'undefined' && data instanceof Image) { + // Always release the image data when clearing out the cached objects. + releaseImageResources(data); + } + } this._objs = Object.create(null); } } diff --git a/src/display/display_utils.js b/src/display/display_utils.js index ee1879d3b..3ad22c345 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -480,6 +480,16 @@ function deprecated(details) { console.log('Deprecated API usage: ' + details); } +function releaseImageResources(img) { + assert(img instanceof Image, 'Invalid `img` parameter.'); + + const url = img.src; + if (typeof url === 'string' && url.startsWith('blob:') && + URL.revokeObjectURL) { + URL.revokeObjectURL(url); + } +} + export { PageViewport, RenderingCancelledException, @@ -496,4 +506,5 @@ export { isValidFetchUrl, loadScript, deprecated, + releaseImageResources, };