Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(): dispose-render concurrency #8220

Merged
merged 26 commits into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/canvas.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ import { Point } from './point.class';
* @chainable
*/
renderAll: function () {
this.cancelRequestedRender();
if (this.destroyed) {
return;
}
if (this.contextTopDirty && !this._groupSelector && !this.isDrawingMode) {
this.clearContext(this.contextTop);
this.contextTopDirty = false;
Expand All @@ -519,9 +523,8 @@ import { Point } from './point.class';
this.renderTopLayer(this.contextTop);
this.hasLostContext = false;
}
var canvasToDrawOn = this.contextContainer;
!this._objectsToRender && (this._objectsToRender = this._chooseObjectsToRender());
this.renderCanvas(canvasToDrawOn, this._objectsToRender);
this.renderCanvas(this.contextContainer, this._objectsToRender);
return this;
},

Expand Down Expand Up @@ -1313,17 +1316,24 @@ import { Point } from './point.class';
},

/**
* Clears a canvas element and removes all event listeners
* @return {fabric.Canvas} thisArg
* @chainable
* Clears the canvas element, disposes objects, removes all event listeners and frees resources
*
* **CAUTION**:
*
* This method is **UNSAFE**.
* You may encounter a race condition using it if there's a requested render.
* Call this method only if you are sure rendering has settled.
* Consider using {@link dispose} as it is **SAFE**
*
* @private
*/
dispose: function () {
destroy: function () {
var wrapperEl = this.wrapperEl,
lowerCanvasEl = this.lowerCanvasEl,
upperCanvasEl = this.upperCanvasEl,
cacheCanvasEl = this.cacheCanvasEl;
this.removeListeners();
this.callSuper('dispose');
this.callSuper('destroy');
wrapperEl.removeChild(upperCanvasEl);
wrapperEl.removeChild(lowerCanvasEl);
this.contextCache = null;
Expand Down
3 changes: 2 additions & 1 deletion src/shapes/object.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,8 @@ import { runningAnimations } from '../util/animation_registry';
// since this canvas is a simple element for the process, we remove references
// to objects in this way in order to avoid object trashing.
canvas._objects = [];
canvas.dispose();
// since render has settled it is safe to destroy canvas
canvas.destroy();
canvas = null;

return canvasEl;
Expand Down
84 changes: 65 additions & 19 deletions src/static_canvas.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { config } from './config';
import { VERSION } from './constants';
import { Point } from './point.class';
import { requestAnimFrame } from './util/animate';
import { removeFromArray } from './util/internals';
import { pick } from './util/misc/pick';

Expand Down Expand Up @@ -675,8 +676,11 @@ import { pick } from './util/misc/pick';
* @chainable
*/
renderAll: function () {
var canvasToDrawOn = this.contextContainer;
this.renderCanvas(canvasToDrawOn, this._objects);
this.cancelRequestedRender();
if (this.destroyed) {
return;
}
this.renderCanvas(this.contextContainer, this._objects);
return this;
},

Expand All @@ -691,7 +695,7 @@ import { pick } from './util/misc/pick';
* @chainable
*/
renderAndReset: function() {
this.isRendering = 0;
this.nextRenderHandle = 0;
this.renderAll();
},

Expand All @@ -703,8 +707,8 @@ import { pick } from './util/misc/pick';
* @chainable
*/
requestRenderAll: function () {
if (!this.isRendering) {
this.isRendering = fabric.util.requestAnimFrame(this.renderAndResetBound);
if (!this.nextRenderHandle && !this.disposed && !this.destroyed) {
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
this.nextRenderHandle = requestAnimFrame(this.renderAndResetBound);
}
return this;
},
Expand Down Expand Up @@ -734,9 +738,9 @@ import { pick } from './util/misc/pick';
},

cancelRequestedRender: function() {
if (this.isRendering) {
fabric.util.cancelAnimFrame(this.isRendering);
this.isRendering = 0;
if (this.nextRenderHandle) {
fabric.util.cancelAnimFrame(this.nextRenderHandle);
this.nextRenderHandle = 0;
}
},

Expand All @@ -747,12 +751,17 @@ import { pick } from './util/misc/pick';
* @return {fabric.Canvas} instance
* @chainable
*/
renderCanvas: function(ctx, objects) {
renderCanvas: function (ctx, objects) {

if (this.destroyed) {
return;
}

var v = this.viewportTransform, path = this.clipPath;
this.cancelRequestedRender();
ShaMan123 marked this conversation as resolved.
Show resolved Hide resolved
this.calcViewportBoundaries();
this.clearContext(ctx);
ctx.imageSmoothingEnabled = this.imageSmoothingEnabled;
// node-canvas
ctx.patternQuality = 'best';
this.fire('before:render', { ctx: ctx, });
this._renderBackground(ctx);
Expand All @@ -778,6 +787,11 @@ import { pick } from './util/misc/pick';
this.drawControls(ctx);
}
this.fire('after:render', { ctx: ctx, });

if (this.__cleanupTask) {
this.__cleanupTask();
this.__cleanupTask = undefined;
}
},

/**
Expand Down Expand Up @@ -1609,16 +1623,49 @@ import { pick } from './util/misc/pick';
},

/**
* Clears a canvas element and dispose objects
* @return {fabric.Canvas} thisArg
* @chainable
* Waits until rendering has settled to destroy the canvas
* @returns {Promise<boolean>} a promise resolving to `true` once the canvas has been destroyed or to `false` if the canvas has was already destroyed
* @throws if aborted by a consequent call
*/
dispose: function () {
// cancel eventually ongoing renders
if (this.isRendering) {
fabric.util.cancelAnimFrame(this.isRendering);
this.isRendering = 0;
}
this.disposed = true;
return new Promise<boolean>((resolve, reject) => {
const task = () => {
this.destroy();
resolve(true);
}
task.kill = reject;
if (this.__cleanupTask) {
this.__cleanupTask.kill('aborted');
}

if (this.destroyed) {
resolve(false);
}
Comment on lines +1637 to +1644
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we doing with those?
i see you want to resolve with true/false in case this is an effective dispose or not.
But why? what does it give us this extra information?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps someone needs to wait until dispose is done to trigger some task that shouldn't start if not disposed

else if (this.nextRenderHandle) {
this.__cleanupTask = task;
}
else {
task();
}
});
},

/**
* Clears the canvas element, disposes objects and frees resources
*
* **CAUTION**:
*
* This method is **UNSAFE**.
* You may encounter a race condition using it if there's a requested render.
* Call this method only if you are sure rendering has settled.
* Consider using {@link dispose} as it is **SAFE**
*
* @private
*/
destroy: function () {
this.destroyed = true;
this.cancelRequestedRender();
this.forEachObject(function(object) {
object.dispose && object.dispose();
});
Expand All @@ -1645,7 +1692,6 @@ import { pick } from './util/misc/pick';
this.lowerCanvasEl.setAttribute('height', this.height);
fabric.util.cleanUpJsdomNode(this.lowerCanvasEl);
this.lowerCanvasEl = undefined;
return this;
},

/**
Expand Down
2 changes: 1 addition & 1 deletion test/lib/visualTestLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
if ((!isOK && QUnit.debugVisual) || QUnit.recreateVisualRefs) {
generateGolden(getGoldeName(golden), renderedCanvas);
}
fabricCanvas.dispose();
await fabricCanvas.dispose();
done();
});
});
Expand Down
111 changes: 2 additions & 109 deletions test/unit/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -2083,72 +2083,7 @@
assert.equal(aGroup._objects[3], circle2);
});

QUnit.test('dispose', function(assert) {
//made local vars to do not dispose the external canvas
var el = fabric.document.createElement('canvas'),
parentEl = fabric.document.createElement('div'),
wrapperEl, lowerCanvasEl, upperCanvasEl;
el.width = 200; el.height = 200;
parentEl.className = 'rootNode';
parentEl.appendChild(el);

fabric.config.configure({ devicePixelRatio: 1.25 });

assert.equal(parentEl.firstChild, el, 'canvas should be appended at partentEl');
assert.equal(parentEl.childNodes.length, 1, 'parentEl has 1 child only');

el.style.position = 'relative';
var elStyle = el.style.cssText;
assert.equal(elStyle, 'position: relative;', 'el style should not be empty');

var canvas = new fabric.Canvas(el, { enableRetinaScaling: true, renderOnAddRemove: false });
wrapperEl = canvas.wrapperEl;
lowerCanvasEl = canvas.lowerCanvasEl;
upperCanvasEl = canvas.upperCanvasEl;
assert.equal(parentEl.childNodes.length, 1, 'parentEl has still 1 child only');
assert.equal(wrapperEl.childNodes.length, 2, 'wrapper should have 2 children');
assert.equal(wrapperEl.tagName, 'DIV', 'We wrapped canvas with DIV');
assert.equal(wrapperEl.className, canvas.containerClass, 'DIV class should be set');
assert.equal(wrapperEl.childNodes[0], lowerCanvasEl, 'First child should be lowerCanvas');
assert.equal(wrapperEl.childNodes[1], upperCanvasEl, 'Second child should be upperCanvas');
assert.equal(canvas._originalCanvasStyle, elStyle, 'saved original canvas style for disposal');
assert.notEqual(el.style.cssText, canvas._originalCanvasStyle, 'canvas el style has been changed');
if (!fabric.isLikelyNode) {
assert.equal(parentEl.childNodes[0], wrapperEl, 'wrapperEl is appendend to rootNode');
}
//looks like i cannot use parentNode
//equal(wrapperEl, lowerCanvasEl.parentNode, 'lowerCanvas is appended to wrapperEl');
//equal(wrapperEl, upperCanvasEl.parentNode, 'upperCanvas is appended to wrapperEl');
//equal(parentEl, wrapperEl.parentNode, 'wrapperEl is appendend to rootNode');
assert.equal(parentEl.childNodes.length, 1, 'parent div should have 1 child');
assert.notEqual(parentEl.firstChild, canvas.getElement(), 'canvas should not be parent div firstChild');
assert.ok(typeof canvas.dispose === 'function');
canvas.add(makeRect(), makeRect(), makeRect());
canvas.item(0).animate('scaleX', 10);
assert.equal(fabric.runningAnimations.length, 1, 'should have a running animation');
canvas.dispose();
canvas.cancelRequestedRender();
assert.equal(fabric.runningAnimations.length, 0, 'dispose should clear running animations');
assert.equal(canvas.getObjects().length, 0, 'dispose should clear canvas');
assert.equal(parentEl.childNodes.length, 1, 'parent has always 1 child');
if (!fabric.isLikelyNode) {
assert.equal(parentEl.childNodes[0], lowerCanvasEl, 'canvas should be back to its firstChild place');
}
assert.equal(canvas.wrapperEl, null, 'wrapperEl should be deleted');
assert.equal(canvas.upperCanvasEl, null, 'upperCanvas should be deleted');
assert.equal(canvas.lowerCanvasEl, null, 'lowerCanvasEl should be deleted');
assert.equal(canvas.cacheCanvasEl, null, 'cacheCanvasEl should be deleted');
assert.equal(canvas.contextTop, null, 'contextTop should be deleted');
assert.equal(canvas.contextCache, null, 'contextCache should be deleted');
assert.equal(canvas._originalCanvasStyle, undefined, 'removed original canvas style');
assert.equal(el.style.cssText, elStyle, 'restored original canvas style');
assert.equal(el.width, 200, 'restored width');
assert.equal(el.height, 200, 'restored height');

});

QUnit.test('dispose + set dimensions', function (assert) {
//made local vars to do not dispose the external canvas
QUnit.test('set dimensions', async function (assert) {
var el = fabric.document.createElement('canvas'),
parentEl = fabric.document.createElement('div');
el.width = 200; el.height = 200;
Expand All @@ -2170,56 +2105,14 @@
assert.equal(canvas._originalCanvasStyle, elStyle, 'saved original canvas style for disposal');
assert.notEqual(el.style.cssText, canvas._originalCanvasStyle, 'canvas el style has been changed');

canvas.dispose();
await canvas.dispose();
assert.equal(canvas._originalCanvasStyle, undefined, 'removed original canvas style');
assert.equal(el.style.cssText, elStyle, 'restored original canvas style');
assert.equal(el.width, 500, 'restored width');
assert.equal(el.height, 500, 'restored height');

});

// QUnit.test('dispose', function(assert) {
// function invokeEventsOnCanvas() {
// // nextSibling because we need to invoke events on upper canvas
// simulateEvent(canvas.getElement().nextSibling, 'mousedown');
// simulateEvent(canvas.getElement().nextSibling, 'mouseup');
// simulateEvent(canvas.getElement().nextSibling, 'mousemove');
// }
// var assertInvocationsCount = function() {
// var message = 'event handler should not be invoked after `dispose`';
// assert.equal(handlerInvocationCounts.__onMouseDown, 1);
// assert.equal(handlerInvocationCounts.__onMouseUp, 1);
// assert.equal(handlerInvocationCounts.__onMouseMove, 1);
// };

// assert.ok(typeof canvas.dispose === 'function');
// canvas.add(makeRect(), makeRect(), makeRect());

// var handlerInvocationCounts = {
// __onMouseDown: 0, __onMouseUp: 0, __onMouseMove: 0
// };

// // hijack event handlers
// canvas.__onMouseDown = function() {
// handlerInvocationCounts.__onMouseDown++;
// };
// canvas.__onMouseUp = function() {
// handlerInvocationCounts.__onMouseUp++;
// };
// canvas.__onMouseMove = function() {
// handlerInvocationCounts.__onMouseMove++;
// };

// invokeEventsOnCanvas();
// assertInvocationsCount();

// canvas.dispose();
// assert.equal(canvas.getObjects().length, 0, 'dispose should clear canvas');

// invokeEventsOnCanvas();
// assertInvocationsCount();
// });

QUnit.test('clone', function(assert) {
var done = assert.async();
assert.ok(typeof canvas.clone === 'function');
Expand Down
Loading