This issue was originally reported in #280 but may also relate to #167 and other potential performance issues in the recording. In #206 I implemented the new mutation observer which will defer the serialization of DOM, which helps us to have a consistent DOM order for the replay. In this implementation, we use an array to represent the `addQueue`. Whenever we need to consume the queue, we will iterate it to make sure there is no dead loop, and then shift the first item to see whether it can be serialized at the new timing. But this implementation may be very slow when there are a lot of newly added DOM since it will do an O(n^2) iteration. For example, if we have three newly added DOM `n1`, `n2`, `n3`, the iteration looks like this: ``` [n1, n2, n3] n1 -> n2 -> n3, consume n3 [n1, n2] n1 -> n2, consume n2 [n1] n1, consume n1 ``` We should have a better performance if te iteration looks like this: ``` [n1, n2, n3] n3, consume n3 [n1, n2] n2, consume n2 [n1] n1, consume n1 ``` Simply reverse the mutation payload does not work, because it does not always as same as the DOM order. So in this patch, we replace the `addQueue` with a double linked list, which can: 1. represent the DOM order in its data structure 2. has an O(1) time complexity when looking up the sibling of a list item 3. has an O(1) time complexity when removing a list item
This commit is contained in:
@@ -15,6 +15,88 @@ import {
|
||||
} from '../types';
|
||||
import { mirror, isBlocked, isAncestorRemoved } from '../utils';
|
||||
|
||||
type DoubleLinkedListNode = {
|
||||
previous: DoubleLinkedListNode | null;
|
||||
next: DoubleLinkedListNode | null;
|
||||
value: NodeInLinkedList;
|
||||
};
|
||||
type NodeInLinkedList = Node & {
|
||||
__ln: DoubleLinkedListNode;
|
||||
};
|
||||
|
||||
function isNodeInLinkedList(n: Node | NodeInLinkedList): n is NodeInLinkedList {
|
||||
return '__ln' in n;
|
||||
}
|
||||
class DoubleLinkedList {
|
||||
public length = 0;
|
||||
public head: DoubleLinkedListNode | null = null;
|
||||
|
||||
public get(position: number) {
|
||||
if (position >= this.length) {
|
||||
throw new Error('Position outside of list range');
|
||||
}
|
||||
|
||||
let current = this.head;
|
||||
for (let index = 0; index < position; index++) {
|
||||
current = current?.next || null;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public addNode(n: Node) {
|
||||
const node: DoubleLinkedListNode = {
|
||||
value: n as NodeInLinkedList,
|
||||
previous: null,
|
||||
next: null,
|
||||
};
|
||||
(n as NodeInLinkedList).__ln = node;
|
||||
if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) {
|
||||
const current = n.previousSibling.__ln.next;
|
||||
node.next = current;
|
||||
node.previous = n.previousSibling.__ln;
|
||||
n.previousSibling.__ln.next = node;
|
||||
if (current) {
|
||||
current.previous = node;
|
||||
}
|
||||
} else if (n.nextSibling && isNodeInLinkedList(n.nextSibling)) {
|
||||
const current = n.nextSibling.__ln.previous;
|
||||
node.previous = current;
|
||||
node.next = n.nextSibling.__ln;
|
||||
n.nextSibling.__ln.previous = node;
|
||||
if (current) {
|
||||
current.next = node;
|
||||
}
|
||||
} else {
|
||||
if (this.head) {
|
||||
this.head.previous = node;
|
||||
}
|
||||
node.next = this.head;
|
||||
this.head = node;
|
||||
}
|
||||
this.length++;
|
||||
}
|
||||
|
||||
public removeNode(n: NodeInLinkedList) {
|
||||
const current = n.__ln;
|
||||
if (!this.head) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!current.previous) {
|
||||
this.head = current.next;
|
||||
if (this.head) {
|
||||
this.head.previous = null;
|
||||
}
|
||||
} else {
|
||||
current.previous.next = current.next;
|
||||
if (current.next) {
|
||||
current.next.previous = current.previous;
|
||||
}
|
||||
}
|
||||
this.length--;
|
||||
}
|
||||
}
|
||||
|
||||
const moveKey = (id: number, parentId: number) => `${id}@${parentId}`;
|
||||
function isINode(n: Node | INode): n is INode {
|
||||
return '__sn' in n;
|
||||
@@ -76,16 +158,23 @@ export default class MutationBuffer {
|
||||
* Sometimes child node may be pushed before its newly added
|
||||
* parent, so we init a queue to store these nodes.
|
||||
*/
|
||||
const addQueue: Node[] = [];
|
||||
const addList = new DoubleLinkedList();
|
||||
const getNextId = (n: Node): number | null => {
|
||||
let nextId =
|
||||
n.nextSibling && mirror.getId((n.nextSibling as unknown) as INode);
|
||||
if (nextId === -1 && isBlocked(n.nextSibling, this.blockClass)) {
|
||||
nextId = null;
|
||||
}
|
||||
return nextId;
|
||||
};
|
||||
const pushAdd = (n: Node) => {
|
||||
if (!n.parentNode) {
|
||||
return;
|
||||
}
|
||||
const parentId = mirror.getId((n.parentNode as Node) as INode);
|
||||
const nextId =
|
||||
n.nextSibling && mirror.getId((n.nextSibling as unknown) as INode);
|
||||
const nextId = getNextId(n);
|
||||
if (parentId === -1 || nextId === -1) {
|
||||
return addQueue.push(n);
|
||||
return addList.addNode(n);
|
||||
}
|
||||
this.adds.push({
|
||||
parentId,
|
||||
@@ -119,12 +208,32 @@ export default class MutationBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
while (addQueue.length) {
|
||||
if (
|
||||
addQueue.every(
|
||||
(n) => mirror.getId((n.parentNode as Node) as INode) === -1,
|
||||
)
|
||||
) {
|
||||
let candidate: DoubleLinkedListNode | null = null;
|
||||
while (addList.length) {
|
||||
let node: DoubleLinkedListNode | null = null;
|
||||
if (candidate) {
|
||||
const parentId = mirror.getId(
|
||||
(candidate.value.parentNode as Node) as INode,
|
||||
);
|
||||
const nextId = getNextId(candidate.value);
|
||||
if (parentId !== -1 && nextId !== -1) {
|
||||
node = candidate;
|
||||
}
|
||||
}
|
||||
if (!node) {
|
||||
for (let index = addList.length - 1; index >= 0; index--) {
|
||||
const _node = addList.get(index)!;
|
||||
const parentId = mirror.getId(
|
||||
(_node.value.parentNode as Node) as INode,
|
||||
);
|
||||
const nextId = getNextId(_node.value);
|
||||
if (parentId !== -1 && nextId !== -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
|
||||
@@ -132,7 +241,9 @@ export default class MutationBuffer {
|
||||
*/
|
||||
break;
|
||||
}
|
||||
pushAdd(addQueue.shift()!);
|
||||
candidate = node.previous;
|
||||
addList.removeNode(node.value);
|
||||
pushAdd(node.value);
|
||||
}
|
||||
|
||||
this.emit();
|
||||
|
||||
@@ -4832,8 +4832,8 @@ exports[`select2 1`] = `
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 68,
|
||||
\\"nextId\\": 69,
|
||||
\\"parentId\\": 73,
|
||||
\\"nextId\\": 74,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"span\\",
|
||||
@@ -4845,8 +4845,8 @@ exports[`select2 1`] = `
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 73,
|
||||
\\"nextId\\": 74,
|
||||
\\"parentId\\": 68,
|
||||
\\"nextId\\": 69,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"span\\",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"arrow-parens": false,
|
||||
"only-arrow-functions": false,
|
||||
"max-line-length": false,
|
||||
"no-empty": false
|
||||
"no-empty": false,
|
||||
"max-classes-per-file": false
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user