Merge pull request #9561 from ahocevar/map-memory-leak

Remove memory leak caused by label cache listeners
This commit is contained in:
Andreas Hocevar
2019-05-16 22:26:56 +02:00
committed by GitHub
11 changed files with 71 additions and 136 deletions

View File

@@ -293,6 +293,11 @@ class PluggableMap extends BaseObject {
*/ */
this.interactions = optionsInternal.interactions || new Collection(); this.interactions = optionsInternal.interactions || new Collection();
/**
* @type {import("./events.js").EventsKey}
*/
this.labelCacheListenerKey_;
/** /**
* @type {Collection<import("./Overlay.js").default>} * @type {Collection<import("./Overlay.js").default>}
* @private * @private
@@ -498,6 +503,24 @@ class PluggableMap extends BaseObject {
overlay.setMap(this); overlay.setMap(this);
} }
/**
* Attach a label cache for listening to font changes.
* @param {import("./events/Target.js").default} labelCache Label cache.
*/
attachLabelCache(labelCache) {
this.detachLabelCache();
this.labelCacheListenerKey_ = listen(labelCache, EventType.CLEAR, this.redrawText.bind(this));
}
/**
* Detach the label cache, i.e. no longer listen to font changes.
*/
detachLabelCache() {
if (this.labelCacheListenerKey_) {
unlistenByKey(this.labelCacheListenerKey_);
}
}
/** /**
* *
* @inheritDoc * @inheritDoc
@@ -515,6 +538,7 @@ class PluggableMap extends BaseObject {
cancelAnimationFrame(this.animationDelayKey_); cancelAnimationFrame(this.animationDelayKey_);
this.animationDelayKey_ = undefined; this.animationDelayKey_ = undefined;
} }
this.detachLabelCache();
this.setTarget(null); this.setTarget(null);
super.disposeInternal(); super.disposeInternal();
} }
@@ -994,7 +1018,6 @@ class PluggableMap extends BaseObject {
} }
if (!targetElement) { if (!targetElement) {
this.renderer_.removeLayerRenderers();
removeNode(this.viewport_); removeNode(this.viewport_);
if (this.handleResize_ !== undefined) { if (this.handleResize_ !== undefined) {
removeEventListener(EventType.RESIZE, this.handleResize_, false); removeEventListener(EventType.RESIZE, this.handleResize_, false);
@@ -1102,6 +1125,19 @@ class PluggableMap extends BaseObject {
this.animationDelay_(); this.animationDelay_();
} }
/**
* Redraws all text after new fonts have loaded
*/
redrawText() {
const layerStates = this.getLayerGroup().getLayerStatesArray();
for (let i = 0, ii = layerStates.length; i < ii; ++i) {
const layer = layerStates[i].layer;
if (layer.hasRenderer()) {
layer.getRenderer().handleFontsChanged();
}
}
}
/** /**
* Request a map rendering (at the next animation frame). * Request a map rendering (at the next animation frame).
* @api * @api

View File

@@ -254,6 +254,13 @@ class Layer extends BaseLayer {
return this.renderer_; return this.renderer_;
} }
/**
* @return {boolean} The layer has a renderer.
*/
hasRenderer() {
return !!this.renderer_;
}
/** /**
* Create a renderer for this layer. * Create a renderer for this layer.
* @return {import("../renderer/Layer.js").default} A layer renderer. * @return {import("../renderer/Layer.js").default} A layer renderer.

View File

@@ -8,6 +8,7 @@ import RenderEventType from '../render/EventType.js';
import MapRenderer from './Map.js'; import MapRenderer from './Map.js';
import SourceState from '../source/State.js'; import SourceState from '../source/State.js';
import {replaceChildren} from '../dom.js'; import {replaceChildren} from '../dom.js';
import {labelCache} from '../render/canvas.js';
/** /**
@@ -22,6 +23,7 @@ class CompositeMapRenderer extends MapRenderer {
*/ */
constructor(map) { constructor(map) {
super(map); super(map);
map.attachLabelCache(labelCache);
/** /**
* @private * @private
@@ -111,7 +113,6 @@ class CompositeMapRenderer extends MapRenderer {
this.renderedVisible_ = true; this.renderedVisible_ = true;
} }
this.scheduleRemoveUnusedLayerRenderers(frameState);
this.scheduleExpireIconCache(frameState); this.scheduleExpireIconCache(frameState);
} }
@@ -128,11 +129,8 @@ class CompositeMapRenderer extends MapRenderer {
for (let i = numLayers - 1; i >= 0; --i) { for (let i = numLayers - 1; i >= 0; --i) {
const layerState = layerStates[i]; const layerState = layerStates[i];
const layer = layerState.layer; const layer = layerState.layer;
if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) { if (layer.hasRenderer() && visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) {
const layerRenderer = this.getLayerRenderer(layer); const layerRenderer = layer.getRenderer();
if (!layerRenderer) {
continue;
}
const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance); const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance);
if (data) { if (data) {
const result = callback(layer, data); const result = callback(layer, data);

View File

@@ -115,6 +115,12 @@ class LayerRenderer extends Observable {
return this.layer_; return this.layer_;
} }
/**
* @abstract
* Perform action necessary to get the layer rendered after new fonts have loaded
*/
handleFontsChanged() {}
/** /**
* Handle changes in image state. * Handle changes in image state.
* @param {import("../events/Event.js").default} event Image change event. * @param {import("../events/Event.js").default} event Image change event.

View File

@@ -3,8 +3,6 @@
*/ */
import {abstract, getUid} from '../util.js'; import {abstract, getUid} from '../util.js';
import Disposable from '../Disposable.js'; import Disposable from '../Disposable.js';
import {listen, unlistenByKey} from '../events.js';
import EventType from '../events/EventType.js';
import {getWidth} from '../extent.js'; import {getWidth} from '../extent.js';
import {TRUE} from '../functions.js'; import {TRUE} from '../functions.js';
import {visibleAtResolution} from '../layer/Layer.js'; import {visibleAtResolution} from '../layer/Layer.js';
@@ -34,18 +32,6 @@ class MapRenderer extends Disposable {
*/ */
this.declutterTree_ = null; this.declutterTree_ = null;
/**
* @private
* @type {!Object<string, import("./Layer.js").default>}
*/
this.layerRenderers_ = {};
/**
* @private
* @type {Object<string, import("../events.js").EventsKey>}
*/
this.layerRendererListeners_ = {};
} }
/** /**
@@ -75,15 +61,6 @@ class MapRenderer extends Disposable {
makeInverse(pixelToCoordinateTransform, coordinateToPixelTransform); makeInverse(pixelToCoordinateTransform, coordinateToPixelTransform);
} }
/**
* Removes all layer renderers.
*/
removeLayerRenderers() {
for (const key in this.layerRenderers_) {
this.removeLayerRendererByKey_(key).dispose();
}
}
/** /**
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate. * @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState. * @param {import("../PluggableMap.js").FrameState} frameState FrameState.
@@ -149,8 +126,8 @@ class MapRenderer extends Disposable {
for (i = numLayers - 1; i >= 0; --i) { for (i = numLayers - 1; i >= 0; --i) {
const layerState = layerStates[i]; const layerState = layerStates[i];
const layer = /** @type {import("../layer/Layer.js").default} */ (layerState.layer); const layer = /** @type {import("../layer/Layer.js").default} */ (layerState.layer);
if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) { if (layer.hasRenderer() && visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) {
const layerRenderer = this.getLayerRenderer(layer); const layerRenderer = layer.getRenderer();
const source = layer.getSource(); const source = layer.getSource();
if (layerRenderer && source) { if (layerRenderer && source) {
const callback = forEachFeatureAtCoordinate.bind(null, layerState.managed); const callback = forEachFeatureAtCoordinate.bind(null, layerState.managed);
@@ -203,35 +180,6 @@ class MapRenderer extends Disposable {
return hasFeature !== undefined; return hasFeature !== undefined;
} }
/**
* @param {import("../layer/Layer.js").default} layer Layer.
* @protected
* @return {import("./Layer.js").default} Layer renderer. May return null.
*/
getLayerRenderer(layer) {
const layerKey = getUid(layer);
if (layerKey in this.layerRenderers_) {
return this.layerRenderers_[layerKey];
}
const renderer = layer.getRenderer();
if (!renderer) {
return null;
}
this.layerRenderers_[layerKey] = renderer;
this.layerRendererListeners_[layerKey] = listen(renderer, EventType.CHANGE, this.handleLayerRendererChange_, this);
return renderer;
}
/**
* @protected
* @return {Object<string, import("./Layer.js").default>} Layer renderers.
*/
getLayerRenderers() {
return this.layerRenderers_;
}
/** /**
* @return {import("../PluggableMap.js").default} Map. * @return {import("../PluggableMap.js").default} Map.
*/ */
@@ -239,29 +187,6 @@ class MapRenderer extends Disposable {
return this.map_; return this.map_;
} }
/**
* Handle changes in a layer renderer.
* @private
*/
handleLayerRendererChange_() {
this.map_.render();
}
/**
* @param {string} layerKey Layer key.
* @return {import("./Layer.js").default} Layer renderer.
* @private
*/
removeLayerRendererByKey_(layerKey) {
const layerRenderer = this.layerRenderers_[layerKey];
delete this.layerRenderers_[layerKey];
unlistenByKey(this.layerRendererListeners_[layerKey]);
delete this.layerRendererListeners_[layerKey];
return layerRenderer;
}
/** /**
* Render. * Render.
* @param {?import("../PluggableMap.js").FrameState} frameState Frame state. * @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
@@ -279,21 +204,6 @@ class MapRenderer extends Disposable {
frameState.postRenderFunctions.push(expireIconCache); frameState.postRenderFunctions.push(expireIconCache);
} }
} }
/**
* @param {!import("../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
scheduleRemoveUnusedLayerRenderers(frameState) {
const layerStatesMap = getLayerStatesMap(frameState.layerStatesArray);
for (const layerKey in this.layerRenderers_) {
if (!(layerKey in layerStatesMap)) {
frameState.postRenderFunctions.push(function() {
this.removeLayerRendererByKey_(layerKey).dispose();
}.bind(this));
}
}
}
} }
@@ -305,15 +215,4 @@ function expireIconCache(map, frameState) {
iconImageCache.expire(); iconImageCache.expire();
} }
/**
* @param {Array<import("../layer/Layer.js").State>} layerStatesArray Layer states array.
* @return {Object<string, import("../layer/Layer.js").State>} States mapped by layer uid.
*/
function getLayerStatesMap(layerStatesArray) {
return layerStatesArray.reduce(function(acc, state) {
acc[getUid(state.layer)] = state;
return acc;
}, {});
}
export default MapRenderer; export default MapRenderer;

View File

@@ -53,6 +53,13 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
super.disposeInternal(); super.disposeInternal();
} }
/**
* @inheritDoc
*/
handleFontsChanged() {
this.vectorRenderer_.handleFontsChanged();
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@@ -3,10 +3,7 @@
*/ */
import {getUid} from '../../util.js'; import {getUid} from '../../util.js';
import ViewHint from '../../ViewHint.js'; import ViewHint from '../../ViewHint.js';
import {listen, unlisten} from '../../events.js';
import EventType from '../../events/EventType.js';
import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js'; import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js';
import {labelCache} from '../../render/canvas.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
import CanvasLayerRenderer from './Layer.js'; import CanvasLayerRenderer from './Layer.js';
@@ -68,16 +65,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @type {boolean} * @type {boolean}
*/ */
this.replayGroupChanged = true; this.replayGroupChanged = true;
listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
}
/**
* @inheritDoc
*/
disposeInternal() {
unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
super.disposeInternal();
} }
/** /**
@@ -211,9 +198,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
} }
/** /**
* @param {import("../../events/Event.js").default} event Event. * @inheritDoc
*/ */
handleFontsChanged_(event) { handleFontsChanged() {
const layer = this.getLayer(); const layer = this.getLayer();
if (layer.getVisible() && this.replayGroup_) { if (layer.getVisible() && this.replayGroup_) {
layer.changed(); layer.changed();

View File

@@ -5,12 +5,11 @@ import {getUid} from '../../util.js';
import {createCanvasContext2D} from '../../dom.js'; import {createCanvasContext2D} from '../../dom.js';
import TileState from '../../TileState.js'; import TileState from '../../TileState.js';
import ViewHint from '../../ViewHint.js'; import ViewHint from '../../ViewHint.js';
import {listen, unlisten, unlistenByKey} from '../../events.js'; import {listen, unlistenByKey} from '../../events.js';
import EventType from '../../events/EventType.js'; import EventType from '../../events/EventType.js';
import {buffer, containsCoordinate, equals, getIntersection, intersects} from '../../extent.js'; import {buffer, containsCoordinate, equals, getIntersection, intersects} from '../../extent.js';
import VectorTileRenderType from '../../layer/VectorTileRenderType.js'; import VectorTileRenderType from '../../layer/VectorTileRenderType.js';
import ReplayType from '../../render/canvas/BuilderType.js'; import ReplayType from '../../render/canvas/BuilderType.js';
import {labelCache} from '../../render/canvas.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import CanvasTileLayerRenderer from './TileLayer.js'; import CanvasTileLayerRenderer from './TileLayer.js';
import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
@@ -132,16 +131,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
// Use nearest lower resolution. // Use nearest lower resolution.
this.zDirection = 1; this.zDirection = 1;
listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
disposeInternal() { disposeInternal() {
unlisten(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
this.overlayContext_.canvas.width = this.overlayContext_.canvas.height = 0; this.overlayContext_.canvas.width = this.overlayContext_.canvas.height = 0;
super.disposeInternal(); super.disposeInternal();
} }
@@ -363,9 +358,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
/** /**
* @param {import("../../events/Event.js").default} event Event. * @inheritDoc
*/ */
handleFontsChanged_(event) { handleFontsChanged() {
const layer = this.getLayer(); const layer = this.getLayer();
if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) { if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) {
layer.changed(); layer.changed();

View File

@@ -49,7 +49,7 @@ describe('ol.renderer.canvas.TileLayer', function() {
}); });
source.updateParams({TIME: '1'}); source.updateParams({TIME: '1'});
map.renderSync(); map.renderSync();
const tiles = map.getRenderer().getLayerRenderer(layer).renderedTiles; const tiles = layer.getRenderer().renderedTiles;
expect(tiles.length).to.be(1); expect(tiles.length).to.be(1);
expect(tiles[0]).to.equal(tile); expect(tiles[0]).to.equal(tile);
expect(tile.inTransition()).to.be(true); expect(tile.inTransition()).to.be(true);

View File

@@ -75,7 +75,7 @@ describe('ol.renderer.canvas.VectorLayer', function() {
style: layerStyle style: layerStyle
}); });
map.addLayer(layer); map.addLayer(layer);
const spy = sinon.spy(map.getRenderer().getLayerRenderer(layer), const spy = sinon.spy(layer.getRenderer(),
'renderFeature'); 'renderFeature');
map.renderSync(); map.renderSync();
expect(spy.getCall(0).args[3]).to.be(layerStyle); expect(spy.getCall(0).args[3]).to.be(layerStyle);

View File

@@ -140,7 +140,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
}); });
it('gives precedence to feature styles over layer styles', function() { it('gives precedence to feature styles over layer styles', function() {
const spy = sinon.spy(map.getRenderer().getLayerRenderer(layer), const spy = sinon.spy(layer.getRenderer(),
'renderFeature'); 'renderFeature');
map.renderSync(); map.renderSync();
expect(spy.getCall(0).args[2]).to.be(layer.getStyle()); expect(spy.getCall(0).args[2]).to.be(layer.getStyle());