diff --git a/.changeset/beige-numbers-enjoy.md b/.changeset/beige-numbers-enjoy.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/beige-numbers-enjoy.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/nervous-poets-grin.md b/.changeset/nervous-poets-grin.md index eb53dd7f53..084e1c5524 100644 --- a/.changeset/nervous-poets-grin.md +++ b/.changeset/nervous-poets-grin.md @@ -6,4 +6,4 @@ 'rrweb-snapshot': patch --- -- [`fe69bd6`](https://github.com/rrweb-io/rrweb/commit/fe69bd6456cead304bfc77cf72c9db0f8c030842) [#1087](https://github.com/rrweb-io/rrweb/pull/1087) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Refactor all suffix of bundled scripts with commonjs module from 'js' to cjs. +Refactor all suffix of bundled scripts with commonjs module from 'js' to cjs [#1087](https://github.com/rrweb-io/rrweb/pull/1087). diff --git a/.changeset/olive-worms-pump.md b/.changeset/olive-worms-pump.md deleted file mode 100644 index a845151cc8..0000000000 --- a/.changeset/olive-worms-pump.md +++ /dev/null @@ -1,2 +0,0 @@ ---- ---- diff --git a/.changeset/real-trains-switch.md b/.changeset/real-trains-switch.md index d102959fdf..71de446836 100644 --- a/.changeset/real-trains-switch.md +++ b/.changeset/real-trains-switch.md @@ -3,4 +3,4 @@ 'rrweb': patch --- -- [`4ee86fe`](https://github.com/rrweb-io/rrweb/commit/4ee86fe66d3e1fe7071f9c8764d82a6fa5c71d57) [#1091](https://github.com/rrweb-io/rrweb/pull/1091) Thanks [@YunFeng0817](https://github.com/YunFeng0817)! - Fix: improve rrdom robustness. +Fix: improve rrdom robustness [#1091](https://github.com/rrweb-io/rrweb/pull/1091). diff --git a/.changeset/serious-ants-juggle.md b/.changeset/serious-ants-juggle.md new file mode 100644 index 0000000000..e475e12267 --- /dev/null +++ b/.changeset/serious-ants-juggle.md @@ -0,0 +1,10 @@ +--- +'rrdom': major +'rrdom-nodejs': major +'rrweb': patch +--- + +Refactor: Improve performance by 80% in a super large benchmark case. + +1. Refactor: change the data structure of childNodes from array to linked list +2. Improve the performance of the "contains" function. New algorithm will reduce the complexity from O(n) to O(logn) diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 6af6e8198d..98ee1696e4 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -3,8 +3,8 @@ import { parseCSSText, camelize, toCSSText } from './style'; export interface IRRNode { parentElement: IRRNode | null; parentNode: IRRNode | null; - childNodes: IRRNode[]; ownerDocument: IRRDocument; + readonly childNodes: IRRNode[]; readonly ELEMENT_NODE: number; readonly TEXT_NODE: number; // corresponding nodeType value of standard HTML Node @@ -16,8 +16,11 @@ export interface IRRNode { lastChild: IRRNode | null; + previousSibling: IRRNode | null; + nextSibling: IRRNode | null; + // If the node is a document or a doctype, textContent returns null. textContent: string | null; contains(node: IRRNode): boolean; @@ -127,11 +130,16 @@ type ConstrainedConstructor> = new ( * This is designed as an abstract class so it should never be instantiated. */ export class BaseRRNode implements IRRNode { - public childNodes: IRRNode[] = []; public parentElement: IRRNode | null = null; public parentNode: IRRNode | null = null; - public textContent: string | null; public ownerDocument: IRRDocument; + public firstChild: IRRNode | null = null; + public lastChild: IRRNode | null = null; + public previousSibling: IRRNode | null = null; + public nextSibling: IRRNode | null = null; + + public textContent: string | null; + public readonly ELEMENT_NODE: number = NodeType.ELEMENT_NODE; public readonly TEXT_NODE: number = NodeType.TEXT_NODE; // corresponding nodeType value of standard HTML Node @@ -144,26 +152,24 @@ export class BaseRRNode implements IRRNode { // } - public get firstChild(): IRRNode | null { - return this.childNodes[0] || null; - } - - public get lastChild(): IRRNode | null { - return this.childNodes[this.childNodes.length - 1] || null; - } - - public get nextSibling(): IRRNode | null { - const parentNode = this.parentNode; - if (!parentNode) return null; - const siblings = parentNode.childNodes; - const index = siblings.indexOf(this); - return siblings[index + 1] || null; + public get childNodes(): IRRNode[] { + const childNodes: IRRNode[] = []; + let childIterator: IRRNode | null = this.firstChild; + while (childIterator) { + childNodes.push(childIterator); + childIterator = childIterator.nextSibling; + } + return childNodes; } public contains(node: IRRNode) { - if (node === this) return true; - for (const child of this.childNodes) { - if (child.contains(node)) return true; + if (!(node instanceof BaseRRNode)) return false; + else if (node.ownerDocument !== this.ownerDocument) return false; + else if (node === this) return true; + + while (node.parentNode) { + if (node.parentNode === this) return true; + node = node.parentNode; } return false; } @@ -202,11 +208,11 @@ export function BaseRRDocumentImpl< public readonly nodeName: '#document' = '#document'; public readonly compatMode: 'BackCompat' | 'CSS1Compat' = 'CSS1Compat'; public readonly RRNodeType = RRNodeType.Document; - public textContent: string | null = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...args: any[]) { super(args); + this.textContent = null; this.ownerDocument = this; } @@ -248,8 +254,8 @@ export function BaseRRDocumentImpl< return this.documentElement; } - public appendChild(childNode: IRRNode): IRRNode { - const nodeType = childNode.RRNodeType; + public appendChild(newChild: IRRNode): IRRNode { + const nodeType = newChild.RRNodeType; if ( nodeType === RRNodeType.Element || nodeType === RRNodeType.DocumentType @@ -262,11 +268,10 @@ export function BaseRRDocumentImpl< ); } } - childNode.parentElement = null; - childNode.parentNode = this; - childNode.ownerDocument = this.ownerDocument; - this.childNodes.push(childNode); - return childNode; + + const child = appendChild(this, newChild); + child.parentElement = null; + return child; } public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { @@ -283,33 +288,19 @@ export function BaseRRDocumentImpl< ); } } - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.childNodes.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.childNodes.splice(childIndex, 0, newChild); - newChild.parentElement = null; - newChild.parentNode = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; - } - - public removeChild(node: IRRNode) { - const indexOfChild = this.childNodes.indexOf(node); - if (indexOfChild === -1) - throw new Error( - "Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode.", - ); - this.childNodes.splice(indexOfChild, 1); - node.parentElement = null; - node.parentNode = null; - return node; + + const child = insertBefore(this, newChild, refChild); + child.parentElement = null; + return child; + } + + public removeChild(node: IRRNode): IRRNode { + return removeChild(this, node); } public open() { - this.childNodes = []; + this.firstChild = null; + this.lastChild = null; } public close() { @@ -415,7 +406,6 @@ export function BaseRRDocumentTypeImpl< public readonly name: string; public readonly publicId: string; public readonly systemId: string; - public textContent: string | null = null; constructor(qualifiedName: string, publicId: string, systemId: string) { super(); @@ -423,6 +413,7 @@ export function BaseRRDocumentTypeImpl< this.publicId = publicId; this.systemId = systemId; this.nodeName = qualifiedName; + this.textContent = null; } toString() { @@ -459,7 +450,9 @@ export function BaseRRElementImpl< } public set textContent(textContent: string) { - this.childNodes = [this.ownerDocument.createTextNode(textContent)]; + this.firstChild = null; + this.lastChild = null; + this.appendChild(this.ownerDocument.createTextNode(textContent)); } public get classList(): ClassList { @@ -528,37 +521,15 @@ export function BaseRRElementImpl< } public appendChild(newChild: IRRNode): IRRNode { - this.childNodes.push(newChild); - newChild.parentNode = this; - newChild.parentElement = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; + return appendChild(this, newChild); } public insertBefore(newChild: IRRNode, refChild: IRRNode | null): IRRNode { - if (refChild === null) return this.appendChild(newChild); - const childIndex = this.childNodes.indexOf(refChild); - if (childIndex == -1) - throw new Error( - "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", - ); - this.childNodes.splice(childIndex, 0, newChild); - newChild.parentElement = this; - newChild.parentNode = this; - newChild.ownerDocument = this.ownerDocument; - return newChild; + return insertBefore(this, newChild, refChild); } public removeChild(node: IRRNode): IRRNode { - const indexOfChild = this.childNodes.indexOf(node); - if (indexOfChild === -1) - throw new Error( - "Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode.", - ); - this.childNodes.splice(indexOfChild, 1); - node.parentElement = null; - node.parentNode = null; - return node; + return removeChild(this, node); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -741,6 +712,65 @@ export type CSSStyleDeclaration = Record & { removeProperty: (name: string) => string; }; +function appendChild(parent: IRRNode, newChild: IRRNode) { + if (parent.lastChild) { + parent.lastChild.nextSibling = newChild; + newChild.previousSibling = parent.lastChild; + } else { + parent.firstChild = newChild; + newChild.previousSibling = null; + } + parent.lastChild = newChild; + newChild.nextSibling = null; + newChild.parentNode = parent; + newChild.parentElement = parent; + newChild.ownerDocument = parent.ownerDocument; + return newChild; +} + +function insertBefore( + parent: IRRNode, + newChild: IRRNode, + refChild: IRRNode | null, +) { + if (!refChild) return appendChild(parent, newChild); + + if (refChild.parentNode !== parent) + throw new Error( + "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", + ); + + newChild.previousSibling = refChild.previousSibling; + refChild.previousSibling = newChild; + newChild.nextSibling = refChild; + + if (newChild.previousSibling) newChild.previousSibling.nextSibling = newChild; + else parent.firstChild = newChild; + + newChild.parentElement = parent; + newChild.parentNode = parent; + newChild.ownerDocument = parent.ownerDocument; + return newChild; +} + +function removeChild(parent: IRRNode, child: IRRNode) { + if (child.parentNode !== parent) + throw new Error( + "Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode.", + ); + if (child.previousSibling) + child.previousSibling.nextSibling = child.nextSibling; + else parent.firstChild = child.nextSibling; + if (child.nextSibling) + child.nextSibling.previousSibling = child.previousSibling; + else parent.lastChild = child.previousSibling; + child.previousSibling = null; + child.nextSibling = null; + child.parentElement = null; + child.parentNode = null; + return child; +} + // Enumerate nodeType value of standard HTML Node. export enum NodeType { PLACEHOLDER, // This isn't a node type. Enum type value starts from zero but NodeType value starts from 1. diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index eda0dd1912..eeddb03e1c 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -131,7 +131,8 @@ export class RRDocument extends BaseRRDocumentImpl(RRNode) { } destroyTree() { - this.childNodes = []; + this.firstChild = null; + this.lastChild = null; this.mirror.reset(); } diff --git a/packages/rrdom/test/diff.test.ts b/packages/rrdom/test/diff.test.ts index 77595adea6..04d638c314 100644 --- a/packages/rrdom/test/diff.test.ts +++ b/packages/rrdom/test/diff.test.ts @@ -1271,7 +1271,7 @@ describe('diff algorithm for rrdom', () => { expect(rrdom.mirror.getId(rrdom)).toBe(-2); expect(rrdom.mirror.getId(rrdom.body)).toBe(-6); - rrdom.childNodes = []; + while (rrdom.firstChild) rrdom.removeChild(rrdom.firstChild); /** * Rebuild the rrdom and make it looks like this: * -7 RRDocument diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 505cc700e4..421dc036e2 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -9,6 +9,7 @@ import { BaseRRMediaElementImpl, BaseRRNode, IRRDocumentType, + IRRNode, } from '../src/document'; describe('Basic RRDocument implementation', () => { @@ -34,6 +35,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -42,56 +44,103 @@ describe('Basic RRDocument implementation', () => { expect(node.toString()).toEqual('RRNode'); }); - it('can get first child node', () => { + it('can get and set first child node', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); - const childNode2 = new RRNode(); expect(parentNode.firstChild).toBeNull(); - parentNode.childNodes = [childNode1]; - expect(parentNode.firstChild).toBe(childNode1); - parentNode.childNodes = [childNode1, childNode2]; + parentNode.firstChild = childNode1; expect(parentNode.firstChild).toBe(childNode1); - parentNode.childNodes = [childNode2, childNode1]; - expect(parentNode.firstChild).toBe(childNode2); + parentNode.firstChild = null; + expect(parentNode.firstChild).toBeNull(); }); - it('can get last child node', () => { + it('can get and set last child node', () => { const parentNode = new RRNode(); const childNode1 = new RRNode(); - const childNode2 = new RRNode(); expect(parentNode.lastChild).toBeNull(); - parentNode.childNodes = [childNode1]; - expect(parentNode.lastChild).toBe(childNode1); - parentNode.childNodes = [childNode1, childNode2]; - expect(parentNode.lastChild).toBe(childNode2); - parentNode.childNodes = [childNode2, childNode1]; + parentNode.lastChild = childNode1; expect(parentNode.lastChild).toBe(childNode1); + parentNode.lastChild = null; + expect(parentNode.lastChild).toBeNull(); + }); + + it('can get and set preSibling', () => { + const node1 = new RRNode(); + const node2 = new RRNode(); + expect(node1.previousSibling).toBeNull(); + node1.previousSibling = node2; + expect(node1.previousSibling).toBe(node2); + node1.previousSibling = null; + expect(node1.previousSibling).toBeNull(); + }); + + it('can get and set nextSibling', () => { + const node1 = new RRNode(); + const node2 = new RRNode(); + expect(node1.nextSibling).toBeNull(); + node1.nextSibling = node2; + expect(node1.nextSibling).toBe(node2); + node1.nextSibling = null; + expect(node1.nextSibling).toBeNull(); }); - it('can get nextSibling', () => { + it('can get childNodes', () => { const parentNode = new RRNode(); + expect(parentNode.childNodes).toBeInstanceOf(Array); + expect(parentNode.childNodes.length).toBe(0); + const childNode1 = new RRNode(); + parentNode.firstChild = childNode1; + parentNode.lastChild = childNode1; + expect(parentNode.childNodes).toEqual([childNode1]); + const childNode2 = new RRNode(); - expect(parentNode.nextSibling).toBeNull(); - expect(childNode1.nextSibling).toBeNull(); - childNode1.parentNode = parentNode; - parentNode.childNodes = [childNode1]; - expect(childNode1.nextSibling).toBeNull(); - childNode2.parentNode = parentNode; - parentNode.childNodes = [childNode1, childNode2]; - expect(childNode1.nextSibling).toBe(childNode2); - expect(childNode2.nextSibling).toBeNull(); + parentNode.lastChild = childNode2; + childNode1.nextSibling = childNode2; + childNode2.previousSibling = childNode1; + expect(parentNode.childNodes).toEqual([childNode1, childNode2]); + + const childNode3 = new RRNode(); + parentNode.lastChild = childNode3; + childNode2.nextSibling = childNode3; + childNode3.previousSibling = childNode2; + expect(parentNode.childNodes).toEqual([ + childNode1, + childNode2, + childNode3, + ]); }); it('should return whether the node contains another node', () => { const parentNode = new RRNode(); + expect(parentNode.contains(parentNode)).toBeTruthy(); + expect(parentNode.contains(null as unknown as IRRNode)).toBeFalsy(); + expect(parentNode.contains(undefined as unknown as IRRNode)).toBeFalsy(); + expect(parentNode.contains({} as unknown as IRRNode)).toBeFalsy(); + expect( + parentNode.contains(new RRDocument().createElement('div')), + ).toBeFalsy(); const childNode1 = new RRNode(); const childNode2 = new RRNode(); - parentNode.childNodes = [childNode1]; + parentNode.firstChild = childNode1; + parentNode.lastChild = childNode1; + childNode1.parentNode = parentNode; expect(parentNode.contains(childNode1)).toBeTruthy(); expect(parentNode.contains(childNode2)).toBeFalsy(); - childNode1.childNodes = [childNode2]; + + parentNode.lastChild = childNode2; + childNode1.nextSibling = childNode2; + childNode2.previousSibling = childNode1; + childNode2.parentNode = childNode1; + expect(parentNode.contains(childNode1)).toBeTruthy(); expect(parentNode.contains(childNode2)).toBeTruthy(); + + const childNode3 = new RRNode(); + expect(parentNode.contains(childNode3)).toBeFalsy(); + childNode2.firstChild = childNode3; + childNode2.lastChild = childNode3; + childNode3.parentNode = childNode2; + expect(parentNode.contains(childNode3)).toBeTruthy(); }); it('should not implement appendChild', () => { @@ -143,6 +192,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -282,7 +332,7 @@ describe('Basic RRDocument implementation', () => { expect(() => node.removeChild(node.createElement('div')), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to execute 'removeChild' on 'RRDocument': The RRNode to be removed is not a child of this RRNode."`, + `"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`, ); expect(node.removeChild(documentType)).toBe(documentType); expect(documentType.parentNode).toBeNull(); @@ -369,6 +419,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -404,6 +455,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -686,23 +738,39 @@ describe('Basic RRDocument implementation', () => { const node = document.createElement('div'); expect(node.childNodes.length).toBe(0); - const child1 = document.createComment('span'); + const child1 = document.createElement('span'); expect(node.appendChild(child1)).toBe(child1); - expect(node.childNodes[0]).toEqual(child1); + expect(node.childNodes[0]).toBe(child1); + expect(node.firstChild).toBe(child1); + expect(node.lastChild).toBe(child1); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBeNull(); expect(child1.parentElement).toBe(node); expect(child1.parentNode).toBe(node); + expect(child1.ownerDocument).toBe(document); + expect(node.contains(child1)).toBeTruthy(); const child2 = document.createElement('p'); expect(node.appendChild(child2)).toBe(child2); - expect(node.childNodes[1]).toEqual(child2); + expect(node.childNodes[1]).toBe(child2); + expect(node.firstChild).toBe(child1); + expect(node.lastChild).toBe(child2); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBe(child2); + expect(child2.previousSibling).toBe(child1); + expect(child2.nextSibling).toBeNull(); expect(child2.parentElement).toBe(node); expect(child2.parentNode).toBe(node); + expect(child2.ownerDocument).toBe(document); + expect(node.contains(child1)).toBeTruthy(); + expect(node.contains(child2)).toBeTruthy(); }); it('can insert new child before an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); const child2 = document.createElement('h2'); + const child3 = document.createElement('h3'); expect(() => node.insertBefore(node, child1), ).toThrowErrorMatchingInlineSnapshot( @@ -710,42 +778,119 @@ describe('Basic RRDocument implementation', () => { ); expect(node.insertBefore(child1, null)).toBe(child1); expect(node.childNodes[0]).toBe(child1); + expect(node.childNodes.length).toBe(1); + expect(node.firstChild).toBe(child1); + expect(node.lastChild).toBe(child1); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBeNull(); expect(child1.parentNode).toBe(node); expect(child1.parentElement).toBe(node); + expect(child1.ownerDocument).toBe(document); + expect(node.contains(child1)).toBeTruthy(); expect(node.insertBefore(child2, child1)).toBe(child2); - expect(node.childNodes.length).toBe(2); - expect(node.childNodes[0]).toBe(child2); - expect(node.childNodes[1]).toBe(child1); + expect(node.childNodes).toEqual([child2, child1]); + expect(node.firstChild).toBe(child2); + expect(node.lastChild).toBe(child1); + expect(child1.previousSibling).toBe(child2); + expect(child1.nextSibling).toBeNull(); + expect(child2.previousSibling).toBeNull(); + expect(child2.nextSibling).toBe(child1); expect(child2.parentNode).toBe(node); expect(child2.parentElement).toBe(node); + expect(child2.ownerDocument).toBe(document); + expect(node.contains(child2)).toBeTruthy(); + expect(node.contains(child1)).toBeTruthy(); + + expect(node.insertBefore(child3, child1)).toBe(child3); + expect(node.childNodes).toEqual([child2, child3, child1]); + expect(node.firstChild).toBe(child2); + expect(node.lastChild).toBe(child1); + expect(child1.previousSibling).toBe(child3); + expect(child1.nextSibling).toBeNull(); + expect(child3.previousSibling).toBe(child2); + expect(child3.nextSibling).toBe(child1); + expect(child2.previousSibling).toBeNull(); + expect(child2.nextSibling).toBe(child3); + expect(child3.parentNode).toBe(node); + expect(child3.parentElement).toBe(node); + expect(child3.ownerDocument).toBe(document); + expect(node.contains(child2)).toBeTruthy(); + expect(node.contains(child3)).toBeTruthy(); + expect(node.contains(child1)).toBeTruthy(); }); it('can remove an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); const child2 = document.createElement('h2'); + const child3 = document.createElement('h3'); node.appendChild(child1); node.appendChild(child2); - expect(node.childNodes.length).toBe(2); - expect(child1.parentNode).toBe(node); - expect(child2.parentNode).toBe(node); - expect(child1.parentElement).toBe(node); - expect(child2.parentElement).toBe(node); + node.appendChild(child3); + expect(node.childNodes).toEqual([child1, child2, child3]); expect(() => node.removeChild(document.createElement('div')), ).toThrowErrorMatchingInlineSnapshot( - `"Failed to execute 'removeChild' on 'RRElement': The RRNode to be removed is not a child of this RRNode."`, + `"Failed to execute 'removeChild' on 'RRNode': The RRNode to be removed is not a child of this RRNode."`, ); - expect(node.removeChild(child1)).toBe(child1); - expect(child1.parentNode).toBeNull(); - expect(child1.parentElement).toBeNull(); - expect(node.childNodes.length).toBe(1); + // Remove the middle child. expect(node.removeChild(child2)).toBe(child2); - expect(node.childNodes.length).toBe(0); + expect(node.childNodes).toEqual([child1, child3]); + expect(node.contains(child2)).toBeFalsy(); + expect(node.firstChild).toBe(child1); + expect(node.lastChild).toBe(child3); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBe(child3); + expect(child3.previousSibling).toBe(child1); + expect(child3.nextSibling).toBeNull(); + expect(child2.previousSibling).toBeNull(); + expect(child2.nextSibling).toBeNull(); expect(child2.parentNode).toBeNull(); expect(child2.parentElement).toBeNull(); + + // Remove the previous child. + expect(node.removeChild(child1)).toBe(child1); + expect(node.childNodes).toEqual([child3]); + expect(node.contains(child1)).toBeFalsy(); + expect(node.firstChild).toBe(child3); + expect(node.lastChild).toBe(child3); + expect(child3.previousSibling).toBeNull(); + expect(child3.nextSibling).toBeNull(); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBeNull(); + expect(child1.parentNode).toBeNull(); + expect(child1.parentElement).toBeNull(); + + node.insertBefore(child1, child3); + expect(node.childNodes).toEqual([child1, child3]); + // Remove the next child. + expect(node.removeChild(child3)).toBe(child3); + expect(node.childNodes).toEqual([child1]); + expect(node.contains(child3)).toBeFalsy(); + expect(node.contains(child1)).toBeTruthy(); + expect(node.firstChild).toBe(child1); + expect(node.lastChild).toBe(child1); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBeNull(); + expect(child3.previousSibling).toBeNull(); + expect(child3.nextSibling).toBeNull(); + expect(child3.parentNode).toBeNull(); + expect(child3.parentElement).toBeNull(); + + // Remove all children. + expect(node.removeChild(child1)).toBe(child1); + expect(node.childNodes).toEqual([]); + expect(node.contains(child1)).toBeFalsy(); + expect(node.contains(child2)).toBeFalsy(); + expect(node.contains(child3)).toBeFalsy(); + expect(node.firstChild).toBeNull(); + expect(node.lastChild).toBeNull(); + expect(child1.previousSibling).toBeNull(); + expect(child1.nextSibling).toBeNull(); + expect(child1.parentNode).toBeNull(); + expect(child1.parentElement).toBeNull(); }); }); @@ -768,6 +913,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -803,6 +949,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -838,6 +985,7 @@ describe('Basic RRDocument implementation', () => { expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); expect(node.lastChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); @@ -870,6 +1018,7 @@ describe('Basic RRDocument implementation', () => { expect(node.ELEMENT_NODE).toBe(document.ELEMENT_NODE); expect(node.TEXT_NODE).toBe(document.TEXT_NODE); expect(node.firstChild).toBeNull(); + expect(node.previousSibling).toBeNull(); expect(node.nextSibling).toBeNull(); expect(node.contains).toBeDefined(); expect(node.appendChild).toBeDefined(); diff --git a/packages/rrweb/src/rrdom/index.ts b/packages/rrweb/src/rrdom/index.ts deleted file mode 100644 index 28b5e59040..0000000000 --- a/packages/rrweb/src/rrdom/index.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { RRdomTreeNode, AnyObject } from './tree-node'; - -class RRdomTree { - private readonly symbol = '__rrdom__'; - - public initialize(object: AnyObject) { - this._node(object); - - return object; - } - - public hasChildren(object: AnyObject): boolean { - return Boolean(this._node(object).hasChildren); - } - - public firstChild(object: AnyObject) { - return this._node(object).firstChild || null; - } - - public lastChild(object: AnyObject) { - return this._node(object).lastChild || null; - } - - public previousSibling(object: AnyObject) { - return this._node(object).previousSibling || null; - } - - public nextSibling(object: AnyObject) { - return this._node(object).nextSibling || null; - } - - public parent(object: AnyObject) { - return this._node(object).parent || null; - } - - public insertAfter(referenceObject: AnyObject, newObject: AnyObject) { - const referenceNode = this._node(referenceObject); - const nextNode = this._node(referenceNode.nextSibling); - const newNode = this._node(newObject); - const parentNode = this._node(referenceNode.parent); - - if (newNode.isAttached) { - throw new Error('Node already attached'); - } - if (!referenceNode) { - throw new Error('Reference node not attached'); - } - - newNode.parent = referenceNode.parent; - newNode.previousSibling = referenceObject; - newNode.nextSibling = referenceNode.nextSibling; - referenceNode.nextSibling = newObject; - - if (nextNode) { - nextNode.previousSibling = newObject; - } - - if (parentNode && parentNode.lastChild === referenceObject) { - parentNode.lastChild = newObject; - } - - if (parentNode) { - parentNode.childrenChanged(); - } - - return newObject; - } - - public insertBefore(referenceObject: AnyObject, newObject: AnyObject) { - const referenceNode = this._node(referenceObject); - const prevNode = this._node(referenceNode.previousSibling); - const newNode = this._node(newObject); - const parentNode = this._node(referenceNode.parent); - - if (newNode.isAttached) { - throw new Error('Node already attached'); - } - if (!referenceNode) { - throw new Error('Reference node not attached'); - } - - newNode.parent = referenceNode.parent; - newNode.previousSibling = referenceNode.previousSibling; - newNode.nextSibling = referenceObject; - referenceNode.previousSibling = newObject; - - if (prevNode) { - prevNode.nextSibling = newObject; - } - - if (parentNode && parentNode.firstChild === referenceObject) { - parentNode.firstChild = newObject; - } - - if (parentNode) { - parentNode.childrenChanged(); - } - - return newObject; - } - - public appendChild(referenceObject: AnyObject, newObject: AnyObject) { - const referenceNode = this._node(referenceObject); - const newNode = this._node(newObject); - - if (newNode.isAttached) { - throw new Error('Node already attached'); - } - if (!referenceNode) { - throw new Error('Reference node not attached'); - } - - if (referenceNode.hasChildren) { - this.insertAfter(referenceNode.lastChild!, newObject); - } else { - newNode.parent = referenceObject; - referenceNode.firstChild = newObject; - referenceNode.lastChild = newObject; - referenceNode.childrenChanged(); - } - - return newObject; - } - - public remove(removeObject: AnyObject) { - const removeNode = this._node(removeObject); - const parentNode = this._node(removeNode.parent); - const prevNode = this._node(removeNode.previousSibling); - const nextNode = this._node(removeNode.nextSibling); - - if (parentNode) { - if (parentNode.firstChild === removeObject) { - parentNode.firstChild = removeNode.nextSibling; - } - - if (parentNode.lastChild === removeObject) { - parentNode.lastChild = removeNode.previousSibling; - } - } - - if (prevNode) { - prevNode.nextSibling = removeNode.nextSibling; - } - - if (nextNode) { - nextNode.previousSibling = removeNode.previousSibling; - } - - removeNode.parent = null; - removeNode.previousSibling = null; - removeNode.nextSibling = null; - removeNode.cachedIndex = -1; - removeNode.cachedIndexVersion = NaN; - - if (parentNode) { - parentNode.childrenChanged(); - } - - return removeObject; - } - - private _node(object: AnyObject | null): RRdomTreeNode { - if (!object) { - throw new Error('Object is falsy'); - } - - if (this.symbol in object) { - return object[this.symbol] as RRdomTreeNode; - } - - return (object[this.symbol] = new RRdomTreeNode()); - } -} diff --git a/packages/rrweb/src/rrdom/tree-node.ts b/packages/rrweb/src/rrdom/tree-node.ts deleted file mode 100644 index 88839ee670..0000000000 --- a/packages/rrweb/src/rrdom/tree-node.ts +++ /dev/null @@ -1,51 +0,0 @@ -export type AnyObject = { [key: string]: any; __rrdom__?: RRdomTreeNode }; - -export class RRdomTreeNode implements AnyObject { - public parent: AnyObject | null = null; - public previousSibling: AnyObject | null = null; - public nextSibling: AnyObject | null = null; - - public firstChild: AnyObject | null = null; - public lastChild: AnyObject | null = null; - - // This value is incremented anytime a children is added or removed - public childrenVersion = 0; - // The last child object which has a cached index - public childIndexCachedUpTo: AnyObject | null = null; - - /** - * This value represents the cached node index, as long as - * cachedIndexVersion matches with the childrenVersion of the parent - */ - public cachedIndex = -1; - public cachedIndexVersion = NaN; - - public get isAttached() { - return Boolean(this.parent || this.previousSibling || this.nextSibling); - } - - public get hasChildren() { - return Boolean(this.firstChild); - } - - public childrenChanged() { - this.childrenVersion = (this.childrenVersion + 1) & 0xffffffff; - this.childIndexCachedUpTo = null; - } - - public getCachedIndex(parentNode: AnyObject) { - if (this.cachedIndexVersion !== parentNode.childrenVersion) { - this.cachedIndexVersion = NaN; - // cachedIndex is no longer valid - return -1; - } - - return this.cachedIndex; - } - - public setCachedIndex(parentNode: AnyObject, index: number) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - this.cachedIndexVersion = parentNode.childrenVersion; - this.cachedIndex = index; - } -} diff --git a/packages/rrweb/test/benchmark/replay-fast-forward.test.ts b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts index b5bb71a378..6fbed1a055 100644 --- a/packages/rrweb/test/benchmark/replay-fast-forward.test.ts +++ b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts @@ -1,12 +1,14 @@ import * as fs from 'fs'; import * as path from 'path'; +import * as https from 'https'; import type { eventWithTime } from '@rrweb/types'; import type { recordOptions } from '../../src/types'; import { launchPuppeteer, ISuite } from '../utils'; const suites: Array<{ title: string; - eval: string; + eval?: string; + eventURL?: string; eventsString?: string; times?: number; // defaults to 5 }> = [ @@ -66,6 +68,12 @@ const suites: Array<{ `, times: 3, }, + { + title: 'real events recorded on bugs.chromium.org', + eventURL: + 'https://raw.githubusercontent.com/rrweb-io/benchmark-events/main/rrdom-benchmark-1.json', + times: 3, + }, ]; function avg(v: number[]): number { @@ -86,9 +94,6 @@ describe('benchmark: replayer fast-forward performance', () => { const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js'); code = fs.readFileSync(bundlePath, 'utf8'); - - for (const suite of suites) - suite.eventsString = await generateEvents(suite.eval); }, 600_000); afterAll(async () => { @@ -99,6 +104,13 @@ describe('benchmark: replayer fast-forward performance', () => { it( suite.title, async () => { + if (suite.eval) suite.eventsString = await generateEvents(suite.eval); + else if (suite.eventURL) { + suite.eventsString = await fetchEventsWithCache( + suite.eventURL, + './temp', + ); + } else throw new Error('Invalid suite'); suite.times = suite.times ?? 5; const durations: number[] = []; for (let i = 0; i < suite.times; i++) { @@ -168,4 +180,37 @@ describe('benchmark: replayer fast-forward performance', () => { await page.close(); return eventsString; } + + /** + * Fetch the recorded events from URL. If the events are already cached, read from the cache. + */ + async function fetchEventsWithCache( + eventURL: string, + cacheFolder: string, + ): Promise { + const fileName = eventURL.split('/').pop() || ''; + const cachePath = path.resolve(__dirname, cacheFolder, fileName); + if (fs.existsSync(cachePath)) return fs.readFileSync(cachePath, 'utf8'); + return new Promise((resolve, reject) => { + https + .get(eventURL, (resp) => { + let data = ''; + resp.on('data', (chunk) => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + const folderAbsolutePath = path.resolve(__dirname, cacheFolder); + if (!fs.existsSync(folderAbsolutePath)) + fs.mkdirSync(path.resolve(__dirname, cacheFolder), { + recursive: true, + }); + fs.writeFileSync(cachePath, data); + }); + }) + .on('error', (err) => { + reject(err); + }); + }); + } });