From 7d32df349d1754c6ac65fc762b4fc05566ffc64a Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Mon, 11 Sep 2023 09:28:37 -0400 Subject: [PATCH 1/5] add benchmark for addList worst case performance --- .../rrweb/test/benchmark/dom-mutation.test.ts | 6 ++++ .../benchmark-dom-mutation-add-reorder.html | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 packages/rrweb/test/html/benchmark-dom-mutation-add-reorder.html diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index a4124c3f98..bc83a83080 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -48,6 +48,12 @@ const suites: Array< eval: 'window.workload()', times: 10, }, + { + title: 'add and reorder 10000 DOM nodes', + html: 'benchmark-dom-mutation-add-reorder.html', + eval: 'window.workload()', + times: 5, + }, ]; function avg(v: number[]): number { diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-add-reorder.html b/packages/rrweb/test/html/benchmark-dom-mutation-add-reorder.html new file mode 100644 index 0000000000..c4b7d24d11 --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation-add-reorder.html @@ -0,0 +1,33 @@ + + + + + + From 60235ef6ec7bfa11cb12cf982cb8a24d0512d696 Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Fri, 8 Dec 2023 16:05:44 -0500 Subject: [PATCH 2/5] benchmark for weird node addition order --- .../rrweb/test/benchmark/dom-mutation.test.ts | 6 +++++ .../benchmark-dom-mutation-random-add.html | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/rrweb/test/html/benchmark-dom-mutation-random-add.html diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index bc83a83080..cf229ce791 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -54,6 +54,12 @@ const suites: Array< eval: 'window.workload()', times: 5, }, + { + title: 'add 5x2000 DOM nodes in random order', + html: 'benchmark-dom-mutation-random-add.html', + eval: 'window.workload()', + times: 5, + }, ]; function avg(v: number[]): number { diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-random-add.html b/packages/rrweb/test/html/benchmark-dom-mutation-random-add.html new file mode 100644 index 0000000000..c937e225e6 --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation-random-add.html @@ -0,0 +1,25 @@ + + + + + + From 6b09c5931bd7b2bc82c89d4af3c082838829cc6f Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Thu, 2 Nov 2023 11:18:01 -0400 Subject: [PATCH 3/5] linear time addList processing --- packages/rrweb/src/record/mutation.ts | 102 ++++++++------------------ 1 file changed, 31 insertions(+), 71 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 7c209605d0..3c8175db20 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -284,17 +284,16 @@ export default class MutationBuffer { } return nextId; }; + const getParentId = (n: Node): number | null => { + if (!n.parentNode) return null; + return isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + }; const pushAdd = (n: Node) => { if (!n.parentNode || !inDom(n)) { return; } - const parentId = isShadowRoot(n.parentNode) - ? this.mirror.getId(getShadowHost(n)) - : this.mirror.getId(n.parentNode); - const nextId = getNextId(n); - if (parentId === -1 || nextId === -1) { - return addList.addNode(n); - } const sn = serializeNodeWithId(n, { doc: this.doc, mirror: this.mirror, @@ -334,12 +333,18 @@ export default class MutationBuffer { }, }); if (sn) { - adds.push({ - parentId, - nextId, - node: sn, - }); - addedIds.add(sn.id); + const parentId = getParentId(n); + const nextId = getNextId(n); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } else if (parentId) { + adds.push({ + parentId, + nextId, + node: sn, + }); + addedIds.add(sn.id); + } } }; @@ -370,67 +375,22 @@ export default class MutationBuffer { } } - let candidate: DoubleLinkedListNode | null = null; while (addList.length) { - let node: DoubleLinkedListNode | null = null; - if (candidate) { - const parentId = this.mirror.getId(candidate.value.parentNode); - const nextId = getNextId(candidate.value); - if (parentId !== -1 && nextId !== -1) { - node = candidate; - } - } - if (!node) { - let tailNode = addList.tail; - while (tailNode) { - const _node = tailNode; - tailNode = tailNode.previous; - // ensure _node is defined before attempting to find value - if (_node) { - const parentId = this.mirror.getId(_node.value.parentNode); - const nextId = getNextId(_node.value); - - if (nextId === -1) continue; - // nextId !== -1 && parentId !== -1 - else if (parentId !== -1) { - node = _node; - break; - } - // nextId !== -1 && parentId === -1 This branch can happen if the node is the child of shadow root - else { - const unhandledNode = _node.value; - // If the node is the direct child of a shadow root, we treat the shadow host as its parent node. - if ( - unhandledNode.parentNode && - unhandledNode.parentNode.nodeType === - Node.DOCUMENT_FRAGMENT_NODE - ) { - const shadowHost = (unhandledNode.parentNode as ShadowRoot) - .host; - const parentId = this.mirror.getId(shadowHost); - if (parentId !== -1) { - node = _node; - break; - } - } - } - } - } - } - if (!node) { - /** - * If all nodes in queue could not find a serialized parent, - * it may be a bug or corner case. We need to escape the - * dead while loop at once. - */ - while (addList.head) { - addList.removeNode(addList.head.value); + const current = addList.head as DoubleLinkedListNode; + const addedNode = current.value; + const sn = this.mirror.getMeta(addedNode); + if (sn) { + const parentId = getParentId(addedNode); + if (parentId) { + adds.push({ + parentId, + nextId: getNextId(addedNode), + node: sn + }); + addedIds.add(sn.id); } - break; } - candidate = node.previous; - addList.removeNode(node.value); - pushAdd(node.value); + addList.removeNode(addedNode); } const payload = { From 63006ebde38f759c8be68ac79b881b60ccc78036 Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Wed, 8 Nov 2023 11:33:05 -0500 Subject: [PATCH 4/5] linear time replay --- packages/rrweb/src/replay/index.ts | 82 +++++++++++++++++++++--------- packages/rrweb/src/utils.ts | 67 ++---------------------- 2 files changed, 62 insertions(+), 87 deletions(-) diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index aac84c2783..cc7285c054 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -66,8 +66,7 @@ import { } from '@rrweb/types'; import { polyfill, - queueToResolveTrees, - iterateResolveTree, + ResolveTree, AppendedIframe, getBaseDimension, hasShadowRoot, @@ -1448,7 +1447,6 @@ export class Replayer { const legacy_missingNodeMap: missingNodeMap = { ...this.legacy_missingNodeRetryMap, }; - const queue: addedNodeMutation[] = []; // next not present at this moment const nextNotInDOM = (mutation: addedNodeMutation) => { @@ -1468,7 +1466,37 @@ export class Replayer { return false; }; - const appendNode = (mutation: addedNodeMutation) => { + const queue = new Set(); + const idMap = new Map(); + + const getOrCreateNode = (nodeId: number) => { + let nodeInTree = idMap.get(nodeId); + if (!nodeInTree) { + nodeInTree = { + id: nodeId, + children: new Map(), + value: null, + }; + idMap.set(nodeId, nodeInTree); + } + return nodeInTree; + }; + const addToQueue = (mutation: addedNodeMutation) => { + const nodeInTree = getOrCreateNode(mutation.node.id); + nodeInTree.value = mutation; + const parentExists = idMap.has(mutation.parentId); + const parent = getOrCreateNode(mutation.parentId); + parent.children.set(mutation.nextId || null, nodeInTree); + + if (queue.has(nodeInTree)) { + queue.delete(nodeInTree); + } + if (!queue.has(parent) && !parentExists) { + queue.add(parent); + } + }; + + const appendNode = (mutation: addedNodeMutation, allowQueue = true) => { if (!this.iframe.contentDocument) { return this.warn('Looks like your replayer has been destroyed.'); } @@ -1480,7 +1508,7 @@ export class Replayer { // is newly added document, maybe the document node of an iframe return this.newDocumentQueue.push(mutation); } - return queue.push(mutation); + return allowQueue ? addToQueue(mutation) : false } if (mutation.node.isShadow) { @@ -1500,7 +1528,7 @@ export class Replayer { next = mirror.getNode(mutation.nextId); } if (nextNotInDOM(mutation)) { - return queue.push(mutation); + return allowQueue ? addToQueue(mutation) : false } if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { @@ -1659,32 +1687,38 @@ export class Replayer { appendNode(mutation); }); + const iterateResolveTree = (tree: ResolveTree, mirror: RRDOMMirror | Mirror, cb: (mutation: addedNodeMutation) => unknown) => { + if (tree.value) { + cb(tree.value); + } + let nextChild = tree.children.get(null); + if (!nextChild) { + const parentNode = mirror.getNode(tree.id); + if (parentNode && parentNode.firstChild) { + const nextId = mirror.getId(parentNode.firstChild as RRNode & Node); + nextChild = tree.children.get(nextId); + } + } + while (nextChild) { + iterateResolveTree(nextChild, mirror, cb); + nextChild = nextChild.value ? tree.children.get(nextChild.value.node.id) : undefined; + } + } const startTime = Date.now(); - while (queue.length) { - // transform queue to resolve tree - const resolveTrees = queueToResolveTrees(queue); - queue.length = 0; + for (const tree of queue) { + iterateResolveTree(tree, mirror, (mutation: addedNodeMutation) => { + appendNode(mutation, false); + }); if (Date.now() - startTime > 500) { this.warn( 'Timeout in the loop, please check the resolve tree data:', - resolveTrees, + queue, ); break; } - for (const tree of resolveTrees) { - const parent = mirror.getNode(tree.value.parentId); - if (!parent) { - this.debug( - 'Drop resolve tree since there is no parent for the root node.', - tree, - ); - } else { - iterateResolveTree(tree, (mutation) => { - appendNode(mutation); - }); - } - } } + queue.clear(); + idMap.clear(); if (Object.keys(legacy_missingNodeMap).length) { Object.assign(this.legacy_missingNodeRetryMap, legacy_missingNodeMap); diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index f426689d2f..d32134797a 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -341,71 +341,12 @@ export function polyfill(win = window) { } } -type ResolveTree = { - value: addedNodeMutation; - children: ResolveTree[]; - parent: ResolveTree | null; +export type ResolveTree = { + id: number, + value: addedNodeMutation | null; + children: Map; }; -export function queueToResolveTrees(queue: addedNodeMutation[]): ResolveTree[] { - const queueNodeMap: Record = {}; - const putIntoMap = ( - m: addedNodeMutation, - parent: ResolveTree | null, - ): ResolveTree => { - const nodeInTree: ResolveTree = { - value: m, - parent, - children: [], - }; - queueNodeMap[m.node.id] = nodeInTree; - return nodeInTree; - }; - - const queueNodeTrees: ResolveTree[] = []; - for (const mutation of queue) { - const { nextId, parentId } = mutation; - if (nextId && nextId in queueNodeMap) { - const nextInTree = queueNodeMap[nextId]; - if (nextInTree.parent) { - const idx = nextInTree.parent.children.indexOf(nextInTree); - nextInTree.parent.children.splice( - idx, - 0, - putIntoMap(mutation, nextInTree.parent), - ); - } else { - const idx = queueNodeTrees.indexOf(nextInTree); - queueNodeTrees.splice(idx, 0, putIntoMap(mutation, null)); - } - continue; - } - if (parentId in queueNodeMap) { - const parentInTree = queueNodeMap[parentId]; - parentInTree.children.push(putIntoMap(mutation, parentInTree)); - continue; - } - queueNodeTrees.push(putIntoMap(mutation, null)); - } - - return queueNodeTrees; -} - -export function iterateResolveTree( - tree: ResolveTree, - cb: (mutation: addedNodeMutation) => unknown, -) { - cb(tree.value); - /** - * The resolve tree was designed to reflect the DOM layout, - * but we need append next sibling first, so we do a reverse - * loop here. - */ - for (let i = tree.children.length - 1; i >= 0; i--) { - iterateResolveTree(tree.children[i], cb); - } -} - export type AppendedIframe = { mutationInQueue: addedNodeMutation; builtNode: HTMLIFrameElement | RRIFrameElement; From d5d3ebdc45b14a5cfccd3efe3a591c33d4a94beb Mon Sep 17 00:00:00 2001 From: mdellanoce Date: Tue, 12 Dec 2023 16:01:58 +0000 Subject: [PATCH 5/5] Apply formatting changes --- packages/rrweb/src/record/mutation.ts | 6 +++--- packages/rrweb/src/replay/index.ts | 16 +++++++++++----- packages/rrweb/src/utils.ts | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 3c8175db20..4e30c5fd92 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -287,8 +287,8 @@ export default class MutationBuffer { const getParentId = (n: Node): number | null => { if (!n.parentNode) return null; return isShadowRoot(n.parentNode) - ? this.mirror.getId(getShadowHost(n)) - : this.mirror.getId(n.parentNode); + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); }; const pushAdd = (n: Node) => { if (!n.parentNode || !inDom(n)) { @@ -385,7 +385,7 @@ export default class MutationBuffer { adds.push({ parentId, nextId: getNextId(addedNode), - node: sn + node: sn, }); addedIds.add(sn.id); } diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index cc7285c054..d38e5b9a41 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -1508,7 +1508,7 @@ export class Replayer { // is newly added document, maybe the document node of an iframe return this.newDocumentQueue.push(mutation); } - return allowQueue ? addToQueue(mutation) : false + return allowQueue ? addToQueue(mutation) : false; } if (mutation.node.isShadow) { @@ -1528,7 +1528,7 @@ export class Replayer { next = mirror.getNode(mutation.nextId); } if (nextNotInDOM(mutation)) { - return allowQueue ? addToQueue(mutation) : false + return allowQueue ? addToQueue(mutation) : false; } if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) { @@ -1687,7 +1687,11 @@ export class Replayer { appendNode(mutation); }); - const iterateResolveTree = (tree: ResolveTree, mirror: RRDOMMirror | Mirror, cb: (mutation: addedNodeMutation) => unknown) => { + const iterateResolveTree = ( + tree: ResolveTree, + mirror: RRDOMMirror | Mirror, + cb: (mutation: addedNodeMutation) => unknown, + ) => { if (tree.value) { cb(tree.value); } @@ -1701,9 +1705,11 @@ export class Replayer { } while (nextChild) { iterateResolveTree(nextChild, mirror, cb); - nextChild = nextChild.value ? tree.children.get(nextChild.value.node.id) : undefined; + nextChild = nextChild.value + ? tree.children.get(nextChild.value.node.id) + : undefined; } - } + }; const startTime = Date.now(); for (const tree of queue) { iterateResolveTree(tree, mirror, (mutation: addedNodeMutation) => { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index d32134797a..979147cb56 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -342,7 +342,7 @@ export function polyfill(win = window) { } export type ResolveTree = { - id: number, + id: number; value: addedNodeMutation | null; children: Map; };