fix exponential blowup in diff algorith; wip

This commit is contained in:
Andreas Rumpf 2017-07-03 23:03:30 +02:00
commit 43ee890356
2 changed files with 96 additions and 37 deletions

View file

@ -34,6 +34,7 @@ type
renderId: int renderId: int
patches: seq[Patch] # we reuse this to save allocations patches: seq[Patch] # we reuse this to save allocations
patchLen: int patchLen: int
recursion: int
var var
@ -161,6 +162,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node =
proc same(n: VNode, e: Node): bool = proc same(n: VNode, e: Node): bool =
if n.kind == VNodeKind.component: if n.kind == VNodeKind.component:
result = same(VComponent(n).expanded, e) result = same(VComponent(n).expanded, e)
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
# we don't check these for now:
result = true
elif toTag[n.kind] == e.nodename: elif toTag[n.kind] == e.nodename:
result = true result = true
if n.kind != VNodeKind.text: if n.kind != VNodeKind.text:
@ -178,7 +182,7 @@ proc replaceById(id: cstring; newTree: Node) =
type type
EqResult = enum EqResult = enum
changed, different, similar, identical changed, different, similar, identical, usenewNode
proc eq(a, b: VNode): EqResult = proc eq(a, b: VNode): EqResult =
if a.kind != b.kind: return different if a.kind != b.kind: return different
@ -274,6 +278,13 @@ proc apply(kxi: KaraxInstance) =
kxi.patchLen = 0 kxi.patchLen = 0
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode)
if oldNode.kind == VNodeKind.text:
echo oldNode.text
return
#doAssert false, "overflow!"
inc kxi.recursion
result = eq(newNode, oldNode) result = eq(newNode, oldNode)
case result case result
of identical, similar: of identical, similar:
@ -293,68 +304,85 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
assert oldNode.kind == newNode.kind assert oldNode.kind == newNode.kind
var commonPrefix = 0 var commonPrefix = 0
let isSpecial = oldNode.kind == VNodeKind.component or
oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
let oldLen = kxi.patchLen let oldLen = kxi.patchLen
when false: assert i < a.len
if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: assert j < b.len
assert current != nil let r = if isSpecial:
assert current.childNodes[j] != nil, $info
assert oldNode.len == current.len
let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk:
diff(a[i], b[j], parent, current, kxi) diff(a[i], b[j], parent, current, kxi)
else: else:
diff(a[i], b[j], current, current.childNodes[j], kxi) (assert j < current.len;
diff(a[i], b[j], current, current.childNodes[j], kxi))
case r case r
of identical, changed, similar: of identical, changed, similar:
a[i] = b[j] a[i] = b[j]
action action
of usenewNode:
b[j] = a[i]
action
of different: of different:
# undo what 'diff' would have done: # undo what 'diff' would have done:
kxi.patchLen = oldLen kxi.patchLen = oldLen
if result != different: result = r #if result != different: result = r
break break
#of similar: # compute common prefix:
# updateStyles(a[i], b[j])
# a[i] = b[j]
# action
while commonPrefix < minLength: while commonPrefix < minLength:
eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"):
inc commonPrefix inc commonPrefix
# compute common suffix:
var oldPos = oldLength - 1 var oldPos = oldLength - 1
var newPos = newLength - 1 var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix: while oldPos >= commonPrefix and newPos >= commonPrefix:
eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"):
dec oldPos dec oldPos
dec newPos dec newPos
echo "came here"
var pos = min(oldPos, newPos) + 1 let pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1: # now the different children are in commonPrefix .. pos - 1:
if diff(newNode[i], oldNode[i], current, current.childNodes[i], when false:
kxi) != different: for i in commonPrefix..pos-1:
newNode[i] = oldNode[i] detach(oldNode[i])
else: kxi.addPatch(pkReplace, current, current.childNodes[i], newNode[i])
result = different when true:
for i in commonPrefix..pos-1:
let r = diff(newNode[i], oldNode[i], current, current.childNodes[i],
kxi)
if r == usenewNode:
oldNode[i] = newNode[i]
elif r != different:
newNode[i] = oldNode[i]
#else:
# result = different
if oldPos + 1 == oldLength: if oldPos + 1 == oldLength:
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i]) kxi.addPatch(pkAppend, current, nil, newNode[i])
result = different result = usenewNode
#result = different
else: else:
let before = current.childNodes[oldPos + 1] let before = current.childNodes[oldPos + 1]
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i]) kxi.addPatch(pkInsertBefore, current, before, newNode[i])
result = different result = usenewNode
#result = different
# XXX call 'attach' here? # XXX call 'attach' here?
for i in pos..oldPos: for i in pos..oldPos:
detach(oldNode[i]) detach(oldNode[i])
#doAssert i < current.childNodes.len #doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil) kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
result = different result = usenewNode #different
# after the applied patch, conceptually the nodes are identical, so
# no further search is required. 'changed' needs to be propagated
# for the component system to work. 'similar' was transformed into
# identical too:
#if result == different or result == similar:
# result = identical
of changed: of changed:
assert oldNode.kind == VNodeKind.component assert oldNode.kind == VNodeKind.component
@ -375,6 +403,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
of different: of different:
detach(oldNode) detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode) kxi.addPatch(pkReplace, parent, current, newNode)
of usenewNode: doAssert(false, "eq returned usenewNode")
dec kxi.recursion
when defined(stats):
proc depth(n: VNode; total: var int): int =
var m = 0
for i in 0..<n.len:
m = max(m, depth(n[i], total))
result = m + 1
inc total
proc dodraw(kxi: KaraxInstance) = proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return if kxi.renderer.isNil: return
@ -391,7 +429,7 @@ proc dodraw(kxi: KaraxInstance) =
#kout cstring"patch len ", patches.len #kout cstring"patch len ", patches.len
apply(kxi) apply(kxi)
kxi.currentTree = newtree kxi.currentTree = newtree
#doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
if not kxi.postRenderCallback.isNil: if not kxi.postRenderCallback.isNil:
kxi.postRenderCallback() kxi.postRenderCallback()
@ -400,6 +438,10 @@ proc dodraw(kxi: KaraxInstance) =
if kxi.toFocus != nil: if kxi.toFocus != nil:
kxi.toFocus.focus() kxi.toFocus.focus()
kxi.renderId = 0 kxi.renderId = 0
kxi.recursion = 0
when defined(stats):
var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}

