From c836e1f0fb337828c0d03cf456007b49d7991c57 Mon Sep 17 00:00:00 2001 From: Brendan Dahl Date: Tue, 27 Jul 2021 19:58:06 -0700 Subject: [PATCH] Improve caching of shading patterns. (bug 1721949) 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. --- src/core/evaluator.js | 12 ++----- src/core/pattern.js | 28 ++------------- src/display/canvas.js | 64 ++++++++++++++++++++++++++++++++--- src/display/pattern_helper.js | 41 +++++++++++----------- 4 files changed, 85 insertions(+), 60 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 517056d23..f79ad57fb 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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, @@ -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; @@ -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}`); @@ -1942,7 +1937,6 @@ class PartialEvaluator { throw new FormatError("No shading object found"); } const patternId = self.parseShading({ - keyObj: shading, shading, resources, localColorSpaceCache, diff --git a/src/core/pattern.js b/src/core/pattern.js index d6a3e29e2..113f402e4 100644 --- a/src/core/pattern.js +++ b/src/core/pattern.js @@ -44,7 +44,6 @@ class Pattern { static parseShading( shading, - matrix, xref, res, handler, @@ -60,7 +59,6 @@ class Pattern { case ShadingType.RADIAL: return new RadialAxialShading( dict, - matrix, xref, res, pdfFunctionFactory, @@ -72,7 +70,6 @@ class Pattern { case ShadingType.TENSOR_PATCH_MESH: return new MeshShading( shading, - matrix, xref, res, pdfFunctionFactory, @@ -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({ @@ -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]; } } @@ -418,7 +397,6 @@ class MeshShading extends BaseShading { constructor( stream, - matrix, xref, resources, pdfFunctionFactory, @@ -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) { @@ -942,7 +919,6 @@ class MeshShading extends BaseShading { this.colors, this.figures, this.bounds, - this.matrix, this.bbox, this.background, ]; diff --git a/src/display/canvas.js b/src/display/canvas.js index 1cf328966..2938e0b99 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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 @@ -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([ @@ -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 @@ -1017,6 +1062,7 @@ class CanvasGraphics { } this.cachedCanvases.clear(); + this.cachedCanvasPatterns.clear(); this.cachedPatterns.clear(); if (this.imageLayer) { @@ -2125,7 +2171,7 @@ class CanvasGraphics { baseTransform ); } else { - pattern = this._getPattern(IR[1]); + pattern = this._getPattern(IR[1], IR[2]); } return pattern; } @@ -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; } diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 72103d377..e6e112fe9 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -46,7 +46,7 @@ class BaseShadingPattern { } class RadialAxialShadingPattern extends BaseShadingPattern { - constructor(IR) { + constructor(IR, cachedCanvasPatterns) { super(); this._type = IR[1]; this._bbox = IR[2]; @@ -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) { @@ -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, @@ -104,8 +104,8 @@ 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); @@ -113,11 +113,12 @@ class RadialAxialShadingPattern extends BaseShadingPattern { 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); @@ -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) { @@ -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]]; } } @@ -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); } } @@ -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":