Browse Source

feat(replay): Filter out `style` mutations when extracting DOM nodes (#83016)

We have an org that has a small handful of replays where the
replayStepper
causes massive perf issues to the extent that it freezes the browser. I
narrowed it down to the `diff()` code inside of `rrdom` and a recent
upstream
PR (https://github.com/getsentry/rrweb/pull/233) seems to have
exacerbated the
problem. I have not been able to figure out the root cause for the perf
issues,
but it seems to be related to CSS and the mutations that add `style`
elements.
We will want to try to identify what exactly in these replays are
causing the
perf issues.

In the meantime we can filter out these mutations. Since we are only
interested
in generating and extracting the HTML for certain breadcrumb events, the
styles
should have no affect on the data we are interested in using.

Closes https://github.com/getsentry/sentry/issues/82221
Billy Vong 2 months ago
parent
commit
7f91555a20

+ 4 - 1
static/app/utils/replays/hooks/useExtractDomNodes.tsx

@@ -10,7 +10,10 @@ export default function useExtractDomNodes({
 }): UseQueryResult<Map<ReplayFrame, Extraction>> {
   return useQuery({
     queryKey: ['getDomNodes', replay],
-    queryFn: () => replay?.getExtractDomNodes(),
+    // Note: we filter out `style` mutations due to perf issues.
+    // We can do this as long as we only need the HTML and not need to
+    // visualize the rendered elements
+    queryFn: () => replay?.getExtractDomNodes({withoutStyles: true}),
     enabled: Boolean(replay),
     gcTime: Infinity,
   });

+ 48 - 15
static/app/utils/replays/replayReader.tsx

@@ -432,22 +432,26 @@ export default class ReplayReader {
     return this.processingErrors().length;
   };
 
-  getExtractDomNodes = memoize(async () => {
-    if (this._fetching) {
-      return null;
-    }
-    const {onVisitFrame, shouldVisitFrame} = extractDomNodes;
-
-    const results = await replayerStepper({
-      frames: this.getDOMFrames(),
-      rrwebEvents: this.getRRWebFrames(),
-      startTimestampMs: this.getReplay().started_at.getTime() ?? 0,
-      onVisitFrame,
-      shouldVisitFrame,
-    });
+  getExtractDomNodes = memoize(
+    async ({withoutStyles}: {withoutStyles?: boolean} = {}) => {
+      if (this._fetching) {
+        return null;
+      }
+      const {onVisitFrame, shouldVisitFrame} = extractDomNodes;
+
+      const results = await replayerStepper({
+        frames: this.getDOMFrames(),
+        rrwebEvents: withoutStyles
+          ? this.getRRWebFramesWithoutStyles()
+          : this.getRRWebFrames(),
+        startTimestampMs: this.getReplay().started_at.getTime() ?? 0,
+        onVisitFrame,
+        shouldVisitFrame,
+      });
 
-    return results;
-  });
+      return results;
+    }
+  );
 
   getClipWindow = () => this._clipWindow;
 
@@ -534,6 +538,35 @@ export default class ReplayReader {
     return eventsWithSnapshots;
   });
 
+  /**
+   * Filter out style mutations as they can cause perf problems especially when
+   * used in replayStepper
+   */
+  getRRWebFramesWithoutStyles = memoize(() => {
+    return this.getRRWebFrames().map(e => {
+      if (
+        e.type === EventType.IncrementalSnapshot &&
+        'source' in e.data &&
+        e.data.source === IncrementalSource.Mutation
+      ) {
+        return {
+          ...e,
+          data: {
+            ...e.data,
+            adds: e.data.adds.filter(
+              add =>
+                !(
+                  (add.node.type === 3 && add.node.isStyle) ||
+                  (add.node.type === 2 && add.node.tagName === 'style')
+                )
+            ),
+          },
+        };
+      }
+      return e;
+    });
+  });
+
   getRRwebTouchEvents = memoize(() =>
     this.getRRWebFramesWithSnapshots().filter(
       e => isTouchEndFrame(e) || isTouchStartFrame(e)