fix: perf optimize on cleanup

pull/6850/head
Peng Xiao 2022-09-28 14:22:46 +08:00
parent b3ecf26b90
commit 01b04fc2f8
6 changed files with 100 additions and 101 deletions

View File

@ -98,7 +98,6 @@ and handles unexpected failure."
(map first))
title (when (gp-block/heading-block? (first ast))
(:title (second (first ast))))
_ (println title (second (first ast)))
body (vec (if title (rest ast) ast))
body (drop-while gp-property/properties-ast? body)
result (cond->

View File

@ -144,7 +144,7 @@ export default function App() {
// Mimic external reload event
React.useEffect(() => {
const interval = setInterval(() => {
setModel(onLoad())
// setModel(onLoad())
}, 2000)
return () => {

View File

@ -60,7 +60,7 @@ export class TLHistory<S extends TLShape = TLShape, K extends TLEventMap = TLEve
const { serialized } = this.app
// Do not persist if the serialized state is the same as the last one
if (!shouldPersist(this.stack[this.pointer], serialized)) {
if (this.stack.length > 0 && !shouldPersist(this.stack[this.pointer], serialized)) {
return
}

View File

@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { intersectRayBounds, TLBounds } from '@tldraw/intersect'
import Vec from '@tldraw/vec'
import { action, computed, makeObservable, observable, reaction, toJS } from 'mobx'
import { action, autorun, computed, makeObservable, observable, toJS, transaction } from 'mobx'
import { BINDING_DISTANCE } from '../../constants'
import { type TLBinding, type TLEventMap, TLResizeCorner, type TLHandle } from '../../types'
import { deepCopy, BoundsUtils, deepEqual, PointUtils } from '../../utils'
import type { TLShape, TLShapeModel, TLLineShape } from '../shapes'
import { TLResizeCorner, type TLBinding, type TLEventMap, type TLHandle } from '../../types'
import { BoundsUtils, deepCopy, PointUtils } from '../../utils'
import type { TLLineShape, TLShape, TLShapeModel } from '../shapes'
import type { TLApp } from '../TLApp'
export interface TLPageModel<S extends TLShape = TLShape> {
@ -35,23 +35,33 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
this.addShapes(...shapes)
makeObservable(this)
// Performance bottleneck!! Optimize me :/
// Instead of watch for every shape change, we should only watch for the changed ones
reaction(
() => ({
id: this.id,
name: this.name,
shapes: toJS(this.shapes.map(shape => toJS(shape.props))),
bindings: toJS(this.bindings),
nonce: this.nonce,
}),
(curr, prev) => {
if (this.app.isInAny('creating')) return
this.cleanup(curr, prev)
autorun(
() => {
const now = performance.now()
const newShapesNouncesMap = Object.fromEntries(
this.shapes.map(shape => [shape.id, shape.nonce])
)
if (this.lastShapesNounces) {
const lastShapesNounces = this.lastShapesNounces
const allIds = new Set([
...Object.keys(newShapesNouncesMap),
...Object.keys(lastShapesNounces),
])
const changedShapeIds = [...allIds].filter(s => {
return lastShapesNounces[s] !== newShapesNouncesMap[s]
})
this.cleanup(changedShapeIds)
}
this.lastShapesNounces = newShapesNouncesMap
},
{
delay: 10,
}
)
}
lastShapesNounces = null as Record<string, number> | null
app: TLApp<S, E>
@observable id: string
@ -212,88 +222,71 @@ export class TLPage<S extends TLShape = TLShape, E extends TLEventMap = TLEventM
return shape
}
/**
* Recalculate binding positions etc. Will also persist state when needed.
*
* @param curr
* @param prev
*/
/** Recalculate binding positions for changed shapes etc. Will also persist state when needed. */
@action
cleanup = (curr: TLPageModel, prev: TLPageModel) => {
const updated = deepCopy(curr)
const changedShapes: Record<string, TLShapeModel<S['props']> | undefined> = {}
const prevShapes = Object.fromEntries(prev.shapes.map(shape => [shape.id, shape]))
const currShapes = Object.fromEntries(curr.shapes.map(shape => [shape.id, shape]))
// TODO: deleted shapes?
curr.shapes.forEach(shape => {
if (deepEqual(shape, prevShapes[shape.id])) {
changedShapes[shape.id] = shape
}
})
cleanup = (changedShapeIds: string[]) => {
// Get bindings related to the changed shapes
const bindingsToUpdate = getRelatedBindings(curr, Object.keys(changedShapes))
const updated = this.serialized
const bindingsToUpdate = getRelatedBindings(updated, changedShapeIds)
const visitedShapes = new Set<string>()
const visitedShapes = new Set<TLShapeModel>()
// Update all of the bindings we've just collected
bindingsToUpdate.forEach(binding => {
if (!updated.bindings[binding.id]) {
return
}
const toShape = currShapes[binding.toId]
const fromShape = currShapes[binding.fromId]
if (!(toShape && fromShape)) {
delete updated.bindings[binding.id]
return
}
if (visitedShapes.has(fromShape)) {
return
}
// We only need to update the binding's "from" shape (an arrow)
// @ts-expect-error ???
const fromDelta = this.updateArrowBindings(this.getShapeById<TLLineShape>(fromShape.id))
visitedShapes.add(fromShape)
if (fromDelta) {
const nextShape = {
...fromShape,
...fromDelta,
transaction(() => {
let changed = false
const newBindings = deepCopy(updated.bindings)
// Update all of the bindings we've just collected
bindingsToUpdate.forEach(binding => {
if (!updated.bindings[binding.id]) {
return
}
const idx = updated.shapes.findIndex(s => s.id === nextShape.id)
if (idx !== -1) {
updated.shapes[idx] = nextShape
}
}
})
// Cleanup outdated bindings
Object.keys(updated.bindings).forEach(id => {
if (
!updated.shapes.find(
shape => shape.id === updated.bindings[id].toId || updated.bindings[id].fromId
const toShape = updated.shapes.find(s => s.id === binding.toId)
const fromShape = updated.shapes.find(s => s.id === binding.fromId)
if (!(toShape && fromShape)) {
delete newBindings[binding.id]
changed = true
return
}
if (visitedShapes.has(fromShape.id)) {
return
}
// We only need to update the binding's "from" shape (an arrow)
// @ts-expect-error ???
const fromDelta = this.updateArrowBindings(this.getShapeById<TLLineShape>(fromShape.id))
visitedShapes.add(fromShape.id)
if (fromDelta) {
const nextShape = {
...fromShape,
...fromDelta,
}
changed = true
this.getShapeById(nextShape.id)?.update(nextShape, false, true)
}
})
// Cleanup outdated bindings
Object.keys(newBindings).forEach(id => {
const binding = this.bindings[id]
const relatedShapes = updated.shapes.filter(
shape => shape.id === binding.fromId || shape.id === binding.toId
)
) {
delete updated.bindings[id]
if (relatedShapes.length === 0) {
delete newBindings[id]
changed = true
}
})
this.update({
bindings: newBindings,
})
if (changed) {
this.app.persist(true)
}
})
if (!deepEqual(updated, curr)) {
this.update({
bindings: updated.bindings,
})
updated.shapes.forEach(shape => {
this.getShapeById(shape.id)?.update(shape)
})
this.app.persist(true)
}
}
private updateArrowBindings = (lineShape: TLLineShape) => {

View File

@ -103,7 +103,7 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
canEdit: TLFlag = false
canBind: TLFlag = false
@observable nonce = 0
@observable nonce = Date.now()
bindingDistance = BINDING_DISTANCE
@ -120,6 +120,10 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
this.nonce = nonce
}
@action incNonce() {
this.nonce++
}
@action setIsDirty(isDirty: boolean) {
this.isDirty = isDirty
}
@ -275,7 +279,6 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
protected getCachedSerialized = (): TLShapeModel<P> => {
if (this.isDirty || !this.lastSerialized) {
transaction(() => {
this.setNonce(Date.now())
this.setIsDirty(false)
this.setLastSerialized(this.getSerialized())
})
@ -297,8 +300,9 @@ export abstract class TLShape<P extends TLShapeProps = TLShapeProps, M = any> {
return props
}
@action update = (props: Partial<TLShapeProps & P & any>, isDeserializing = false) => {
if (!(isDeserializing || this.isDirty)) this.isDirty = true
@action update = (props: Partial<TLShapeProps & P & any>, isDeserializing = false, skipNounce = false) => {
if (!(isDeserializing || this.isDirty)) this.setIsDirty(true)
if (!isDeserializing && !skipNounce) this.incNonce()
Object.assign(this.props, this.validateProps(props as Partial<TLShapeProps> & Partial<P>))
return this
}

View File

@ -1,4 +1,5 @@
import { Vec } from '@tldraw/vec'
import { transaction } from 'mobx'
import { type TLEventMap, TLCursor, type TLEvents } from '../../../../types'
import { uniqueId } from '../../../../utils'
import type { TLShape } from '../../../shapes'
@ -45,9 +46,11 @@ export class TranslatingState<
}
}
selectedShapes.forEach(shape =>
shape.update({ point: Vec.add(initialPoints[shape.id], delta) })
)
transaction(() => {
selectedShapes.forEach(shape =>
shape.update({ point: Vec.add(initialPoints[shape.id], delta) })
)
})
}
private startCloning() {