diff --git a/cjs/index.js b/cjs/index.js index 0a71705..99856d2 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -43,7 +43,7 @@ module.exports = (parentNode, a, b, get, before) => { const node = bEnd < bLength ? (bStart ? (get(b[bStart - 1], -0).nextSibling) : - get(b[bEnd - bStart], 0)) : + get(b[bEnd], 0)) : before; while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node); @@ -67,23 +67,8 @@ module.exports = (parentNode, a, b, get, before) => { aEnd--; bEnd--; } - // single last swap: fast path - else if ((aEnd - aStart) === 1 && (bEnd - bStart) === 1) { - // we could be in a situation where the node was either unknown, - // be at the end of the future nodes list, or be in the middle - if (map && map.has(a[aStart])) { - // in the end or middle case, find out where to insert it - parentNode.insertBefore( - get(b[bStart], 1), - get(bEnd < bLength ? b[bEnd] : before, 0) - ); - } - // if the node is unknown, just replace it with the new one - else - parentNode.replaceChild(get(b[bStart], 1), get(a[aStart], -1)); - aStart++; - bStart++; - } + // The once here single last swap "fast path" has been removed in v1.1.0 + // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 // reverse swap: also fast path else if ( a[aStart] === b[bEnd - 1] && @@ -95,10 +80,10 @@ module.exports = (parentNode, a, b, get, before) => { // or asymmetric too // [1, 2, 3, 4, 5] // [1, 2, 3, 5, 6, 4] - const node = get(a[--aEnd], -1).nextSibling; + const node = get(a[--aEnd], -0).nextSibling; parentNode.insertBefore( get(b[bStart++], 1), - get(a[aStart++], -1).nextSibling + get(a[aStart++], -0).nextSibling ); parentNode.insertBefore(get(b[--bEnd], 1), node); // mark the future index as identical (yeah, it's dirty, but cheap ๐Ÿ‘) diff --git a/esm/index.js b/esm/index.js index 4d193a0..e500d56 100644 --- a/esm/index.js +++ b/esm/index.js @@ -42,7 +42,7 @@ export default (parentNode, a, b, get, before) => { const node = bEnd < bLength ? (bStart ? (get(b[bStart - 1], -0).nextSibling) : - get(b[bEnd - bStart], 0)) : + get(b[bEnd], 0)) : before; while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node); @@ -66,23 +66,8 @@ export default (parentNode, a, b, get, before) => { aEnd--; bEnd--; } - // single last swap: fast path - else if ((aEnd - aStart) === 1 && (bEnd - bStart) === 1) { - // we could be in a situation where the node was either unknown, - // be at the end of the future nodes list, or be in the middle - if (map && map.has(a[aStart])) { - // in the end or middle case, find out where to insert it - parentNode.insertBefore( - get(b[bStart], 1), - get(bEnd < bLength ? b[bEnd] : before, 0) - ); - } - // if the node is unknown, just replace it with the new one - else - parentNode.replaceChild(get(b[bStart], 1), get(a[aStart], -1)); - aStart++; - bStart++; - } + // The once here single last swap "fast path" has been removed in v1.1.0 + // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 // reverse swap: also fast path else if ( a[aStart] === b[bEnd - 1] && @@ -94,10 +79,10 @@ export default (parentNode, a, b, get, before) => { // or asymmetric too // [1, 2, 3, 4, 5] // [1, 2, 3, 5, 6, 4] - const node = get(a[--aEnd], -1).nextSibling; + const node = get(a[--aEnd], -0).nextSibling; parentNode.insertBefore( get(b[bStart++], 1), - get(a[aStart++], -1).nextSibling + get(a[aStart++], -0).nextSibling ); parentNode.insertBefore(get(b[--bEnd], 1), node); // mark the future index as identical (yeah, it's dirty, but cheap ๐Ÿ‘) diff --git a/index.js b/index.js index 7614887..5f8f7f5 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,6 @@ var udomdiff = (function (exports) { var aStart = 0; var bStart = 0; var map = null; - while (aStart < aEnd || bStart < bEnd) { // append head, tail, or nodes in between: fast path if (aEnd === aStart) { @@ -43,122 +42,107 @@ var udomdiff = (function (exports) { // need to be added are not at the end, and in such case // the node to `insertBefore`, if the index is more than 0 // must be retrieved, otherwise it's gonna be the first item. - var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd - bStart], 0) : before; - - while (bStart < bEnd) { - parentNode.insertBefore(get(b[bStart++], 1), node); - } - } // remove head or tail: fast path + var node = bEnd < bLength ? bStart ? get(b[bStart - 1], -0).nextSibling : get(b[bEnd], 0) : before; + while (bStart < bEnd) parentNode.insertBefore(get(b[bStart++], 1), node); + } + // remove head or tail: fast path else if (bEnd === bStart) { - while (aStart < aEnd) { - // remove the node only if it's unknown or not live - if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1)); - aStart++; + while (aStart < aEnd) { + // remove the node only if it's unknown or not live + if (!map || !map.has(a[aStart])) parentNode.removeChild(get(a[aStart], -1)); + aStart++; + } + } + // same node: fast path + else if (a[aStart] === b[bStart]) { + aStart++; + bStart++; + } + // same tail: fast path + else if (a[aEnd - 1] === b[bEnd - 1]) { + aEnd--; + bEnd--; + } + // The once here single last swap "fast path" has been removed in v1.1.0 + // https://github.com/WebReflection/udomdiff/blob/single-final-swap/esm/index.js#L69-L85 + // reverse swap: also fast path + else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { + // this is a "shrink" operation that could happen in these cases: + // [1, 2, 3, 4, 5] + // [1, 4, 3, 2, 5] + // or asymmetric too + // [1, 2, 3, 4, 5] + // [1, 2, 3, 5, 6, 4] + var _node = get(a[--aEnd], -0).nextSibling; + parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -0).nextSibling); + parentNode.insertBefore(get(b[--bEnd], 1), _node); + // mark the future index as identical (yeah, it's dirty, but cheap ๐Ÿ‘) + // The main reason to do this, is that when a[aEnd] will be reached, + // the loop will likely be on the fast path, as identical to b[bEnd]. + // In the best case scenario, the next loop will skip the tail, + // but in the worst one, this node will be considered as already + // processed, bailing out pretty quickly from the map index check + a[aEnd] = b[bEnd]; + } + // map based fallback, "slow" path + else { + // the map requires an O(bEnd - bStart) operation once + // to store all future nodes indexes for later purposes. + // In the worst case scenario, this is a full O(N) cost, + // and such scenario happens at least when all nodes are different, + // but also if both first and last items of the lists are different + if (!map) { + map = new Map(); + var i = bStart; + while (i < bEnd) map.set(b[i], i++); + } + // if it's a future node, hence it needs some handling + if (map.has(a[aStart])) { + // grab the index of such node, 'cause it might have been processed + var index = map.get(a[aStart]); + // if it's not already processed, look on demand for the next LCS + if (bStart < index && index < bEnd) { + var _i = aStart; + // counts the amount of nodes that are the same in the future + var sequence = 1; + while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) sequence++; + // effort decision here: if the sequence is longer than replaces + // needed to reach such sequence, which would brings again this loop + // to the fast path, prepend the difference before a sequence, + // and move only the future list index forward, so that aStart + // and bStart will be aligned again, hence on the fast path. + // An example considering aStart and bStart are both 0: + // a: [1, 2, 3, 4] + // b: [7, 1, 2, 3, 6] + // this would place 7 before 1 and, from that time on, 1, 2, and 3 + // will be processed at zero cost + if (sequence > index - bStart) { + var _node2 = get(a[aStart], 0); + while (bStart < index) parentNode.insertBefore(get(b[bStart++], 1), _node2); + } + // if the effort wasn't good enough, fallback to a replace, + // moving both source and target indexes forward, hoping that some + // similar node will be found later on, to go back to the fast path + else { + parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1)); + } } - } // same node: fast path - else if (a[aStart] === b[bStart]) { - aStart++; - bStart++; - } // same tail: fast path - else if (a[aEnd - 1] === b[bEnd - 1]) { - aEnd--; - bEnd--; - } // single last swap: fast path - else if (aEnd - aStart === 1 && bEnd - bStart === 1) { - // we could be in a situation where the node was either unknown, - // be at the end of the future nodes list, or be in the middle - if (map && map.has(a[aStart])) { - // in the end or middle case, find out where to insert it - parentNode.insertBefore(get(b[bStart], 1), get(bEnd < bLength ? b[bEnd] : before, 0)); - } // if the node is unknown, just replace it with the new one - else parentNode.replaceChild(get(b[bStart], 1), get(a[aStart], -1)); - - aStart++; - bStart++; - } // reverse swap: also fast path - else if (a[aStart] === b[bEnd - 1] && b[bStart] === a[aEnd - 1]) { - // this is a "shrink" operation that could happen in these cases: - // [1, 2, 3, 4, 5] - // [1, 4, 3, 2, 5] - // or asymmetric too - // [1, 2, 3, 4, 5] - // [1, 2, 3, 5, 6, 4] - var _node = get(a[--aEnd], -1).nextSibling; - parentNode.insertBefore(get(b[bStart++], 1), get(a[aStart++], -1).nextSibling); - parentNode.insertBefore(get(b[--bEnd], 1), _node); // mark the future index as identical (yeah, it's dirty, but cheap ๐Ÿ‘) - // The main reason to do this, is that when a[aEnd] will be reached, - // the loop will likely be on the fast path, as identical to b[bEnd]. - // In the best case scenario, the next loop will skip the tail, - // but in the worst one, this node will be considered as already - // processed, bailing out pretty quickly from the map index check - - a[aEnd] = b[bEnd]; - } // map based fallback, "slow" path - else { - // the map requires an O(bEnd - bStart) operation once - // to store all future nodes indexes for later purposes. - // In the worst case scenario, this is a full O(N) cost, - // and such scenario happens at least when all nodes are different, - // but also if both first and last items of the lists are different - if (!map) { - map = new Map(); - var i = bStart; - - while (i < bEnd) { - map.set(b[i], i++); - } - } // if it's a future node, hence it needs some handling - - - if (map.has(a[aStart])) { - // grab the index of such node, 'cause it might have been processed - var index = map.get(a[aStart]); // if it's not already processed, look on demand for the next LCS - - if (bStart < index && index < bEnd) { - var _i = aStart; // counts the amount of nodes that are the same in the future - - var sequence = 1; - - while (++_i < aEnd && _i < bEnd && map.get(a[_i]) === index + sequence) { - sequence++; - } // effort decision here: if the sequence is longer than replaces - // needed to reach such sequence, which would brings again this loop - // to the fast path, prepend the difference before a sequence, - // and move only the future list index forward, so that aStart - // and bStart will be aligned again, hence on the fast path. - // An example considering aStart and bStart are both 0: - // a: [1, 2, 3, 4] - // b: [7, 1, 2, 3, 6] - // this would place 7 before 1 and, from that time on, 1, 2, and 3 - // will be processed at zero cost - - - if (sequence > index - bStart) { - var _node2 = get(a[aStart], 0); - - while (bStart < index) { - parentNode.insertBefore(get(b[bStart++], 1), _node2); - } - } // if the effort wasn't good enough, fallback to a replace, - // moving both source and target indexes forward, hoping that some - // similar node will be found later on, to go back to the fast path - else { - parentNode.replaceChild(get(b[bStart++], 1), get(a[aStart++], -1)); - } - } // otherwise move the source forward, 'cause there's nothing to do - else aStart++; - } // this node has no meaning in the future list, so it's more than safe - // to remove it, and check the next live node out instead, meaning - // that only the live list index should be forwarded - else parentNode.removeChild(get(a[aStart++], -1)); - } + // otherwise move the source forward, 'cause there's nothing to do + else aStart++; + } + // this node has no meaning in the future list, so it's more than safe + // to remove it, and check the next live node out instead, meaning + // that only the live list index should be forwarded + else parentNode.removeChild(get(a[aStart++], -1)); + } } - return b; }); - exports.default = index; + exports["default"] = index; + + Object.defineProperty(exports, '__esModule', { value: true }); return exports; -}({}).default); +})({}).default; diff --git a/min.js b/min.js index 86adc0d..a3bbcc8 100644 --- a/min.js +++ b/min.js @@ -1 +1 @@ -var udomdiff=function(e){"use strict";return e.default=function(e,r,i,f,l){for(var n=i.length,s=r.length,t=n,o=0,a=0,v=null;o(e.default=function(e,r,i,f,l){for(var n=i.length,t=r.length,o=n,s=0,a=0,v=null;s{const f=t.length;let s=i.length,n=f,o=0,c=0,h=null;for(;or-c){const f=l(i[o],0);for(;c{const f=i.length;let n=t.length,s=f,o=0,c=0,u=null;for(;or-c){const f=l(t[o],0);for(;c { + const {parentNode} = node; + node.parentNode = null; + if (parentNode) { + const {childNodes} = parentNode; + const i = childNodes.indexOf(node); + if (-1 < i) + childNodes.splice(i, 1); + } +}; + +class Siblings { + get nextSibling() { + const {parentNode} = this; + if (parentNode) { + const {childNodes} = parentNode; + const i = childNodes.indexOf(this) + 1; + if (0 < i && i < childNodes.length) + return childNodes[i]; + } + return null; + } + get previousSibling() { + const {parentNode} = this; + if (parentNode) { + const {childNodes} = parentNode; + const i = childNodes.indexOf(this) - 1; + if (-1 < i) + return childNodes[i]; + } + return null; + } +} + +class Nody extends Siblings { + constructor(textContent) { + super(); + this.parentNode = null; + this.textContent = textContent; + } +} + +class Dommy extends Siblings { + constructor(tagName) { + super(); + this.parentNode = null; + this.childNodes = []; + this.tagName = tagName; + } + get firstChild() { + return this.childNodes[0]; + } + get lastChild() { + return this.childNodes[this.childNodes.length - 1]; + } + get textContent() { + return this.childNodes.map(node => node.textContent).join(''); + } + set textContent(value) { + this.childNodes.splice(0).forEach(remove); + if (value) + this.appendChild(document.createTextNode(value)); + } + appendChild(newNode) { + if (!newNode) + throw new Error('invalid appendChild'); + remove(newNode); + this.childNodes.push(newNode); + newNode.parentNode = this; + return newNode; + } + insertBefore(newNode, oldNode) { + if (newNode !== oldNode) { + remove(newNode); + const {childNodes} = this; + if (oldNode) { + const i = childNodes.indexOf(oldNode); + if (i < 0) + throw new Error('invalid insertBefore'); + childNodes.splice(i, 0, newNode); + } + else + childNodes.push(newNode); + newNode.parentNode = this; + } + return newNode; + } + removeChild(oldNode) { + const {childNodes} = this; + const i = childNodes.indexOf(oldNode); + if (i < 0) + throw new Error('invalid removeChild'); + childNodes.splice(i, 1); + oldNode.parentNode = null; + return oldNode; + } + replaceChild(newNode, oldNode) { + remove(newNode); + const {childNodes} = this; + const i = childNodes.indexOf(oldNode); + if (i < 0) + throw new Error('invalid replaceChild'); + childNodes[i] = newNode; + oldNode.parentNode = null; + newNode.parentNode = this; + return newNode; + } +} + +module.exports = { + createElement: tagName => new Dommy(tagName), + createTextNode: textContent => new Nody(textContent) +}; diff --git a/test/js-diff-benchmark.js b/test/js-diff-benchmark.js new file mode 100644 index 0000000..7046358 --- /dev/null +++ b/test/js-diff-benchmark.js @@ -0,0 +1,316 @@ +// source: https://github.com/luwes/js-diff-benchmark +const fs = require('fs'); +const c = require('ansi-colors'); +var Terser = require('terser'); +const gzipSize = require('gzip-size'); +const Table = require('cli-table'); +const microtime = require('microtime'); +const document = require('./dommy.js'); +const get = o => o; + +const libs = [ + 'udomdiff w/out before', + 'udomdiff with before' +]; + +const cols = [ + '', + '1k', + 'Repl', + 'Shufle', + 'Invers', + 'Clear', + 'Append', + 'Prepend', + 'Swap2', + 'Up10th', + '10k', + 'Swap2', + 'Total', + 'Size', +]; + +const table = new Table({ + head: cols, + colAligns: cols.map(() => 'middle'), + style: { + head: ['green'], + }, +}); + +let shuffleSeed; + +// in case we'd like to test "pinnability" of the differ +let before;// = document.createTextNode(''); + +const parent = document.createElement('div'); + +const { + clear, reset, verifyNodes, + random, reverse, + create1000, create10000, + append1000, prepend1000, + swapRows, updateEach10thRow +} = require('./utils.js')(document, parent, () => before); + +libs.forEach((lib, i) => { + if (i) + before = document.createTextNode(''); + + const libResults = []; + table.push({ [lib]: libResults }); + + const file = `../cjs/index.js`; + const diff = require(file); + + var code = fs.readFileSync(require.resolve(file), 'utf8'); + var gzip = gzipSize.sync(Terser.minify(code).code); + + // clean up the parent + // clean up the parent + parent.textContent = ''; + if (before) + parent.appendChild(before); + + //* warm up + checking everything works upfront + let childNodes = create1000(diff, []); + console.assert( + verifyNodes(childNodes, 1000), + '%s warmup create', + lib + ); + + childNodes = create1000(diff, childNodes); + console.assert( + verifyNodes(childNodes, 1000), + '%s warmup replace', + lib + ); + + if (!shuffleSeed) { + // create a fixed shuffled seed so each library does the same. + const shuffle = childNodes.slice().sort( + () => Math.random() - Math.random() + ); + shuffleSeed = shuffle.map((node) => childNodes.indexOf(node)); + } + + childNodes = append1000(diff, childNodes); + console.assert( + verifyNodes(childNodes, 2000), + '%s warmup append', + lib + ); + childNodes = prepend1000(diff, childNodes); + console.assert( + verifyNodes(childNodes, 3000), + '%s warmup prepend', + lib + ); + childNodes = clear(diff, childNodes); + console.assert( + verifyNodes(childNodes, 0), + '%s warmup clear', + lib + ); + childNodes = create10000(diff, childNodes); + console.assert( + verifyNodes(childNodes, 10000), + '%s warmup 10k', + lib + ); + childNodes = clear(diff, childNodes); + console.assert( + verifyNodes(childNodes, 0), + '%s warmup clear 10k', + lib + ); + childNodes = create1000(diff, childNodes); + childNodes = swapRows(diff, childNodes); + console.assert(childNodes[1].textContent == 998, '%s warmup swap', lib); + console.assert(childNodes[998].textContent == 1, '%s warmup swap', lib); + childNodes = clear(diff, childNodes); + childNodes = create1000(diff, childNodes); + childNodes = updateEach10thRow(diff, childNodes); + console.assert( + /!$/.test(childNodes[0].textContent), + '%s warmup update', + lib + ); + console.assert( + !/!$/.test(childNodes[1].textContent), + '%s warmup update', + lib + ); + console.assert( + /!$/.test(childNodes[10].textContent), + '%s warmup update', + lib + ); + childNodes = clear(diff, childNodes); + console.assert( + verifyNodes(childNodes, 0), + '%s warmup clear', + lib + ); + //*/ + + // console.time(lib.toUpperCase()); + + const totalStart = microtime.now(); + + let begin; + const start = () => { + reset(); + begin = microtime.now(); + }; + const stop = (count, operationMax) => { + const end = microtime.now() - begin; + const delta = count - operationMax; + libResults.push(`${(end / 1000).toPrecision(2)}ms + ${c.gray(count)}${ + count > operationMax + ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`) + : '' + }`.replace(/^\s+/m, '')); + }; + + // actual benchmark + + start(); + childNodes = create1000(diff, childNodes); + stop(parent.count(), 1000); + console.assert( + verifyNodes(childNodes, 1000), + '%s 1k', + lib + ); + + start(); + childNodes = create1000(diff, childNodes); + stop(parent.count(), 2000); + console.assert( + verifyNodes(childNodes, 1000), + '%s replace', + lib + ); + + start(); + childNodes = random(shuffleSeed, diff, childNodes); + stop(parent.count(), 2000); + console.assert( + verifyNodes(childNodes, 1000), + '%s random', + lib + ); + + start(); + childNodes = reverse(diff, childNodes); + stop(parent.count(), 2000); + console.assert( + verifyNodes(childNodes, 1000), + '%s reverse', + lib + ); + + start(); + childNodes = clear(diff, childNodes); + stop(parent.count(), 1000); + console.assert( + verifyNodes(childNodes, 0), + '%s clear', + lib + ); + + childNodes = create1000(diff, childNodes); + + start(); + childNodes = append1000(diff, childNodes); + stop(parent.count(), 2000); + console.assert( + verifyNodes(childNodes, 2000), + '%s append 1k', + lib + ); + + start(); + childNodes = prepend1000(diff, childNodes); + stop(parent.count(), 1000); + console.assert( + verifyNodes(childNodes, 3000), + '%s prepend 1k', + lib + ); + + childNodes = clear(diff, childNodes); + childNodes = create1000(diff, childNodes); + + start(); + childNodes = swapRows(diff, childNodes); + stop(parent.count(), 4); + console.assert( + parent.childNodes[1].textContent == 998 && + parent.childNodes[998].textContent == 1 && + verifyNodes(childNodes, 1000), + '%s swap2 1k', + lib + ); + + start(); + childNodes = updateEach10thRow(diff, childNodes); + stop(parent.count(), 200); + console.assert( + verifyNodes(childNodes, 1000), + '%s update 10th', + lib + ); + + childNodes = clear(diff, childNodes); + + start(); + childNodes = create10000(diff, childNodes); + stop(parent.count(), 10000); + console.assert( + verifyNodes(childNodes, 10000), + '%s 10k', + lib + ); + + start(); + childNodes = swapRows(diff, childNodes); + stop(parent.count(), 4); + console.assert( + parent.childNodes[1].textContent == 9998 && + parent.childNodes[9998].textContent == 1 && + verifyNodes(childNodes, 10000), + '%s swap2 10k', + lib + ); + + childNodes = clear(diff, childNodes); + reset(); + + //*/ + + libResults.push(`${((microtime.now() - totalStart) / 1000).toPrecision(3)}ms`); + libResults.push(`${gzip}B`); + + // const used = process.memoryUsage().heapUsed / 1024 / 1024; + // console.log(`The script uses approximately ${Math.round(used * 100) / 100} MB`); + + try { + if (global.gc) { + global.gc(); + } + } catch (e) { + process.exit(); + } +}); + +table.sort((a, b) => { + a = Object.values(a)[0]; + b = Object.values(b)[0]; + return parseInt(a[a.length - 2]) - parseInt(b[b.length - 2]); +}); + +console.log(table.toString()); diff --git a/test/js-fb.js b/test/js-fb.js deleted file mode 100644 index 76f1f9f..0000000 --- a/test/js-fb.js +++ /dev/null @@ -1,229 +0,0 @@ -const udomdiff = require('../cjs'); - -const {Dommy, Nody, get} = require('./utils.js'); - -let parent = new Dommy(); - -const append1000 = parent => { - const start = parent.childNodes.length - 1; - const childNodes = parent.childNodes.slice(); - for (let i = 0; i < 1000; i++) - childNodes.push(new Nody(parent, start + i)); - return udomdiff( - parent, - parent.childNodes, - childNodes, - get, - parent.lastElementChild - ); -}; - -const clear = parent => { - return udomdiff( - parent, - parent.childNodes, - [], - get, - parent.lastElementChild - ); -}; - -const create1000 = parent => { - const start = parent.childNodes.length; - const childNodes = []; - for (let i = 0; i < 1000; i++) - childNodes.push(new Nody(parent, start + i)); - return udomdiff( - parent, - parent.childNodes, - childNodes, - get, - parent.lastElementChild - ); -}; - -const create10000 = parent => { - const childNodes = []; - for (let i = 0; i < 10000; i++) - childNodes.push(new Nody(parent, i)); - return udomdiff( - parent, - parent.childNodes, - childNodes, - get, - parent.lastElementChild - ); -}; - -const reverseRows = parent => { - return udomdiff( - parent, - parent.childNodes, - parent.childNodes.slice().reverse(), - get, - parent.lastElementChild - ); -}; - -const shuffleRows = parent => { - return udomdiff( - parent, - parent.childNodes, - parent.childNodes.slice().sort(() => Math.random() - Math.random()), - get, - parent.lastElementChild - ); -}; - -const swapRows = parent => { - const childNodes = parent.childNodes.slice(); - const $1 = childNodes[1]; - childNodes[1] = childNodes[998]; - childNodes[998] = $1; - return udomdiff( - parent, - parent.childNodes, - childNodes, - get, - parent.lastElementChild - ); -}; - -const updateEach10thRow = parent => { - const childNodes = parent.childNodes.slice(); - for (let i = 0; i < childNodes.length; i += 10) - childNodes[i].value += '!'; - return udomdiff( - parent, - parent.childNodes, - childNodes, - get, - parent.lastElementChild - ); -}; - -//* warm up + checking everything works upfront -create1000(parent); -console.assert(parent.childNodes.length === 1000); -append1000(parent); -console.assert(parent.childNodes.length === 2000); -clear(parent); -console.assert(parent.childNodes.length === 0); -create10000(parent); -console.assert(parent.childNodes.length === 10000); -clear(parent); -console.assert(parent.childNodes.length === 0); -create1000(parent); -swapRows(parent); -console.assert(parent.childNodes[1].value == 998); -console.assert(parent.childNodes[998].value == 1); -clear(parent); -create1000(parent); -updateEach10thRow(parent); -console.assert(/!$/.test(parent.childNodes[0].value)); -console.assert(!/!$/.test(parent.childNodes[1].value)); -console.assert(/!$/.test(parent.childNodes[10].value)); -clear(parent); -console.assert(parent.childNodes.length === 0); -//*/ - -console.time('js-frameworks-benchmark'); - -// actual benchmark -parent.reset(); -console.time('create 1000'); -var rows = create1000(parent); -console.timeEnd('create 1000'); -console.assert(parent.childNodes.every((row, i) => row === rows[i])); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -console.time('clear'); -var rows = clear(parent); -console.timeEnd('clear'); -console.assert(parent.childNodes.every((row, i) => row === rows[i]) && rows.length === 0); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -create1000(parent); -parent.reset(); -console.time('replace 1000'); -var rows = create1000(parent); -console.timeEnd('replace 1000'); -console.assert(parent.childNodes.every((row, i) => row === rows[i])); -console.log('operations', parent.count(), '\n'); -clear(parent); -parent.reset(); - -create1000(parent); -parent.reset(); -console.time('append 1000'); -var rows = append1000(parent); -console.timeEnd('append 1000'); -console.assert(parent.childNodes.every((row, i) => row === rows[i]) && rows.length === 2000); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -console.time('append more'); -var rows = append1000(parent); -console.timeEnd('append more'); -console.assert(parent.childNodes.every((row, i) => row === rows[i]) && rows.length === 3000); -console.log('operations', parent.count(), '\n'); -parent.reset(); -clear(parent); - -create1000(parent); -parent.reset(); -console.time('swap rows'); -swapRows(parent); -console.timeEnd('swap rows'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -create1000(parent); -parent.reset(); -console.time('update every 10th row'); -updateEach10thRow(parent); -console.timeEnd('update every 10th row'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -create1000(parent); -parent.reset(); -console.time('shuffle rows'); -shuffleRows(parent); -console.timeEnd('shuffle rows'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -create1000(parent); -parent.reset(); -console.time('reverse rows'); -reverseRows(parent); -console.timeEnd('reverse rows'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -clear(parent); -parent.reset(); -console.time('create 10000 rows'); -create10000(parent); -console.timeEnd('create 10000 rows'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -console.time('swap over 10000 rows'); -swapRows(parent); -console.timeEnd('swap over 10000 rows'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -console.time('clear 10000'); -clear(parent); -console.timeEnd('clear 10000'); -console.log('operations', parent.count(), '\n'); -parent.reset(); - -//*/ - -console.timeEnd('js-frameworks-benchmark'); diff --git a/test/utils.js b/test/utils.js index 62c91ca..f905f57 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,65 +1,119 @@ -class Dommy { - constructor() { - this.reset(); - this.lastElementChild = new Nody(this, ''); - this._childNodes = [this.lastElementChild]; - } - get childNodes() { - return this._childNodes.slice(0, -1); - } - get textContent() { - return this.childNodes.map(node => node.value).join(''); - } - insertBefore(newNode, liveNode) { - if (!liveNode) liveNode = this.lastElementChild; - this.operations.push(`insertBefore(${newNode.value}, ${liveNode.value})`); - if (newNode === liveNode) return; - this._removeChild(newNode); - const index = this._childNodes.indexOf(liveNode); - if (index < 0) - throw new Error('invalid insertBefore'); - this._childNodes.splice(index, 0, newNode); - } - replaceChild(newNode, oldNode) { - this.operations.push(`replaceChild(${newNode.value}, ${oldNode.value})`); - this._removeChild(newNode); - const index = this.childNodes.indexOf(oldNode); - if (index < 0) - throw new Error('invalid replaceChild'); - this._childNodes.splice(index, 1, newNode); - } - removeChild(node) { - this.operations.push(`removeChild(${node.value})`); - const index = this.childNodes.indexOf(node); - if (index < 0) - throw new Error('invalid removeChild'); - this._childNodes.splice(index, 1); - } - count() { - return this.operations.length; - } - reset() { - this.operations = []; - } - _removeChild(node) { - // use childNodes instead of _childNodes - // to preserve lastElementChild - const index = this.childNodes.indexOf(node); - if (-1 < index) - this._childNodes.splice(index, 1); - } -} - -class Nody { - constructor(dommy, value) { - this.dommy = dommy; - this.value = value; - } - get nextSibling() { - const {childNodes, lastElementChild} = this.dommy; - const index = childNodes.indexOf(this) + 1; - return index < childNodes.length ? childNodes[index] : lastElementChild; - } -} - -module.exports = {Dommy, Nody, get: o => o}; +const get = o => o; +module.exports = (document, container, before) => { + const mutations = []; + const { + appendChild, + insertBefore, + removeChild, + replaceChild + } = container; + container.count = () => mutations.length; + container.appendChild = function (newNode) { + const {textContent} = newNode; + if (newNode.parentNode) + mutations.push(`append: drop(${textContent})`); + mutations.push(`append: add(${textContent})`); + return appendChild.call(this, newNode); + }; + container.insertBefore = function (newNode, oldNode) { + const {textContent} = newNode; + if (newNode.parentNode) + mutations.push(`insert: drop(${textContent})`); + mutations.push( + oldNode ? + `insert: put(${textContent}) before (${oldNode.textContent})` : + `insert: add(${textContent})` + ); + return insertBefore.call(this, newNode, oldNode); + }; + container.removeChild = function (oldNode) { + mutations.push(`remove: drop(${oldNode.textContent})`); + return removeChild.call(this, oldNode); + }; + container.replaceChild = function (newNode, oldNode) { + const {textContent} = newNode; + mutations.push(`replace: drop(${oldNode.textContent})`); + if (newNode.parentNode) + mutations.push(`replace: drop(${textContent})`); + mutations.push(`replace: put(${textContent})`); + return replaceChild.call(this, newNode, oldNode); + }; + const createNode = text => { + const node = document.createElement('p'); + node.appendChild(document.createTextNode(text)); + return node; + }; + return { + // Benchnmark Utilities + reset() { + mutations.splice(0); + }, + verifyNodes(childNodes, expected) { + return childNodes.length === expected && + childNodes.every((row, i) => row === container.childNodes[i]) && + container.childNodes.length === expected + (before() ? 1 : 0) && + (!before || container.childNodes[expected] === before()); + }, + // Benchnmark Functions + random(shuffleSeed, diff, oldNodes) { + return diff( + container, + oldNodes, + shuffleSeed.map((newIdx) => oldNodes[newIdx]), + get, + before() + ); + }, + reverse(diff, oldNodes) { + return diff(container, oldNodes, oldNodes.slice().reverse(), get, before()); + }, + append1000(diff, oldNodes) { + const start = oldNodes.length; + const childNodes = oldNodes.slice(); + for (let i = 0; i < 1000; i++) + childNodes.push(createNode(start + i)); + return diff(container, oldNodes, childNodes, get, before()); + }, + clear(diff, oldNodes) { + return diff(container, oldNodes, [], get, before()); + }, + create1000(diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 1000; i++) + childNodes.push(createNode(i)); + return diff(container, oldNodes, childNodes, get, before()); + }, + create10000(diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 10000; i++) + childNodes.push(createNode(i)); + return diff(container, oldNodes, childNodes, get, before()); + }, + prepend1000(diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 1000; i++) + childNodes.push(createNode(-i)); + return diff( + container, + oldNodes, + childNodes.reverse().concat(oldNodes), + get, + before() + ); + }, + swapRows(diff, oldNodes) { + const childNodes = oldNodes.slice(); + const $1 = childNodes[1]; + const index = childNodes.length - 2; + childNodes[1] = childNodes[index]; + childNodes[index] = $1; + return diff(container, oldNodes, childNodes, get, before()); + }, + updateEach10thRow(diff, oldNodes) { + const childNodes = oldNodes.slice(); + for (let i = 0; i < childNodes.length; i += 10) + childNodes[i] = createNode(i + '!'); + return diff(container, oldNodes, childNodes, get, before()); + } + }; +};