From 51c6e3da201b9b41e24f02efabd326e015d32b7b Mon Sep 17 00:00:00 2001 From: Vadim Korolik Date: Mon, 23 Sep 2024 15:17:11 -0500 Subject: [PATCH] improve canvas serialization performance --- packages/rrweb-snapshot/src/snapshot.ts | 12 +++++-- .../record/observers/canvas/canvas-manager.ts | 33 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts index 1f07858f..a665971d 100644 --- a/packages/rrweb-snapshot/src/snapshot.ts +++ b/packages/rrweb-snapshot/src/snapshot.ts @@ -518,7 +518,8 @@ function serializeTextNode( cssCaptured?: boolean; }, ): serializedNode { - const { needsMask, maskTextFn, privacySetting, rootId, cssCaptured } = options; + const { needsMask, maskTextFn, privacySetting, rootId, cssCaptured } = + options; // 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 parent = dom.parentNode(n); @@ -551,7 +552,11 @@ function serializeTextNode( n.parentElement?.getAttribute('data-hl-record'); const obfuscateDefaultPrivacy = privacySetting === 'default' && shouldObfuscateTextByDefault(textContent); - if ((enableStrictPrivacy || obfuscateDefaultPrivacy) && !highlightOverwriteRecord && parentTagName) { + if ( + (enableStrictPrivacy || obfuscateDefaultPrivacy) && + !highlightOverwriteRecord && + parentTagName + ) { const IGNORE_TAG_NAMES = new Set([ 'HEAD', 'TITLE', @@ -704,10 +709,13 @@ function serializeElementNode( if ((n as ICanvas).__context === '2d') { // only record this on 2d canvas if (!is2DCanvasBlank(n as HTMLCanvasElement)) { + /** Start of Highlight Code + * Avoid capturing non-blank 2d canvas here - defer to canvas-manager.ts for better performance attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL( dataURLOptions.type, dataURLOptions.quality, ); + End of Highlight Code **/ } } else if (!('__context' in n)) { // context is unknown, better not call getContext to trigger it diff --git a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts index c2babfcb..33624653 100644 --- a/packages/rrweb/src/record/observers/canvas/canvas-manager.ts +++ b/packages/rrweb/src/record/observers/canvas/canvas-manager.ts @@ -142,7 +142,7 @@ export class CanvasManager { ], }, ], - } + }; this.debug(null, 'canvas worker recording mutation', mutation); this.mutationCb(mutation); }; @@ -337,9 +337,27 @@ export class CanvasManager { let rafId: number; const elementFoundTime: Map = new Map(); + + const querySelectorAll = ( + node: ParentNode, + selector: T, + ): (T extends 'canvas' ? HTMLCanvasElement : HTMLVideoElement)[] => { + type NodeType = T extends 'canvas' ? HTMLCanvasElement : HTMLVideoElement; + const nodes: NodeType[] = []; + node.querySelectorAll(selector).forEach((n) => nodes.push(n as NodeType)); + const nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE); + let currentNode: Element | null; + while ((currentNode = nodeIterator.nextNode() as Element | null)) { + if (currentNode?.shadowRoot) { + nodes.push(...querySelectorAll(currentNode.shadowRoot, selector)); + } + } + return nodes; + }; + const getCanvas = (timestamp: DOMHighResTimeStamp): HTMLCanvasElement[] => { const matchedCanvas: HTMLCanvasElement[] = []; - win.document.querySelectorAll('canvas').forEach((canvas) => { + querySelectorAll(win.document, 'canvas').forEach((canvas) => { if (!isBlocked(canvas, blockClass, blockSelector, true)) { this.debug(canvas, 'discovered canvas'); matchedCanvas.push(canvas); @@ -355,7 +373,7 @@ export class CanvasManager { const getVideos = (timestamp: DOMHighResTimeStamp): HTMLVideoElement[] => { const matchedVideos: HTMLVideoElement[] = []; if (recordVideos) { - win.document.querySelectorAll('video').forEach((video) => { + querySelectorAll(win.document, 'video').forEach((video) => { if (video.src !== '' && video.src.indexOf('blob:') === -1) return; if (!isBlocked(video, blockClass, blockSelector, true)) { matchedVideos.push(video); @@ -428,7 +446,14 @@ export class CanvasManager { // video is not yet ready... this retry on the next sampling iteration. // we don't want to crash the worker by sending an undefined bitmap // if the video is not yet rendered. - if (video.width === 0 || video.height === 0 || actualWidth === 0 || actualHeight === 0 || boxWidth === 0 || boxHeight === 0) { + if ( + video.width === 0 || + video.height === 0 || + actualWidth === 0 || + actualHeight === 0 || + boxWidth === 0 || + boxHeight === 0 + ) { this.debug(video, 'not yet ready', { width: video.width, height: video.height,