diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json index 98e4547838..9ed387c2be 100644 --- a/packages/rrweb/package.json +++ b/packages/rrweb/package.json @@ -5,8 +5,8 @@ "scripts": { "prepare": "npm run prepack", "prepack": "npm run bundle", - "test": "npm run bundle:browser && jest", - "test:headless": "npm run bundle:browser && PUPPETEER_HEADLESS=true jest", + "test": "npm run bundle:browser && jest --testPathIgnorePatterns test/benchmark", + "test:headless": "PUPPETEER_HEADLESS=true npm run test", "test:watch": "PUPPETEER_HEADLESS=true npm run test -- --watch", "repl": "npm run bundle:browser && node scripts/repl.js", "dev": "yarn bundle:browser --watch", @@ -15,7 +15,8 @@ "typings": "tsc -d --declarationDir typings", "check-types": "tsc -noEmit", "prepublish": "npm run typings && npm run bundle", - "lint": "yarn eslint src" + "lint": "yarn eslint src", + "benchmark": "jest test/benchmark" }, "repository": { "type": "git", diff --git a/packages/rrweb/test/benchmark/replay-fast-forward.test.ts b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts new file mode 100644 index 0000000000..4e17acdfdf --- /dev/null +++ b/packages/rrweb/test/benchmark/replay-fast-forward.test.ts @@ -0,0 +1,170 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { eventWithTime, recordOptions } from '../../src/types'; +import { launchPuppeteer, ISuite } from '../utils'; + +const suites: Array<{ + title: string; + eval: string; + eventsString?: string; + times?: number; // defaults to 5 +}> = [ + { + title: 'append 70 x 70 x 70 elements', + eval: ` + () => { + return new Promise((resolve) => { + const branches = 70; + const depth = 3; + // append children for the current node + function expand(node, depth) { + if (depth == 0) return; + for (let b = 0; b < branches; b++) { + const child = document.createElement('div'); + node.appendChild(child); + expand(child, depth - 1); + } + } + const frag = document.createDocumentFragment(); + const node = document.createElement('div'); + expand(node, depth); + frag.appendChild(node); + document.body.appendChild(frag); + resolve(); + }); + }; + `, + times: 3, + }, + { + title: 'append 1000 elements and reverse their order', + eval: ` + () => { + return new Promise(async (resolve) => { + const branches = 1000; + function waitForTimeout(timeout) { + return new Promise((resolve) => setTimeout(() => resolve(), timeout)); + } + const frag = document.createDocumentFragment(); + const node = document.createElement('div'); + for (let b = 0; b < branches; b++) { + const child = document.createElement('div'); + node.appendChild(child); + child.textContent = b + 1; + } + frag.appendChild(node); + document.body.appendChild(frag); + const children = node.children; + await waitForTimeout(0); + // reverse the order of children + for (let i = children.length - 1; i >= 0; i--) + node.appendChild(node.children[i]); + resolve(); + }); + }; + `, + times: 3, + }, +]; + +function avg(v: number[]): number { + return v.reduce((prev, cur) => prev + cur, 0) / v.length; +} + +describe('benchmark: replayer fast-forward performance', () => { + jest.setTimeout(240000); + let code: ISuite['code']; + let page: ISuite['page']; + let browser: ISuite['browser']; + + beforeAll(async () => { + browser = await launchPuppeteer({ + headless: true, + args: ['--disable-dev-shm-usage'], + }); + + 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 () => { + await browser.close(); + }); + + for (const suite of suites) { + it( + suite.title, + async () => { + suite.times = suite.times ?? 5; + const durations: number[] = []; + for (let i = 0; i < suite.times; i++) { + page = await browser.newPage(); + await page.goto('about:blank'); + await page.setContent(` + + + `); + const duration = (await page.evaluate(() => { + const replayer = new (window as any).rrweb.Replayer( + (window as any).events, + ); + const start = Date.now(); + replayer.play(replayer.getMetaData().totalTime + 100); + return Date.now() - start; + })) as number; + durations.push(duration); + await page.close(); + } + + console.table([ + { + title: suite.title, + times: suite.times, + duration: avg(durations), + durations: durations.join(', '), + }, + ]); + }, + 60_000, + ); + } + + /** + * Get the recorded events after the mutation function is executed. + */ + async function generateEvents(mutateNodesFn: string): Promise { + const page = await browser.newPage(); + + await page.goto('about:blank'); + await page.setContent(` + + `); + const eventsString = (await page.evaluate((mutateNodesFn) => { + return new Promise((resolve) => { + const events: eventWithTime[] = []; + const options: recordOptions = { + emit: (event) => { + events.push(event); + }, + }; + const record = (window as any).rrweb.record; + record(options); + eval(mutateNodesFn)().then(() => { + resolve(JSON.stringify(events)); + }); + }); + }, mutateNodesFn)) as string; + + await page.close(); + return eventsString; + } +});