optimized diff algorithm further; fixed a stupid bug; cleaned up the tests

This commit is contained in:
Andreas Rumpf 2017-06-26 21:03:39 +02:00
commit 8a1424669f
5 changed files with 117 additions and 168 deletions

View file

@ -20,3 +20,5 @@ proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".} proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".}
proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}
proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.}

View file

@ -208,12 +208,13 @@ type
parent, current: Node parent, current: Node
n: VNode n: VNode
proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current: Node; proc addPatch(patches: JSeq[Patch]; k: PatchKind; parent, current: Node;
n: VNode) = n: VNode) =
patches.add(Patch(k: k, parent: parent, current: current, n: n)) patches.add(Patch(k: k, parent: parent, current: current, n: n))
proc apply(patches: seq[Patch]; kxi: KaraxInstance) = proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) =
for p in patches: for i in 0..<patches.len:
let p = patches[i]
case p.k case p.k
of pkReplace: of pkReplace:
let nn = vnodeToDom(p.n, kxi) let nn = vnodeToDom(p.n, kxi)
@ -230,8 +231,7 @@ proc apply(patches: seq[Patch]; kxi: KaraxInstance) =
let nn = vnodeToDom(p.n, kxi) let nn = vnodeToDom(p.n, kxi)
p.parent.insertBefore(nn, p.current) p.parent.insertBefore(nn, p.current)
proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): EqResult =
kxi: KaraxInstance): EqResult =
result = eq(newNode, oldNode) result = eq(newNode, oldNode)
case result case result
of identical: of identical:
@ -239,7 +239,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch
let newLength = newNode.len let newLength = newNode.len
var oldLength = oldNode.len var oldLength = oldNode.len
let minLength = min(newLength, oldLength) let minLength = min(newLength, oldLength)
if minLength == 0: return result if newLength == 0 and oldLength == 0: return result
assert oldNode.kind == newNode.kind assert oldNode.kind == newNode.kind
var commonPrefix = 0 var commonPrefix = 0
@ -254,16 +254,16 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch
let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk: oldNode.kind == VNodeKind.dthunk:
diff(parent, current, a[i], b[j], patches, kxi) diff(a[i], b[j], parent, current, patches)
else: else:
diff(current, current.childNodes[j], a[i], b[j], patches, kxi) diff(a[i], b[j], current, current.childNodes[j], patches)
case r case r
of identical, changed, similar: of identical, changed, similar:
a[i] = b[j] a[i] = b[j]
action action
of different: of different:
# undo what 'diff' would have done: # undo what 'diff' would have done:
setLen(patches, oldLen) shrink(patches, oldLen)
if result != different: result = r if result != different: result = r
break break
#of similar: #of similar:
@ -284,8 +284,8 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch
var pos = min(oldPos, newPos) + 1 var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1: for i in commonPrefix..pos-1:
if diff(current, current.childNodes[i], if diff(newNode[i], oldNode[i], current, current.childNodes[i],
newNode[i], oldNode[i], patches, kxi) != different: patches) != different:
newNode[i] = oldNode[i] newNode[i] = oldNode[i]
else: else:
result = different result = different
@ -318,7 +318,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch
detach(oldNode) detach(oldNode)
patches.addPatch(pkReplace, parent, current, x.expanded) patches.addPatch(pkReplace, parent, current, x.expanded)
else: else:
let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) let res = diff(x.expanded, oldExpanded, parent, current, patches)
if res != different: if res != different:
x.expanded = oldExpanded x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil" assert oldExpanded.dom != nil, "old expanded.dom is nil"
@ -339,8 +339,8 @@ proc dodraw(kxi: KaraxInstance) =
replaceById(kxi.rootId, asdom) replaceById(kxi.rootId, asdom)
else: else:
let olddom = document.getElementById(kxi.rootId) let olddom = document.getElementById(kxi.rootId)
var patches: seq[Patch] = @[] var patches = newJSeq[Patch]()
discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) discard diff(newtree, kxi.currentTree, nil, olddom, patches)
patches.apply(kxi) patches.apply(kxi)
kxi.currentTree = newtree kxi.currentTree = newtree
#doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -365,6 +365,8 @@ proc redraw*(kxi: KaraxInstance = kxi) =
else: else:
dodraw(kxi) dodraw(kxi)
proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
proc init(ev: Event) = proc init(ev: Event) =
reqFrame(proc () = kxi.dodraw) reqFrame(proc () = kxi.dodraw)

View file

@ -3,7 +3,6 @@
<html> <html>
<head> <head>
<title>Diff dom tests</title> <title>Diff dom tests</title>
<link rel="stylesheet" href="style.css">
</head> </head>
<body id="body"> <body id="body">

View file

