From 30e8d5dea147fcb131d8cb32a8878055133fbc34 Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Thu, 8 Oct 2020 17:33:23 +0200 Subject: [PATCH] Add local caching of TilingPatterns in `PartialEvaluator.getOperatorList` (issue 2765 and 8473) In practice it's not uncommon for PDF documents to re-use the same TilingPatterns more than once, and parsing them is essentially equal to parsing of a (small) page since a `getOperatorList` call is required. By caching the internal TilingPattern representation we can thus avoid having to re-parse the same data over and over, and there's also *less* asynchronous parsing required for repeated TilingPatterns. Initially I had intended to include (standard) benchmark results with this patch, however it's not entirely clear that this is actually necessary here given the preliminary results. When testing this manually in the development viewer, using `pdfBug=Stats`, the following (approximate) reduction in rendering times were observed when comparing `master` against this patch: - http://pubs.usgs.gov/sim/3067/pdf/sim3067sheet-2.pdf (from issue 2765): `6800 ms` -> `4100 ms`. - https://github.com/mozilla/pdf.js/files/1046131/stepped.pdf (from issue 8473): `54000 ms` -> `13000 ms` - https://github.com/mozilla/pdf.js/files/1046130/proof.pdf (from issue 8473): `5900 ms` -> `2500 ms` As always, whenever you're dealing with documents which are "slow", there's usually a certain level of subjectivity involved with regards to what's deemed acceptable performance. Hence it's not clear to me that we want to regard any of the referenced issues as fixed, however the improvements are significant enough to warrant caching of TilingPatterns in my opinion. --- src/core/evaluator.js | 166 ++++++++++++++++++++++++---------------- src/core/image_utils.js | 24 ++++++ src/core/pattern.js | 4 +- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 1441f542d594b..f5a7f36879356 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -81,6 +81,7 @@ import { LocalColorSpaceCache, LocalGStateCache, LocalImageCache, + LocalTilingPatternCache, } from "./image_utils.js"; import { bidi } from "./bidi.js"; import { ColorSpace } from "./colorspace.js"; @@ -716,12 +717,14 @@ class PartialEvaluator { handleTilingType( fn, - args, + color, resources, pattern, patternDict, operatorList, - task + task, + cacheKey, + localTilingPatternCache ) { // Create an IR of the pattern code. const tilingOpList = new OperatorList(); @@ -739,38 +742,39 @@ class PartialEvaluator { operatorList: tilingOpList, }) .then(function () { - return getTilingPatternIR( - { - fnArray: tilingOpList.fnArray, - argsArray: tilingOpList.argsArray, - }, + const operatorListIR = tilingOpList.getIR(); + const tilingPatternIR = getTilingPatternIR( + operatorListIR, patternDict, - args + color ); + // Add the dependencies to the parent operator list so they are + // resolved before the sub operator list is executed synchronously. + operatorList.addDependencies(tilingOpList.dependencies); + operatorList.addOp(fn, tilingPatternIR); + + if (cacheKey) { + localTilingPatternCache.set(cacheKey, patternDict.objId, { + operatorListIR, + dict: patternDict, + }); + } }) - .then( - function (tilingPatternIR) { - // Add the dependencies to the parent operator list so they are - // resolved before the sub operator list is executed synchronously. - operatorList.addDependencies(tilingOpList.dependencies); - operatorList.addOp(fn, tilingPatternIR); - }, - reason => { - if (reason instanceof AbortException) { - return; - } - if (this.options.ignoreErrors) { - // Error(s) in the TilingPattern -- sending unsupported feature - // notification and allow rendering to continue. - this.handler.send("UnsupportedFeature", { - featureId: UNSUPPORTED_FEATURES.errorTilingPattern, - }); - warn(`handleTilingType - ignoring pattern: "${reason}".`); - return; - } - throw reason; + .catch(reason => { + if (reason instanceof AbortException) { + return; } - ); + if (this.options.ignoreErrors) { + // Error(s) in the TilingPattern -- sending unsupported feature + // notification and allow rendering to continue. + this.handler.send("UnsupportedFeature", { + featureId: UNSUPPORTED_FEATURES.errorTilingPattern, + }); + warn(`handleTilingType - ignoring pattern: "${reason}".`); + return; + } + throw reason; + }); } handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) { @@ -1221,7 +1225,7 @@ class PartialEvaluator { }); } - async handleColorN( + handleColorN( operatorList, fn, args, @@ -1229,43 +1233,70 @@ class PartialEvaluator { patterns, resources, task, - localColorSpaceCache + localColorSpaceCache, + localTilingPatternCache ) { // compile tiling patterns - var patternName = args[args.length - 1]; + const patternName = args[args.length - 1]; // SCN/scn applies patterns along with normal colors - var pattern; - if (isName(patternName) && (pattern = patterns.get(patternName.name))) { - var dict = isStream(pattern) ? pattern.dict : pattern; - var typeNum = dict.get("PatternType"); - - if (typeNum === PatternType.TILING) { - var color = cs.base ? cs.base.getRgb(args, 0) : null; - return this.handleTilingType( - fn, - color, - resources, - pattern, - dict, - operatorList, - task - ); - } else if (typeNum === PatternType.SHADING) { - var shading = dict.get("Shading"); - var matrix = dict.getArray("Matrix"); - pattern = Pattern.parseShading( - shading, - matrix, - this.xref, - resources, - this.handler, - this._pdfFunctionFactory, - localColorSpaceCache - ); - operatorList.addOp(fn, pattern.getIR()); - return undefined; + if (patternName instanceof Name) { + const localTilingPattern = localTilingPatternCache.getByName(patternName); + if (localTilingPattern) { + try { + const color = cs.base ? cs.base.getRgb(args, 0) : null; + const tilingPatternIR = getTilingPatternIR( + localTilingPattern.operatorListIR, + localTilingPattern.dict, + color + ); + operatorList.addOp(fn, tilingPatternIR); + return undefined; + } catch (ex) { + if (ex instanceof MissingDataException) { + throw ex; + } + // Handle any errors during normal TilingPattern parsing. + } + } + // TODO: Attempt to lookup cached TilingPatterns by reference as well, + // if and only if there are PDF documents where doing so would + // significantly improve performance. + + let pattern = patterns.get(patternName.name); + if (pattern) { + var dict = isStream(pattern) ? pattern.dict : pattern; + var typeNum = dict.get("PatternType"); + + if (typeNum === PatternType.TILING) { + const color = cs.base ? cs.base.getRgb(args, 0) : null; + return this.handleTilingType( + fn, + color, + resources, + pattern, + dict, + operatorList, + task, + patternName, + localTilingPatternCache + ); + } else if (typeNum === PatternType.SHADING) { + var shading = dict.get("Shading"); + var matrix = dict.getArray("Matrix"); + pattern = Pattern.parseShading( + shading, + matrix, + this.xref, + resources, + this.handler, + this._pdfFunctionFactory, + localColorSpaceCache + ); + operatorList.addOp(fn, pattern.getIR()); + return undefined; + } + throw new FormatError(`Unknown PatternType: ${typeNum}`); } - throw new FormatError(`Unknown PatternType: ${typeNum}`); } throw new FormatError(`Unknown PatternName: ${patternName}`); } @@ -1349,6 +1380,7 @@ class PartialEvaluator { const localImageCache = new LocalImageCache(); const localColorSpaceCache = new LocalColorSpaceCache(); const localGStateCache = new LocalGStateCache(); + const localTilingPatternCache = new LocalTilingPatternCache(); var xobjs = resources.get("XObject") || Dict.empty; var patterns = resources.get("Pattern") || Dict.empty; @@ -1704,7 +1736,8 @@ class PartialEvaluator { patterns, resources, task, - localColorSpaceCache + localColorSpaceCache, + localTilingPatternCache ) ); return; @@ -1724,7 +1757,8 @@ class PartialEvaluator { patterns, resources, task, - localColorSpaceCache + localColorSpaceCache, + localTilingPatternCache ) ); return; diff --git a/src/core/image_utils.js b/src/core/image_utils.js index d1faf37dd76ee..6cb485f454bad 100644 --- a/src/core/image_utils.js +++ b/src/core/image_utils.js @@ -133,6 +133,29 @@ class LocalGStateCache extends BaseLocalCache { } } +class LocalTilingPatternCache extends BaseLocalCache { + set(name, ref = null, data) { + if (!name) { + throw new Error( + 'LocalTilingPatternCache.set - expected "name" argument.' + ); + } + if (ref) { + if (this._imageCache.has(ref)) { + return; + } + this._nameRefMap.set(name, ref); + this._imageCache.put(ref, data); + return; + } + // name + if (this._imageMap.has(name)) { + return; + } + this._imageMap.set(name, data); + } +} + class GlobalImageCache { static get NUM_PAGES_THRESHOLD() { return shadow(this, "NUM_PAGES_THRESHOLD", 2); @@ -231,5 +254,6 @@ export { LocalColorSpaceCache, LocalFunctionCache, LocalGStateCache, + LocalTilingPatternCache, GlobalImageCache, }; diff --git a/src/core/pattern.js b/src/core/pattern.js index 34f0ab13857fc..3c81d78245890 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -967,7 +967,7 @@ Shadings.Dummy = (function DummyClosure() { return Dummy; })(); -function getTilingPatternIR(operatorList, dict, args) { +function getTilingPatternIR(operatorList, dict, color) { const matrix = dict.getArray("Matrix"); const bbox = Util.normalizeRect(dict.getArray("BBox")); const xstep = dict.get("XStep"); @@ -983,7 +983,7 @@ function getTilingPatternIR(operatorList, dict, args) { return [ "TilingPattern", - args, + color, operatorList, matrix, bbox,