View file

@ -9,12 +9,27 @@ proc hasDom(n: Vnode) =
doAssert n.dom != nil doAssert n.dom != nil
for i in 0..<n.len: hasDom(n[i]) for i in 0..<n.len: hasDom(n[i])
proc doDiff(a, b: VNode) = proc shortRepr(n: VNode): string =
var patches = newJSeq[Patch]() if n == nil:
echo diff(b, a, nil, vnodeToDom(a, kxi), patches) result = "nil"
for i in 0..<patches.len: elif n.kind == VNodeKind.text:
echo patches[i] result = $n.text
else:
result = $n.kind
for i in 0..<n.len:
result &= " " & shortRepr(n[i])
proc doDiff(a, b: VNode; expected: varargs[string]) =
discard diff(b, a, nil, vnodeToDom(a, kxi), kxi)
for i in 0..<kxi.patchLen:
let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n)
echo "got ", p
if i >= expected.len:
echo "patches differ; expected nothing but got: ", p
elif p != expected[i]:
echo "patches differ; expected ", expected[i], " but got: ", p
#hasDom(kxi.currentTree) #hasDom(kxi.currentTree)
kxi.patchLen = 0
proc testAppend() = proc testAppend() =
let a = buildHtml(tdiv): let a = buildHtml(tdiv):
@ -26,7 +41,7 @@ proc testAppend() =
li: text "A" li: text "A"
li: text "B" li: text "B"
li: text "C" li: text "C"
doDiff(a, b) doDiff(a, b, "pkAppend li C")
proc testInsert() = proc testInsert() =
let a = buildHtml(tdiv): let a = buildHtml(tdiv):
@ -38,7 +53,7 @@ proc testInsert() =
li: text "A" li: text "A"
li: text "B" li: text "B"
li: text "C" li: text "C"
doDiff(a, b) doDiff(a, b, "pkInsert li B")
proc testDelete() = proc testDelete() =
let a = buildHtml(tdiv): let a = buildHtml(tdiv):
@ -49,10 +64,12 @@ proc testDelete() =
let b = buildHtml(tdiv): let b = buildHtml(tdiv):
ul: ul:
discard discard
doDiff(a, b) doDiff(a, b, "pkDetach li A", "pkRemove nil",
"pkDetach li B", "pkRemove nil",
"pkDetach li C", "pkRemove nil")
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard)
testAppend() #testAppend()
testInsert() testInsert()
testDelete() #testDelete()