From 0c7f5b7cf4998e07a52bf8cafe5f5e8474e84135 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Fri, 13 May 2022 10:57:53 +0800 Subject: [PATCH 1/3] housekeeping: refine test utils --- packages/rrweb/src/record/mutation.ts | 2 +- .../rrweb/test/benchmark/dom-mutation.test.ts | 0 packages/rrweb/test/e2e/webgl.test.ts | 31 +++++--------- packages/rrweb/test/integration.test.ts | 22 ++-------- packages/rrweb/test/utils.ts | 40 ++++++++++++++----- packages/rrweb/tslint.json | 3 +- 6 files changed, 47 insertions(+), 51 deletions(-) create mode 100644 packages/rrweb/test/benchmark/dom-mutation.test.ts diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 80718f2589..fb14f8eaeb 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -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, diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/rrweb/test/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index f77c92b193..80522514e6 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -8,6 +8,7 @@ import { getServerURL, replaceLast, waitForRAF, + generateRecordSnippet, } from '../utils'; import type { recordOptions, eventWithTime } from '../../src/types'; import { toMatchImageSnapshot } from 'jest-image-snapshot'; @@ -59,26 +60,14 @@ describe('e2e webgl', () => { ` `, ); }; - 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, @@ -86,15 +75,15 @@ describe('e2e webgl', () => { 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;}', }); }; diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 4c92be6218..5f13419883 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1,3 +1,4 @@ +// tslint:disable:no-console import * as fs from 'fs'; import * as path from 'path'; import type * as http from 'http'; @@ -9,6 +10,7 @@ import { launchPuppeteer, waitForRAF, replaceLast, + generateRecordSnippet, } from './utils'; import { recordOptions, eventWithTime, EventType } from '../src/types'; import { visitSnapshot, NodeType } from 'rrweb-snapshot'; @@ -20,10 +22,6 @@ interface ISuite { browser: puppeteer.Browser; } -interface IMimeType { - [key: string]: string; -} - describe('record integration tests', function (this: ISuite) { jest.setTimeout(10_000); @@ -40,19 +38,7 @@ describe('record integration tests', function (this: ISuite) { `, @@ -73,7 +59,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; }); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 89f62c3fa4..0a17711ec9 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -1,3 +1,4 @@ +// tslint:disable:no-console no-any import { NodeType } from 'rrweb-snapshot'; import { EventType, @@ -7,6 +8,7 @@ import { Optional, mouseInteractionData, event, + recordOptions, } from '../src/types'; import * as puppeteer from 'puppeteer'; import { format } from 'prettier'; @@ -43,7 +45,7 @@ export const startServer = (defaultPort: number = 3030) => const sanitizePath = path .normalize(parsedUrl.pathname!) .replace(/^(\.\.[\/\\])+/, ''); - let pathname = path.join(__dirname, sanitizePath); + const pathname = path.join(__dirname, sanitizePath); try { const data = fs.readFileSync(pathname); @@ -179,9 +181,9 @@ function stringifyDomSnapshot(mhtml: string): string { .rewrite() // rewrite all links .spit(); // return all contents - const newResult: Array<{ filename: string; content: string }> = result.map( + const newResult: { filename: string; content: string }[] = result.map( (asset: { filename: string; content: string }) => { - let { filename, content } = asset; + const { filename, content } = asset; let res: string | undefined; if (filename.includes('frame')) { res = format(content, { @@ -226,7 +228,7 @@ export function stripBase64(events: eventWithTime[]) { if (!obj || typeof obj !== 'object') return obj; if (Array.isArray(obj)) return (obj.map((e) => walk(e)) as unknown) as T; const newObj: Partial = {}; - for (let prop in obj) { + for (const prop in obj) { const value = obj[prop]; if (prop === 'base64' && typeof value === 'string') { let index = base64Strings.indexOf(value); @@ -241,15 +243,15 @@ export function stripBase64(events: eventWithTime[]) { return newObj as T; } - return events.map((event) => { + return events.map((evt) => { if ( - event.type === EventType.IncrementalSnapshot && - event.data.source === IncrementalSource.CanvasMutation + evt.type === EventType.IncrementalSnapshot && + evt.data.source === IncrementalSource.CanvasMutation ) { - const newData = walk(event.data); - return { ...event, data: newData }; + const newData = walk(evt.data); + return { ...evt, data: newData }; } - return event; + return evt; }); } @@ -525,3 +527,21 @@ export async function waitForRAF(page: puppeteer.Page) { }); }); } + +export function generateRecordSnippet(options: recordOptions) { + return ` + 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} + }); + `; +} diff --git a/packages/rrweb/tslint.json b/packages/rrweb/tslint.json index ac74b2e5d7..c49e23ea37 100644 --- a/packages/rrweb/tslint.json +++ b/packages/rrweb/tslint.json @@ -24,7 +24,8 @@ "trailing-comma": false, "curly": false, "no-namespace": false, - "interface-name": false + "interface-name": false, + "forin": false }, "rulesDirectory": [] } From 892eed85b250821adeef49cd90ae083a924d914a Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Fri, 13 May 2022 12:03:20 +0800 Subject: [PATCH 2/3] setup benchmark tests --- .../rrweb/test/benchmark/dom-mutation.test.ts | 118 ++++++++++++++++++ packages/rrweb/test/e2e/webgl.test.ts | 11 +- .../test/html/benchmark-dom-mutation.html | 25 ++++ packages/rrweb/test/integration.test.ts | 9 +- packages/rrweb/test/utils.ts | 15 ++- 5 files changed, 158 insertions(+), 20 deletions(-) create mode 100644 packages/rrweb/test/html/benchmark-dom-mutation.html diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index e69de29bb2..fa8ff574a9 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -0,0 +1,118 @@ +// 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, + '', + ` + + + `, + ); + }; + + const suites: { + title: string; + html: string; + times?: number; // default to 5 + }[] = [ + { + title: 'create 1000x10 DOM nodes', + html: 'benchmark-dom-mutation.html', + }, + ]; + + for (const suite of suites) { + it(suite.title, async () => { + page = await browser.newPage(); + page.on('console', (message) => + console.log( + `${message.type().substr(0, 3).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 = { + 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(', '), + }, + ]); + }); + } +}); diff --git a/packages/rrweb/test/e2e/webgl.test.ts b/packages/rrweb/test/e2e/webgl.test.ts index 80522514e6..1acdcc0037 100644 --- a/packages/rrweb/test/e2e/webgl.test.ts +++ b/packages/rrweb/test/e2e/webgl.test.ts @@ -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'; @@ -9,20 +8,12 @@ import { 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']; diff --git a/packages/rrweb/test/html/benchmark-dom-mutation.html b/packages/rrweb/test/html/benchmark-dom-mutation.html new file mode 100644 index 0000000000..40f4d30534 --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation.html @@ -0,0 +1,25 @@ + + + + diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts index 5f13419883..797d537346 100644 --- a/packages/rrweb/test/integration.test.ts +++ b/packages/rrweb/test/integration.test.ts @@ -1,7 +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, @@ -11,17 +10,11 @@ import { 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; -} - describe('record integration tests', function (this: ISuite) { jest.setTimeout(10_000); diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts index 0a17711ec9..2eb3a6a881 100644 --- a/packages/rrweb/test/utils.ts +++ b/packages/rrweb/test/utils.ts @@ -17,15 +17,17 @@ import * as http from 'http'; import * as url from 'url'; import * as fs from 'fs'; -export async function launchPuppeteer() { +export async function launchPuppeteer( + options?: Parameters[0], +) { return await puppeteer.launch({ headless: process.env.PUPPETEER_HEADLESS ? true : false, defaultViewport: { width: 1920, height: 1080, }, - // devtools: true, args: ['--no-sandbox'], + ...options, }); } @@ -33,6 +35,15 @@ interface IMimeType { [key: string]: string; } +export interface ISuite { + server: http.Server; + serverURL: string; + code: string; + browser: puppeteer.Browser; + page: puppeteer.Page; + events: eventWithTime[]; +} + export const startServer = (defaultPort: number = 3030) => new Promise((resolve) => { const mimeType: IMimeType = { From 5bfd4a7a0d1a2af1ab5654d3095d6d41a7e9b911 Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Fri, 13 May 2022 17:38:05 +0800 Subject: [PATCH 3/3] improve snapshot attributes loop perf --- packages/rrweb-snapshot/src/snapshot.ts | 11 +++++++++-- packages/rrweb/test/benchmark/dom-mutation.test.ts | 5 ++--- packages/rrweb/test/html/benchmark-dom-mutation.html | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 9ccb67b52d..dded72ebd1 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -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) { diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index fa8ff574a9..d2883ad0ae 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -57,6 +57,7 @@ describe('benchmark: mutation observer', () => { { title: 'create 1000x10 DOM nodes', html: 'benchmark-dom-mutation.html', + times: 10, }, ]; @@ -64,9 +65,7 @@ describe('benchmark: mutation observer', () => { it(suite.title, async () => { page = await browser.newPage(); page.on('console', (message) => - console.log( - `${message.type().substr(0, 3).toUpperCase()} ${message.text()}`, - ), + console.log(`${message.type().toUpperCase()} ${message.text()}`), ); const times = suite.times ?? 5; diff --git a/packages/rrweb/test/html/benchmark-dom-mutation.html b/packages/rrweb/test/html/benchmark-dom-mutation.html index 40f4d30534..2921964c86 100644 --- a/packages/rrweb/test/html/benchmark-dom-mutation.html +++ b/packages/rrweb/test/html/benchmark-dom-mutation.html @@ -4,6 +4,7 @@ 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; @@ -18,8 +19,9 @@ current.appendChild(child); current = child; } - document.body.appendChild(node); + frag.appendChild(node); } + document.body.appendChild(frag); };