From 41e146b0fead58e65bdd5f149a63b28a5ce097a9 Mon Sep 17 00:00:00 2001 From: Michael Dellanoce Date: Wed, 8 Nov 2023 11:33:05 -0500 Subject: [PATCH] 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;