Skip to content

Commit

Permalink
Improve caching of shading patterns. (bug 1721949)
Browse files Browse the repository at this point in the history
The PDF in bug 1721949 uses many unique pattern objects
that references the same shading many times. This caused
a new canvas pattern to be created and cached many times
driving up memory use.

To fix, I've changed the cache in the worker to key off the
shading object and instead send the shading and matrix
separately. While that worked well to fix the above bug,
there could be PDFs that use many shading that could
cause memory issues, so I've also added a LRU cache
on the main thread for canvas patterns. This should prevent
memory use from getting too high.
  • Loading branch information
brendandahl committed Jul 28, 2021
1 parent 777d890 commit c836e1f
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 60 deletions.
12 changes: 3 additions & 9 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -1315,20 +1315,17 @@ class PartialEvaluator {
}

parseShading({
keyObj,
shading,
resources,
localColorSpaceCache,
localShadingPatternCache,
matrix = null,
}) {
// Shadings and patterns may be referenced by the same name but the resource
// dictionary could be different so we can't use the name for the cache key.
let id = localShadingPatternCache.get(keyObj);
let id = localShadingPatternCache.get(shading);
if (!id) {
var shadingFill = Pattern.parseShading(
shading,
matrix,
this.xref,
resources,
this.handler,
Expand All @@ -1337,7 +1334,7 @@ class PartialEvaluator {
);
const patternIR = shadingFill.getIR();
id = `pattern_${this.idFactory.createObjId()}`;
localShadingPatternCache.set(keyObj, id);
localShadingPatternCache.set(shading, id);
this.handler.send("obj", [id, this.pageIndex, "Pattern", patternIR]);
}
return id;
Expand Down Expand Up @@ -1402,14 +1399,12 @@ class PartialEvaluator {
const shading = dict.get("Shading");
const matrix = dict.getArray("Matrix");
const objId = this.parseShading({
keyObj: pattern,
shading,
matrix,
resources,
localColorSpaceCache,
localShadingPatternCache,
});
operatorList.addOp(fn, ["Shading", objId]);
operatorList.addOp(fn, ["Shading", objId, matrix]);
return undefined;
}
throw new FormatError(`Unknown PatternType: ${typeNum}`);
Expand Down Expand Up @@ -1942,7 +1937,6 @@ class PartialEvaluator {
throw new FormatError("No shading object found");
}
const patternId = self.parseShading({
keyObj: shading,
shading,
resources,
localColorSpaceCache,
Expand Down
28 changes: 2 additions & 26 deletions src/core/pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class Pattern {

static parseShading(
shading,
matrix,
xref,
res,
handler,
Expand All @@ -60,7 +59,6 @@ class Pattern {
case ShadingType.RADIAL:
return new RadialAxialShading(
dict,
matrix,
xref,
res,
pdfFunctionFactory,
Expand All @@ -72,7 +70,6 @@ class Pattern {
case ShadingType.TENSOR_PATCH_MESH:
return new MeshShading(
shading,
matrix,
xref,
res,
pdfFunctionFactory,
Expand Down Expand Up @@ -115,16 +112,8 @@ class BaseShading {
// Radial and axial shading have very similar implementations
// If needed, the implementations can be broken into two classes.
class RadialAxialShading extends BaseShading {
constructor(
dict,
matrix,
xref,
resources,
pdfFunctionFactory,
localColorSpaceCache
) {
constructor(dict, xref, resources, pdfFunctionFactory, localColorSpaceCache) {
super();
this.matrix = matrix;
this.coordsArr = dict.getArray("Coords");
this.shadingType = dict.get("ShadingType");
const cs = ColorSpace.parse({
Expand Down Expand Up @@ -244,17 +233,7 @@ class RadialAxialShading extends BaseShading {
unreachable(`getPattern type unknown: ${shadingType}`);
}

return [
"RadialAxial",
type,
this.bbox,
this.colorStops,
p0,
p1,
r0,
r1,
this.matrix,
];
return ["RadialAxial", type, this.bbox, this.colorStops, p0, p1, r0, r1];
}
}

Expand Down Expand Up @@ -418,7 +397,6 @@ class MeshShading extends BaseShading {

constructor(
stream,
matrix,
xref,
resources,
pdfFunctionFactory,
Expand All @@ -429,7 +407,6 @@ class MeshShading extends BaseShading {
throw new FormatError("Mesh data is not a stream");
}
const dict = stream.dict;
this.matrix = matrix;
this.shadingType = dict.get("ShadingType");
const bbox = dict.getArray("BBox");
if (Array.isArray(bbox) && bbox.length === 4) {
Expand Down Expand Up @@ -942,7 +919,6 @@ class MeshShading extends BaseShading {
this.colors,
this.figures,
this.bounds,
this.matrix,
this.bbox,
this.background,
];
Expand Down
64 changes: 59 additions & 5 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const MIN_FONT_SIZE = 16;
const MAX_FONT_SIZE = 100;
const MAX_GROUP_SIZE = 4096;

// This value comes from sampling a few PDFs that re-use patterns, there doesn't
// seem to be any that benefit from caching more than 2 patterns.
const MAX_CACHED_CANVAS_PATTERNS = 2;

// Defines the time the `executeOperatorList`-method is going to be executing
// before it stops and shedules a continue of execution.
const EXECUTION_TIME = 15; // ms
Expand Down Expand Up @@ -231,6 +235,46 @@ class CachedCanvases {
}
}

/**
* Least recently used cache implemented with a JS Map. JS Map keys are ordered
* by last insertion.
*/
class LRUCache {
constructor(maxSize = 0) {
this._cache = new Map();
this._maxSize = maxSize;
}

has(key) {
return this._cache.has(key);
}

get(key) {
if (this._cache.has(key)) {
// Delete and set the value so it's moved to the end of the map iteration.
const value = this._cache.get(key);
this._cache.delete(key);
this._cache.set(key, value);
}
return this._cache.get(key);
}

set(key, value) {
if (this._maxSize <= 0) {
return;
}
if (this._cache.size + 1 > this._maxSize) {
// Delete the least recently used.
this._cache.delete(this._cache.keys().next().value);
}
this._cache.set(key, value);
}

clear() {
this._cache.clear();
}
}

function compileType3Glyph(imgData) {
const POINT_TO_PROCESS_LIMIT = 1000;
const POINT_TYPES = new Uint8Array([
Expand Down Expand Up @@ -866,6 +910,7 @@ class CanvasGraphics {
this.markedContentStack = [];
this.optionalContentConfig = optionalContentConfig;
this.cachedCanvases = new CachedCanvases(this.canvasFactory);
this.cachedCanvasPatterns = new LRUCache(MAX_CACHED_CANVAS_PATTERNS);
this.cachedPatterns = new Map();
if (canvasCtx) {
// NOTE: if mozCurrentTransform is polyfilled, then the current state of
Expand Down Expand Up @@ -1017,6 +1062,7 @@ class CanvasGraphics {
}

this.cachedCanvases.clear();
this.cachedCanvasPatterns.clear();
this.cachedPatterns.clear();

if (this.imageLayer) {
Expand Down Expand Up @@ -2125,7 +2171,7 @@ class CanvasGraphics {
baseTransform
);
} else {
pattern = this._getPattern(IR[1]);
pattern = this._getPattern(IR[1], IR[2]);
}
return pattern;
}
Expand All @@ -2152,12 +2198,20 @@ class CanvasGraphics {
this.current.patternFill = false;
}

_getPattern(objId) {
_getPattern(objId, matrix = null) {
let pattern;
if (this.cachedPatterns.has(objId)) {
return this.cachedPatterns.get(objId);
pattern = this.cachedPatterns.get(objId);
} else {
pattern = getShadingPattern(
this.objs.get(objId),
this.cachedCanvasPatterns
);
this.cachedPatterns.set(objId, pattern);
}
if (matrix) {
pattern.matrix = matrix;
}
const pattern = getShadingPattern(this.objs.get(objId));
this.cachedPatterns.set(objId, pattern);
return pattern;
}

Expand Down
41 changes: 21 additions & 20 deletions src/display/pattern_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class BaseShadingPattern {
}

class RadialAxialShadingPattern extends BaseShadingPattern {
constructor(IR) {
constructor(IR, cachedCanvasPatterns) {
super();
this._type = IR[1];
this._bbox = IR[2];
Expand All @@ -55,8 +55,8 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._p1 = IR[5];
this._r0 = IR[6];
this._r1 = IR[7];
this._matrix = IR[8];
this._patternCache = null;
this.matrix = null;
this.cachedCanvasPatterns = cachedCanvasPatterns;
}

_createGradient(ctx) {
Expand Down Expand Up @@ -87,10 +87,10 @@ class RadialAxialShadingPattern extends BaseShadingPattern {

getPattern(ctx, owner, inverse, shadingFill = false) {
let pattern;
if (this._patternCache) {
pattern = this._patternCache;
} else {
if (!shadingFill) {
if (!shadingFill) {
if (this.cachedCanvasPatterns.has(this)) {
pattern = this.cachedCanvasPatterns.get(this);
} else {
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
owner.ctx.canvas.width,
Expand All @@ -104,20 +104,21 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
tmpCtx.rect(0, 0, tmpCtx.canvas.width, tmpCtx.canvas.height);

tmpCtx.setTransform.apply(tmpCtx, owner.baseTransform);
if (this._matrix) {
tmpCtx.transform.apply(tmpCtx, this._matrix);
if (this.matrix) {
tmpCtx.transform.apply(tmpCtx, this.matrix);
}
applyBoundingBox(tmpCtx, this._bbox);

tmpCtx.fillStyle = this._createGradient(tmpCtx);
tmpCtx.fill();

pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
} else {
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
this.cachedCanvasPatterns.set(this, pattern);
}
this._patternCache = pattern;
} else {
// Don't bother caching gradients, they are quick to rebuild.
applyBoundingBox(ctx, this._bbox);
pattern = this._createGradient(ctx);
}
if (!shadingFill) {
const domMatrix = new DOMMatrix(inverse);
Expand Down Expand Up @@ -305,9 +306,9 @@ class MeshShadingPattern extends BaseShadingPattern {
this._colors = IR[3];
this._figures = IR[4];
this._bounds = IR[5];
this._matrix = IR[6];
this._bbox = IR[7];
this._background = IR[8];
this.matrix = null;
}

_createMeshCanvas(combinedScale, backgroundColor, cachedCanvases) {
Expand Down Expand Up @@ -389,8 +390,8 @@ class MeshShadingPattern extends BaseShadingPattern {
} else {
// Obtain scale from matrix and current transformation matrix.
scale = Util.singularValueDecompose2dScale(owner.baseTransform);
if (this._matrix) {
const matrixScale = Util.singularValueDecompose2dScale(this._matrix);
if (this.matrix) {
const matrixScale = Util.singularValueDecompose2dScale(this.matrix);
scale = [scale[0] * matrixScale[0], scale[1] * matrixScale[1]];
}
}
Expand All @@ -405,8 +406,8 @@ class MeshShadingPattern extends BaseShadingPattern {

if (!shadingFill) {
ctx.setTransform.apply(ctx, owner.baseTransform);
if (this._matrix) {
ctx.transform.apply(ctx, this._matrix);
if (this.matrix) {
ctx.transform.apply(ctx, this.matrix);
}
}

Expand All @@ -426,10 +427,10 @@ class DummyShadingPattern extends BaseShadingPattern {
}
}

function getShadingPattern(IR) {
function getShadingPattern(IR, cachedCanvasPatterns) {
switch (IR[0]) {
case "RadialAxial":
return new RadialAxialShadingPattern(IR);
return new RadialAxialShadingPattern(IR, cachedCanvasPatterns);
case "Mesh":
return new MeshShadingPattern(IR);
case "Dummy":
Expand Down

0 comments on commit c836e1f

Please sign in to comment.