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.
This commit is contained in:
Jonas Jenwald 2020-10-08 17:33:23 +02:00
parent 99a2302d88
commit 30e8d5dea1
3 changed files with 127 additions and 69 deletions

View File

@ -81,6 +81,7 @@ import {
LocalColorSpaceCache, LocalColorSpaceCache,
LocalGStateCache, LocalGStateCache,
LocalImageCache, LocalImageCache,
LocalTilingPatternCache,
} from "./image_utils.js"; } from "./image_utils.js";
import { bidi } from "./bidi.js"; import { bidi } from "./bidi.js";
import { ColorSpace } from "./colorspace.js"; import { ColorSpace } from "./colorspace.js";
@ -716,12 +717,14 @@ class PartialEvaluator {
handleTilingType( handleTilingType(
fn, fn,
args, color,
resources, resources,
pattern, pattern,
patternDict, patternDict,
operatorList, operatorList,
task task,
cacheKey,
localTilingPatternCache
) { ) {
// Create an IR of the pattern code. // Create an IR of the pattern code.
const tilingOpList = new OperatorList(); const tilingOpList = new OperatorList();
@ -739,38 +742,39 @@ class PartialEvaluator {
operatorList: tilingOpList, operatorList: tilingOpList,
}) })
.then(function () { .then(function () {
return getTilingPatternIR( const operatorListIR = tilingOpList.getIR();
{ const tilingPatternIR = getTilingPatternIR(
fnArray: tilingOpList.fnArray, operatorListIR,
argsArray: tilingOpList.argsArray,
},
patternDict, patternDict,
args color
); );
}) // Add the dependencies to the parent operator list so they are
.then( // resolved before the sub operator list is executed synchronously.
function (tilingPatternIR) { operatorList.addDependencies(tilingOpList.dependencies);
// Add the dependencies to the parent operator list so they are operatorList.addOp(fn, tilingPatternIR);
// resolved before the sub operator list is executed synchronously.
operatorList.addDependencies(tilingOpList.dependencies); if (cacheKey) {
operatorList.addOp(fn, tilingPatternIR); localTilingPatternCache.set(cacheKey, patternDict.objId, {
}, operatorListIR,
reason => { dict: patternDict,
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) { handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) {
@ -1221,7 +1225,7 @@ class PartialEvaluator {
}); });
} }
async handleColorN( handleColorN(
operatorList, operatorList,
fn, fn,
args, args,
@ -1229,43 +1233,70 @@ class PartialEvaluator {
patterns, patterns,
resources, resources,
task, task,
localColorSpaceCache localColorSpaceCache,
localTilingPatternCache
) { ) {
// compile tiling patterns // compile tiling patterns
var patternName = args[args.length - 1]; const patternName = args[args.length - 1];
// SCN/scn applies patterns along with normal colors // SCN/scn applies patterns along with normal colors
var pattern; if (patternName instanceof Name) {
if (isName(patternName) && (pattern = patterns.get(patternName.name))) { const localTilingPattern = localTilingPatternCache.getByName(patternName);
var dict = isStream(pattern) ? pattern.dict : pattern; if (localTilingPattern) {
var typeNum = dict.get("PatternType"); try {
const color = cs.base ? cs.base.getRgb(args, 0) : null;
if (typeNum === PatternType.TILING) { const tilingPatternIR = getTilingPatternIR(
var color = cs.base ? cs.base.getRgb(args, 0) : null; localTilingPattern.operatorListIR,
return this.handleTilingType( localTilingPattern.dict,
fn, color
color, );
resources, operatorList.addOp(fn, tilingPatternIR);
pattern, return undefined;
dict, } catch (ex) {
operatorList, if (ex instanceof MissingDataException) {
task throw ex;
); }
} else if (typeNum === PatternType.SHADING) { // Handle any errors during normal TilingPattern parsing.
var shading = dict.get("Shading"); }
var matrix = dict.getArray("Matrix"); }
pattern = Pattern.parseShading( // TODO: Attempt to lookup cached TilingPatterns by reference as well,
shading, // if and only if there are PDF documents where doing so would
matrix, // significantly improve performance.
this.xref,
resources, let pattern = patterns.get(patternName.name);
this.handler, if (pattern) {
this._pdfFunctionFactory, var dict = isStream(pattern) ? pattern.dict : pattern;
localColorSpaceCache var typeNum = dict.get("PatternType");
);
operatorList.addOp(fn, pattern.getIR()); if (typeNum === PatternType.TILING) {
return undefined; 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}`); throw new FormatError(`Unknown PatternName: ${patternName}`);
} }
@ -1349,6 +1380,7 @@ class PartialEvaluator {
const localImageCache = new LocalImageCache(); const localImageCache = new LocalImageCache();
const localColorSpaceCache = new LocalColorSpaceCache(); const localColorSpaceCache = new LocalColorSpaceCache();
const localGStateCache = new LocalGStateCache(); const localGStateCache = new LocalGStateCache();
const localTilingPatternCache = new LocalTilingPatternCache();
var xobjs = resources.get("XObject") || Dict.empty; var xobjs = resources.get("XObject") || Dict.empty;
var patterns = resources.get("Pattern") || Dict.empty; var patterns = resources.get("Pattern") || Dict.empty;
@ -1704,7 +1736,8 @@ class PartialEvaluator {
patterns, patterns,
resources, resources,
task, task,
localColorSpaceCache localColorSpaceCache,
localTilingPatternCache
) )
); );
return; return;
@ -1724,7 +1757,8 @@ class PartialEvaluator {
patterns, patterns,
resources, resources,
task, task,
localColorSpaceCache localColorSpaceCache,
localTilingPatternCache
) )
); );
return; return;

View File

@ -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 { class GlobalImageCache {
static get NUM_PAGES_THRESHOLD() { static get NUM_PAGES_THRESHOLD() {
return shadow(this, "NUM_PAGES_THRESHOLD", 2); return shadow(this, "NUM_PAGES_THRESHOLD", 2);
@ -231,5 +254,6 @@ export {
LocalColorSpaceCache, LocalColorSpaceCache,
LocalFunctionCache, LocalFunctionCache,
LocalGStateCache, LocalGStateCache,
LocalTilingPatternCache,
GlobalImageCache, GlobalImageCache,
}; };

View File

@ -967,7 +967,7 @@ Shadings.Dummy = (function DummyClosure() {
return Dummy; return Dummy;
})(); })();
function getTilingPatternIR(operatorList, dict, args) { function getTilingPatternIR(operatorList, dict, color) {
const matrix = dict.getArray("Matrix"); const matrix = dict.getArray("Matrix");
const bbox = Util.normalizeRect(dict.getArray("BBox")); const bbox = Util.normalizeRect(dict.getArray("BBox"));
const xstep = dict.get("XStep"); const xstep = dict.get("XStep");
@ -983,7 +983,7 @@ function getTilingPatternIR(operatorList, dict, args) {
return [ return [
"TilingPattern", "TilingPattern",
args, color,
operatorList, operatorList,
matrix, matrix,
bbox, bbox,