Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,15 @@ function serializeNode(
);
const tagName = getValidTagName(n as HTMLElement);
let attributes: attributes = {};
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
attributes[name] = transformAttribute(doc, tagName, name, value);
const len = (n as HTMLElement).attributes.length;
for (let i = 0; i < len; i++) {
const attr = (n as HTMLElement).attributes[i];
attributes[attr.name] = transformAttribute(
doc,
tagName,
attr.name,
attr.value,
);
}
// remote css
if (tagName === 'link' && inlineStylesheet) {
Expand Down
2 changes: 1 addition & 1 deletion packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export default class MutationBuffer {
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
}
let sn = serializeNodeWithId(n, {
const sn = serializeNodeWithId(n, {
doc: this.doc,
mirror: this.mirror,
blockClass: this.blockClass,
Expand Down
117 changes: 117 additions & 0 deletions packages/rrweb/test/benchmark/dom-mutation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// tslint:disable:no-console no-any
import * as fs from 'fs';
import * as path from 'path';
import type { eventWithTime, recordOptions } from '../../src/types';
import { startServer, launchPuppeteer, replaceLast, ISuite } from '../utils';

function avg(v: number[]): number {
return v.reduce((prev, cur) => prev + cur, 0) / v.length;
}

describe('benchmark: mutation observer', () => {
let code: ISuite['code'];
let page: ISuite['page'];
let browser: ISuite['browser'];
let server: ISuite['server'];

beforeAll(async () => {
server = await startServer();
browser = await launchPuppeteer({
dumpio: true,
headless: true,
});

const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
code = fs.readFileSync(bundlePath, 'utf8');
});

afterEach(async () => {
await page.close();
});

afterAll(async () => {
server.close();
await browser.close();
});

const getHtml = (fileName: string): string => {
const filePath = path.resolve(__dirname, `../html/${fileName}`);
const html = fs.readFileSync(filePath, 'utf8');
return replaceLast(
html,
'</body>',
`
<script>
${code}
</script>
</body>
`,
);
};

const suites: {
title: string;
html: string;
times?: number; // default to 5
}[] = [
{
title: 'create 1000x10 DOM nodes',
html: 'benchmark-dom-mutation.html',
times: 10,
},
];

for (const suite of suites) {
it(suite.title, async () => {
page = await browser.newPage();
page.on('console', (message) =>
console.log(`${message.type().toUpperCase()} ${message.text()}`),
);

const times = suite.times ?? 5;
const durations: number[] = [];
for (let i = 0; i < times; i++) {
await page.goto('about:blank');
await page.setContent(getHtml.call(this, suite.html));
const duration = (await page.evaluate(() => {
return new Promise((resolve, reject) => {
let start = 0;
let lastEvent: eventWithTime | null;
const options: recordOptions<eventWithTime> = {
emit: (event) => {
// console.log(event.type, event.timestamp);
if (event.type !== 5 || event.data.tag !== 'FTAG') {
lastEvent = event;
return;
}
if (!lastEvent) {
reject('no events recorded');
return;
}
resolve(lastEvent.timestamp - start);
},
};
const record = (window as any).rrweb.record;
record(options);

(window as any).workload();

start = Date.now();
setTimeout(() => {
record.addCustomEvent('FTAG', {});
}, 0);
});
})) as number;
durations.push(duration);
}

console.table([
{
...suite,
duration: avg(durations),
durations: durations.join(', '),
},
]);
});
}
});
42 changes: 11 additions & 31 deletions packages/rrweb/test/e2e/webgl.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import type * as puppeteer from 'puppeteer';
Expand All @@ -8,20 +7,13 @@ import {
getServerURL,
replaceLast,
waitForRAF,
generateRecordSnippet,
ISuite,
} from '../utils';
import type { recordOptions, eventWithTime } from '../../src/types';
import { toMatchImageSnapshot } from 'jest-image-snapshot';
expect.extend({ toMatchImageSnapshot });

interface ISuite {
code: string;
browser: puppeteer.Browser;
server: http.Server;
page: puppeteer.Page;
events: eventWithTime[];
serverURL: string;
}

describe('e2e webgl', () => {
let code: ISuite['code'];
let page: ISuite['page'];
Expand Down Expand Up @@ -59,42 +51,30 @@ describe('e2e webgl', () => {
`
<script>
${code}
window.snapshots = [];
rrweb.record({
emit: event => {
window.snapshots.push(event);
},
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas},
plugins: ${options.plugins}
});
${generateRecordSnippet(options)}
</script>
</body>
`,
);
};

const fakeGoto = async (page: puppeteer.Page, url: string) => {
const fakeGoto = async (p: puppeteer.Page, url: string) => {
const intercept = async (request: puppeteer.HTTPRequest) => {
await request.respond({
status: 200,
contentType: 'text/html',
body: ' ', // non-empty string or page will load indefinitely
});
};
await page.setRequestInterception(true);
page.on('request', intercept);
await page.goto(url);
page.off('request', intercept);
await page.setRequestInterception(false);
await p.setRequestInterception(true);
p.on('request', intercept);
await p.goto(url);
p.off('request', intercept);
await p.setRequestInterception(false);
};

const hideMouseAnimation = async (page: puppeteer.Page) => {
await page.addStyleTag({
const hideMouseAnimation = async (p: puppeteer.Page) => {
await p.addStyleTag({
content: '.replayer-mouse-tail{display: none !important;}',
});
};
Expand Down
27 changes: 27 additions & 0 deletions packages/rrweb/test/html/benchmark-dom-mutation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<html>
<body></body>
<script>
window.workload = () => {
const branches = 1000;
const depth = 10;
const frag = document.createDocumentFragment();
for (let b = 0; b < branches; b++) {
const node = document.createElement('div');
let d = 0;
node.setAttribute('branch', b.toString());
node.setAttribute('depth', d.toString());
let current = node;
while (d < depth - 1) {
d++;
const child = document.createElement('div');
child.setAttribute('branch', b.toString());
child.setAttribute('depth', d.toString());
current.appendChild(child);
current = child;
}
frag.appendChild(node);
}
document.body.appendChild(frag);
};
</script>
</html>
31 changes: 5 additions & 26 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// tslint:disable:no-console
import * as fs from 'fs';
import * as path from 'path';
import type * as http from 'http';
import type * as puppeteer from 'puppeteer';
import {
assertSnapshot,
Expand All @@ -9,21 +9,12 @@ import {
launchPuppeteer,
waitForRAF,
replaceLast,
generateRecordSnippet,
ISuite,
} from './utils';
import { recordOptions, eventWithTime, EventType } from '../src/types';
import { visitSnapshot, NodeType } from 'rrweb-snapshot';

interface ISuite {
server: http.Server;
serverURL: string;
code: string;
browser: puppeteer.Browser;
}

interface IMimeType {
[key: string]: string;
}

describe('record integration tests', function (this: ISuite) {
jest.setTimeout(10_000);

Expand All @@ -40,19 +31,7 @@ describe('record integration tests', function (this: ISuite) {
<script>
${code}
window.Date.now = () => new Date(Date.UTC(2018, 10, 15, 8)).valueOf();
window.snapshots = [];
rrweb.record({
emit: event => {
window.snapshots.push(event);
},
maskTextSelector: ${JSON.stringify(options.maskTextSelector)},
maskAllInputs: ${options.maskAllInputs},
maskInputOptions: ${JSON.stringify(options.maskAllInputs)},
userTriggeredOnInput: ${options.userTriggeredOnInput},
maskTextFn: ${options.maskTextFn},
recordCanvas: ${options.recordCanvas},
plugins: ${options.plugins}
});
${generateRecordSnippet(options)}
</script>
</body>
`,
Expand All @@ -73,7 +52,7 @@ describe('record integration tests', function (this: ISuite) {
const pluginsCode = [
path.resolve(__dirname, '../dist/plugins/console-record.min.js'),
]
.map((path) => fs.readFileSync(path, 'utf8'))
.map((p) => fs.readFileSync(p, 'utf8'))
.join();
code = fs.readFileSync(bundlePath, 'utf8') + pluginsCode;
});
Expand Down
Loading