Skip to content

Commit

Permalink
linear time replay
Browse files Browse the repository at this point in the history
  • Loading branch information
mdellanoce committed Nov 8, 2023
1 parent 541a66a commit 41e146b
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 87 deletions.
82 changes: 58 additions & 24 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ import {
} from '@rrweb/types';
import {
polyfill,
queueToResolveTrees,
iterateResolveTree,
ResolveTree,
AppendedIframe,
getBaseDimension,
hasShadowRoot,
Expand Down Expand Up @@ -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) => {
Expand All @@ -1468,7 +1466,37 @@ export class Replayer {
return false;
};

const appendNode = (mutation: addedNodeMutation) => {
const queue = new Set<ResolveTree>();
const idMap = new Map<number, ResolveTree>();

const getOrCreateNode = (nodeId: number) => {
let nodeInTree = idMap.get(nodeId);
if (!nodeInTree) {
nodeInTree = {
id: nodeId,
children: new Map<number | null, ResolveTree>(),
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.');
}
Expand All @@ -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) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
67 changes: 4 additions & 63 deletions packages/rrweb/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | null, ResolveTree>;
};

export function queueToResolveTrees(queue: addedNodeMutation[]): ResolveTree[] {
const queueNodeMap: Record<number, ResolveTree> = {};
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;
Expand Down

0 comments on commit 41e146b

Please sign in to comment.