From b9b5bd5b81ef3fb1b6072b32e3a82b027f27d5c6 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 18 Mar 2024 14:33:36 +0000 Subject: [PATCH] [fix] Batch tick events (#3181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes an issue where events happening on tick were not batched. ![Kapture 2024-03-17 at 22 49 52](https://github.com/tldraw/tldraw/assets/23072548/2bcfa335-a38f-46c4-a3f3-434cac61b6ce) We were listening to the `tick` event directly from the state node, rather than passing the event into the state chart at the top. This meant that it was bypassing the regular state chart rules, which was what got me looking at this; but then I noticed that we also weren't batching the changes, either. This causes computed stuff to re-compute after each atom is updated within the `onTick` handler, which can be a LOT. Before: image After: image It's not game breaking but it's important enough to hotfix at least in the dot com. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Select many shapes. 2. Resize them. ### Release Notes - Fix a performance issue effecting resizing multiple shapes. --- packages/editor/api-report.md | 13 ++- packages/editor/api/api.json | 90 ++++++++++++------- packages/editor/src/index.ts | 1 - packages/editor/src/lib/editor/Editor.ts | 8 ++ .../lib/editor/managers/ScribbleManager.ts | 23 +---- .../editor/src/lib/editor/tools/StateNode.ts | 7 +- .../src/lib/editor/types/event-types.ts | 13 +-- packages/tldraw/api-report.md | 1 - .../tools/SelectTool/childStates/Brushing.ts | 3 +- .../tools/SelectTool/childStates/Resizing.ts | 3 +- .../SelectTool/childStates/Translating.ts | 3 +- 11 files changed, 87 insertions(+), 78 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index e3a6d8906..d2df6ea09 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -1782,7 +1782,7 @@ export abstract class StateNode implements Partial { // (undocumented) onRightClick?: TLEventHandlers['onRightClick']; // (undocumented) - onTick?: TLTickEventHandler; + onTick?: TLEventHandlers['onTick']; // (undocumented) onTripleClick?: TLEventHandlers['onTripleClick']; // (undocumented) @@ -2102,13 +2102,15 @@ export interface TLEventHandlers { // (undocumented) onRightClick: TLPointerEvent; // (undocumented) + onTick: TLTickEvent; + // (undocumented) onTripleClick: TLClickEvent; // (undocumented) onWheel: TLWheelEvent; } // @public (undocumented) -export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLWheelEventInfo; +export type TLEventInfo = TLCancelEventInfo | TLClickEventInfo | TLCompleteEventInfo | TLInterruptEventInfo | TLKeyboardEventInfo | TLPinchEventInfo | TLPointerEventInfo | TLTickEventInfo | TLWheelEventInfo; // @public (undocumented) export interface TLEventMap { @@ -2155,7 +2157,7 @@ export interface TLEventMap { export type TLEventMapHandler = (...args: TLEventMap[T]) => void; // @public (undocumented) -export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName; +export type TLEventName = 'cancel' | 'complete' | 'interrupt' | 'tick' | 'wheel' | TLCLickEventName | TLKeyboardEventName | TLPinchEventName | TLPointerEventName; // @public (undocumented) export type TLExitEventHandler = (info: any, to: string) => void; @@ -2571,10 +2573,7 @@ export type TLSvgOptions = { }; // @public (undocumented) -export type TLTickEvent = (elapsed: number) => void; - -// @public (undocumented) -export type TLTickEventHandler = () => void; +export type TLTickEvent = (info: TLTickEventInfo) => void; // @public export interface TLUserPreferences { diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 0b38a1d21..6f5d87ed9 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -34154,8 +34154,12 @@ }, { "kind": "Reference", - "text": "TLTickEventHandler", - "canonicalReference": "@tldraw/editor!TLTickEventHandler:type" + "text": "TLEventHandlers", + "canonicalReference": "@tldraw/editor!TLEventHandlers:interface" + }, + { + "kind": "Content", + "text": "['onTick']" }, { "kind": "Content", @@ -34168,7 +34172,7 @@ "name": "onTick", "propertyTypeTokenRange": { "startIndex": 1, - "endIndex": 2 + "endIndex": 3 }, "isStatic": false, "isProtected": false, @@ -37534,6 +37538,34 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!TLEventHandlers#onTick:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onTick: " + }, + { + "kind": "Reference", + "text": "TLTickEvent", + "canonicalReference": "@tldraw/editor!TLTickEvent:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "onTick", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TLEventHandlers#onTripleClick:member", @@ -37665,6 +37697,15 @@ "kind": "Content", "text": " | " }, + { + "kind": "Reference", + "text": "TLTickEventInfo", + "canonicalReference": "@tldraw/editor!~TLTickEventInfo:type" + }, + { + "kind": "Content", + "text": " | " + }, { "kind": "Reference", "text": "TLWheelEventInfo", @@ -37680,7 +37721,7 @@ "name": "TLEventInfo", "typeTokenRange": { "startIndex": 1, - "endIndex": 16 + "endIndex": 18 } }, { @@ -38133,7 +38174,7 @@ }, { "kind": "Content", - "text": "'cancel' | 'complete' | 'interrupt' | 'wheel' | " + "text": "'cancel' | 'complete' | 'interrupt' | 'tick' | 'wheel' | " }, { "kind": "Reference", @@ -41562,7 +41603,16 @@ }, { "kind": "Content", - "text": "(elapsed: number) => void" + "text": "(info: " + }, + { + "kind": "Reference", + "text": "TLTickEventInfo", + "canonicalReference": "@tldraw/editor!~TLTickEventInfo:type" + }, + { + "kind": "Content", + "text": ") => void" }, { "kind": "Content", @@ -41574,33 +41624,7 @@ "name": "TLTickEvent", "typeTokenRange": { "startIndex": 1, - "endIndex": 2 - } - }, - { - "kind": "TypeAlias", - "canonicalReference": "@tldraw/editor!TLTickEventHandler:type", - "docComment": "/**\n * @public\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "export type TLTickEventHandler = " - }, - { - "kind": "Content", - "text": "() => void" - }, - { - "kind": "Content", - "text": ";" - } - ], - "fileUrlPath": "packages/editor/src/lib/editor/types/event-types.ts", - "releaseTag": "Public", - "name": "TLTickEventHandler", - "typeTokenRange": { - "startIndex": 1, - "endIndex": 2 + "endIndex": 4 } }, { diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index b9650e506..8f7f42c9f 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -223,7 +223,6 @@ export { type TLPointerEventName, type TLPointerEventTarget, type TLTickEvent, - type TLTickEventHandler, type TLWheelEvent, type TLWheelEventInfo, type UiEvent, diff --git a/packages/editor/src/lib/editor/Editor.ts b/packages/editor/src/lib/editor/Editor.ts index de43e3bc5..b22a3b4bc 100644 --- a/packages/editor/src/lib/editor/Editor.ts +++ b/packages/editor/src/lib/editor/Editor.ts @@ -637,6 +637,8 @@ export class Editor extends EventEmitter { this.updateRenderingBounds() + this.on('tick', this.tick) + requestAnimationFrame(() => { this._tickManager.start() }) @@ -8380,6 +8382,12 @@ export class Editor extends EventEmitter { ]) } + /** @internal */ + private tick = (elapsed = 0) => { + this.dispatch({ type: 'misc', name: 'tick', elapsed }) + this.scribbles.tick(elapsed) + } + /** * Dispatch a cancel event. * diff --git a/packages/editor/src/lib/editor/managers/ScribbleManager.ts b/packages/editor/src/lib/editor/managers/ScribbleManager.ts index 11c45bfe2..5b232caf5 100644 --- a/packages/editor/src/lib/editor/managers/ScribbleManager.ts +++ b/packages/editor/src/lib/editor/managers/ScribbleManager.ts @@ -2,7 +2,6 @@ import { TLScribble, VecModel } from '@tldraw/tlschema' import { Vec } from '../../primitives/Vec' import { uniqueId } from '../../utils/uniqueId' import { Editor } from '../Editor' -import { TLTickEvent } from '../types/event-types' type ScribbleItem = { id: string @@ -41,26 +40,12 @@ export class ScribbleManager { next: null, } this.scribbleItems.set(id, item) - if (this.state === 'paused') { - this.resume() - } return item } - resume() { - this.state = 'running' - this.editor.addListener('tick', this.tick) - } - - pause() { - this.editor.removeListener('tick', this.tick) - this.state = 'paused' - } - reset() { this.editor.updateInstanceState({ scribbles: [] }) this.scribbleItems.clear() - this.pause() } /** @@ -99,7 +84,8 @@ export class ScribbleManager { * @param elapsed - The number of milliseconds since the last tick. * @public */ - tick: TLTickEvent = (elapsed) => { + tick = (elapsed: number) => { + if (this.scribbleItems.size === 0) return this.editor.batch(() => { this.scribbleItems.forEach((item) => { // let the item get at least eight points before @@ -190,11 +176,6 @@ export class ScribbleManager { })) .slice(-5), // limit to three as a minor sanity check }) - - // If we've removed all the scribbles, stop ticking - if (this.scribbleItems.size === 0) { - this.pause() - } }) } } diff --git a/packages/editor/src/lib/editor/tools/StateNode.ts b/packages/editor/src/lib/editor/tools/StateNode.ts index 4744c7d67..5c5378b63 100644 --- a/packages/editor/src/lib/editor/tools/StateNode.ts +++ b/packages/editor/src/lib/editor/tools/StateNode.ts @@ -7,7 +7,6 @@ import { TLEventInfo, TLExitEventHandler, TLPinchEventInfo, - TLTickEventHandler, } from '../types/event-types' type TLStateNodeType = 'branch' | 'leaf' | 'root' @@ -158,7 +157,7 @@ export abstract class StateNode implements Partial { enter = (info: any, from: string) => { this._isActive.set(true) this.onEnter?.(info, from) - if (this.onTick) this.editor.on('tick', this.onTick) + if (this.children && this.initial && this.getIsActive()) { const initial = this.children[this.initial] this._current.set(initial) @@ -169,8 +168,8 @@ export abstract class StateNode implements Partial { // todo: move this logic into transition exit = (info: any, from: string) => { this._isActive.set(false) - if (this.onTick) this.editor.off('tick', this.onTick) this.onExit?.(info, from) + if (!this.getIsActive()) { this.getCurrent()?.exit(info, from) } @@ -211,8 +210,8 @@ export abstract class StateNode implements Partial { onCancel?: TLEventHandlers['onCancel'] onComplete?: TLEventHandlers['onComplete'] onInterrupt?: TLEventHandlers['onInterrupt'] + onTick?: TLEventHandlers['onTick'] onEnter?: TLEnterEventHandler onExit?: TLExitEventHandler - onTick?: TLTickEventHandler } diff --git a/packages/editor/src/lib/editor/types/event-types.ts b/packages/editor/src/lib/editor/types/event-types.ts index d1959c9a3..5fa41deb8 100644 --- a/packages/editor/src/lib/editor/types/event-types.ts +++ b/packages/editor/src/lib/editor/types/event-types.ts @@ -39,6 +39,7 @@ export type TLEventName = | 'cancel' | 'complete' | 'interrupt' + | 'tick' /** @public */ export interface TLBaseEventInfo { @@ -99,6 +100,8 @@ export type TLCancelEventInfo = { type: 'misc'; name: 'cancel' } export type TLCompleteEventInfo = { type: 'misc'; name: 'complete' } /** @public */ export type TLInterruptEventInfo = { type: 'misc'; name: 'interrupt' } +/** @public */ +export type TLTickEventInfo = { type: 'misc'; name: 'tick'; elapsed: number } /** @public */ export type TLEventInfo = @@ -110,6 +113,7 @@ export type TLEventInfo = | TLCancelEventInfo | TLCompleteEventInfo | TLInterruptEventInfo + | TLTickEventInfo /** @public */ export type TLPointerEvent = (info: TLPointerEventInfo) => void @@ -127,6 +131,8 @@ export type TLCancelEvent = (info: TLCancelEventInfo) => void export type TLCompleteEvent = (info: TLCompleteEventInfo) => void /** @public */ export type TLInterruptEvent = (info: TLInterruptEventInfo) => void +/** @public */ +export type TLTickEvent = (info: TLTickEventInfo) => void /** @public */ export type UiEvent = @@ -137,8 +143,6 @@ export type UiEvent = | TLCancelEvent | TLCompleteEvent -/** @public */ -export type TLTickEventHandler = () => void /** @public */ export type TLEnterEventHandler = (info: any, from: string) => void /** @public */ @@ -161,6 +165,7 @@ export interface TLEventHandlers { onCancel: TLCancelEvent onComplete: TLCompleteEvent onInterrupt: TLInterruptEvent + onTick: TLTickEvent } /** @public */ @@ -183,7 +188,5 @@ export const EVENT_NAME_MAP: Record< double_click: 'onDoubleClick', triple_click: 'onTripleClick', quadruple_click: 'onQuadrupleClick', + tick: 'onTick', } - -/** @public */ -export type TLTickEvent = (elapsed: number) => void diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 45da508ec..5b45dafe6 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -114,7 +114,6 @@ import { TLStore } from '@tldraw/editor'; import { TLStoreWithStatus } from '@tldraw/editor'; import { TLSvgOptions } from '@tldraw/editor'; import { TLTextShape } from '@tldraw/editor'; -import { TLTickEventHandler } from '@tldraw/editor'; import { TLUnknownShape } from '@tldraw/editor'; import { TLVideoShape } from '@tldraw/editor'; import { UnionValidator } from '@tldraw/editor'; diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts index 4258dbd0e..5cc228b00 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts @@ -13,7 +13,6 @@ import { TLPointerEventInfo, TLShape, TLShapeId, - TLTickEventHandler, Vec, moveCameraWhenCloseToEdge, pointInPolygon, @@ -67,7 +66,7 @@ export class Brushing extends StateNode { this.editor.updateInstanceState({ brush: null }) } - override onTick: TLTickEventHandler = () => { + override onTick = () => { moveCameraWhenCloseToEdge(this.editor) if (this.isDirty) { this.isDirty = false diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts index 6c9a76280..69a921c81 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts @@ -13,7 +13,6 @@ import { TLShape, TLShapeId, TLShapePartial, - TLTickEventHandler, Vec, VecLike, areAnglesCompatible, @@ -74,7 +73,7 @@ export class Resizing extends StateNode { this.updateShapes() } - override onTick: TLTickEventHandler = () => { + override onTick = () => { moveCameraWhenCloseToEdge(this.editor) if (!this.isDirty) return this.isDirty = false diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts index 2952c3857..2b4c75d5b 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts @@ -10,7 +10,6 @@ import { TLPointerEventInfo, TLShape, TLShapePartial, - TLTickEventHandler, Vec, compact, isPageId, @@ -95,7 +94,7 @@ export class Translating extends StateNode { this.dragAndDropManager.clear() } - override onTick: TLTickEventHandler = () => { + override onTick = () => { this.dragAndDropManager.updateDroppingNode( this.snapshot.movingShapes, this.updateParentTransforms