Speed up snapshotting of many new dom nodes (#903)
* Speed up snapshotting of many new dom nodes By avoiding reflow we shave about 15-25% off our snapshotting time * Improve newlyAddedElement docs * Optimize needMaskingText by using el.closest and less recursion * Serve all rrweb dist files * Split serializeNode into smaller functions Makes it easier to profile * Slow down cpu enhance tracing on fast machines * Increase timeout * Perf: only loop through ancestors when they have something to compare to * Perf: `hasNode` is cheaper than `getMeta` * Perf: If parents where already checked, no need to do it again * Perf: reverse for loops are faster Because they only do the .lenght check once. In this case I don't think we'll see much performance gains if any * Clean up code * Perf: check ancestors once with isBlocked * guessing this might fixes canvas test * Update packages/rrweb/src/record/observers/canvas/webgl.ts Co-authored-by: yz-yu <yanzhen@smartx.com> * Fix #904 (#906) Properly remove crossorigin attribute * Bump minimist from 1.2.5 to 1.2.6 (#902) Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6. - [Release notes](https://github.com/substack/minimist/releases) - [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6) --- updated-dependencies: - dependency-name: minimist dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: yz-yu <yanzhen@smartx.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1131,6 +1131,126 @@ exports[`record is safe to checkout during async callbacks 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record should record scroll position 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 1,
|
||||
\\"name\\": \\"html\\",
|
||||
\\"publicId\\": \\"\\",
|
||||
\\"systemId\\": \\"\\",
|
||||
\\"id\\": 2
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 4
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 6
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"size\\": \\"40\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 7
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||
\\"id\\": 8
|
||||
}
|
||||
],
|
||||
\\"id\\": 5
|
||||
}
|
||||
],
|
||||
\\"id\\": 3
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 5,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"p\\",
|
||||
\\"attributes\\": {
|
||||
\\"style\\": \\"overflow: auto; height: Npx; width: Npx;\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 9
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"parentId\\": 9,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"testtesttesttesttesttesttesttesttesttest\\",
|
||||
\\"id\\": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 3,
|
||||
\\"id\\": 9,
|
||||
\\"x\\": 10,
|
||||
\\"y\\": 10
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`record without CSSGroupingRule support captures nested stylesheet rules 1`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
// tslint:disable:no-console no-any
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { Page } from 'puppeteer';
|
||||
import type { eventWithTime, recordOptions } from '../../src/types';
|
||||
import { startServer, launchPuppeteer, replaceLast, ISuite } from '../utils';
|
||||
import { startServer, launchPuppeteer, ISuite, getServerURL } from '../utils';
|
||||
|
||||
const suites: Array<
|
||||
{
|
||||
title: string;
|
||||
eval: string;
|
||||
times?: number; // defaults to 5
|
||||
} & ({ html: string } | { url: string })
|
||||
> = [
|
||||
// {
|
||||
// title: 'benchmarking external website',
|
||||
// url: 'http://localhost:5050',
|
||||
// eval: 'document.querySelector("button").click()',
|
||||
// times: 10,
|
||||
// },
|
||||
{
|
||||
title: 'create 1000x10 DOM nodes',
|
||||
html: 'benchmark-dom-mutation.html',
|
||||
eval: 'window.workload()',
|
||||
times: 10,
|
||||
},
|
||||
{
|
||||
title: 'create 1000x10x2 DOM nodes and remove a bunch of them',
|
||||
html: 'benchmark-dom-mutation-add-and-remove.html',
|
||||
eval: 'window.workload()',
|
||||
times: 10,
|
||||
},
|
||||
];
|
||||
|
||||
function avg(v: number[]): number {
|
||||
return v.reduce((prev, cur) => prev + cur, 0) / v.length;
|
||||
}
|
||||
|
||||
describe('benchmark: mutation observer', () => {
|
||||
let code: ISuite['code'];
|
||||
jest.setTimeout(240000);
|
||||
let page: ISuite['page'];
|
||||
let browser: ISuite['browser'];
|
||||
let server: ISuite['server'];
|
||||
@@ -20,9 +48,6 @@ describe('benchmark: mutation observer', () => {
|
||||
dumpio: true,
|
||||
headless: true,
|
||||
});
|
||||
|
||||
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
|
||||
code = fs.readFileSync(bundlePath, 'utf8');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -36,30 +61,19 @@ describe('benchmark: mutation observer', () => {
|
||||
|
||||
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>
|
||||
`,
|
||||
);
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
};
|
||||
|
||||
const suites: {
|
||||
title: string;
|
||||
html: string;
|
||||
times?: number; // default to 5
|
||||
}[] = [
|
||||
{
|
||||
title: 'create 1000x10 DOM nodes',
|
||||
html: 'benchmark-dom-mutation.html',
|
||||
times: 10,
|
||||
},
|
||||
];
|
||||
const addRecordingScript = async (page: Page) => {
|
||||
// const scriptUrl = `${getServerURL(server)}/rrweb-1.1.3.js`;
|
||||
const scriptUrl = `${getServerURL(server)}/rrweb.js`;
|
||||
await page.evaluate((url) => {
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.src = url;
|
||||
document.head.append(scriptEl);
|
||||
}, scriptUrl);
|
||||
await page.waitForFunction('window.rrweb');
|
||||
};
|
||||
|
||||
for (const suite of suites) {
|
||||
it(suite.title, async () => {
|
||||
@@ -68,12 +82,19 @@ describe('benchmark: mutation observer', () => {
|
||||
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(() => {
|
||||
const loadPage = async () => {
|
||||
if ('html' in suite) {
|
||||
await page.goto('about:blank');
|
||||
await page.setContent(getHtml.call(this, suite.html));
|
||||
} else {
|
||||
await page.goto(suite.url);
|
||||
}
|
||||
|
||||
await addRecordingScript(page);
|
||||
};
|
||||
|
||||
const getDuration = async (): Promise<number> => {
|
||||
return (await page.evaluate((triggerWorkloadScript) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let start = 0;
|
||||
let lastEvent: eventWithTime | null;
|
||||
@@ -94,14 +115,55 @@ describe('benchmark: mutation observer', () => {
|
||||
const record = (window as any).rrweb.record;
|
||||
record(options);
|
||||
|
||||
(window as any).workload();
|
||||
|
||||
start = Date.now();
|
||||
setTimeout(() => {
|
||||
eval(triggerWorkloadScript);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
record.addCustomEvent('FTAG', {});
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
})) as number;
|
||||
}, suite.eval)) as number;
|
||||
};
|
||||
|
||||
// generate profile.json file
|
||||
const profileFilename = `profile-${new Date().toISOString()}.json`;
|
||||
const tempDirectory = path.resolve(path.join(__dirname, '../../temp'));
|
||||
fs.mkdirSync(tempDirectory, { recursive: true });
|
||||
const profilePath = path.resolve(tempDirectory, profileFilename);
|
||||
|
||||
const client = await page.target().createCDPSession();
|
||||
await client.send('Emulation.setCPUThrottlingRate', { rate: 6 });
|
||||
|
||||
await page.tracing.start({
|
||||
path: profilePath,
|
||||
screenshots: true,
|
||||
categories: [
|
||||
'-*',
|
||||
'devtools.timeline',
|
||||
'v8.execute',
|
||||
'disabled-by-default-devtools.timeline',
|
||||
'disabled-by-default-devtools.timeline.frame',
|
||||
'toplevel',
|
||||
'blink.console',
|
||||
'blink.user_timing',
|
||||
'latencyInfo',
|
||||
'disabled-by-default-devtools.timeline.stack',
|
||||
'disabled-by-default-v8.cpu_profiler',
|
||||
'disabled-by-default-v8.cpu_profiler.hires',
|
||||
],
|
||||
});
|
||||
await loadPage();
|
||||
await getDuration();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.tracing.stop();
|
||||
await client.send('Emulation.setCPUThrottlingRate', { rate: 1 });
|
||||
|
||||
// calculate durations
|
||||
const times = suite.times ?? 5;
|
||||
const durations: number[] = [];
|
||||
for (let i = 0; i < times; i++) {
|
||||
await loadPage();
|
||||
const duration = await getDuration();
|
||||
durations.push(duration);
|
||||
}
|
||||
|
||||
@@ -112,6 +174,7 @@ describe('benchmark: mutation observer', () => {
|
||||
durations: durations.join(', '),
|
||||
},
|
||||
]);
|
||||
console.log('profile: ', profilePath);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<html>
|
||||
<body></body>
|
||||
<script>
|
||||
function add() {
|
||||
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);
|
||||
}
|
||||
|
||||
function remove() {
|
||||
// const divs = Array.from(document.querySelectorAll('div'));
|
||||
// const half = divs.length / 2;
|
||||
// while (divs.length > half) {
|
||||
// const i = (divs.length * Math.random()) | 0;
|
||||
// divs[i].remove();
|
||||
// divs.splice(i, 1);
|
||||
// }
|
||||
document.querySelectorAll('div').forEach((node) => {
|
||||
node.parentNode.removeChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
window.workload = () => {
|
||||
add();
|
||||
remove();
|
||||
add();
|
||||
};
|
||||
</script>
|
||||
</html>
|
||||
@@ -192,6 +192,23 @@ describe('record', function (this: ISuite) {
|
||||
assertSnapshot(ctx.events);
|
||||
});
|
||||
|
||||
it('should record scroll position', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
const p = document.createElement('p');
|
||||
p.innerText = 'testtesttesttesttesttesttesttesttesttest';
|
||||
p.setAttribute('style', 'overflow: auto; height: 1px; width: 1px;');
|
||||
document.body.appendChild(p);
|
||||
p.scrollTop = 10;
|
||||
p.scrollLeft = 10;
|
||||
});
|
||||
await waitForRAF(ctx.page);
|
||||
assertSnapshot(ctx.events);
|
||||
});
|
||||
|
||||
it('can add custom event', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
const { record, addCustomEvent } = ((window as unknown) as IWindow).rrweb;
|
||||
|
||||
@@ -56,7 +56,11 @@ export const startServer = (defaultPort: number = 3030) =>
|
||||
const sanitizePath = path
|
||||
.normalize(parsedUrl.pathname!)
|
||||
.replace(/^(\.\.[\/\\])+/, '');
|
||||
const pathname = path.join(__dirname, sanitizePath);
|
||||
|
||||
let pathname = path.join(__dirname, sanitizePath);
|
||||
if (/^\/rrweb.*\.js.*/.test(sanitizePath)) {
|
||||
pathname = path.join(__dirname, `../dist`, sanitizePath);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = fs.readFileSync(pathname);
|
||||
|
||||
Reference in New Issue
Block a user