From d22ca8adbaa13bdc5815e4503eb19806a38928c8 Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Wed, 15 Apr 2020 09:17:24 +0200 Subject: [PATCH 01/10] Removing single final/last row swap As udomdiff manages regardless the final swap through the "slow path", which is irrelevant for a single swap operation, while the code size related to this final step is quite heavy, I've decided to prefer a reduced production size over optimizing this case which occurrs when: * there is only one operation to do, irrelevant real-world optimization * it's at the end of more complex operations, still irrelevant Performance wise, it doesn't seem to be a crytical change, but it's easy to also eventually rollback if nevessary, as the removed block has been flagged within the current code. --- cjs/index.js | 19 +----- esm/index.js | 19 +----- index.js | 168 ++++++++++++++++++++++++--------------------------- min.js | 2 +- new.js | 2 +- 5 files changed, 85 insertions(+), 125 deletions(-) diff --git a/cjs/index.js b/cjs/index.js index 0a71705..4566c30 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -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] && diff --git a/esm/index.js b/esm/index.js index 4d193a0..fdc7508 100644 --- a/esm/index.js +++ b/esm/index.js @@ -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] && diff --git a/index.js b/index.js index 7614887..ae0827e 100644 --- a/index.js +++ b/index.js @@ -63,95 +63,85 @@ var udomdiff = (function (exports) { 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)); - } + } // 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], -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)); + } } return b; diff --git a/min.js b/min.js index 86adc0d..edec11e 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{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 n=i.length;let r=t.length,s=n,o=0,c=0,g=null;for(;ol-c){const n=f(t[o],0);for(;c Date: Wed, 15 Apr 2020 09:23:57 +0200 Subject: [PATCH 02/10] 1.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index badcd06..bcc91c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "udomdiff", - "version": "1.0.11", + "version": "1.1.0", "description": "An essential diffing algorithm for ยตhtml", "main": "cjs/index.js", "scripts": { From 187f71ff984e1e7c6b9d6b7747f11236a1b262d1 Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Fri, 17 Apr 2020 17:03:39 +0200 Subject: [PATCH 03/10] better benchmark --- package.json | 6 + test/dommy.js | 131 +++++++++++++ test/js-diff-benchmark.js | 400 ++++++++++++++++++++++++++++++++++++++ test/js-fb.js | 229 ---------------------- test/utils.js | 65 ------- 5 files changed, 537 insertions(+), 294 deletions(-) create mode 100644 test/dommy.js create mode 100644 test/js-diff-benchmark.js delete mode 100644 test/js-fb.js delete mode 100644 test/utils.js diff --git a/package.json b/package.json index bcc91c3..d988114 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "fix:default": "sed -i 's/({})/({}).default/' index.js && sed -i 's/({})/({}).default/' new.js && sed -i 's/({})/({}).default/' min.js", "coveralls": "nyc report --reporter=text-lcov | coveralls", "test": "nyc node test/index.js", + "bench": "node --expose-gc test/js-diff-benchmark.js", "size": "cat min.js | brotli | wc -c && cat new.js | brotli | wc -c" }, "keywords": [ @@ -25,13 +26,18 @@ "devDependencies": { "@babel/core": "^7.9.0", "@babel/preset-env": "^7.9.0", + "ansi-colors": "^4.1.1", "ascjs": "^3.1.2", + "cli-table": "^0.3.1", "coveralls": "^3.0.11", + "gzip-size": "^5.1.1", + "microtime": "^3.0.0", "nyc": "^15.0.1", "rollup": "^2.3.2", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-terser": "^5.3.0", + "terser": "^4.6.11", "uglify-js": "^3.8.1" }, "module": "esm/index.js", diff --git a/test/dommy.js b/test/dommy.js new file mode 100644 index 0000000..112520a --- /dev/null +++ b/test/dommy.js @@ -0,0 +1,131 @@ +/** + * ISC License + * + * Copyright (c) 2020, Andrea Giammarchi, @WebReflection + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +const remove = node => { + 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..294552d --- /dev/null +++ b/test/js-diff-benchmark.js @@ -0,0 +1,400 @@ +// 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' +]; + +const cols = [ + '', + '1k', + '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(''); +let parent = document.createElement('div'); +instrument(parent); + +libs.forEach((lib) => { + + const libResults = []; + table.push({ [lib.slice(0, 8)]: 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 + parent.textContent = ''; + if (before) + parent.appendChild(before); + + //* warm up + checking everything works upfront + let childNodes = create1000(parent, diff, []); + console.assert(verifyNodes(parent, childNodes, 1000), '%s warmup create', 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(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 2000), + '%s warmup append', + lib + ); + childNodes = prepend1000(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 3000), + '%s warmup prepend', + lib + ); + childNodes = clear(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 0), + '%s warmup clear', + lib + ); + childNodes = create10000(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 10000), + '%s warmup 10k', + lib + ); + childNodes = clear(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 0), + '%s warmup clear 10k', + lib + ); + childNodes = create1000(parent, diff, childNodes); + childNodes = swapRows(parent, diff, childNodes); + console.assert(childNodes[1].textContent == 998, '%s warmup swap', lib); + console.assert(childNodes[998].textContent == 1, '%s warmup swap', lib); + childNodes = clear(parent, diff, childNodes); + childNodes = create1000(parent, diff, childNodes); + childNodes = updateEach10thRow(parent, 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(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 0), + '%s warmup clear', + lib + ); + //*/ + + // console.time(lib.toUpperCase()); + + const totalStart = microtime.now(); + + let begin; + const start = () => (begin = microtime.now()); + const stop = (count, operationMax) => { + const delta = count - operationMax; + libResults.push(`${round((microtime.now() - begin) / 1000)}ms +${c.gray(count)}${ + count > operationMax + ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`) + : '' + }`); + }; + + // actual benchmark + reset(parent); + start(); + childNodes = create1000(parent, diff, childNodes); + stop(parent.operations.length, 1000); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s 1k', + lib + ); + reset(parent); + + start(); + childNodes = random(parent, diff, childNodes); + stop(parent.operations.length, 1000); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s random', + lib + ); + reset(parent); + + start(); + childNodes = reverse(parent, diff, childNodes); + stop(parent.operations.length, 1000); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s reverse', + lib + ); + reset(parent); + + start(); + childNodes = clear(parent, diff, childNodes); + stop(parent.operations.length, 1000); + console.assert( + verifyNodes(parent, childNodes, 0), + '%s clear', + lib + ); + reset(parent); + + childNodes = create1000(parent, diff, childNodes); + reset(parent); + childNodes = create1000(parent, diff, childNodes); + console.assert(verifyNodes(parent, childNodes, 1000)); + childNodes = clear(parent, diff, childNodes); + reset(parent); + + childNodes = create1000(parent, diff, childNodes); + reset(parent); + start(); + childNodes = append1000(parent, diff, childNodes); + stop(parent.operations.length, 2000); + console.assert( + verifyNodes(parent, childNodes, 2000), + '%s append 1k', + lib + ); + reset(parent); + + start(); + childNodes = prepend1000(parent, diff, childNodes); + stop(parent.operations.length, 1000); + console.assert( + verifyNodes(parent, childNodes, 3000), + '%s prepend 1k', + lib + ); + reset(parent); + childNodes = clear(parent, diff, childNodes); + + childNodes = create1000(parent, diff, childNodes); + reset(parent); + start(); + childNodes = swapRows(parent, diff, childNodes); + console.assert(verifyNodes(parent, childNodes, 1000)); + stop(parent.operations.length, 2); + reset(parent); + + childNodes = create1000(parent, diff, childNodes); + reset(parent); + start(); + childNodes = updateEach10thRow(parent, diff, childNodes); + stop(parent.operations.length, 200); + reset(parent); + + childNodes = clear(parent, diff, childNodes); + reset(parent); + start(); + childNodes = create10000(parent, diff, childNodes); + stop(parent.operations.length, 10000); + reset(parent); + + start(); + childNodes = swapRows(parent, diff, childNodes); + stop(parent.operations.length, 2); + reset(parent); + + childNodes = clear(parent, diff, childNodes); + reset(parent); + + //*/ + + libResults.push(`${round((microtime.now() - totalStart) / 1000)}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()); + + +// Benchnmark Utilities + +function instrument(parent) { + const { + appendChild, + insertBefore, + removeChild, + replaceChild + } = parent; + parent.operations = []; + parent.appendChild = function (newNode) { + this.operations.push(`appendChild(${newNode.textContent})`); + return appendChild.call(this, newNode); + }; + parent.insertBefore = function (newNode, oldNode) { + this.operations.push( + oldNode ? + `insertBefore(${newNode.textContent}, ${oldNode.textContent})` : + `insertBefore(${newNode.textContent})` + ); + return insertBefore.call(this, newNode, oldNode); + }; + parent.removeChild = function (oldNode) { + this.operations.push(`removeChild(${oldNode.textContent})`); + return removeChild.call(this, oldNode); + }; + parent.replaceChild = function (newNode, oldNode) { + this.operations.push( + `delete#replaceChild(${newNode.textContent}, ${oldNode.textContent})` + ); + this.operations.push( + `insert#replaceChild(${newNode.textContent}, ${oldNode.textContent})` + ); + return replaceChild.call(this, newNode, oldNode); + }; +} + +function reset(parent) { + parent.operations.splice(0); +} + +function round(num) { + return Math.round((num + Number.EPSILON) * 10) / 10; +} + +function verifyNodes(parent, childNodes, expected) { + return childNodes.length === expected && + childNodes.every((row, i) => row === parent.childNodes[i]) && + parent.childNodes.length === expected + (before ? 1 : 0) && + (!before || parent.childNodes[expected] === before); +} + + +// Benchnmark Functions + +function random(parent, diff, oldNodes) { + return diff( + parent, + oldNodes, + shuffleSeed.map((newIdx) => oldNodes[newIdx]), + get, + before + ); +} + +function reverse(parent, diff, oldNodes) { + return diff(parent, oldNodes, oldNodes.slice().reverse(), get, before); +} + +function append1000(parent, diff, oldNodes) { + const start = oldNodes.length; + const childNodes = oldNodes.slice(); + for (let i = 0; i < 1000; i++) + childNodes.push(document.createTextNode(parent, start + i)); + return diff(parent, oldNodes, childNodes, get, before); +} + +function clear(parent, diff, oldNodes) { + return diff(parent, oldNodes, [], get, before); +} + +function create1000(parent, diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 1000; i++) + childNodes.push(document.createTextNode(i)); + return diff(parent, oldNodes, childNodes, get, before); +} + +function create10000(parent, diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 10000; i++) + childNodes.push(document.createTextNode(i)); + return diff(parent, oldNodes, childNodes, get, before); +} + +function prepend1000(parent, diff, oldNodes) { + const childNodes = []; + for (let i = 0; i < 1000; i++) + childNodes.push(document.createTextNode(parent, -i)); + return diff( + parent, + oldNodes, + childNodes.reverse().concat(oldNodes), + get, + before + ); +} + +function swapRows(parent, diff, oldNodes) { + const childNodes = oldNodes.slice(); + const $1 = childNodes[1]; + const index = childNodes.length - 2; + childNodes[1] = childNodes[index]; + childNodes[index] = $1; + return diff(parent, oldNodes, childNodes, get, before); +} + +function updateEach10thRow(parent, diff, oldNodes) { + const childNodes = oldNodes.slice(); + for (let i = 0; i < childNodes.length; i += 10) + childNodes[i] = document.createTextNode(i + '!'); + return diff(parent, oldNodes, childNodes, get, before); +} 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 deleted file mode 100644 index 62c91ca..0000000 --- a/test/utils.js +++ /dev/null @@ -1,65 +0,0 @@ -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}; From 593e123536bd50ae576fcf1c0b987dfc0ef56390 Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Sat, 18 Apr 2020 12:17:14 +0200 Subject: [PATCH 04/10] improved benchmark with and without before --- package.json | 2 +- test/js-diff-benchmark.js | 101 +++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index d988114..775f667 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "fix:default": "sed -i 's/({})/({}).default/' index.js && sed -i 's/({})/({}).default/' new.js && sed -i 's/({})/({}).default/' min.js", "coveralls": "nyc report --reporter=text-lcov | coveralls", "test": "nyc node test/index.js", - "bench": "node --expose-gc test/js-diff-benchmark.js", + "benchmark": "node --expose-gc test/js-diff-benchmark.js", "size": "cat min.js | brotli | wc -c && cat new.js | brotli | wc -c" }, "keywords": [ diff --git a/test/js-diff-benchmark.js b/test/js-diff-benchmark.js index 294552d..9e7f62d 100644 --- a/test/js-diff-benchmark.js +++ b/test/js-diff-benchmark.js @@ -9,12 +9,14 @@ const document = require('./dommy.js'); const get = o => o; const libs = [ - 'udomdiff' + 'udomdiff w/out before', + 'udomdiff with before' ]; const cols = [ '', '1k', + 'Repl', 'Shufle', 'Invers', 'Clear', @@ -39,14 +41,17 @@ const table = new Table({ let shuffleSeed; // in case we'd like to test "pinnability" of the differ -let before = document.createTextNode(''); -let parent = document.createElement('div'); +let before; + +const parent = document.createElement('div'); instrument(parent); -libs.forEach((lib) => { +libs.forEach((lib, i) => { + if (i) + before = document.createTextNode(''); const libResults = []; - table.push({ [lib.slice(0, 8)]: libResults }); + table.push({ [lib]: libResults }); const file = `../cjs/index.js`; const diff = require(file); @@ -61,7 +66,18 @@ libs.forEach((lib) => { //* warm up + checking everything works upfront let childNodes = create1000(parent, diff, []); - console.assert(verifyNodes(parent, childNodes, 1000), '%s warmup create', lib); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s warmup create', + lib + ); + + childNodes = create1000(parent, diff, childNodes); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s warmup replace', + lib + ); if (!shuffleSeed) { // create a fixed shuffled seed so each library does the same. @@ -136,19 +152,23 @@ libs.forEach((lib) => { const totalStart = microtime.now(); let begin; - const start = () => (begin = microtime.now()); + const start = () => { + reset(parent); + begin = microtime.now(); + }; const stop = (count, operationMax) => { + const end = microtime.now() - begin; const delta = count - operationMax; - libResults.push(`${round((microtime.now() - begin) / 1000)}ms -${c.gray(count)}${ + libResults.push(`${round(end / 1000)}ms + ${c.gray(count)}${ count > operationMax ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`) : '' - }`); + }`.replace(/^\s+/m, '')); }; // actual benchmark - reset(parent); + start(); childNodes = create1000(parent, diff, childNodes); stop(parent.operations.length, 1000); @@ -157,7 +177,15 @@ ${c.gray(count)}${ '%s 1k', lib ); - reset(parent); + + start(); + childNodes = create1000(parent, diff, childNodes); + stop(parent.operations.length, 2000); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s replace', + lib + ); start(); childNodes = random(parent, diff, childNodes); @@ -167,7 +195,6 @@ ${c.gray(count)}${ '%s random', lib ); - reset(parent); start(); childNodes = reverse(parent, diff, childNodes); @@ -177,7 +204,6 @@ ${c.gray(count)}${ '%s reverse', lib ); - reset(parent); start(); childNodes = clear(parent, diff, childNodes); @@ -187,17 +213,9 @@ ${c.gray(count)}${ '%s clear', lib ); - reset(parent); childNodes = create1000(parent, diff, childNodes); - reset(parent); - childNodes = create1000(parent, diff, childNodes); - console.assert(verifyNodes(parent, childNodes, 1000)); - childNodes = clear(parent, diff, childNodes); - reset(parent); - childNodes = create1000(parent, diff, childNodes); - reset(parent); start(); childNodes = append1000(parent, diff, childNodes); stop(parent.operations.length, 2000); @@ -206,7 +224,6 @@ ${c.gray(count)}${ '%s append 1k', lib ); - reset(parent); start(); childNodes = prepend1000(parent, diff, childNodes); @@ -216,35 +233,51 @@ ${c.gray(count)}${ '%s prepend 1k', lib ); - reset(parent); - childNodes = clear(parent, diff, childNodes); + childNodes = clear(parent, diff, childNodes); childNodes = create1000(parent, diff, childNodes); - reset(parent); + start(); childNodes = swapRows(parent, diff, childNodes); - console.assert(verifyNodes(parent, childNodes, 1000)); stop(parent.operations.length, 2); - reset(parent); + console.assert( + parent.childNodes[1].textContent == 998 && + parent.childNodes[998].textContent == 1 && + verifyNodes(parent, childNodes, 1000), + '%s swap2 1k', + lib + ); - childNodes = create1000(parent, diff, childNodes); - reset(parent); start(); childNodes = updateEach10thRow(parent, diff, childNodes); stop(parent.operations.length, 200); - reset(parent); + console.assert( + verifyNodes(parent, childNodes, 1000), + '%s update 10th', + lib + ); childNodes = clear(parent, diff, childNodes); - reset(parent); + start(); childNodes = create10000(parent, diff, childNodes); stop(parent.operations.length, 10000); - reset(parent); + console.assert( + verifyNodes(parent, childNodes, 10000), + '%s 10k', + lib + ); start(); childNodes = swapRows(parent, diff, childNodes); stop(parent.operations.length, 2); - reset(parent); + console.assert( + parent.childNodes[1].textContent == 9998 && + parent.childNodes[9998].textContent == 1 && + verifyNodes(parent, childNodes, 10000), + '%s swap2 10k', + lib + ); childNodes = clear(parent, diff, childNodes); reset(parent); From 4c616f79660a562ee238edc7933c5e2d5537b89c Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Sat, 18 Apr 2020 17:30:55 +0200 Subject: [PATCH 05/10] updated tests --- test/js-diff-benchmark.js | 54 ++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/test/js-diff-benchmark.js b/test/js-diff-benchmark.js index 9e7f62d..8bb22fa 100644 --- a/test/js-diff-benchmark.js +++ b/test/js-diff-benchmark.js @@ -59,6 +59,7 @@ libs.forEach((lib, i) => { 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) @@ -171,7 +172,7 @@ libs.forEach((lib, i) => { start(); childNodes = create1000(parent, diff, childNodes); - stop(parent.operations.length, 1000); + stop(parent.mutations.length, 1000); console.assert( verifyNodes(parent, childNodes, 1000), '%s 1k', @@ -180,7 +181,7 @@ libs.forEach((lib, i) => { start(); childNodes = create1000(parent, diff, childNodes); - stop(parent.operations.length, 2000); + stop(parent.mutations.length, 2000); console.assert( verifyNodes(parent, childNodes, 1000), '%s replace', @@ -189,7 +190,7 @@ libs.forEach((lib, i) => { start(); childNodes = random(parent, diff, childNodes); - stop(parent.operations.length, 1000); + stop(parent.mutations.length, 2000); console.assert( verifyNodes(parent, childNodes, 1000), '%s random', @@ -198,7 +199,7 @@ libs.forEach((lib, i) => { start(); childNodes = reverse(parent, diff, childNodes); - stop(parent.operations.length, 1000); + stop(parent.mutations.length, 2000); console.assert( verifyNodes(parent, childNodes, 1000), '%s reverse', @@ -207,7 +208,7 @@ libs.forEach((lib, i) => { start(); childNodes = clear(parent, diff, childNodes); - stop(parent.operations.length, 1000); + stop(parent.mutations.length, 1000); console.assert( verifyNodes(parent, childNodes, 0), '%s clear', @@ -218,7 +219,7 @@ libs.forEach((lib, i) => { start(); childNodes = append1000(parent, diff, childNodes); - stop(parent.operations.length, 2000); + stop(parent.mutations.length, 2000); console.assert( verifyNodes(parent, childNodes, 2000), '%s append 1k', @@ -227,7 +228,7 @@ libs.forEach((lib, i) => { start(); childNodes = prepend1000(parent, diff, childNodes); - stop(parent.operations.length, 1000); + stop(parent.mutations.length, 1000); console.assert( verifyNodes(parent, childNodes, 3000), '%s prepend 1k', @@ -239,7 +240,7 @@ libs.forEach((lib, i) => { start(); childNodes = swapRows(parent, diff, childNodes); - stop(parent.operations.length, 2); + stop(parent.mutations.length, 4); console.assert( parent.childNodes[1].textContent == 998 && parent.childNodes[998].textContent == 1 && @@ -250,7 +251,7 @@ libs.forEach((lib, i) => { start(); childNodes = updateEach10thRow(parent, diff, childNodes); - stop(parent.operations.length, 200); + stop(parent.mutations.length, 200); console.assert( verifyNodes(parent, childNodes, 1000), '%s update 10th', @@ -261,7 +262,7 @@ libs.forEach((lib, i) => { start(); childNodes = create10000(parent, diff, childNodes); - stop(parent.operations.length, 10000); + stop(parent.mutations.length, 10000); console.assert( verifyNodes(parent, childNodes, 10000), '%s 10k', @@ -270,7 +271,7 @@ libs.forEach((lib, i) => { start(); childNodes = swapRows(parent, diff, childNodes); - stop(parent.operations.length, 2); + stop(parent.mutations.length, 4); console.assert( parent.childNodes[1].textContent == 9998 && parent.childNodes[9998].textContent == 1 && @@ -317,36 +318,41 @@ function instrument(parent) { removeChild, replaceChild } = parent; - parent.operations = []; + parent.mutations = []; parent.appendChild = function (newNode) { - this.operations.push(`appendChild(${newNode.textContent})`); + const {textContent} = newNode; + if (newNode.parentNode) + this.mutations.push(`append: drop(${textContent})`); + this.mutations.push(`append: add(${textContent})`); return appendChild.call(this, newNode); }; parent.insertBefore = function (newNode, oldNode) { - this.operations.push( + const {textContent} = newNode; + if (newNode.parentNode) + this.mutations.push(`insert: drop(${textContent})`); + this.mutations.push( oldNode ? - `insertBefore(${newNode.textContent}, ${oldNode.textContent})` : - `insertBefore(${newNode.textContent})` + `insert: put(${textContent}) before (${oldNode.textContent})` : + `insert: add(${textContent})` ); return insertBefore.call(this, newNode, oldNode); }; parent.removeChild = function (oldNode) { - this.operations.push(`removeChild(${oldNode.textContent})`); + this.mutations.push(`remove: drop(${oldNode.textContent})`); return removeChild.call(this, oldNode); }; parent.replaceChild = function (newNode, oldNode) { - this.operations.push( - `delete#replaceChild(${newNode.textContent}, ${oldNode.textContent})` - ); - this.operations.push( - `insert#replaceChild(${newNode.textContent}, ${oldNode.textContent})` - ); + const {textContent} = newNode; + this.mutations.push(`replace: drop(${oldNode.textContent})`); + if (newNode.parentNode) + this.mutations.push(`replace: drop(${textContent})`); + this.mutations.push(`replace: put(${textContent})`); return replaceChild.call(this, newNode, oldNode); }; } function reset(parent) { - parent.operations.splice(0); + parent.mutations.splice(0); } function round(num) { From 5fccd17eca250001d8ed04455d51ecd03f87f231 Mon Sep 17 00:00:00 2001 From: Andrea Giammarchi Date: Sun, 19 Apr 2020 10:55:44 +0200 Subject: [PATCH 06/10] using external utility --- index.js | 194 ++++++++++++++------------- min.js | 2 +- new.js | 2 +- package.json | 2 +- test/js-diff-benchmark.js | 267 ++++++++++---------------------------- test/utils.js | 119 +++++++++++++++++ 6 files changed, 288 insertions(+), 298 deletions(-) create mode 100644 test/utils.js diff --git a/index.js b/index.js index ae0827e..2c0e9b7 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) { @@ -44,111 +43,106 @@ var udomdiff = (function (exports) { // 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 + 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], -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)); + } } - } // 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], -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 edec11e..6bd938b 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,t=r.length,s=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 n=i.length;let r=t.length,s=n,o=0,c=0,g=null;for(;ol-c){const n=f(t[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 parent = document.createElement('div'); -instrument(parent); + +const { + clear, reset, verifyNodes, + random, reverse, + create1000, create10000, + append1000, prepend1000, + swapRows, updateEach10thRow +} = require('./utils.js')(document, parent, () => before); libs.forEach((lib, i) => { if (i) @@ -66,16 +73,16 @@ libs.forEach((lib, i) => { parent.appendChild(before); //* warm up + checking everything works upfront - let childNodes = create1000(parent, diff, []); + let childNodes = create1000(diff, []); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s warmup create', lib ); - childNodes = create1000(parent, diff, childNodes); + childNodes = create1000(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s warmup replace', lib ); @@ -88,43 +95,43 @@ libs.forEach((lib, i) => { shuffleSeed = shuffle.map((node) => childNodes.indexOf(node)); } - childNodes = append1000(parent, diff, childNodes); + childNodes = append1000(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 2000), + verifyNodes(childNodes, 2000), '%s warmup append', lib ); - childNodes = prepend1000(parent, diff, childNodes); + childNodes = prepend1000(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 3000), + verifyNodes(childNodes, 3000), '%s warmup prepend', lib ); - childNodes = clear(parent, diff, childNodes); + childNodes = clear(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 0), + verifyNodes(childNodes, 0), '%s warmup clear', lib ); - childNodes = create10000(parent, diff, childNodes); + childNodes = create10000(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 10000), + verifyNodes(childNodes, 10000), '%s warmup 10k', lib ); - childNodes = clear(parent, diff, childNodes); + childNodes = clear(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 0), + verifyNodes(childNodes, 0), '%s warmup clear 10k', lib ); - childNodes = create1000(parent, diff, childNodes); - childNodes = swapRows(parent, diff, childNodes); + 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(parent, diff, childNodes); - childNodes = create1000(parent, diff, childNodes); - childNodes = updateEach10thRow(parent, diff, childNodes); + childNodes = clear(diff, childNodes); + childNodes = create1000(diff, childNodes); + childNodes = updateEach10thRow(diff, childNodes); console.assert( /!$/.test(childNodes[0].textContent), '%s warmup update', @@ -140,9 +147,9 @@ libs.forEach((lib, i) => { '%s warmup update', lib ); - childNodes = clear(parent, diff, childNodes); + childNodes = clear(diff, childNodes); console.assert( - verifyNodes(parent, childNodes, 0), + verifyNodes(childNodes, 0), '%s warmup clear', lib ); @@ -154,13 +161,13 @@ libs.forEach((lib, i) => { let begin; const start = () => { - reset(parent); + reset(); begin = microtime.now(); }; const stop = (count, operationMax) => { const end = microtime.now() - begin; const delta = count - operationMax; - libResults.push(`${round(end / 1000)}ms + libResults.push(`${(end / 1000).toPrecision(2)}ms ${c.gray(count)}${ count > operationMax ? (delta > 99 ? '\n' : ' ') + c.bgRed.black(`+${delta}`) @@ -171,121 +178,121 @@ libs.forEach((lib, i) => { // actual benchmark start(); - childNodes = create1000(parent, diff, childNodes); - stop(parent.mutations.length, 1000); + childNodes = create1000(diff, childNodes); + stop(parent.count(), 1000); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s 1k', lib ); start(); - childNodes = create1000(parent, diff, childNodes); - stop(parent.mutations.length, 2000); + childNodes = create1000(diff, childNodes); + stop(parent.count(), 2000); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s replace', lib ); start(); - childNodes = random(parent, diff, childNodes); - stop(parent.mutations.length, 2000); + childNodes = random(shuffleSeed, diff, childNodes); + stop(parent.count(), 2000); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s random', lib ); start(); - childNodes = reverse(parent, diff, childNodes); - stop(parent.mutations.length, 2000); + childNodes = reverse(diff, childNodes); + stop(parent.count(), 2000); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s reverse', lib ); start(); - childNodes = clear(parent, diff, childNodes); - stop(parent.mutations.length, 1000); + childNodes = clear(diff, childNodes); + stop(parent.count(), 1000); console.assert( - verifyNodes(parent, childNodes, 0), + verifyNodes(childNodes, 0), '%s clear', lib ); - childNodes = create1000(parent, diff, childNodes); + childNodes = create1000(diff, childNodes); start(); - childNodes = append1000(parent, diff, childNodes); - stop(parent.mutations.length, 2000); + childNodes = append1000(diff, childNodes); + stop(parent.count(), 2000); console.assert( - verifyNodes(parent, childNodes, 2000), + verifyNodes(childNodes, 2000), '%s append 1k', lib ); start(); - childNodes = prepend1000(parent, diff, childNodes); - stop(parent.mutations.length, 1000); + childNodes = prepend1000(diff, childNodes); + stop(parent.count(), 1000); console.assert( - verifyNodes(parent, childNodes, 3000), + verifyNodes(childNodes, 3000), '%s prepend 1k', lib ); - childNodes = clear(parent, diff, childNodes); - childNodes = create1000(parent, diff, childNodes); + childNodes = clear(diff, childNodes); + childNodes = create1000(diff, childNodes); start(); - childNodes = swapRows(parent, diff, childNodes); - stop(parent.mutations.length, 4); + childNodes = swapRows(diff, childNodes); + stop(parent.count(), 4); console.assert( parent.childNodes[1].textContent == 998 && parent.childNodes[998].textContent == 1 && - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s swap2 1k', lib ); start(); - childNodes = updateEach10thRow(parent, diff, childNodes); - stop(parent.mutations.length, 200); + childNodes = updateEach10thRow(diff, childNodes); + stop(parent.count(), 200); console.assert( - verifyNodes(parent, childNodes, 1000), + verifyNodes(childNodes, 1000), '%s update 10th', lib ); - childNodes = clear(parent, diff, childNodes); + childNodes = clear(diff, childNodes); start(); - childNodes = create10000(parent, diff, childNodes); - stop(parent.mutations.length, 10000); + childNodes = create10000(diff, childNodes); + stop(parent.count(), 10000); console.assert( - verifyNodes(parent, childNodes, 10000), + verifyNodes(childNodes, 10000), '%s 10k', lib ); start(); - childNodes = swapRows(parent, diff, childNodes); - stop(parent.mutations.length, 4); + childNodes = swapRows(diff, childNodes); + stop(parent.count(), 4); console.assert( parent.childNodes[1].textContent == 9998 && parent.childNodes[9998].textContent == 1 && - verifyNodes(parent, childNodes, 10000), + verifyNodes(childNodes, 10000), '%s swap2 10k', lib ); - childNodes = clear(parent, diff, childNodes); - reset(parent); + childNodes = clear(diff, childNodes); + reset(); //*/ - libResults.push(`${round((microtime.now() - totalStart) / 1000)}ms`); + libResults.push(`${((microtime.now() - totalStart) / 1000).toPrecision(3)}ms`); libResults.push(`${gzip}B`); // const used = process.memoryUsage().heapUsed / 1024 / 1024; @@ -307,133 +314,3 @@ table.sort((a, b) => { }); console.log(table.toString()); - - -// Benchnmark Utilities - -function instrument(parent) { - const { - appendChild, - insertBefore, - removeChild, - replaceChild - } = parent; - parent.mutations = []; - parent.appendChild = function (newNode) { - const {textContent} = newNode; - if (newNode.parentNode) - this.mutations.push(`append: drop(${textContent})`); - this.mutations.push(`append: add(${textContent})`); - return appendChild.call(this, newNode); - }; - parent.insertBefore = function (newNode, oldNode) { - const {textContent} = newNode; - if (newNode.parentNode) - this.mutations.push(`insert: drop(${textContent})`); - this.mutations.push( - oldNode ? - `insert: put(${textContent}) before (${oldNode.textContent})` : - `insert: add(${textContent})` - ); - return insertBefore.call(this, newNode, oldNode); - }; - parent.removeChild = function (oldNode) { - this.mutations.push(`remove: drop(${oldNode.textContent})`); - return removeChild.call(this, oldNode); - }; - parent.replaceChild = function (newNode, oldNode) { - const {textContent} = newNode; - this.mutations.push(`replace: drop(${oldNode.textContent})`); - if (newNode.parentNode) - this.mutations.push(`replace: drop(${textContent})`); - this.mutations.push(`replace: put(${textContent})`); - return replaceChild.call(this, newNode, oldNode); - }; -} - -function reset(parent) { - parent.mutations.splice(0); -} - -function round(num) { - return Math.round((num + Number.EPSILON) * 10) / 10; -} - -function verifyNodes(parent, childNodes, expected) { - return childNodes.length === expected && - childNodes.every((row, i) => row === parent.childNodes[i]) && - parent.childNodes.length === expected + (before ? 1 : 0) && - (!before || parent.childNodes[expected] === before); -} - - -// Benchnmark Functions - -function random(parent, diff, oldNodes) { - return diff( - parent, - oldNodes, - shuffleSeed.map((newIdx) => oldNodes[newIdx]), - get, - before - ); -} - -function reverse(parent, diff, oldNodes) { - return diff(parent, oldNodes, oldNodes.slice().reverse(), get, before); -} - -function append1000(parent, diff, oldNodes) { - const start = oldNodes.length; - const childNodes = oldNodes.slice(); - for (let i = 0; i < 1000; i++) - childNodes.push(document.createTextNode(parent, start + i)); - return diff(parent, oldNodes, childNodes, get, before); -} - -function clear(parent, diff, oldNodes) { - return diff(parent, oldNodes, [], get, before); -} - -function create1000(parent, diff, oldNodes) { - const childNodes = []; - for (let i = 0; i < 1000; i++) - childNodes.push(document.createTextNode(i)); - return diff(parent, oldNodes, childNodes, get, before); -} - -function create10000(parent, diff, oldNodes) { - const childNodes = []; - for (let i = 0; i < 10000; i++) - childNodes.push(document.createTextNode(i)); - return diff(parent, oldNodes, childNodes, get, before); -} - -function prepend1000(parent, diff, oldNodes) { - const childNodes = []; - for (let i = 0; i < 1000; i++) - childNodes.push(document.createTextNode(parent, -i)); - return diff( - parent, - oldNodes, - childNodes.reverse().concat(oldNodes), - get, - before - ); -} - -function swapRows(parent, diff, oldNodes) { - const childNodes = oldNodes.slice(); - const $1 = childNodes[1]; - const index = childNodes.length - 2; - childNodes[1] = childNodes[index]; - childNodes[index] = $1; - return diff(parent, oldNodes, childNodes, get, before); -} - -function updateEach10thRow(parent, diff, oldNodes) { - const childNodes = oldNodes.slice(); - for (let i = 0; i < childNodes.length; i += 10) - childNodes[i] = document.createTextNode(i + '!'); - return diff(parent, oldNodes, childNodes, get, before); -} diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 0000000..f905f57 --- /dev/null +++ b/test/utils.js @@ -0,0 +1,119 @@ +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()); + } + }; +}; From 974fb3ddd5ab7ab2ae4c9d1254638fa1bd79965e Mon Sep 17 00:00:00 2001 From: webreflection Date: Wed, 13 Nov 2024 17:59:20 +0100 Subject: [PATCH 07/10] Removed the only unnecessary operation I could find --- cjs/index.js | 2 +- esm/index.js | 2 +- index.js | 2 +- min.js | 2 +- new.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cjs/index.js b/cjs/index.js index 4566c30..9db2caa 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); diff --git a/esm/index.js b/esm/index.js index fdc7508..a9cf2ff 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); diff --git a/index.js b/index.js index 2c0e9b7..bbcf0b5 100644 --- a/index.js +++ b/index.js @@ -42,7 +42,7 @@ 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; + 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 diff --git a/min.js b/min.js index 6bd938b..f130b86 100644 --- a/min.js +++ b/min.js @@ -1 +1 @@ -var udomdiff=(e=>(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(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=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 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 Date: Wed, 13 Nov 2024 17:59:23 +0100 Subject: [PATCH 08/10] 1.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fbf01c..6116e8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "udomdiff", - "version": "1.1.0", + "version": "1.1.1", "description": "An essential diffing algorithm for ยตhtml", "main": "cjs/index.js", "scripts": { From d30362c411b9d4931fd2670526e0ade90c2ac028 Mon Sep 17 00:00:00 2001 From: webreflection Date: Wed, 27 Nov 2024 10:54:02 +0100 Subject: [PATCH 09/10] Fixed a fragment issue with shrink operation --- cjs/index.js | 4 ++-- esm/index.js | 4 ++-- index.js | 4 ++-- min.js | 2 +- new.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cjs/index.js b/cjs/index.js index 9db2caa..99856d2 100644 --- a/cjs/index.js +++ b/cjs/index.js @@ -80,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 a9cf2ff..e500d56 100644 --- a/esm/index.js +++ b/esm/index.js @@ -79,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 bbcf0b5..5f8f7f5 100644 --- a/index.js +++ b/index.js @@ -73,8 +73,8 @@ var udomdiff = (function (exports) { // 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); + 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, diff --git a/min.js b/min.js index f130b86..a3bbcc8 100644 --- a/min.js +++ b/min.js @@ -1 +1 @@ -var udomdiff=(e=>(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(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=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 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 Date: Wed, 27 Nov 2024 10:54:06 +0100 Subject: [PATCH 10/10] 1.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6116e8c..6d108b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "udomdiff", - "version": "1.1.1", + "version": "1.1.2", "description": "An essential diffing algorithm for ยตhtml", "main": "cjs/index.js", "scripts": {