From cc905766c6bf55a10e10e0bb5a7539f9e3af5d75 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 28 Jul 2023 15:56:11 -0400 Subject: [PATCH 01/20] perf(snapshot): optimize code flow of slimDomExcluded --- packages/rrweb-snapshot/src/snapshot.ts | 58 ++++++++++++------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index bb18542d26..b23bb164a0 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -841,6 +841,10 @@ function slimDOMExcluded( // TODO: convert IE conditional comments to real nodes return true; } else if (sn.type === NodeType.Element) { + const snAttributeName = lowerIfExists(sn.attributes.name); + const snAttributeRel = lowerIfExists(sn.attributes.rel); + const snAttributeProperty = lowerIfExists(sn.attributes.property); + if ( slimDOMOptions.script && // script tag @@ -861,33 +865,30 @@ function slimDOMExcluded( slimDOMOptions.headFavicon && ((sn.tagName === 'link' && sn.attributes.rel === 'shortcut icon') || (sn.tagName === 'meta' && - (lowerIfExists(sn.attributes.name).match( - /^msapplication-tile(image|color)$/, - ) || - lowerIfExists(sn.attributes.name) === 'application-name' || - lowerIfExists(sn.attributes.rel) === 'icon' || - lowerIfExists(sn.attributes.rel) === 'apple-touch-icon' || - lowerIfExists(sn.attributes.rel) === 'shortcut icon'))) + (snAttributeName === 'application-name' || + snAttributeRel === 'icon' || + snAttributeRel === 'apple-touch-icon' || + snAttributeRel === 'shortcut icon' || + /^msapplication-tile(image|color)$/.test(snAttributeName)))) ) { return true; } else if (sn.tagName === 'meta') { if ( slimDOMOptions.headMetaDescKeywords && - lowerIfExists(sn.attributes.name).match(/^description|keywords$/) + (snAttributeName === 'description' || snAttributeName === 'keywords') ) { return true; } else if ( - slimDOMOptions.headMetaSocial && - (lowerIfExists(sn.attributes.property).match(/^(og|twitter|fb):/) || // og = opengraph (facebook) - lowerIfExists(sn.attributes.name).match(/^(og|twitter):/) || - lowerIfExists(sn.attributes.name) === 'pinterest') + (slimDOMOptions.headMetaSocial && snAttributeName === 'pinterest') || + /^(og|twitter|fb):/.test(snAttributeProperty) || // og = opengraph (facebook) + /^(og|twitter):/.test(snAttributeName) ) { return true; } else if ( slimDOMOptions.headMetaRobots && - (lowerIfExists(sn.attributes.name) === 'robots' || - lowerIfExists(sn.attributes.name) === 'googlebot' || - lowerIfExists(sn.attributes.name) === 'bingbot') + (snAttributeName === 'robots' || + snAttributeName === 'googlebot' || + snAttributeName === 'bingbot') ) { return true; } else if ( @@ -899,24 +900,23 @@ function slimDOMExcluded( return true; } else if ( slimDOMOptions.headMetaAuthorship && - (lowerIfExists(sn.attributes.name) === 'author' || - lowerIfExists(sn.attributes.name) === 'generator' || - lowerIfExists(sn.attributes.name) === 'framework' || - lowerIfExists(sn.attributes.name) === 'publisher' || - lowerIfExists(sn.attributes.name) === 'progid' || - lowerIfExists(sn.attributes.property).match(/^article:/) || - lowerIfExists(sn.attributes.property).match(/^product:/)) + (snAttributeName === 'author' || + snAttributeName === 'generator' || + snAttributeName === 'framework' || + snAttributeName === 'publisher' || + snAttributeName === 'progid' || + /^article|product:/.test(snAttributeProperty)) ) { return true; } else if ( slimDOMOptions.headMetaVerification && - (lowerIfExists(sn.attributes.name) === 'google-site-verification' || - lowerIfExists(sn.attributes.name) === 'yandex-verification' || - lowerIfExists(sn.attributes.name) === 'csrf-token' || - lowerIfExists(sn.attributes.name) === 'p:domain_verify' || - lowerIfExists(sn.attributes.name) === 'verify-v1' || - lowerIfExists(sn.attributes.name) === 'verification' || - lowerIfExists(sn.attributes.name) === 'shopify-checkout-api-token') + (snAttributeName === 'google-site-verification' || + snAttributeName === 'yandex-verification' || + snAttributeName === 'csrf-token' || + snAttributeName === 'p:domain_verify' || + snAttributeName === 'verify-v1' || + snAttributeName === 'verification' || + snAttributeName === 'shopify-checkout-api-token') ) { return true; } From 18ca33523bb9abccebad4b6942c913cccf94a086 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 28 Jul 2023 16:12:45 -0400 Subject: [PATCH 02/20] perf(snapshot): remove lowerIfExists --- packages/rrweb-snapshot/src/snapshot.ts | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index b23bb164a0..66101ec581 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -823,16 +823,6 @@ function serializeElementNode( }; } -function lowerIfExists( - maybeAttr: string | number | boolean | undefined | null, -): string { - if (maybeAttr === undefined || maybeAttr === null) { - return ''; - } else { - return (maybeAttr as string).toLowerCase(); - } -} - function slimDOMExcluded( sn: serializedNode, slimDOMOptions: SlimDOMOptions, @@ -841,9 +831,14 @@ function slimDOMExcluded( // TODO: convert IE conditional comments to real nodes return true; } else if (sn.type === NodeType.Element) { - const snAttributeName = lowerIfExists(sn.attributes.name); - const snAttributeRel = lowerIfExists(sn.attributes.rel); - const snAttributeProperty = lowerIfExists(sn.attributes.property); + /* eslint-disable */ + // @ts-ignore + const snAttributeName: string = sn.attributes.name ? sn.attributes.name.toLowerCase() : ""; + // @ts-ignore + const snAttributeRel: string = sn.attributes.name ? sn.attributes.name.toLowerCase() : ""; + // @ts-ignore + const snAttributeProperty: string = sn.attributes.property ? sn.attributes.property.toLowerCase() : ""; + /* eslint-enable */ if ( slimDOMOptions.script && @@ -879,9 +874,9 @@ function slimDOMExcluded( ) { return true; } else if ( - (slimDOMOptions.headMetaSocial && snAttributeName === 'pinterest') || + (slimDOMOptions.headMetaSocial && ( /^(og|twitter|fb):/.test(snAttributeProperty) || // og = opengraph (facebook) - /^(og|twitter):/.test(snAttributeName) + /^(og|twitter):/.test(snAttributeName))) ) { return true; } else if ( From ba66b777febad2440b0a728a6fb65e9ec2372c15 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 28 Jul 2023 17:03:20 -0400 Subject: [PATCH 03/20] perf(snapshot): return early --- packages/rrweb-snapshot/src/snapshot.ts | 78 +++++++++++++++++-------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 66101ec581..f3a80d0875 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -1,3 +1,4 @@ +import { text } from 'stream/consumers'; import { serializedNode, serializedNodeWithId, @@ -273,6 +274,10 @@ export function _isBlockedElement( blockClass: string | RegExp, blockSelector: string | null, ): boolean { + if(!blockClass && !blockSelector) { + return false + } + try { if (typeof blockClass === 'string') { if (element.classList.contains(blockClass)) { @@ -322,13 +327,17 @@ export function needMaskingText( maskTextClass: string | RegExp, maskTextSelector: string | null, ): boolean { - try { - const el: HTMLElement | null = - node.nodeType === node.ELEMENT_NODE - ? (node as HTMLElement) - : node.parentElement; - if (el === null) return false; + if (!maskTextClass && !maskTextSelector) { + return false; + } + + const el: HTMLElement | null = + node.nodeType === node.ELEMENT_NODE + ? (node as HTMLElement) + : node.parentElement; + if (el === null) return false; + try { if (typeof maskTextClass === 'string') { if (el.classList.contains(maskTextClass)) return true; if (el.closest(`.${maskTextClass}`)) return true; @@ -552,8 +561,8 @@ function serializeTextNode( // The parent node may not be a html element which has a tagName attribute. // So just let it be undefined which is ok in this use case. const parentTagName = n.parentNode && (n.parentNode as HTMLElement).tagName; - let textContent = n.textContent; const isStyle = parentTagName === 'STYLE' ? true : undefined; + let textContent = n.textContent; const isScript = parentTagName === 'SCRIPT' ? true : undefined; if (isStyle && textContent) { try { @@ -579,15 +588,22 @@ function serializeTextNode( if (isScript) { textContent = 'SCRIPT_PLACEHOLDER'; } + if ( !isStyle && !isScript && textContent && needMaskingText(n, maskTextClass, maskTextSelector) ) { - textContent = maskTextFn - ? maskTextFn(textContent) - : textContent.replace(/[\S]/g, '*'); + return { + type: NodeType.Text, + textContent: + (maskTextFn + ? maskTextFn(textContent) + : textContent.replace(/[\S]/g, '*')) || '', + isStyle, + rootId, + }; } return { @@ -832,12 +848,18 @@ function slimDOMExcluded( return true; } else if (sn.type === NodeType.Element) { /* eslint-disable */ - // @ts-ignore - const snAttributeName: string = sn.attributes.name ? sn.attributes.name.toLowerCase() : ""; - // @ts-ignore - const snAttributeRel: string = sn.attributes.name ? sn.attributes.name.toLowerCase() : ""; - // @ts-ignore - const snAttributeProperty: string = sn.attributes.property ? sn.attributes.property.toLowerCase() : ""; + const snAttributeName: string = sn.attributes.name + ? // @ts-ignore + sn.attributes.name.toLowerCase() + : ''; + const snAttributeRel: string = sn.attributes.name + ? // @ts-ignore + sn.attributes.name.toLowerCase() + : ''; + const snAttributeProperty: string = sn.attributes.property + ? // @ts-ignore + sn.attributes.property.toLowerCase() + : ''; /* eslint-enable */ if ( @@ -874,9 +896,9 @@ function slimDOMExcluded( ) { return true; } else if ( - (slimDOMOptions.headMetaSocial && ( - /^(og|twitter|fb):/.test(snAttributeProperty) || // og = opengraph (facebook) - /^(og|twitter):/.test(snAttributeName))) + slimDOMOptions.headMetaSocial && + (/^(og|twitter|fb):/.test(snAttributeProperty) || // og = opengraph (facebook) + /^(og|twitter):/.test(snAttributeName)) ) { return true; } else if ( @@ -1051,6 +1073,10 @@ export function serializeNodeWithId( ) { preserveWhiteSpace = false; } + + const isEl = isElement(n); + const isShadow = isEl && n.shadowRoot && isNativeShadowDom(n.shadowRoot); + const bypassOptions = { doc, mirror, @@ -1075,22 +1101,24 @@ export function serializeNodeWithId( stylesheetLoadTimeout, keepIframeSrcFn, }; - for (const childN of Array.from(n.childNodes)) { + + n.childNodes.forEach((childN) => { const serializedChildNode = serializeNodeWithId(childN, bypassOptions); if (serializedChildNode) { serializedNode.childNodes.push(serializedChildNode); } - } + }); if (isElement(n) && n.shadowRoot) { - for (const childN of Array.from(n.shadowRoot.childNodes)) { + n.shadowRoot.childNodes.forEach((childN) => { const serializedChildNode = serializeNodeWithId(childN, bypassOptions); if (serializedChildNode) { - isNativeShadowDom(n.shadowRoot) && - (serializedChildNode.isShadow = true); + if(isNativeShadowDom(n.shadowRoot!)){ + serializedChildNode.isShadow = true; + } serializedNode.childNodes.push(serializedChildNode); } - } + }); } } From dc4c7b1204a2605742fb11ee795e9245fa5a3d71 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Fri, 28 Jul 2023 21:24:50 -0400 Subject: [PATCH 04/20] perf(snapshot): test --- packages/rrweb-snapshot/src/snapshot.ts | 47 +++++---- packages/rrweb/src/record/mutation.ts | 97 +++++++++++-------- .../src/record/processed-node-manager.ts | 6 +- packages/rrweb/src/utils.ts | 2 + 4 files changed, 89 insertions(+), 63 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index f3a80d0875..7617427930 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -1,4 +1,3 @@ -import { text } from 'stream/consumers'; import { serializedNode, serializedNodeWithId, @@ -53,13 +52,17 @@ function getValidTagName(element: HTMLElement): Lowercase { } function stringifyStyleSheet(sheet: CSSStyleSheet): string { - return sheet.cssRules - ? Array.from(sheet.cssRules) - .map((rule) => - rule.cssText ? validateStringifiedCssRule(rule.cssText) : '', - ) - .join('') - : ''; + if(sheet.cssRules.length === 0) { + return ""; + } + + const buffer: string[] = new Array(sheet.cssRules.length); + + for(let i = 0; i < sheet.cssRules.length; i++) { + const rule = sheet.cssRules[i]; + buffer[i] = rule.cssText ? validateStringifiedCssRule(rule.cssText) : ''; + } + return buffer.join(''); } function extractOrigin(url: string): string { @@ -148,7 +151,7 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { function collectCharacters(regEx: RegExp) { let chars: string; - const match = regEx.exec(attributeValue.substring(pos)); + const match = attributeValue.substring(pos).match(regEx) if (match) { chars = match[0]; pos += chars.length; @@ -839,14 +842,25 @@ function serializeElementNode( }; } +const MS_APPLICATION_TILE_REGEXP = /^msapplication-tile(image|color)$/; +const OG_TWITTER_OR_FB_REGEXP = /^(og|twitter|fb):/; +const OG_TWITTER_REGEXP = /^(og|twitter):/; +const ARTICLE_PRODUCT_REGEXP = /^(article|product):/; + function slimDOMExcluded( sn: serializedNode, slimDOMOptions: SlimDOMOptions, ): boolean { - if (slimDOMOptions.comment && sn.type === NodeType.Comment) { + if(sn.type !== NodeType.Element && sn.type !== NodeType.Comment) { + return false + } + + if (sn.type === NodeType.Comment && slimDOMOptions.comment) { // TODO: convert IE conditional comments to real nodes return true; - } else if (sn.type === NodeType.Element) { + } + + if (sn.type === NodeType.Element) { /* eslint-disable */ const snAttributeName: string = sn.attributes.name ? // @ts-ignore @@ -886,7 +900,7 @@ function slimDOMExcluded( snAttributeRel === 'icon' || snAttributeRel === 'apple-touch-icon' || snAttributeRel === 'shortcut icon' || - /^msapplication-tile(image|color)$/.test(snAttributeName)))) + MS_APPLICATION_TILE_REGEXP.test(snAttributeName)))) ) { return true; } else if (sn.tagName === 'meta') { @@ -897,8 +911,8 @@ function slimDOMExcluded( return true; } else if ( slimDOMOptions.headMetaSocial && - (/^(og|twitter|fb):/.test(snAttributeProperty) || // og = opengraph (facebook) - /^(og|twitter):/.test(snAttributeName)) + (OG_TWITTER_OR_FB_REGEXP.test(snAttributeProperty) || // og = opengraph (facebook) + OG_TWITTER_REGEXP.test(snAttributeName)) ) { return true; } else if ( @@ -922,7 +936,7 @@ function slimDOMExcluded( snAttributeName === 'framework' || snAttributeName === 'publisher' || snAttributeName === 'progid' || - /^article|product:/.test(snAttributeProperty)) + ARTICLE_PRODUCT_REGEXP.test(snAttributeProperty)) ) { return true; } else if ( @@ -1074,9 +1088,6 @@ export function serializeNodeWithId( preserveWhiteSpace = false; } - const isEl = isElement(n); - const isShadow = isEl && n.shadowRoot && isNativeShadowDom(n.shadowRoot); - const bypassOptions = { doc, mirror, diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 39a6107635..b52d63083c 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -133,6 +133,16 @@ class DoubleLinkedList { const moveKey = (id: number, parentId: number) => `${id}@${parentId}`; +const getNextId = (n: Node, mirror: observerParam['mirror']): number | null => { + let ns: Node | null = n; + let nextId: number | null = IGNORED_NODE; // slimDOM: ignored + while (nextId === IGNORED_NODE) { + ns = ns && ns.nextSibling; + nextId = ns && mirror.getId(ns); + } + return nextId; +}; + /** * controls behaviour of a MutationObserver */ @@ -272,15 +282,6 @@ export default class MutationBuffer { * parent, so we init a queue to store these nodes. */ const addList = new DoubleLinkedList(); - const getNextId = (n: Node): number | null => { - let ns: Node | null = n; - let nextId: number | null = IGNORED_NODE; // slimDOM: ignored - while (nextId === IGNORED_NODE) { - ns = ns && ns.nextSibling; - nextId = ns && this.mirror.getId(ns); - } - return nextId; - }; const pushAdd = (n: Node) => { if (!n.parentNode || !inDom(n)) { return; @@ -288,7 +289,7 @@ export default class MutationBuffer { const parentId = isShadowRoot(n.parentNode) ? this.mirror.getId(getShadowHost(n)) : this.mirror.getId(n.parentNode); - const nextId = getNextId(n); + const nextId = getNextId(n, this.mirror); if (parentId === -1 || nextId === -1) { return addList.addNode(n); } @@ -371,7 +372,7 @@ export default class MutationBuffer { let node: DoubleLinkedListNode | null = null; if (candidate) { const parentId = this.mirror.getId(candidate.value.parentNode); - const nextId = getNextId(candidate.value); + const nextId = getNextId(candidate.value, this.mirror); if (parentId !== -1 && nextId !== -1) { node = candidate; } @@ -384,7 +385,7 @@ export default class MutationBuffer { // ensure _node is defined before attempting to find value if (_node) { const parentId = this.mirror.getId(_node.value.parentNode); - const nextId = getNextId(_node.value); + const nextId = getNextId(_node.value, this.mirror); if (nextId === -1) continue; // nextId !== -1 && parentId !== -1 @@ -671,39 +672,51 @@ export default class MutationBuffer { /** * Make sure you check if `n`'s parent is blocked before calling this function * */ - private genAdds = (n: Node, target?: Node) => { - // this node was already recorded in other buffer, ignore it - if (this.processedNodeManager.inOtherBuffer(n, this)) return; - - // if n is added to set, there is no need to travel it and its' children again - if (this.addedSet.has(n) || this.movedSet.has(n)) return; - - if (this.mirror.hasNode(n)) { - if (isIgnored(n, this.mirror)) { - return; - } - this.movedSet.add(n); - let targetId: number | null = null; - if (target && this.mirror.hasNode(target)) { - targetId = this.mirror.getId(target); - } - if (targetId && targetId !== -1) { - this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + private genAdds = (node: Node, t?: Node) => { + let rp = 0; + const queue: [Node, Node|undefined][] = new Array<[Node, Node|undefined]>(1000); + queue[0] = [node, t]; + + while (queue[rp]) { + const next = queue[rp] + if(!next) break; + const [n, target] = next; + rp++; + // this node was already recorded in other buffer, ignore it + if (this.processedNodeManager.inOtherBuffer(n, this)) continue; + + // if n is added to set, there is no need to travel it and its' children again + if (this.addedSet.has(n) || this.movedSet.has(n)) continue; + + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { + continue + } + this.movedSet.add(n); + let targetId: number | null = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } else { + this.addedSet.add(n); + this.droppedSet.delete(n); } - } else { - this.addedSet.add(n); - this.droppedSet.delete(n); - } - // if this node is blocked `serializeNode` will turn it into a placeholder element - // but we have to remove it's children otherwise they will be added as placeholders too - if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { - n.childNodes.forEach((childN) => this.genAdds(childN)); - if (hasShadowRoot(n)) { - n.shadowRoot.childNodes.forEach((childN) => { - this.processedNodeManager.add(childN, this); - this.genAdds(childN, n); + // if this node is blocked `serializeNode` will turn it into a placeholder element + // but we have to remove it's children otherwise they will be added as placeholders too + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => { + queue.push([childN, undefined]) }); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + queue.push([childN, n]) + }); + } } } }; diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index b5d6c4b679..e656d3504e 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -21,9 +21,9 @@ export default class ProcessedNodeManager { public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) { const buffers = this.nodeMap.get(node); - return ( - buffers && Array.from(buffers).some((buffer) => buffer !== thisBuffer) - ); + if(!buffers) return false; + + return Array.from(buffers).some((buffer) => buffer !== thisBuffer) } public add(node: Node, buffer: MutationBuffer) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 604c8810e2..0e22436ccf 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -229,9 +229,11 @@ export function isBlocked( blockSelector: string | null, checkAncestors: boolean, ): boolean { + if(!blockClass && !blockSelector) return false; if (!node) { return false; } + const el: HTMLElement | null = node.nodeType === node.ELEMENT_NODE ? (node as HTMLElement) From 85b23bf5d65263ed355c795b457c4e3fc2b08bf9 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sat, 29 Jul 2023 10:18:03 -0400 Subject: [PATCH 05/20] perf(mutation): shift n^2 --- packages/rrweb/src/record/mutation.ts | 72 ++++++++++++++++----------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index b52d63083c..1f2afe12a1 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -341,7 +341,7 @@ export default class MutationBuffer { }; while (this.mapRemoves.length) { - this.mirror.removeNodeFromMap(this.mapRemoves.shift()!); + this.mirror.removeNodeFromMap(this.mapRemoves.pop()!); } for (const n of this.movedSet) { @@ -384,12 +384,13 @@ export default class MutationBuffer { tailNode = tailNode.previous; // ensure _node is defined before attempting to find value if (_node) { - const parentId = this.mirror.getId(_node.value.parentNode); const nextId = getNextId(_node.value, this.mirror); - if (nextId === -1) continue; + + const parentId = this.mirror.getId(_node.value.parentNode); + // nextId !== -1 && parentId !== -1 - else if (parentId !== -1) { + if (parentId !== -1) { node = _node; break; } @@ -430,21 +431,33 @@ export default class MutationBuffer { pushAdd(node.value); } + const texts = []; + for(let i = 0; i < this.texts.length; i++){ + if(!this.mirror.getId(this.texts[i].node)){ + continue; + } + texts.push({ + id: this.mirror.getId(this.texts[i].node), + value: this.texts[i].value, + }); + } + + const attributes = []; + for(let i = 0; i < this.attributes.length; i++){ + if(!this.mirror.getId(this.attributes[i].node)){ + continue; + } + attributes.push({ + id: this.mirror.getId(this.attributes[i].node), + attributes: this.attributes[i].attributes, + }); + } + const payload = { - texts: this.texts - .map((text) => ({ - id: this.mirror.getId(text.node), - value: text.value, - })) + texts, // text mutation's id was not in the mirror map means the target node has been removed - .filter((text) => this.mirror.has(text.id)), - attributes: this.attributes - .map((attribute) => ({ - id: this.mirror.getId(attribute.node), - attributes: attribute.attributes, - })) + attributes, // attribute mutation's id was not in the mirror map means the target node has been removed - .filter((attribute) => this.mirror.has(attribute.id)), removes: this.removes, adds, }; @@ -462,9 +475,9 @@ export default class MutationBuffer { this.texts = []; this.attributes = []; this.removes = []; - this.addedSet = new Set(); - this.movedSet = new Set(); - this.droppedSet = new Set(); + this.addedSet.clear(); + this.movedSet.clear(); + this.droppedSet.clear(); this.movedMap = {}; this.mutationCb(payload); @@ -673,15 +686,18 @@ export default class MutationBuffer { * Make sure you check if `n`'s parent is blocked before calling this function * */ private genAdds = (node: Node, t?: Node) => { - let rp = 0; const queue: [Node, Node|undefined][] = new Array<[Node, Node|undefined]>(1000); - queue[0] = [node, t]; - - while (queue[rp]) { - const next = queue[rp] - if(!next) break; + let rp = -1; + let wp = -1; + queue[++wp] = [node, t]; + + while (rp < wp) { + const next = queue[++rp] + if(!next){ + throw new Error("Add queue is corrupt, there is no next item to process") + } const [n, target] = next; - rp++; + // this node was already recorded in other buffer, ignore it if (this.processedNodeManager.inOtherBuffer(n, this)) continue; @@ -709,12 +725,12 @@ export default class MutationBuffer { // but we have to remove it's children otherwise they will be added as placeholders too if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { n.childNodes.forEach((childN) => { - queue.push([childN, undefined]) + queue[++wp] = [childN, undefined] }); if (hasShadowRoot(n)) { n.shadowRoot.childNodes.forEach((childN) => { this.processedNodeManager.add(childN, this); - queue.push([childN, n]) + queue[++wp] = [childN, n] }); } } From bad990b1ad453ec68a6ec9f910d5332576b8482f Mon Sep 17 00:00:00 2001 From: JonasBa Date: Sun, 30 Jul 2023 11:24:25 -0400 Subject: [PATCH 06/20] perf(mutation): improve process --- packages/rrweb-snapshot/src/snapshot.ts | 4 +- packages/rrweb/src/record/mutation.ts | 218 ++++++++++-------- .../src/record/processed-node-manager.ts | 2 +- packages/rrweb/src/utils.ts | 11 +- 4 files changed, 126 insertions(+), 109 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 7617427930..29178e1d75 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -343,14 +343,14 @@ export function needMaskingText( try { if (typeof maskTextClass === 'string') { if (el.classList.contains(maskTextClass)) return true; - if (el.closest(`.${maskTextClass}`)) return true; + if (el.matches(`.${maskTextClass} *`)) return true; } else { if (classMatchesRegex(el, maskTextClass, true)) return true; } if (maskTextSelector) { if (el.matches(maskTextSelector)) return true; - if (el.closest(maskTextSelector)) return true; + if (el.matches(`${maskTextSelector} *`)) return true; } } catch (e) { // diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 1f2afe12a1..93b56812f6 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -267,6 +267,66 @@ export default class MutationBuffer { this.emit(); // clears buffer if not locked/frozen }; + + private pushAdd(n: Node, adds: addedNodeMutation[], addList: DoubleLinkedList) { + if (!n.parentNode || !inDom(n)) { + return; + } + const parentId = isShadowRoot(n.parentNode) + ? this.mirror.getId(getShadowHost(n)) + : this.mirror.getId(n.parentNode); + const nextId = getNextId(n, this.mirror); + if (parentId === -1 || nextId === -1) { + return addList.addNode(n); + } + const sn = serializeNodeWithId(n, { + doc: this.doc, + mirror: this.mirror, + blockClass: this.blockClass, + blockSelector: this.blockSelector, + maskTextClass: this.maskTextClass, + maskTextSelector: this.maskTextSelector, + skipChild: true, + newlyAddedElement: true, + inlineStylesheet: this.inlineStylesheet, + maskInputOptions: this.maskInputOptions, + maskTextFn: this.maskTextFn, + maskInputFn: this.maskInputFn, + slimDOMOptions: this.slimDOMOptions, + dataURLOptions: this.dataURLOptions, + recordCanvas: this.recordCanvas, + inlineImages: this.inlineImages, + onSerialize: (currentN) => { + if (isSerializedIframe(currentN, this.mirror)) { + this.iframeManager.addIframe(currentN as HTMLIFrameElement); + } + if (isSerializedStylesheet(currentN, this.mirror)) { + this.stylesheetManager.trackLinkElement( + currentN as HTMLLinkElement, + ); + } + if (hasShadowRoot(n)) { + this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); + } + }, + onIframeLoad: (iframe, childSn) => { + this.iframeManager.attachIframe(iframe, childSn); + this.shadowDomManager.observeAttachShadow(iframe); + }, + onStylesheetLoad: (link, childSn) => { + this.stylesheetManager.attachLinkElement(link, childSn); + }, + }); + if (sn) { + adds.push({ + parentId, + nextId, + node: sn, + }); + } + } + + public emit = () => { if (this.frozen || this.locked) { return; @@ -282,64 +342,7 @@ export default class MutationBuffer { * parent, so we init a queue to store these nodes. */ const addList = new DoubleLinkedList(); - const pushAdd = (n: Node) => { - if (!n.parentNode || !inDom(n)) { - return; - } - const parentId = isShadowRoot(n.parentNode) - ? this.mirror.getId(getShadowHost(n)) - : this.mirror.getId(n.parentNode); - const nextId = getNextId(n, this.mirror); - if (parentId === -1 || nextId === -1) { - return addList.addNode(n); - } - const sn = serializeNodeWithId(n, { - doc: this.doc, - mirror: this.mirror, - blockClass: this.blockClass, - blockSelector: this.blockSelector, - maskTextClass: this.maskTextClass, - maskTextSelector: this.maskTextSelector, - skipChild: true, - newlyAddedElement: true, - inlineStylesheet: this.inlineStylesheet, - maskInputOptions: this.maskInputOptions, - maskTextFn: this.maskTextFn, - maskInputFn: this.maskInputFn, - slimDOMOptions: this.slimDOMOptions, - dataURLOptions: this.dataURLOptions, - recordCanvas: this.recordCanvas, - inlineImages: this.inlineImages, - onSerialize: (currentN) => { - if (isSerializedIframe(currentN, this.mirror)) { - this.iframeManager.addIframe(currentN as HTMLIFrameElement); - } - if (isSerializedStylesheet(currentN, this.mirror)) { - this.stylesheetManager.trackLinkElement( - currentN as HTMLLinkElement, - ); - } - if (hasShadowRoot(n)) { - this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); - } - }, - onIframeLoad: (iframe, childSn) => { - this.iframeManager.attachIframe(iframe, childSn); - this.shadowDomManager.observeAttachShadow(iframe); - }, - onStylesheetLoad: (link, childSn) => { - this.stylesheetManager.attachLinkElement(link, childSn); - }, - }); - if (sn) { - adds.push({ - parentId, - nextId, - node: sn, - }); - } - }; - + while (this.mapRemoves.length) { this.mirror.removeNodeFromMap(this.mapRemoves.pop()!); } @@ -351,7 +354,7 @@ export default class MutationBuffer { ) { continue; } - pushAdd(n); + this.pushAdd(n, adds, addList); } for (const n of this.addedSet) { @@ -359,9 +362,9 @@ export default class MutationBuffer { !isAncestorInSet(this.droppedSet, n) && !isParentRemoved(this.removes, n, this.mirror) ) { - pushAdd(n); + this.pushAdd(n, adds, addList); } else if (isAncestorInSet(this.movedSet, n)) { - pushAdd(n); + this.pushAdd(n, adds, addList); } else { this.droppedSet.add(n); } @@ -428,7 +431,7 @@ export default class MutationBuffer { } candidate = node.previous; addList.removeNode(node.value); - pushAdd(node.value); + this.pushAdd(node.value, adds, addList); } const texts = []; @@ -487,28 +490,20 @@ export default class MutationBuffer { if (isIgnored(m.target, this.mirror)) { return; } - let unattachedDoc; - try { - // avoid upsetting original document from a Content Security point of view - unattachedDoc = document.implementation.createHTMLDocument(); - } catch (e) { - // fallback to more direct method - unattachedDoc = this.doc; - } switch (m.type) { case 'characterData': { const value = m.target.textContent; if ( - !isBlocked(m.target, this.blockClass, this.blockSelector, false) && - value !== m.oldValue + value !== m.oldValue && + !isBlocked(m.target, this.blockClass, this.blockSelector, false) ) { this.texts.push({ value: - needMaskingText( + value && needMaskingText( m.target, this.maskTextClass, this.maskTextSelector, - ) && value + ) ? this.maskTextFn ? this.maskTextFn(value) : value.replace(/[\S]/g, '*') @@ -536,15 +531,12 @@ export default class MutationBuffer { }); } if ( - isBlocked(m.target, this.blockClass, this.blockSelector, false) || - value === m.oldValue + value === m.oldValue || + isBlocked(m.target, this.blockClass, this.blockSelector, false) ) { return; } - let item: attributeCursor | undefined = this.attributes.find( - (a) => a.node === m.target, - ); if ( target.tagName === 'IFRAME' && attributeName === 'src' && @@ -558,6 +550,9 @@ export default class MutationBuffer { return; } } + let item: attributeCursor | undefined = this.attributes.find( + (a) => a.node === m.target, + ); if (!item) { item = { node: m.target, @@ -577,6 +572,14 @@ export default class MutationBuffer { } if (attributeName === 'style') { + let unattachedDoc; + try { + // avoid upsetting original document from a Content Security point of view + unattachedDoc = document.implementation.createHTMLDocument(); + } catch (e) { + // fallback to more direct method + unattachedDoc = this.doc; + } const old = unattachedDoc.createElement('span'); if (m.oldValue) { old.setAttribute('style', m.oldValue); @@ -633,9 +636,9 @@ export default class MutationBuffer { ? this.mirror.getId(m.target.host) : this.mirror.getId(m.target); if ( - isBlocked(m.target, this.blockClass, this.blockSelector, false) || isIgnored(n, this.mirror) || - !isSerialized(n, this.mirror) + !isSerialized(n, this.mirror) || + isBlocked(m.target, this.blockClass, this.blockSelector, false) ) { return; } @@ -685,14 +688,14 @@ export default class MutationBuffer { /** * Make sure you check if `n`'s parent is blocked before calling this function * */ + genAddsQueue: [Node, Node|undefined][] = new Array<[Node, Node|undefined]>(1000); private genAdds = (node: Node, t?: Node) => { - const queue: [Node, Node|undefined][] = new Array<[Node, Node|undefined]>(1000); let rp = -1; let wp = -1; - queue[++wp] = [node, t]; + this.genAddsQueue[++wp] = [node, t]; while (rp < wp) { - const next = queue[++rp] + const next = this.genAddsQueue[++rp] if(!next){ throw new Error("Add queue is corrupt, there is no next item to process") } @@ -721,18 +724,20 @@ export default class MutationBuffer { this.droppedSet.delete(n); } + const isNodeBlocked = isBlocked(n, this.blockClass, this.blockSelector, false); + if(isNodeBlocked){ + return + } // if this node is blocked `serializeNode` will turn it into a placeholder element // but we have to remove it's children otherwise they will be added as placeholders too - if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { - n.childNodes.forEach((childN) => { - queue[++wp] = [childN, undefined] + n.childNodes.forEach((childN) => { + this.genAddsQueue[++wp] = [childN, undefined] + }); + if (hasShadowRoot(n)) { + n.shadowRoot.childNodes.forEach((childN) => { + this.processedNodeManager.add(childN, this); + this.genAddsQueue[++wp] = [childN, n] }); - if (hasShadowRoot(n)) { - n.shadowRoot.childNodes.forEach((childN) => { - this.processedNodeManager.add(childN, this); - queue[++wp] = [childN, n] - }); - } } } }; @@ -745,8 +750,13 @@ export default class MutationBuffer { * that. */ function deepDelete(addsSet: Set, n: Node) { - addsSet.delete(n); - n.childNodes.forEach((childN) => deepDelete(addsSet, childN)); + const deleteQueue = [n]; + + while(deleteQueue.length) { + const node = deleteQueue.pop()!; + addsSet.delete(node); + node.childNodes.forEach((childN) => deleteQueue.push(childN)); + } } function isParentRemoved( @@ -755,23 +765,33 @@ function isParentRemoved( mirror: Mirror, ): boolean { if (removes.length === 0) return false; - return _isParentRemoved(removes, n, mirror); + return _isParentRemoved(removes, n, mirror, undefined); } function _isParentRemoved( removes: removedNodeMutation[], n: Node, mirror: Mirror, + removeSet: Set | undefined ): boolean { const { parentNode } = n; if (!parentNode) { return false; } - const parentId = mirror.getId(parentNode); - if (removes.some((r) => r.id === parentId)) { + + const removedLookupSet = removeSet || new Set(); + const parentId = mirror.getId(parentNode) + for(let i = 0; i < removes.length; i++){ + const removedNode = removes[i]; + if(removedNode.id === parentId){ + return true + } + removedLookupSet.add(removedNode.id); + } + if (removedLookupSet.has(parentId)) { return true; } - return _isParentRemoved(removes, parentNode, mirror); + return _isParentRemoved(removes, parentNode, mirror, removedLookupSet); } function isAncestorInSet(set: Set, n: Node): boolean { diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index e656d3504e..c8e047232b 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -23,7 +23,7 @@ export default class ProcessedNodeManager { const buffers = this.nodeMap.get(node); if(!buffers) return false; - return Array.from(buffers).some((buffer) => buffer !== thisBuffer) + return buffers.has(thisBuffer); } public add(node: Node, buffer: MutationBuffer) { diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 0e22436ccf..d09d5544bc 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -229,10 +229,10 @@ export function isBlocked( blockSelector: string | null, checkAncestors: boolean, ): boolean { - if(!blockClass && !blockSelector) return false; if (!node) { return false; } + if(!blockClass && !blockSelector) return false; const el: HTMLElement | null = node.nodeType === node.ELEMENT_NODE @@ -242,17 +242,14 @@ export function isBlocked( try { if (typeof blockClass === 'string') { - if (el.classList.contains(blockClass)) return true; - if (checkAncestors && el.closest('.' + blockClass) !== null) return true; - } else { - if (classMatchesRegex(el, blockClass, checkAncestors)) return true; + return el.classList.contains(blockClass) || (checkAncestors && el.matches(`.${blockClass} *`)); } + return classMatchesRegex(el, blockClass, checkAncestors) } catch (e) { // e } if (blockSelector) { - if (el.matches(blockSelector)) return true; - if (checkAncestors && el.closest(blockSelector) !== null) return true; + return el.matches(blockSelector) || (checkAncestors && el.matches(`${blockSelector} *`)); } return false; } From 5d59621f3a0d10c988b19d221db6cf6bfad9b9f2 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 1 Aug 2023 06:33:45 -0400 Subject: [PATCH 07/20] fix(mutation): revert regexp exec --- packages/rrweb-snapshot/src/snapshot.ts | 6 +++--- packages/rrweb/src/record/mutation.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 29178e1d75..ca7fdfcc20 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -151,7 +151,7 @@ function getAbsoluteSrcsetString(doc: Document, attributeValue: string) { function collectCharacters(regEx: RegExp) { let chars: string; - const match = attributeValue.substring(pos).match(regEx) + const match = regEx.exec(attributeValue.substring(pos)); if (match) { chars = match[0]; pos += chars.length; @@ -866,9 +866,9 @@ function slimDOMExcluded( ? // @ts-ignore sn.attributes.name.toLowerCase() : ''; - const snAttributeRel: string = sn.attributes.name + const snAttributeRel: string = sn.attributes.rel ? // @ts-ignore - sn.attributes.name.toLowerCase() + sn.attributes.rel.toLowerCase() : ''; const snAttributeProperty: string = sn.attributes.property ? // @ts-ignore diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 93b56812f6..e078553e52 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -447,11 +447,12 @@ export default class MutationBuffer { const attributes = []; for(let i = 0; i < this.attributes.length; i++){ - if(!this.mirror.getId(this.attributes[i].node)){ + const id = this.mirror.getId(this.attributes[i].node); + if(!id && id !== 0){ continue; } attributes.push({ - id: this.mirror.getId(this.attributes[i].node), + id, attributes: this.attributes[i].attributes, }); } From 58ab15cc73203eed759772633d7b4f22d06647cf Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 1 Aug 2023 07:16:36 -0400 Subject: [PATCH 08/20] fix(prettier): apply formatting --- packages/rrweb-snapshot/src/snapshot.ts | 18 ++--- packages/rrweb/src/record/mutation.ts | 79 +++++++++++-------- .../src/record/processed-node-manager.ts | 2 +- packages/rrweb/src/utils.ts | 16 ++-- 4 files changed, 66 insertions(+), 49 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index ca7fdfcc20..182cb067cc 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -52,13 +52,13 @@ function getValidTagName(element: HTMLElement): Lowercase { } function stringifyStyleSheet(sheet: CSSStyleSheet): string { - if(sheet.cssRules.length === 0) { - return ""; + if (sheet.cssRules.length === 0) { + return ''; } const buffer: string[] = new Array(sheet.cssRules.length); - for(let i = 0; i < sheet.cssRules.length; i++) { + for (let i = 0; i < sheet.cssRules.length; i++) { const rule = sheet.cssRules[i]; buffer[i] = rule.cssText ? validateStringifiedCssRule(rule.cssText) : ''; } @@ -277,8 +277,8 @@ export function _isBlockedElement( blockClass: string | RegExp, blockSelector: string | null, ): boolean { - if(!blockClass && !blockSelector) { - return false + if (!blockClass && !blockSelector) { + return false; } try { @@ -851,15 +851,15 @@ function slimDOMExcluded( sn: serializedNode, slimDOMOptions: SlimDOMOptions, ): boolean { - if(sn.type !== NodeType.Element && sn.type !== NodeType.Comment) { - return false + if (sn.type !== NodeType.Element && sn.type !== NodeType.Comment) { + return false; } if (sn.type === NodeType.Comment && slimDOMOptions.comment) { // TODO: convert IE conditional comments to real nodes return true; } - + if (sn.type === NodeType.Element) { /* eslint-disable */ const snAttributeName: string = sn.attributes.name @@ -1124,7 +1124,7 @@ export function serializeNodeWithId( n.shadowRoot.childNodes.forEach((childN) => { const serializedChildNode = serializeNodeWithId(childN, bypassOptions); if (serializedChildNode) { - if(isNativeShadowDom(n.shadowRoot!)){ + if (isNativeShadowDom(n.shadowRoot!)) { serializedChildNode.isShadow = true; } serializedNode.childNodes.push(serializedChildNode); diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index e078553e52..7f5f0ccc99 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -267,8 +267,11 @@ export default class MutationBuffer { this.emit(); // clears buffer if not locked/frozen }; - - private pushAdd(n: Node, adds: addedNodeMutation[], addList: DoubleLinkedList) { + private pushAdd( + n: Node, + adds: addedNodeMutation[], + addList: DoubleLinkedList, + ) { if (!n.parentNode || !inDom(n)) { return; } @@ -301,9 +304,7 @@ export default class MutationBuffer { this.iframeManager.addIframe(currentN as HTMLIFrameElement); } if (isSerializedStylesheet(currentN, this.mirror)) { - this.stylesheetManager.trackLinkElement( - currentN as HTMLLinkElement, - ); + this.stylesheetManager.trackLinkElement(currentN as HTMLLinkElement); } if (hasShadowRoot(n)) { this.shadowDomManager.addShadowRoot(n.shadowRoot, this.doc); @@ -326,7 +327,6 @@ export default class MutationBuffer { } } - public emit = () => { if (this.frozen || this.locked) { return; @@ -342,7 +342,7 @@ export default class MutationBuffer { * parent, so we init a queue to store these nodes. */ const addList = new DoubleLinkedList(); - + while (this.mapRemoves.length) { this.mirror.removeNodeFromMap(this.mapRemoves.pop()!); } @@ -389,7 +389,7 @@ export default class MutationBuffer { if (_node) { const nextId = getNextId(_node.value, this.mirror); if (nextId === -1) continue; - + const parentId = this.mirror.getId(_node.value.parentNode); // nextId !== -1 && parentId !== -1 @@ -435,8 +435,8 @@ export default class MutationBuffer { } const texts = []; - for(let i = 0; i < this.texts.length; i++){ - if(!this.mirror.getId(this.texts[i].node)){ + for (let i = 0; i < this.texts.length; i++) { + if (!this.mirror.getId(this.texts[i].node)) { continue; } texts.push({ @@ -446,9 +446,9 @@ export default class MutationBuffer { } const attributes = []; - for(let i = 0; i < this.attributes.length; i++){ + for (let i = 0; i < this.attributes.length; i++) { const id = this.mirror.getId(this.attributes[i].node); - if(!id && id !== 0){ + if (!id && id !== 0) { continue; } attributes.push({ @@ -459,9 +459,9 @@ export default class MutationBuffer { const payload = { texts, - // text mutation's id was not in the mirror map means the target node has been removed + // text mutation's id was not in the mirror map means the target node has been removed attributes, - // attribute mutation's id was not in the mirror map means the target node has been removed + // attribute mutation's id was not in the mirror map means the target node has been removed removes: this.removes, adds, }; @@ -500,7 +500,8 @@ export default class MutationBuffer { ) { this.texts.push({ value: - value && needMaskingText( + value && + needMaskingText( m.target, this.maskTextClass, this.maskTextSelector, @@ -636,9 +637,10 @@ export default class MutationBuffer { const parentId = isShadowRoot(m.target) ? this.mirror.getId(m.target.host) : this.mirror.getId(m.target); + if ( - isIgnored(n, this.mirror) || - !isSerialized(n, this.mirror) || + nodeId === IGNORED_NODE || + nodeId === -1 || isBlocked(m.target, this.blockClass, this.blockSelector, false) ) { return; @@ -689,16 +691,20 @@ export default class MutationBuffer { /** * Make sure you check if `n`'s parent is blocked before calling this function * */ - genAddsQueue: [Node, Node|undefined][] = new Array<[Node, Node|undefined]>(1000); + genAddsQueue: [Node, Node | undefined][] = new Array< + [Node, Node | undefined] + >(1000); private genAdds = (node: Node, t?: Node) => { let rp = -1; let wp = -1; this.genAddsQueue[++wp] = [node, t]; while (rp < wp) { - const next = this.genAddsQueue[++rp] - if(!next){ - throw new Error("Add queue is corrupt, there is no next item to process") + const next = this.genAddsQueue[++rp]; + if (!next) { + throw new Error( + 'Add queue is corrupt, there is no next item to process', + ); } const [n, target] = next; @@ -710,7 +716,7 @@ export default class MutationBuffer { if (this.mirror.hasNode(n)) { if (isIgnored(n, this.mirror)) { - continue + continue; } this.movedSet.add(n); let targetId: number | null = null; @@ -725,19 +731,24 @@ export default class MutationBuffer { this.droppedSet.delete(n); } - const isNodeBlocked = isBlocked(n, this.blockClass, this.blockSelector, false); - if(isNodeBlocked){ - return + const isNodeBlocked = isBlocked( + n, + this.blockClass, + this.blockSelector, + false, + ); + if (isNodeBlocked) { + return; } // if this node is blocked `serializeNode` will turn it into a placeholder element // but we have to remove it's children otherwise they will be added as placeholders too n.childNodes.forEach((childN) => { - this.genAddsQueue[++wp] = [childN, undefined] + this.genAddsQueue[++wp] = [childN, undefined]; }); if (hasShadowRoot(n)) { n.shadowRoot.childNodes.forEach((childN) => { this.processedNodeManager.add(childN, this); - this.genAddsQueue[++wp] = [childN, n] + this.genAddsQueue[++wp] = [childN, n]; }); } } @@ -753,7 +764,7 @@ export default class MutationBuffer { function deepDelete(addsSet: Set, n: Node) { const deleteQueue = [n]; - while(deleteQueue.length) { + while (deleteQueue.length) { const node = deleteQueue.pop()!; addsSet.delete(node); node.childNodes.forEach((childN) => deleteQueue.push(childN)); @@ -773,19 +784,19 @@ function _isParentRemoved( removes: removedNodeMutation[], n: Node, mirror: Mirror, - removeSet: Set | undefined + removeSet: Set | undefined, ): boolean { const { parentNode } = n; if (!parentNode) { return false; } - + const removedLookupSet = removeSet || new Set(); - const parentId = mirror.getId(parentNode) - for(let i = 0; i < removes.length; i++){ + const parentId = mirror.getId(parentNode); + for (let i = 0; i < removes.length; i++) { const removedNode = removes[i]; - if(removedNode.id === parentId){ - return true + if (removedNode.id === parentId) { + return true; } removedLookupSet.add(removedNode.id); } diff --git a/packages/rrweb/src/record/processed-node-manager.ts b/packages/rrweb/src/record/processed-node-manager.ts index c8e047232b..a0ed1947ea 100644 --- a/packages/rrweb/src/record/processed-node-manager.ts +++ b/packages/rrweb/src/record/processed-node-manager.ts @@ -21,7 +21,7 @@ export default class ProcessedNodeManager { public inOtherBuffer(node: Node, thisBuffer: MutationBuffer) { const buffers = this.nodeMap.get(node); - if(!buffers) return false; + if (!buffers) return false; return buffers.has(thisBuffer); } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index d09d5544bc..c0b03b0aa5 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -232,8 +232,8 @@ export function isBlocked( if (!node) { return false; } - if(!blockClass && !blockSelector) return false; - + if (!blockClass && !blockSelector) return false; + const el: HTMLElement | null = node.nodeType === node.ELEMENT_NODE ? (node as HTMLElement) @@ -242,14 +242,20 @@ export function isBlocked( try { if (typeof blockClass === 'string') { - return el.classList.contains(blockClass) || (checkAncestors && el.matches(`.${blockClass} *`)); + return ( + el.classList.contains(blockClass) || + (checkAncestors && el.matches(`.${blockClass} *`)) + ); } - return classMatchesRegex(el, blockClass, checkAncestors) + return classMatchesRegex(el, blockClass, checkAncestors); } catch (e) { // e } if (blockSelector) { - return el.matches(blockSelector) || (checkAncestors && el.matches(`${blockSelector} *`)); + return ( + el.matches(blockSelector) || + (checkAncestors && el.matches(`${blockSelector} *`)) + ); } return false; } From f6c87389a833ad9f6c18ee663d0e9ef11c28e366 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 3 Aug 2023 14:34:32 -0400 Subject: [PATCH 09/20] fix: filter condition --- packages/rrweb/src/record/mutation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 13d07be905..d7388e5e1a 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -437,11 +437,11 @@ export default class MutationBuffer { const texts = []; for (let i = 0; i < this.texts.length; i++) { const id = this.mirror.getId(this.texts[i].node); - if (addedIds.has(id) || this.mirror.has(id)) { + if (addedIds.has(id) || !this.mirror.has(id)) { continue; } texts.push({ - id: this.mirror.getId(this.texts[i].node), + id, value: this.texts[i].value, }); } @@ -449,7 +449,7 @@ export default class MutationBuffer { const attributes = []; for (let i = 0; i < this.attributes.length; i++) { const id = this.mirror.getId(this.attributes[i].node); - if (addedIds.has(id) || this.mirror.has(id)) { + if (addedIds.has(id) || !this.mirror.has(id)) { continue; } From e6bb17ec582347d5b7f7a0d2bb6b7354fe82d64f Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 3 Aug 2023 14:37:57 -0400 Subject: [PATCH 10/20] fix: remove unused import --- packages/rrweb/src/record/mutation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index d7388e5e1a..13232afaf7 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -24,7 +24,6 @@ import { isBlocked, isAncestorRemoved, isIgnored, - isSerialized, hasShadowRoot, isSerializedIframe, isSerializedStylesheet, From a92738424b78e501b65dad5bf1574dd7ead2a809 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 3 Aug 2023 14:42:20 -0400 Subject: [PATCH 11/20] fix: lint --- packages/rrweb/src/record/mutation.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 13232afaf7..2de8ef4aa9 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -455,7 +455,9 @@ export default class MutationBuffer { const { attributes: elAttributes } = this.attributes[i]; if (typeof elAttributes.style === 'string') { const diffAsStr = JSON.stringify(this.attributes[i].styleDiff); - const unchangedAsStr = JSON.stringify(this.attributes[i]._unchangedStyles); + const unchangedAsStr = JSON.stringify( + this.attributes[i]._unchangedStyles, + ); // check if the style diff is actually shorter than the regular string based mutation // (which was the whole point of #464 'compact style mutation'). if (diffAsStr.length < elAttributes.style.length) { @@ -602,13 +604,13 @@ export default class MutationBuffer { ); if (attributeName === 'style') { let unattachedDoc; - try { - // avoid upsetting original document from a Content Security point of view - unattachedDoc = document.implementation.createHTMLDocument(); - } catch (e) { - // fallback to more direct method - unattachedDoc = this.doc; - } + try { + // avoid upsetting original document from a Content Security point of view + unattachedDoc = document.implementation.createHTMLDocument(); + } catch (e) { + // fallback to more direct method + unattachedDoc = this.doc; + } const old = unattachedDoc.createElement('span'); if (m.oldValue) { old.setAttribute('style', m.oldValue); From 23512e64ac36f63a11c5e30bad05516f20a54ac8 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 3 Aug 2023 14:46:52 -0400 Subject: [PATCH 12/20] test: update snapshots --- .../__snapshots__/integration.test.ts.snap | 301 ++++++++---------- .../test/__snapshots__/record.test.ts.snap | 16 +- .../cross-origin-iframes.test.ts.snap | 26 +- 3 files changed, 157 insertions(+), 186 deletions(-) diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap index fe33ef3c7d..638d71132f 100644 --- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap @@ -566,6 +566,12 @@ exports[`record integration tests can freeze mutations 1`] = ` \\"source\\": 0, \\"texts\\": [], \\"attributes\\": [ + { + \\"id\\": 20, + \\"attributes\\": { + \\"foo\\": \\"bar\\" + } + }, { \\"id\\": 5, \\"attributes\\": { @@ -2923,11 +2929,38 @@ exports[`record integration tests can record node mutations 1`] = ` \\"class\\": \\"select2-container select2-dropdown-open select2-container-active\\" } }, + { + \\"id\\": 36, + \\"attributes\\": { + \\"id\\": \\"select2-drop\\", + \\"style\\": \\"left: Npx; width: Npx; top: Npx; bottom: auto; display: block;\\", + \\"class\\": \\"select2-drop select2-display-none select2-with-searchbox select2-drop-active\\" + } + }, + { + \\"id\\": 70, + \\"attributes\\": { + \\"style\\": \\"\\" + } + }, + { + \\"id\\": 42, + \\"attributes\\": { + \\"class\\": \\"select2-input select2-focused\\", + \\"aria-activedescendant\\": \\"select2-result-label-2\\" + } + }, { \\"id\\": 35, \\"attributes\\": { \\"disabled\\": \\"\\" } + }, + { + \\"id\\": 72, + \\"attributes\\": { + \\"class\\": \\"select2-results-dept-0 select2-result select2-result-selectable select2-highlighted\\" + } } ], \\"removes\\": [ @@ -2983,15 +3016,6 @@ exports[`record integration tests can record node mutations 1`] = ` \\"id\\": 28 } }, - { - \\"parentId\\": 28, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"A\\", - \\"id\\": 29 - } - }, { \\"parentId\\": 26, \\"nextId\\": 31, @@ -3028,6 +3052,15 @@ exports[`record integration tests can record node mutations 1`] = ` \\"id\\": 32 } }, + { + \\"parentId\\": 28, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"A\\", + \\"id\\": 29 + } + }, { \\"parentId\\": 32, \\"nextId\\": null, @@ -3078,6 +3111,30 @@ exports[`record integration tests can record node mutations 1`] = ` \\"id\\": 38 } }, + { + \\"parentId\\": 36, + \\"nextId\\": 45, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\" \\", + \\"id\\": 44 + } + }, + { + \\"parentId\\": 36, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 2, + \\"tagName\\": \\"ul\\", + \\"attributes\\": { + \\"class\\": \\"select2-results\\", + \\"role\\": \\"listbox\\", + \\"id\\": \\"select2-results-1\\" + }, + \\"childNodes\\": [], + \\"id\\": 45 + } + }, { \\"parentId\\": 38, \\"nextId\\": 40, @@ -3144,30 +3201,6 @@ exports[`record integration tests can record node mutations 1`] = ` \\"id\\": 43 } }, - { - \\"parentId\\": 36, - \\"nextId\\": 45, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\" \\", - \\"id\\": 44 - } - }, - { - \\"parentId\\": 36, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"ul\\", - \\"attributes\\": { - \\"class\\": \\"select2-results\\", - \\"role\\": \\"listbox\\", - \\"id\\": \\"select2-results-1\\" - }, - \\"childNodes\\": [], - \\"id\\": 45 - } - }, { \\"parentId\\": 45, \\"nextId\\": null, @@ -4582,7 +4615,15 @@ exports[`record integration tests handles null attribute values 1`] = ` \\"data\\": { \\"source\\": 0, \\"texts\\": [], - \\"attributes\\": [], + \\"attributes\\": [ + { + \\"id\\": 20, + \\"attributes\\": { + \\"aria-label\\": \\"label\\", + \\"id\\": \\"test-li\\" + } + } + ], \\"removes\\": [], \\"adds\\": [ { @@ -9007,6 +9048,15 @@ exports[`record integration tests should not record input values if dynamically \\"id\\": 21 } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 3, + \\"id\\": 21, + \\"x\\": 16.5, + \\"y\\": 0 + } + }, { \\"type\\": 3, \\"data\\": { @@ -10098,6 +10148,15 @@ exports[`record integration tests should record DOM node movement 1 1`] = ` \\"id\\": 14 } }, + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + }, { \\"parentId\\": 14, \\"nextId\\": 16, @@ -10118,15 +10177,6 @@ exports[`record integration tests should record DOM node movement 1 1`] = ` \\"id\\": 16 } }, - { - \\"parentId\\": 16, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"1\\", - \\"id\\": 17 - } - }, { \\"parentId\\": 14, \\"nextId\\": null, @@ -10137,12 +10187,12 @@ exports[`record integration tests should record DOM node movement 1 1`] = ` } }, { - \\"parentId\\": 12, + \\"parentId\\": 16, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 + \\"textContent\\": \\"1\\", + \\"id\\": 17 } } ] @@ -10354,6 +10404,15 @@ exports[`record integration tests should record DOM node movement 2 1`] = ` \\"id\\": 14 } }, + { + \\"parentId\\": 12, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"id\\": 19 + } + }, { \\"parentId\\": 14, \\"nextId\\": 16, @@ -10374,15 +10433,6 @@ exports[`record integration tests should record DOM node movement 2 1`] = ` \\"id\\": 16 } }, - { - \\"parentId\\": 16, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"1\\", - \\"id\\": 17 - } - }, { \\"parentId\\": 14, \\"nextId\\": null, @@ -10393,12 +10443,12 @@ exports[`record integration tests should record DOM node movement 2 1`] = ` } }, { - \\"parentId\\": 12, + \\"parentId\\": 16, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", - \\"id\\": 19 + \\"textContent\\": \\"1\\", + \\"id\\": 17 } }, { @@ -13721,18 +13771,6 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"id\\": 9, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } } ] } @@ -13750,18 +13788,6 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` } ], \\"adds\\": [ - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } - }, { \\"parentId\\": 4, \\"nextId\\": null, @@ -13770,11 +13796,11 @@ exports[`record integration tests should record moved shadow DOM 1`] = ` \\"tagName\\": \\"div\\", \\"attributes\\": {}, \\"childNodes\\": [], - \\"id\\": 11 + \\"id\\": 10 } }, { - \\"parentId\\": 11, + \\"parentId\\": 10, \\"nextId\\": null, \\"node\\": { \\"type\\": 2, @@ -13905,68 +13931,6 @@ exports[`record integration tests should record moved shadow DOM 2 1`] = ` \\"id\\": 10, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 10, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"p\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 11, - \\"isShadow\\": true - } - }, - { - \\"parentId\\": 10, - \\"nextId\\": 11, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"span\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 12, - \\"isShadow\\": true - } - }, - { - \\"parentId\\": 12, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 13 - } - } - ] - } - }, - { - \\"type\\": 3, - \\"data\\": { - \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], - \\"removes\\": [ - { - \\"parentId\\": 12, - \\"id\\": 13 - } - ], - \\"adds\\": [ - { - \\"parentId\\": 11, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 13 - } } ] } @@ -14603,6 +14567,14 @@ exports[`record integration tests should record nested iframes and shadow doms 1 \\"isAttachIframe\\": true } }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 4, + \\"width\\": 1920, + \\"height\\": 1080 + } + }, { \\"type\\": 3, \\"data\\": { @@ -15179,18 +15151,6 @@ exports[`record integration tests should record shadow DOM 2 1`] = ` \\"id\\": 9, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } } ] } @@ -15297,18 +15257,6 @@ exports[`record integration tests should record shadow DOM 3 1`] = ` \\"id\\": 9, \\"isShadowHost\\": true } - }, - { - \\"parentId\\": 9, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 2, - \\"tagName\\": \\"input\\", - \\"attributes\\": {}, - \\"childNodes\\": [], - \\"id\\": 10, - \\"isShadow\\": true - } } ] } @@ -15817,7 +15765,18 @@ exports[`record integration tests should record shadow doms polyfilled by synthe \\"id\\": 31, \\"isShadowHost\\": true } - }, + } + ] + } + }, + { + \\"type\\": 3, + \\"data\\": { + \\"source\\": 0, + \\"texts\\": [], + \\"attributes\\": [], + \\"removes\\": [], + \\"adds\\": [ { \\"parentId\\": 31, \\"nextId\\": null, diff --git a/packages/rrweb/test/__snapshots__/record.test.ts.snap b/packages/rrweb/test/__snapshots__/record.test.ts.snap index cb40f3328d..cbf073362b 100644 --- a/packages/rrweb/test/__snapshots__/record.test.ts.snap +++ b/packages/rrweb/test/__snapshots__/record.test.ts.snap @@ -3514,8 +3514,20 @@ exports[`record no need for attribute mutations on adds 1`] = ` \\"type\\": 3, \\"data\\": { \\"source\\": 0, - \\"texts\\": [], - \\"attributes\\": [], + \\"texts\\": [ + { + \\"id\\": 10, + \\"value\\": \\"y\\" + } + ], + \\"attributes\\": [ + { + \\"id\\": 9, + \\"attributes\\": { + \\"data-test\\": \\"x\\" + } + } + ], \\"removes\\": [], \\"adds\\": [ { diff --git a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap index 5caabe69ee..cd6e7ddf6f 100644 --- a/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap +++ b/packages/rrweb/test/record/__snapshots__/cross-origin-iframes.test.ts.snap @@ -4069,6 +4069,16 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] \\"id\\": 26 } }, + { + \\"parentId\\": 24, + \\"nextId\\": null, + \\"node\\": { + \\"type\\": 3, + \\"textContent\\": \\"\\\\n \\", + \\"rootId\\": 11, + \\"id\\": 31 + } + }, { \\"parentId\\": 26, \\"nextId\\": 28, @@ -4091,16 +4101,6 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] \\"id\\": 28 } }, - { - \\"parentId\\": 28, - \\"nextId\\": null, - \\"node\\": { - \\"type\\": 3, - \\"textContent\\": \\"1\\", - \\"rootId\\": 11, - \\"id\\": 29 - } - }, { \\"parentId\\": 26, \\"nextId\\": null, @@ -4112,13 +4112,13 @@ exports[`cross origin iframes move-node.html should record DOM node movement 1`] } }, { - \\"parentId\\": 24, + \\"parentId\\": 28, \\"nextId\\": null, \\"node\\": { \\"type\\": 3, - \\"textContent\\": \\"\\\\n \\", + \\"textContent\\": \\"1\\", \\"rootId\\": 11, - \\"id\\": 31 + \\"id\\": 29 } }, { From 510d82dd336d630a51980f62003386f4b1813aad Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 7 Aug 2023 15:38:23 -0400 Subject: [PATCH 13/20] revert(genAdds): revert queue implementation --- packages/rrweb/src/record/mutation.ts | 76 +++++++++------------------ 1 file changed, 25 insertions(+), 51 deletions(-) diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 2de8ef4aa9..5e9e18e2b5 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -709,64 +709,38 @@ export default class MutationBuffer { /** * Make sure you check if `n`'s parent is blocked before calling this function * */ - genAddsQueue: [Node, Node | undefined][] = new Array< - [Node, Node | undefined] - >(1000); - private genAdds = (node: Node, t?: Node) => { - let rp = -1; - let wp = -1; - this.genAddsQueue[++wp] = [node, t]; - - while (rp < wp) { - const next = this.genAddsQueue[++rp]; - if (!next) { - throw new Error( - 'Add queue is corrupt, there is no next item to process', - ); - } - const [n, target] = next; + private genAdds = (n: Node, target?: Node) => { + // this node was already recorded in other buffer, ignore it + if (this.processedNodeManager.inOtherBuffer(n, this)) return; - // this node was already recorded in other buffer, ignore it - if (this.processedNodeManager.inOtherBuffer(n, this)) continue; + // if n is added to set, there is no need to travel it and its' children again + if (this.addedSet.has(n) || this.movedSet.has(n)) return; - // if n is added to set, there is no need to travel it and its' children again - if (this.addedSet.has(n) || this.movedSet.has(n)) continue; - - if (this.mirror.hasNode(n)) { - if (isIgnored(n, this.mirror)) { - continue; - } - this.movedSet.add(n); - let targetId: number | null = null; - if (target && this.mirror.hasNode(target)) { - targetId = this.mirror.getId(target); - } - if (targetId && targetId !== -1) { - this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; - } - } else { - this.addedSet.add(n); - this.droppedSet.delete(n); - } - - const isNodeBlocked = isBlocked( - n, - this.blockClass, - this.blockSelector, - false, - ); - if (isNodeBlocked) { + if (this.mirror.hasNode(n)) { + if (isIgnored(n, this.mirror)) { return; } - // if this node is blocked `serializeNode` will turn it into a placeholder element - // but we have to remove it's children otherwise they will be added as placeholders too - n.childNodes.forEach((childN) => { - this.genAddsQueue[++wp] = [childN, undefined]; - }); + this.movedSet.add(n); + let targetId: number | null = null; + if (target && this.mirror.hasNode(target)) { + targetId = this.mirror.getId(target); + } + if (targetId && targetId !== -1) { + this.movedMap[moveKey(this.mirror.getId(n), targetId)] = true; + } + } else { + this.addedSet.add(n); + this.droppedSet.delete(n); + } + + // if this node is blocked `serializeNode` will turn it into a placeholder element + // but we have to remove it's children otherwise they will be added as placeholders too + if (!isBlocked(n, this.blockClass, this.blockSelector, false)) { + n.childNodes.forEach((childN) => this.genAdds(childN)); if (hasShadowRoot(n)) { n.shadowRoot.childNodes.forEach((childN) => { this.processedNodeManager.add(childN, this); - this.genAddsQueue[++wp] = [childN, n]; + this.genAdds(childN, n); }); } } From fab0d3d8ce6175771b11e08f5c1435449b309fd6 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 7 Aug 2023 15:40:18 -0400 Subject: [PATCH 14/20] revert(genAdds): revert snapshot --- .../test/__snapshots__/integration.test.ts.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap index 529a51eeff..d6d424d92e 100644 --- a/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap +++ b/packages/rrweb-snapshot/test/__snapshots__/integration.test.ts.snap @@ -363,6 +363,12 @@ exports[`integration tests [html file]: picture-in-frame.html 1`] = ` " `; +exports[`integration tests [html file]: picture-with-inline-onload.html 1`] = ` +" + \\"This + " +`; + exports[`integration tests [html file]: preload.html 1`] = ` " From e18825343427cc3c425a8f086e406bcb6dbf5bfa Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 09:44:41 -0400 Subject: [PATCH 15/20] feat(benchmark): add blockClass and blockSelector benchmarks --- .../rrweb/test/benchmark/dom-mutation.test.ts | 58 +++++++++++++------ .../benchmark-dom-mutation-add-blocked.html | 32 ++++++++++ 2 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 3da794db45..9e4832a432 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -10,6 +10,7 @@ const suites: Array< title: string; eval: string; times?: number; // defaults to 5 + options?: recordOptions } & ({ html: string } | { url: string }) > = [ // { @@ -18,30 +19,48 @@ const suites: Array< // 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, + // }, + // { + // title: 'create 1000 DOM nodes and append into its previous looped node', + // html: 'benchmark-dom-mutation-multiple-descendant-add.html', + // eval: 'window.workload()', + // times: 5, + // }, + // { + // title: 'create 10000 DOM nodes and move it to new container', + // html: 'benchmark-dom-mutation-add-and-move.html', + // eval: 'window.workload()', + // times: 5, + // }, { - 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, - }, - { - title: 'create 1000 DOM nodes and append into its previous looped node', - html: 'benchmark-dom-mutation-multiple-descendant-add.html', + title: 'Create 1000x10 DOM nodes that are blocked using blockClass', + html: 'benchmark-dom-mutation-add-blocked.html', eval: 'window.workload()', times: 5, + options: { + blockClass: "blocked" + } }, { - title: 'create 10000 DOM nodes and move it to new container', - html: 'benchmark-dom-mutation-add-and-move.html', + title: 'Create 1000x10 DOM nodes that are blocked using blockSelector', + html: 'benchmark-dom-mutation-add-blocked.html', eval: 'window.workload()', times: 5, - }, + options: { + blockSelector: ".blocked" + } + } ]; function avg(v: number[]): number { @@ -106,7 +125,7 @@ describe('benchmark: mutation observer', () => { }; const getDuration = async (): Promise => { - return (await page.evaluate((triggerWorkloadScript) => { + return (await page.evaluate((triggerWorkloadScript, replayOptions) => { return new Promise((resolve, reject) => { let start = 0; let lastEvent: eventWithTime | null; @@ -123,6 +142,7 @@ describe('benchmark: mutation observer', () => { } resolve(lastEvent.timestamp - start); }, + ...replayOptions }; const record = (window as any).rrweb.record; record(options); @@ -134,7 +154,7 @@ describe('benchmark: mutation observer', () => { record.addCustomEvent('FTAG', {}); }); }); - }, suite.eval)) as number; + }, suite.eval, suite.options ?? {})) as number; }; // generate profile.json file diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html b/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html new file mode 100644 index 0000000000..aaff7ef4ed --- /dev/null +++ b/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html @@ -0,0 +1,32 @@ + + + + From 8750f7f205ce5f4a7ed2c56f828d3b78bdb37bdf Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 10:12:39 -0400 Subject: [PATCH 16/20] Revert "feat(benchmark): add blockClass and blockSelector benchmarks" This reverts commit e18825343427cc3c425a8f086e406bcb6dbf5bfa. --- .../rrweb/test/benchmark/dom-mutation.test.ts | 58 ++++++------------- .../benchmark-dom-mutation-add-blocked.html | 32 ---------- 2 files changed, 19 insertions(+), 71 deletions(-) delete mode 100644 packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html diff --git a/packages/rrweb/test/benchmark/dom-mutation.test.ts b/packages/rrweb/test/benchmark/dom-mutation.test.ts index 9e4832a432..3da794db45 100644 --- a/packages/rrweb/test/benchmark/dom-mutation.test.ts +++ b/packages/rrweb/test/benchmark/dom-mutation.test.ts @@ -10,7 +10,6 @@ const suites: Array< title: string; eval: string; times?: number; // defaults to 5 - options?: recordOptions } & ({ html: string } | { url: string }) > = [ // { @@ -19,48 +18,30 @@ const suites: Array< // 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, - // }, - // { - // title: 'create 1000 DOM nodes and append into its previous looped node', - // html: 'benchmark-dom-mutation-multiple-descendant-add.html', - // eval: 'window.workload()', - // times: 5, - // }, - // { - // title: 'create 10000 DOM nodes and move it to new container', - // html: 'benchmark-dom-mutation-add-and-move.html', - // eval: 'window.workload()', - // times: 5, - // }, { - title: 'Create 1000x10 DOM nodes that are blocked using blockClass', - html: 'benchmark-dom-mutation-add-blocked.html', + 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, + }, + { + title: 'create 1000 DOM nodes and append into its previous looped node', + html: 'benchmark-dom-mutation-multiple-descendant-add.html', eval: 'window.workload()', times: 5, - options: { - blockClass: "blocked" - } }, { - title: 'Create 1000x10 DOM nodes that are blocked using blockSelector', - html: 'benchmark-dom-mutation-add-blocked.html', + title: 'create 10000 DOM nodes and move it to new container', + html: 'benchmark-dom-mutation-add-and-move.html', eval: 'window.workload()', times: 5, - options: { - blockSelector: ".blocked" - } - } + }, ]; function avg(v: number[]): number { @@ -125,7 +106,7 @@ describe('benchmark: mutation observer', () => { }; const getDuration = async (): Promise => { - return (await page.evaluate((triggerWorkloadScript, replayOptions) => { + return (await page.evaluate((triggerWorkloadScript) => { return new Promise((resolve, reject) => { let start = 0; let lastEvent: eventWithTime | null; @@ -142,7 +123,6 @@ describe('benchmark: mutation observer', () => { } resolve(lastEvent.timestamp - start); }, - ...replayOptions }; const record = (window as any).rrweb.record; record(options); @@ -154,7 +134,7 @@ describe('benchmark: mutation observer', () => { record.addCustomEvent('FTAG', {}); }); }); - }, suite.eval, suite.options ?? {})) as number; + }, suite.eval)) as number; }; // generate profile.json file diff --git a/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html b/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html deleted file mode 100644 index aaff7ef4ed..0000000000 --- a/packages/rrweb/test/html/benchmark-dom-mutation-add-blocked.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - From 5b05e6bd12e4b53811d0ac5d18fe252f84d94cff Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 13:13:52 -0400 Subject: [PATCH 17/20] ref(perf): less recursion and a few minor optimizations --- packages/rrdom/src/index.ts | 13 ++++-- packages/rrweb-snapshot/src/snapshot.ts | 30 ++++++++----- packages/rrweb-snapshot/src/utils.ts | 24 ++++++++--- packages/rrweb/src/record/mutation.ts | 56 +++++++++++++++---------- packages/rrweb/src/utils.ts | 10 ++++- 5 files changed, 90 insertions(+), 43 deletions(-) diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index eeddb03e1c..f285eec31d 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -367,11 +367,16 @@ export class Mirror implements IMirror { // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: RRNode) { - const id = this.getId(n); - this.idNodeMap.delete(id); + const queue = [n]; - if (n.childNodes) { - n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + while (queue.length > 0) { + const n = queue.pop()!; + const id = this.getId(n); + this.idNodeMap.delete(id); + + if (n.childNodes) { + n.childNodes.forEach((childNode) => queue.push(childNode)); + } } } has(id: number): boolean { diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 182cb067cc..11428f6b5e 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -283,6 +283,10 @@ export function _isBlockedElement( try { if (typeof blockClass === 'string') { + if(!element.classList.length) return false; + if(element.classList.length === 1 && element.classList[0] === blockClass) { + return true; + } if (element.classList.contains(blockClass)) { return true; } @@ -339,9 +343,12 @@ export function needMaskingText( ? (node as HTMLElement) : node.parentElement; - if (el === null) return false; + if (!el) return false; + try { if (typeof maskTextClass === 'string') { + if(!el.classList.length) return false; + if(el.classList.length === 1 && el.classList[0] === maskTextClass) return true; if (el.classList.contains(maskTextClass)) return true; if (el.matches(`.${maskTextClass} *`)) return true; } else { @@ -657,14 +664,14 @@ function serializeElementNode( const len = n.attributes.length; for (let i = 0; i < len; i++) { const attr = n.attributes[i]; - if (!ignoreAttribute(tagName, attr.name, attr.value)) { - attributes[attr.name] = transformAttribute( - doc, - tagName, - toLowerCase(attr.name), - attr.value, - ); - } + if (ignoreAttribute(tagName, attr.name, attr.value)) continue; + + attributes[attr.name] = transformAttribute( + doc, + tagName, + toLowerCase(attr.name), + attr.value, + ); } // remote css if (tagName === 'link' && inlineStylesheet) { @@ -1054,7 +1061,10 @@ export function serializeNodeWithId( id = genId(); } - const serializedNode = Object.assign(_serializedNode, { id }); + const serializedNode = { + ..._serializedNode, + id, + }; // add IGNORED_NODE to mirror to track nextSiblings mirror.add(n, serializedNode); diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 06e3b7a010..85f91ad62d 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -101,7 +101,11 @@ export class Mirror implements IMirror { getId(n: Node | undefined | null): number { if (!n) return -1; - const id = this.getMeta(n)?.id; + if(!this.nodeMetaMap.has(n)) { + return -1; + } + + const id = this.getMetaId(n); // if n is not a serialized Node, use -1 as its id. return id ?? -1; @@ -119,16 +123,26 @@ export class Mirror implements IMirror { return this.nodeMetaMap.get(n) || null; } + getMetaId(n: Node): number | null { + return this.getMeta(n)?.id ?? null; + } + // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: Node) { const id = this.getId(n); this.idNodeMap.delete(id); - if (n.childNodes) { - n.childNodes.forEach((childNode) => - this.removeNodeFromMap(childNode as unknown as Node), - ); + const removeQueue = [n]; + + while(removeQueue.length){ + const node = removeQueue.pop()!; + this.idNodeMap.delete(this.getId(node)); + + if (node.childNodes) { + node.childNodes.forEach(c => removeQueue.push(c)) + } + } } has(id: number): boolean { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5e9e18e2b5..6505fe0be8 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -769,33 +769,39 @@ function isParentRemoved( mirror: Mirror, ): boolean { if (removes.length === 0) return false; - return _isParentRemoved(removes, n, mirror, undefined); + return _isParentRemoved(removes, n, mirror, new Set()); } function _isParentRemoved( removes: removedNodeMutation[], n: Node, mirror: Mirror, - removeSet: Set | undefined, + removeSet: Set, ): boolean { - const { parentNode } = n; - if (!parentNode) { - return false; - } + const queue = [n]; + while (queue.length) { + const { parentNode } = queue.pop()!; + if (!parentNode) { + return false; + } - const removedLookupSet = removeSet || new Set(); - const parentId = mirror.getId(parentNode); - for (let i = 0; i < removes.length; i++) { - const removedNode = removes[i]; - if (removedNode.id === parentId) { + const parentId = mirror.getId(parentNode); + if (removeSet.has(parentId)) { return true; } - removedLookupSet.add(removedNode.id); - } - if (removedLookupSet.has(parentId)) { - return true; + + for (let i = 0; i < removes.length; i++) { + const removedNode = removes[i]; + if (removedNode.id === parentId) { + return true; + } + removeSet.add(removedNode.id); + } + + queue.push(parentNode); } - return _isParentRemoved(removes, parentNode, mirror, removedLookupSet); + + return false; } function isAncestorInSet(set: Set, n: Node): boolean { @@ -804,12 +810,16 @@ function isAncestorInSet(set: Set, n: Node): boolean { } function _isAncestorInSet(set: Set, n: Node): boolean { - const { parentNode } = n; - if (!parentNode) { - return false; - } - if (set.has(parentNode)) { - return true; + const queue = [n]; + while (queue.length) { + const { parentNode } = queue.pop()!; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + queue.push(parentNode); } - return _isAncestorInSet(set, parentNode); + return false; } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index c0b03b0aa5..91c032fb0d 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -238,13 +238,21 @@ export function isBlocked( node.nodeType === node.ELEMENT_NODE ? (node as HTMLElement) : node.parentElement; + if (!el) return false; try { if (typeof blockClass === 'string') { + if(!checkAncestors){ + if(el.classList.length === 1) return el.classList[0] === blockClass; + return el.classList.length > 0 && el.classList.contains(blockClass); + } + + if(el.classList.length === 1) return el.classList[0] === blockClass || el.matches(`.${blockClass} *`); return ( + el.classList.length > 0 && el.classList.contains(blockClass) || - (checkAncestors && el.matches(`.${blockClass} *`)) + el.matches(`.${blockClass} *`) ); } return classMatchesRegex(el, blockClass, checkAncestors); From 074fdd9dd69502e32356b68f6136f8a0130bf537 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 13:50:21 -0400 Subject: [PATCH 18/20] Revert "ref(perf): less recursion and a few minor optimizations" This reverts commit 5b05e6bd12e4b53811d0ac5d18fe252f84d94cff. --- packages/rrdom/src/index.ts | 13 ++---- packages/rrweb-snapshot/src/snapshot.ts | 30 +++++-------- packages/rrweb-snapshot/src/utils.ts | 24 +++-------- packages/rrweb/src/record/mutation.ts | 56 ++++++++++--------------- packages/rrweb/src/utils.ts | 10 +---- 5 files changed, 43 insertions(+), 90 deletions(-) diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index f285eec31d..eeddb03e1c 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -367,16 +367,11 @@ export class Mirror implements IMirror { // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: RRNode) { - const queue = [n]; + const id = this.getId(n); + this.idNodeMap.delete(id); - while (queue.length > 0) { - const n = queue.pop()!; - const id = this.getId(n); - this.idNodeMap.delete(id); - - if (n.childNodes) { - n.childNodes.forEach((childNode) => queue.push(childNode)); - } + if (n.childNodes) { + n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); } } has(id: number): boolean { diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 11428f6b5e..182cb067cc 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -283,10 +283,6 @@ export function _isBlockedElement( try { if (typeof blockClass === 'string') { - if(!element.classList.length) return false; - if(element.classList.length === 1 && element.classList[0] === blockClass) { - return true; - } if (element.classList.contains(blockClass)) { return true; } @@ -343,12 +339,9 @@ export function needMaskingText( ? (node as HTMLElement) : node.parentElement; - if (!el) return false; - + if (el === null) return false; try { if (typeof maskTextClass === 'string') { - if(!el.classList.length) return false; - if(el.classList.length === 1 && el.classList[0] === maskTextClass) return true; if (el.classList.contains(maskTextClass)) return true; if (el.matches(`.${maskTextClass} *`)) return true; } else { @@ -664,14 +657,14 @@ function serializeElementNode( const len = n.attributes.length; for (let i = 0; i < len; i++) { const attr = n.attributes[i]; - if (ignoreAttribute(tagName, attr.name, attr.value)) continue; - - attributes[attr.name] = transformAttribute( - doc, - tagName, - toLowerCase(attr.name), - attr.value, - ); + if (!ignoreAttribute(tagName, attr.name, attr.value)) { + attributes[attr.name] = transformAttribute( + doc, + tagName, + toLowerCase(attr.name), + attr.value, + ); + } } // remote css if (tagName === 'link' && inlineStylesheet) { @@ -1061,10 +1054,7 @@ export function serializeNodeWithId( id = genId(); } - const serializedNode = { - ..._serializedNode, - id, - }; + const serializedNode = Object.assign(_serializedNode, { id }); // add IGNORED_NODE to mirror to track nextSiblings mirror.add(n, serializedNode); diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 85f91ad62d..06e3b7a010 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -101,11 +101,7 @@ export class Mirror implements IMirror { getId(n: Node | undefined | null): number { if (!n) return -1; - if(!this.nodeMetaMap.has(n)) { - return -1; - } - - const id = this.getMetaId(n); + const id = this.getMeta(n)?.id; // if n is not a serialized Node, use -1 as its id. return id ?? -1; @@ -123,26 +119,16 @@ export class Mirror implements IMirror { return this.nodeMetaMap.get(n) || null; } - getMetaId(n: Node): number | null { - return this.getMeta(n)?.id ?? null; - } - // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: Node) { const id = this.getId(n); this.idNodeMap.delete(id); - const removeQueue = [n]; - - while(removeQueue.length){ - const node = removeQueue.pop()!; - this.idNodeMap.delete(this.getId(node)); - - if (node.childNodes) { - node.childNodes.forEach(c => removeQueue.push(c)) - } - + if (n.childNodes) { + n.childNodes.forEach((childNode) => + this.removeNodeFromMap(childNode as unknown as Node), + ); } } has(id: number): boolean { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 6505fe0be8..5e9e18e2b5 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -769,39 +769,33 @@ function isParentRemoved( mirror: Mirror, ): boolean { if (removes.length === 0) return false; - return _isParentRemoved(removes, n, mirror, new Set()); + return _isParentRemoved(removes, n, mirror, undefined); } function _isParentRemoved( removes: removedNodeMutation[], n: Node, mirror: Mirror, - removeSet: Set, + removeSet: Set | undefined, ): boolean { - const queue = [n]; - while (queue.length) { - const { parentNode } = queue.pop()!; - if (!parentNode) { - return false; - } + const { parentNode } = n; + if (!parentNode) { + return false; + } - const parentId = mirror.getId(parentNode); - if (removeSet.has(parentId)) { + const removedLookupSet = removeSet || new Set(); + const parentId = mirror.getId(parentNode); + for (let i = 0; i < removes.length; i++) { + const removedNode = removes[i]; + if (removedNode.id === parentId) { return true; } - - for (let i = 0; i < removes.length; i++) { - const removedNode = removes[i]; - if (removedNode.id === parentId) { - return true; - } - removeSet.add(removedNode.id); - } - - queue.push(parentNode); + removedLookupSet.add(removedNode.id); } - - return false; + if (removedLookupSet.has(parentId)) { + return true; + } + return _isParentRemoved(removes, parentNode, mirror, removedLookupSet); } function isAncestorInSet(set: Set, n: Node): boolean { @@ -810,16 +804,12 @@ function isAncestorInSet(set: Set, n: Node): boolean { } function _isAncestorInSet(set: Set, n: Node): boolean { - const queue = [n]; - while (queue.length) { - const { parentNode } = queue.pop()!; - if (!parentNode) { - return false; - } - if (set.has(parentNode)) { - return true; - } - queue.push(parentNode); + const { parentNode } = n; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; } - return false; + return _isAncestorInSet(set, parentNode); } diff --git a/packages/rrweb/src/utils.ts b/packages/rrweb/src/utils.ts index 91c032fb0d..c0b03b0aa5 100644 --- a/packages/rrweb/src/utils.ts +++ b/packages/rrweb/src/utils.ts @@ -238,21 +238,13 @@ export function isBlocked( node.nodeType === node.ELEMENT_NODE ? (node as HTMLElement) : node.parentElement; - if (!el) return false; try { if (typeof blockClass === 'string') { - if(!checkAncestors){ - if(el.classList.length === 1) return el.classList[0] === blockClass; - return el.classList.length > 0 && el.classList.contains(blockClass); - } - - if(el.classList.length === 1) return el.classList[0] === blockClass || el.matches(`.${blockClass} *`); return ( - el.classList.length > 0 && el.classList.contains(blockClass) || - el.matches(`.${blockClass} *`) + (checkAncestors && el.matches(`.${blockClass} *`)) ); } return classMatchesRegex(el, blockClass, checkAncestors); From eab4b250213c03eefa1ac94314d0c7b2cc68d670 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 14:28:45 -0400 Subject: [PATCH 19/20] ref(perf): less recursion --- packages/rrdom/src/index.ts | 14 ++++--- packages/rrweb-snapshot/src/utils.ts | 14 ++++--- packages/rrweb/src/record/mutation.ts | 56 ++++++++++++++++----------- 3 files changed, 50 insertions(+), 34 deletions(-) diff --git a/packages/rrdom/src/index.ts b/packages/rrdom/src/index.ts index eeddb03e1c..dc33455bd3 100644 --- a/packages/rrdom/src/index.ts +++ b/packages/rrdom/src/index.ts @@ -367,11 +367,15 @@ export class Mirror implements IMirror { // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: RRNode) { - const id = this.getId(n); - this.idNodeMap.delete(id); - - if (n.childNodes) { - n.childNodes.forEach((childNode) => this.removeNodeFromMap(childNode)); + const queue = [n]; + while (queue.length > 0) { + const n = queue.pop()!; + const id = this.getId(n); + this.idNodeMap.delete(id); + + if (n.childNodes) { + n.childNodes.forEach((childNode) => queue.push(childNode)); + } } } has(id: number): boolean { diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 06e3b7a010..a2ef2210c2 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -122,13 +122,15 @@ export class Mirror implements IMirror { // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: Node) { - const id = this.getId(n); - this.idNodeMap.delete(id); + const removeQueue = [n]; - if (n.childNodes) { - n.childNodes.forEach((childNode) => - this.removeNodeFromMap(childNode as unknown as Node), - ); + while(removeQueue.length){ + const node = removeQueue.pop()!; + this.idNodeMap.delete(this.getId(node)); + + if (node.childNodes) { + node.childNodes.forEach(c => removeQueue.push(c)) + } } } has(id: number): boolean { diff --git a/packages/rrweb/src/record/mutation.ts b/packages/rrweb/src/record/mutation.ts index 5e9e18e2b5..1bd540443f 100644 --- a/packages/rrweb/src/record/mutation.ts +++ b/packages/rrweb/src/record/mutation.ts @@ -769,33 +769,39 @@ function isParentRemoved( mirror: Mirror, ): boolean { if (removes.length === 0) return false; - return _isParentRemoved(removes, n, mirror, undefined); + return _isParentRemoved(removes, n, mirror, new Set()); } function _isParentRemoved( removes: removedNodeMutation[], n: Node, mirror: Mirror, - removeSet: Set | undefined, + removeSet: Set, ): boolean { - const { parentNode } = n; - if (!parentNode) { - return false; - } + const queue = [n]; + while (queue.length) { + const { parentNode } = queue.pop()!; + + if (!parentNode) { + return false; + } - const removedLookupSet = removeSet || new Set(); - const parentId = mirror.getId(parentNode); - for (let i = 0; i < removes.length; i++) { - const removedNode = removes[i]; - if (removedNode.id === parentId) { + const parentId = mirror.getId(parentNode); + if (removeSet.has(parentId)) { return true; } - removedLookupSet.add(removedNode.id); - } - if (removedLookupSet.has(parentId)) { - return true; + for (let i = 0; i < removes.length; i++) { + const removedNode = removes[i]; + if (removedNode.id === parentId) { + return true; + } + removeSet.add(removedNode.id); + } + + queue.push(parentNode); } - return _isParentRemoved(removes, parentNode, mirror, removedLookupSet); + + return false; } function isAncestorInSet(set: Set, n: Node): boolean { @@ -804,12 +810,16 @@ function isAncestorInSet(set: Set, n: Node): boolean { } function _isAncestorInSet(set: Set, n: Node): boolean { - const { parentNode } = n; - if (!parentNode) { - return false; - } - if (set.has(parentNode)) { - return true; + const queue = [n]; + while (queue.length) { + const { parentNode } = queue.pop()!; + if (!parentNode) { + return false; + } + if (set.has(parentNode)) { + return true; + } + queue.push(parentNode); } - return _isAncestorInSet(set, parentNode); + return false; } From eed46b055b05b4115b7e2fa31b25d3b05c57bf95 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 10 Aug 2023 14:53:13 -0400 Subject: [PATCH 20/20] fix(lint): format doc --- packages/rrweb-snapshot/src/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index a2ef2210c2..f870772a57 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -124,12 +124,12 @@ export class Mirror implements IMirror { removeNodeFromMap(n: Node) { const removeQueue = [n]; - while(removeQueue.length){ + while (removeQueue.length) { const node = removeQueue.pop()!; this.idNodeMap.delete(this.getId(node)); if (node.childNodes) { - node.childNodes.forEach(c => removeQueue.push(c)) + node.childNodes.forEach((c) => removeQueue.push(c)); } } }