From 7eb0bea1e701e0e53767dc7e9405e732ced33dbe Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 09:30:02 -0400 Subject: [PATCH 1/9] ref: isParentRemoved to cache subtree --- packages/rrweb/src/record/mutation.ts | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index a798441969..c87d4a286d 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -168,6 +168,7 @@ export default class MutationBuffer { private addedSet = new Set(); private movedSet = new Set(); private droppedSet = new Set(); + private removesSubTreeCache = new Set(); private mutationCb: observerParam['mutationCb']; private blockClass: observerParam['blockClass']; @@ -353,7 +354,7 @@ export default class MutationBuffer { for (const n of this.movedSet) { if ( - isParentRemoved(this.removes, n, this.mirror) && + isParentRemoved(this.removesSubTreeCache, n, this.mirror) && !this.movedSet.has(n.parentNode!) ) { continue; @@ -364,7 +365,7 @@ export default class MutationBuffer { for (const n of this.addedSet) { if ( !isAncestorInSet(this.droppedSet, n) && - !isParentRemoved(this.removes, n, this.mirror) + !isParentRemoved(this.removesSubTreeCache, n, this.mirror) ) { pushAdd(n); } else if (isAncestorInSet(this.movedSet, n)) { @@ -506,6 +507,7 @@ export default class MutationBuffer { this.addedSet = new Set(); this.movedSet = new Set(); this.droppedSet = new Set(); + this.removesSubTreeCache = new Set(); this.movedMap = {}; this.mutationCb(payload); @@ -726,6 +728,7 @@ export default class MutationBuffer { ? true : undefined, }); + processRemoves(n, this.removesSubTreeCache); } this.mapRemoves.push(n); }); @@ -788,29 +791,35 @@ function deepDelete(addsSet: Set, n: Node) { n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); } +function processRemoves(n: Node, cache: Set) { + const queue = [n] + + while (queue.length) { + const next = queue.pop()!; + if (cache.has(next)) continue; + cache.add(next); + next.childNodes.forEach((n) => queue.push(n)); + } + + return; +} + function isParentRemoved( - removes: removedNodeMutation[], + removes: Set, n: Node, mirror: Mirror, ): boolean { - if (removes.length === 0) return false; + if (removes.size === 0) return false; return _isParentRemoved(removes, n, mirror); } function _isParentRemoved( - removes: removedNodeMutation[], + removes: Set, n: Node, - mirror: Mirror, + _mirror: Mirror, ): boolean { - let node: ParentNode | null = n.parentNode; - while (node) { - const parentId = mirror.getId(node); - if (removes.some((r) => r.id === parentId)) { - return true; - } - node = node.parentNode; - } - return false; + if(!n.parentNode) return false; + return removes.has(n.parentNode) } function isAncestorInSet(set: Set, n: Node): boolean { From 359be3a92164186c5097c890fe9187c313e189e0 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 09:36:33 -0400 Subject: [PATCH 2/9] ref: cache at insertion too --- packages/rrweb/src/record/mutation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index c87d4a286d..16f31cc3e2 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -792,6 +792,7 @@ function deepDelete(addsSet: Set, n: Node) { } function processRemoves(n: Node, cache: Set) { + if(cache.has(n)) return; const queue = [n] while (queue.length) { From ed5e795d67a269c7e0f7dd8d75ce8bca185dab47 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 09:38:32 -0400 Subject: [PATCH 3/9] ref: remove wrapper function --- packages/rrweb/src/record/mutation.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 16f31cc3e2..d40e4dc2b6 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -6,7 +6,6 @@ import { isShadowRoot, needMaskingText, maskInputValue, - Mirror, isNativeShadowDom, getInputType, toLowerCase, @@ -354,7 +353,7 @@ export default class MutationBuffer { for (const n of this.movedSet) { if ( - isParentRemoved(this.removesSubTreeCache, n, this.mirror) && + isParentRemoved(this.removesSubTreeCache, n) && !this.movedSet.has(n.parentNode!) ) { continue; @@ -365,7 +364,7 @@ export default class MutationBuffer { for (const n of this.addedSet) { if ( !isAncestorInSet(this.droppedSet, n) && - !isParentRemoved(this.removesSubTreeCache, n, this.mirror) + !isParentRemoved(this.removesSubTreeCache, n) ) { pushAdd(n); } else if (isAncestorInSet(this.movedSet, n)) { @@ -808,18 +807,8 @@ function processRemoves(n: Node, cache: Set) { function isParentRemoved( removes: Set, n: Node, - mirror: Mirror, ): boolean { - if (removes.size === 0) return false; - return _isParentRemoved(removes, n, mirror); -} - -function _isParentRemoved( - removes: Set, - n: Node, - _mirror: Mirror, -): boolean { - if(!n.parentNode) return false; + if(!n.parentNode || !removes.size) return false; return removes.has(n.parentNode) } From 5d369cc594b931e12fc24ff328cacc36b049093f Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 09:38:57 -0400 Subject: [PATCH 4/9] format --- packages/rrweb/src/record/mutation.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index d40e4dc2b6..f72af3d1b9 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -791,8 +791,8 @@ function deepDelete(addsSet: Set, n: Node) { } function processRemoves(n: Node, cache: Set) { - if(cache.has(n)) return; - const queue = [n] + if (cache.has(n)) return; + const queue = [n]; while (queue.length) { const next = queue.pop()!; @@ -804,12 +804,9 @@ function processRemoves(n: Node, cache: Set) { return; } -function isParentRemoved( - removes: Set, - n: Node, -): boolean { - if(!n.parentNode || !removes.size) return false; - return removes.has(n.parentNode) +function isParentRemoved(removes: Set, n: Node): boolean { + if (!n.parentNode || !removes.size) return false; + return removes.has(n.parentNode); } function isAncestorInSet(set: Set, n: Node): boolean { From 22072016a113890491de42cf41addc537753e13e Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 19 Jul 2024 21:23:25 -0400 Subject: [PATCH 5/9] fix: benchmarks --- .../rrweb/test/benchmark/dom-mutation.test.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 96b809e94b..0afb7623e5 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -89,17 +89,6 @@ describe('benchmark: mutation observer', () => { return fs.readFileSync(filePath, 'utf8'); }; - const addRecordingScript = async (page: Page) => { - // const scriptUrl = `${getServerURL(server)}/rrweb-1.1.3.js`; - const scriptUrl = `${getServerURL(server)}/rrweb.umd.cjs`; - 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 () => { page = await browser.newPage(); @@ -110,12 +99,18 @@ describe('benchmark: mutation observer', () => { const loadPage = async () => { if ('html' in suite) { await page.goto('about:blank'); + const code = fs + .readFileSync(path.resolve(__dirname, '../../dist/rrweb.umd.cjs')) + .toString(); + await page.setContent(` + + `); await page.setContent(getHtml.call(this, suite.html)); } else { await page.goto(suite.url); } - - await addRecordingScript(page); }; const getDuration = async (): Promise => { @@ -151,7 +146,7 @@ describe('benchmark: mutation observer', () => { }; // generate profile.json file - const profileFilename = `profile-${new Date().toISOString()}.json`; + const profileFilename = `profile-${suite.title}-${new Date().toISOString()}.json`; const tempDirectory = path.resolve(path.join(__dirname, '../../temp')); fs.mkdirSync(tempDirectory, { recursive: true }); const profilePath = path.resolve(tempDirectory, profileFilename); From 9fd33b4d2e6a206dc75fa068e13307d8524f1d1c Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 09:58:46 -0400 Subject: [PATCH 6/9] add changeset --- .changeset/swift-pots-search.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/swift-pots-search.md diff --git a/.changeset/swift-pots-search.md b/.changeset/swift-pots-search.md new file mode 100644 index 0000000000..ec13fb6316 --- /dev/null +++ b/.changeset/swift-pots-search.md @@ -0,0 +1,5 @@ +--- +"rrweb": minor +--- + +Optimize isParentRemoved check From a1dc95b3c484451de33598e2602dd48bee89fd52 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 10:01:18 -0400 Subject: [PATCH 7/9] add comment --- packages/rrweb/src/record/mutation.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index f72af3d1b9..b78529d86a 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -790,6 +790,11 @@ function deepDelete(addsSet: Set, n: Node) { n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); } +/** + * When a node is removed, process it's subtree such that the lookup + * for its children can be done efficiently and without traversing its + * entire parent chain. + */ function processRemoves(n: Node, cache: Set) { if (cache.has(n)) return; const queue = [n]; From 08619a6b152dc47ca7240d3b22b4a1e69765fe1e Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 20 Jul 2024 13:05:12 -0400 Subject: [PATCH 8/9] format --- packages/rrweb/src/record/mutation.ts | 2 +- packages/rrweb/test/benchmark/dom-mutation.test.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index b78529d86a..a3cd01f894 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -791,7 +791,7 @@ function deepDelete(addsSet: Set, n: Node) { } /** - * When a node is removed, process it's subtree such that the lookup + * When a node is removed, process it's subtree such that the lookup * for its children can be done efficiently and without traversing its * entire parent chain. */ diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 0afb7623e5..8e60879331 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -146,7 +146,9 @@ describe('benchmark: mutation observer', () => { }; // generate profile.json file - const profileFilename = `profile-${suite.title}-${new Date().toISOString()}.json`; + const profileFilename = `profile-${ + suite.title + }-${new Date().toISOString()}.json`; const tempDirectory = path.resolve(path.join(__dirname, '../../temp')); fs.mkdirSync(tempDirectory, { recursive: true }); const profilePath = path.resolve(tempDirectory, profileFilename); From b10de2e76e08b6737b7250a3607c390b7e4f7984 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 19 Sep 2024 10:29:26 -0400 Subject: [PATCH 9/9] revert profile change --- .../rrweb/test/benchmark/dom-mutation.test.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 8e60879331..96b809e94b 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -89,6 +89,17 @@ describe('benchmark: mutation observer', () => { return fs.readFileSync(filePath, 'utf8'); }; + const addRecordingScript = async (page: Page) => { + // const scriptUrl = `${getServerURL(server)}/rrweb-1.1.3.js`; + const scriptUrl = `${getServerURL(server)}/rrweb.umd.cjs`; + 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 () => { page = await browser.newPage(); @@ -99,18 +110,12 @@ describe('benchmark: mutation observer', () => { const loadPage = async () => { if ('html' in suite) { await page.goto('about:blank'); - const code = fs - .readFileSync(path.resolve(__dirname, '../../dist/rrweb.umd.cjs')) - .toString(); - await page.setContent(` - - `); await page.setContent(getHtml.call(this, suite.html)); } else { await page.goto(suite.url); } + + await addRecordingScript(page); }; const getDuration = async (): Promise => { @@ -146,9 +151,7 @@ describe('benchmark: mutation observer', () => { }; // generate profile.json file - const profileFilename = `profile-${ - suite.title - }-${new Date().toISOString()}.json`; + 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);