Skip to content

Commit 30e8d5d

Browse files
committed
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.
1 parent 99a2302 commit 30e8d5d

File tree

3 files changed

+126
-68
lines changed

3 files changed

+126
-68
lines changed

src/core/evaluator.js

Lines changed: 100 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
LocalColorSpaceCache,
8282
LocalGStateCache,
8383
LocalImageCache,
84+
LocalTilingPatternCache,
8485
} from "./image_utils.js";
8586
import { bidi } from "./bidi.js";
8687
import { ColorSpace } from "./colorspace.js";
@@ -716,12 +717,14 @@ class PartialEvaluator {
716717

717718
handleTilingType(
718719
fn,
719-
args,
720+
color,
720721
resources,
721722
pattern,
722723
patternDict,
723724
operatorList,
724-
task
725+
task,
726+
cacheKey,
727+
localTilingPatternCache
725728
) {
726729
// Create an IR of the pattern code.
727730
const tilingOpList = new OperatorList();
@@ -739,38 +742,39 @@ class PartialEvaluator {
739742
operatorList: tilingOpList,
740743
})
741744
.then(function () {
742-
return getTilingPatternIR(
743-
{
744-
fnArray: tilingOpList.fnArray,
745-
argsArray: tilingOpList.argsArray,
746-
},
745+
const operatorListIR = tilingOpList.getIR();
746+
const tilingPatternIR = getTilingPatternIR(
747+
operatorListIR,
747748
patternDict,
748-
args
749+
color
749750
);
751+
// Add the dependencies to the parent operator list so they are
752+
// resolved before the sub operator list is executed synchronously.
753+
operatorList.addDependencies(tilingOpList.dependencies);
754+
operatorList.addOp(fn, tilingPatternIR);
755+
756+
if (cacheKey) {
757+
localTilingPatternCache.set(cacheKey, patternDict.objId, {
758+
operatorListIR,
759+
dict: patternDict,
760+
});
761+
}
750762
})
751-
.then(
752-
function (tilingPatternIR) {
753-
// Add the dependencies to the parent operator list so they are
754-
// resolved before the sub operator list is executed synchronously.
755-
operatorList.addDependencies(tilingOpList.dependencies);
756-
operatorList.addOp(fn, tilingPatternIR);
757-
},
758-
reason => {
759-
if (reason instanceof AbortException) {
760-
return;
761-
}
762-
if (this.options.ignoreErrors) {
763-
// Error(s) in the TilingPattern -- sending unsupported feature
764-
// notification and allow rendering to continue.
765-
this.handler.send("UnsupportedFeature", {
766-
featureId: UNSUPPORTED_FEATURES.errorTilingPattern,
767-
});
768-
warn(`handleTilingType - ignoring pattern: "${reason}".`);
769-
return;
770-
}
771-
throw reason;
763+
.catch(reason => {
764+
if (reason instanceof AbortException) {
765+
return;
772766
}
773-
);
767+
if (this.options.ignoreErrors) {
768+
// Error(s) in the TilingPattern -- sending unsupported feature
769+
// notification and allow rendering to continue.
770+
this.handler.send("UnsupportedFeature", {
771+
featureId: UNSUPPORTED_FEATURES.errorTilingPattern,
772+
});
773+
warn(`handleTilingType - ignoring pattern: "${reason}".`);
774+
return;
775+
}
776+
throw reason;
777+
});
774778
}
775779

776780
handleSetFont(resources, fontArgs, fontRef, operatorList, task, state) {
@@ -1221,51 +1225,78 @@ class PartialEvaluator {
12211225
});
12221226
}
12231227

1224-
async handleColorN(
1228+
handleColorN(
12251229
operatorList,
12261230
fn,
12271231
args,
12281232
cs,
12291233
patterns,
12301234
resources,
12311235
task,
1232-
localColorSpaceCache
1236+
localColorSpaceCache,
1237+
localTilingPatternCache
12331238
) {
12341239
// compile tiling patterns
1235-
var patternName = args[args.length - 1];
1240+
const patternName = args[args.length - 1];
12361241
// SCN/scn applies patterns along with normal colors
1237-
var pattern;
1238-
if (isName(patternName) && (pattern = patterns.get(patternName.name))) {
1239-
var dict = isStream(pattern) ? pattern.dict : pattern;
1240-
var typeNum = dict.get("PatternType");
1241-
1242-
if (typeNum === PatternType.TILING) {
1243-
var color = cs.base ? cs.base.getRgb(args, 0) : null;
1244-
return this.handleTilingType(
1245-
fn,
1246-
color,
1247-
resources,
1248-
pattern,
1249-
dict,
1250-
operatorList,
1251-
task
1252-
);
1253-
} else if (typeNum === PatternType.SHADING) {
1254-
var shading = dict.get("Shading");
1255-
var matrix = dict.getArray("Matrix");
1256-
pattern = Pattern.parseShading(
1257-
shading,
1258-
matrix,
1259-
this.xref,
1260-
resources,
1261-
this.handler,
1262-
this._pdfFunctionFactory,
1263-
localColorSpaceCache
1264-
);
1265-
operatorList.addOp(fn, pattern.getIR());
1266-
return undefined;
1242+
if (patternName instanceof Name) {
1243+
const localTilingPattern = localTilingPatternCache.getByName(patternName);
1244+
if (localTilingPattern) {
1245+
try {
1246+
const color = cs.base ? cs.base.getRgb(args, 0) : null;
1247+
const tilingPatternIR = getTilingPatternIR(
1248+
localTilingPattern.operatorListIR,
1249+
localTilingPattern.dict,
1250+
color
1251+
);
1252+
operatorList.addOp(fn, tilingPatternIR);
1253+
return undefined;
1254+
} catch (ex) {
1255+
if (ex instanceof MissingDataException) {
1256+
throw ex;
1257+
}
1258+
// Handle any errors during normal TilingPattern parsing.
1259+
}
1260+
}
1261+
// TODO: Attempt to lookup cached TilingPatterns by reference as well,
1262+
// if and only if there are PDF documents where doing so would
1263+
// significantly improve performance.
1264+
1265+
let pattern = patterns.get(patternName.name);
1266+
if (pattern) {
1267+
var dict = isStream(pattern) ? pattern.dict : pattern;
1268+
var typeNum = dict.get("PatternType");
1269+
1270+
if (typeNum === PatternType.TILING) {
1271+
const color = cs.base ? cs.base.getRgb(args, 0) : null;
1272+
return this.handleTilingType(
1273+
fn,
1274+
color,
1275+
resources,
1276+
pattern,
1277+
dict,
1278+
operatorList,
1279+
task,
1280+
patternName,
1281+
localTilingPatternCache
1282+
);
1283+
} else if (typeNum === PatternType.SHADING) {
1284+
var shading = dict.get("Shading");
1285+
var matrix = dict.getArray("Matrix");
1286+
pattern = Pattern.parseShading(
1287+
shading,
1288+
matrix,
1289+
this.xref,
1290+
resources,
1291+
this.handler,
1292+
this._pdfFunctionFactory,
1293+
localColorSpaceCache
1294+
);
1295+
operatorList.addOp(fn, pattern.getIR());
1296+
return undefined;
1297+
}
1298+
throw new FormatError(`Unknown PatternType: ${typeNum}`);
12671299
}
1268-
throw new FormatError(`Unknown PatternType: ${typeNum}`);
12691300
}
12701301
throw new FormatError(`Unknown PatternName: ${patternName}`);
12711302
}
@@ -1349,6 +1380,7 @@ class PartialEvaluator {
13491380
const localImageCache = new LocalImageCache();
13501381
const localColorSpaceCache = new LocalColorSpaceCache();
13511382
const localGStateCache = new LocalGStateCache();
1383+
const localTilingPatternCache = new LocalTilingPatternCache();
13521384

13531385
var xobjs = resources.get("XObject") || Dict.empty;
13541386
var patterns = resources.get("Pattern") || Dict.empty;
@@ -1704,7 +1736,8 @@ class PartialEvaluator {
17041736
patterns,
17051737
resources,
17061738
task,
1707-
localColorSpaceCache
1739+
localColorSpaceCache,
1740+
localTilingPatternCache
17081741
)
17091742
);
17101743
return;
@@ -1724,7 +1757,8 @@ class PartialEvaluator {
17241757
patterns,
17251758
resources,
17261759
task,
1727-
localColorSpaceCache
1760+
localColorSpaceCache,
1761+
localTilingPatternCache
17281762
)
17291763
);
17301764
return;

src/core/image_utils.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,29 @@ class LocalGStateCache extends BaseLocalCache {
133133
}
134134
}
135135

136+
class LocalTilingPatternCache extends BaseLocalCache {
137+
set(name, ref = null, data) {
138+
if (!name) {
139+
throw new Error(
140+
'LocalTilingPatternCache.set - expected "name" argument.'
141+
);
142+
}
143+
if (ref) {
144+
if (this._imageCache.has(ref)) {
145+
return;
146+
}
147+
this._nameRefMap.set(name, ref);
148+
this._imageCache.put(ref, data);
149+
return;
150+
}
151+
// name
152+
if (this._imageMap.has(name)) {
153+
return;
154+
}
155+
this._imageMap.set(name, data);
156+
}
157+
}
158+
136159
class GlobalImageCache {
137160
static get NUM_PAGES_THRESHOLD() {
138161
return shadow(this, "NUM_PAGES_THRESHOLD", 2);
@@ -231,5 +254,6 @@ export {
231254
LocalColorSpaceCache,
232255
LocalFunctionCache,
233256
LocalGStateCache,
257+
LocalTilingPatternCache,
234258
GlobalImageCache,
235259
};

src/core/pattern.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ Shadings.Dummy = (function DummyClosure() {
967967
return Dummy;
968968
})();
969969

970-
function getTilingPatternIR(operatorList, dict, args) {
970+
function getTilingPatternIR(operatorList, dict, color) {
971971
const matrix = dict.getArray("Matrix");
972972
const bbox = Util.normalizeRect(dict.getArray("BBox"));
973973
const xstep = dict.get("XStep");
@@ -983,7 +983,7 @@ function getTilingPatternIR(operatorList, dict, args) {
983983

984984
return [
985985
"TilingPattern",
986-
args,
986+
color,
987987
operatorList,
988988
matrix,
989989
bbox,

0 commit comments

Comments
 (0)