@ -2,182 +2,107 @@
import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils
var var
entries: seq[cstring] entries: seq[cstring]
results: seq[cstring] results: seq[cstring]
timeout: Timeout
proc reset() = proc reset() =
results.add cstring"reset started" entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] redrawSync()
redraw()
results.add cstring"reset finished"
proc checkOrder(order : seq[int]): bool = proc checkOrder(order: seq[int]): bool =
var ul = getElementById("ul") var ul = getElementById("ul")
if ul == nil or len(ul.children) != len(order): if ul == nil or len(ul.children) != len(order):
return false kout ul, len(order)
var pos = 0 return false
for child in ul.children: var pos = 0
if child.id != $order[pos]: for child in ul.children:
return false if child.id != $order[pos]:
inc pos kout pos
return true return false
inc pos
return true
proc check1() = proc check(name: cstring; order: seq[int]) =
let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5]) let result = checkOrder(order)
if result: results.add name & (if result: cstring" - OK" else: cstring" -FAIL")
results.add cstring"test1 - OK"
else:
results.add cstring"test1 - FAIL"
# result: 0 1 2 3 4 7 5 # result: 0 1 2 3 4 7 5
proc test1() = proc test1() =
results.add cstring"test1 started" results.add cstring"test1 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
entries.insert(cstring("7"), 5) entries.insert(cstring("7"), 5)
redraw() redrawSync()
timeout = setTimeout(check1, 20) check("test1", @[0, 1, 2, 3, 4, 7, 5])
proc check2() =
let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5])
if result:
results.add cstring"test1 - OK"
else:
results.add cstring"test2 - FAIL"
# result: 8 0 1 2 3 4 7 5
proc test2() = proc test2() =
results.add cstring"test2 started" results.add cstring"test2 started"
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
entries.insert(cstring("7"), 5) entries.insert(cstring("7"), 5)
entries.insert(cstring("8"), 0) entries.insert(cstring("8"), 0)
redraw() redrawSync()
timeout = setTimeout(check2, 20) check("test2", @[8, 0, 1, 2, 3, 4, 7, 5])
proc check3() =
let result = checkOrder(@[2, 3, 4, 1])
if result:
results.add cstring"test3 - OK"
else:
results.add cstring"test3 - FAIL"
# result: 2 3 4 1
proc test3() = proc test3() =
results.add cstring"test3 started" results.add cstring"test3 started"
entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ] entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")]
redraw() redrawSync()
timeout = setTimeout(check3, 20) check("test3", @[2, 3, 4, 1])
proc check4() =
let result = checkOrder(@[5, 6, 7, 8])
if result:
results.add cstring"test4 - OK"
else:
results.add cstring"test4 - FAIL"
# result: 5 6 7 8
proc test4() = proc test4() =
results.add cstring"test4 started" results.add cstring"test4 started"
entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ]
redraw() redrawSync()
timeout = setTimeout(check4, 20) check("test4", @[5, 6, 7, 8])
proc check5() =
let result = checkOrder(@[0, 1, 3, 5, 4, 5])
if result:
results.add cstring"test5 - OK"
else:
results.add cstring"test5 - FAIL"
# result: 0 1 3 5 4 5
proc test5() = proc test5() =
results.add cstring"test5 started" results.add cstring"test5 started"
entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ] entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")]
redraw() redrawSync()
timeout = setTimeout(check5, 20) check("test 5", @[0, 1, 3, 5, 4, 5])
proc check6() =
let result = checkOrder(@[])
if result:
results.add cstring"test6 - OK"
else:
results.add cstring"test6 - FAIL"
# result: empty
proc test6() = proc test6() =
results.add cstring"test6 started" results.add cstring"test6 started"
entries = @[] entries = @[]
redraw() redrawSync()
timeout = setTimeout(check6, 20) check("test 6", @[])
proc check7() =
let result = checkOrder(@[2])
if result:
results.add cstring"test7 - OK"
else:
results.add cstring"test7 - FAIL"
redraw()
# result: 2 # result: 2
proc test7() = proc test7() =
results.add cstring"test7 started" results.add cstring"test7 started"
entries = @[cstring("2")] entries = @[cstring("2")]
redraw() redrawSync()
timeout = setTimeout(check7, 20) check("test 7", @[2])
proc createEntry(id: int): VNode = proc createEntry(id: int): VNode =
result = buildHtml(): result = buildHtml():
button(id="" & $id): button(id="" & $id):
text $id text $id
proc createDom(): VNode = proc createDom(): VNode =
result = buildHtml(tdiv()): result = buildHtml(tdiv()):
ul(id="ul"): ul(id="ul"):
for e in entries: for e in entries:
createEntry(parseInt(e)) createEntry(parseInt(e))
for r in results: for r in results:
tdiv: tdiv:
text r text r
proc onload() = proc onload() =
for i in 0..5: # 0_000: for i in 0..5: # 0_000:
entries.add(cstring($i)) entries.add(cstring($i))
test1()
reset()
test2()
reset()
test3()
reset()
test4()
reset()
test5()
reset()
test6()
reset()
test7()
var dtReset = 100
var dtTest = 500
var t = dtReset
timeout = setTimeout(test1, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test2, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test3, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test4, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test5, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test6, t)
t += dtTest
timeout = setTimeout(reset, t)
t += dtReset
timeout = setTimeout(test7, t)
onload()
setRenderer createDom setRenderer createDom
onload()

View file

@ -2,10 +2,19 @@
include "../src/karax" include "../src/karax"
import "../src/karaxdsl" import "../src/karaxdsl"
proc hasDom(n: Vnode) =
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
discard
else:
doAssert n.dom != nil
for i in 0..<n.len: hasDom(n[i])
proc doDiff(a, b: VNode) = proc doDiff(a, b: VNode) =
var patches: seq[Patch] = @[] var patches = newJSeq[Patch]()
echo diff(nil, vnodeToDom(a, kxi), b, a, patches, kxi) echo diff(b, a, nil, vnodeToDom(a, kxi), patches)
echo patches for i in 0..<patches.len:
echo patches[i]
#hasDom(kxi.currentTree)
proc testAppend() = proc testAppend() =
let a = buildHtml(tdiv): let a = buildHtml(tdiv):
@ -31,7 +40,19 @@ proc testInsert() =
li: text "C" li: text "C"
doDiff(a, b) doDiff(a, b)
proc testDelete() =
let a = buildHtml(tdiv):
ul:
li: text "A"
li: text "B"
li: text "C"
let b = buildHtml(tdiv):
ul:
discard
doDiff(a, b)
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard)
#testAppend() testAppend()
testInsert() testInsert()
testDelete()