improve rrdom performance (#1127)
* add more check to rrdom to make diff algorithm more robust * fix: selector match in iframe is case-insensitive add try catch to some fragile points * test: increase timeout value for Jest * improve code style * fix: failed to execute insertBefore on Node in the diff function this happens when ids of doctype or html element are changed in the virtual dom also improve the code quality * refactor diff function to make the code cleaner * fix: virtual nodes are passed to plugin's onBuild function * refactor the diff function and adjust the order of diff work. * call afterAppend hook in a consistent traversal order * improve the performance of the "contains" function reduce the complexity from O(n) to O(logn) a specific benchmark is needed to add further * add a real events for benchmark * refactor: change the data structure of childNodes from array to linked list * remove legacy code in rrweb package * update unit tests * update change log
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user