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:
parent
99a2302d88
commit
30e8d5dea1
@ -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
|
||||
);
|
||||
})
|
||||
.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;
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
);
|
||||
})
|
||||
.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;
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user