Browse Source

fix(flamegraph): Filter system frames when building profile (#51253)

Lifts the frame filtering logic to when the profile is built. This means
that the frame merging logic is correctly applied when the same frame
appears adjacent to each other. This also ensures to adjust the config
space when in flamegraph mode to the weight of the flamegraph so hidden
frames are not taking up horizontal space.
Tony Xiao 1 year ago
parent
commit
50849ad6a2

+ 21 - 2
static/app/components/profiling/aggregateFlamegraphPanel.tsx

@@ -10,10 +10,17 @@ import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {FlamegraphStateProvider} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/flamegraphContextProvider';
 import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
+import {Frame} from 'sentry/utils/profiling/frame';
 import {useAggregateFlamegraphQuery} from 'sentry/utils/profiling/hooks/useAggregateFlamegraphQuery';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider';
 
 export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
+  const [hideSystemFrames, setHideSystemFrames] = useLocalStorageState(
+    'profiling-flamegraph-collapsed-frames',
+    true
+  );
+
   const {data, isLoading} = useAggregateFlamegraphQuery({transaction});
 
   const isEmpty = data?.shared.frames.length === 0;
@@ -38,7 +45,12 @@ export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
           }
         />
       </Flex>
-      <ProfileGroupProvider type="flamegraph" input={data ?? null} traceID="">
+      <ProfileGroupProvider
+        type="flamegraph"
+        input={data ?? null}
+        traceID=""
+        frameFilter={hideSystemFrames ? applicationFrameOnly : undefined}
+      >
         <FlamegraphStateProvider
           initialState={{
             preferences: {
@@ -57,7 +69,10 @@ export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
                     <p>{t(`A flamegraph isn't available for your query`)}</p>
                   </EmptyStateWarning>
                 ) : (
-                  <AggregateFlamegraph />
+                  <AggregateFlamegraph
+                    hideSystemFrames={hideSystemFrames}
+                    setHideSystemFrames={setHideSystemFrames}
+                  />
                 )}
               </Flex>
             </Panel>
@@ -68,6 +83,10 @@ export function AggregateFlamegraphPanel({transaction}: {transaction: string}) {
   );
 }
 
+function applicationFrameOnly(frame: Frame): boolean {
+  return frame.is_application;
+}
+
 export const HeaderTitle = styled('span')`
   ${p => p.theme.text.cardTitle};
   color: ${p => p.theme.headingColor};

+ 9 - 13
static/app/components/profiling/flamegraph/aggregateFlamegraph.tsx

@@ -17,7 +17,6 @@ import SwitchButton from 'sentry/components/switchButton';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {defined} from 'sentry/utils';
-import {CallTreeNode} from 'sentry/utils/profiling/callTreeNode';
 import {
   CanvasPoolManager,
   useCanvasScheduler,
@@ -40,12 +39,16 @@ import {
 import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL';
 import {Rect} from 'sentry/utils/profiling/speedscope';
 import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio';
-import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider';
 
 const LOADING_OR_FALLBACK_FLAMEGRAPH = FlamegraphModel.Empty();
 
-export function AggregateFlamegraph(): ReactElement {
+interface AggregateFlamegraphProps {
+  hideSystemFrames: boolean;
+  setHideSystemFrames: (hideSystemFrames: boolean) => void;
+}
+
+export function AggregateFlamegraph(props: AggregateFlamegraphProps): ReactElement {
   const devicePixelRatio = useDevicePixelRatio();
   const dispatch = useDispatchFlamegraphState();
 
@@ -62,10 +65,6 @@ export function AggregateFlamegraph(): ReactElement {
   const [flamegraphOverlayCanvasRef, setFlamegraphOverlayCanvasRef] =
     useState<HTMLCanvasElement | null>(null);
 
-  const [hideSystemFrames, setHideSystemFrames] = useLocalStorageState(
-    'profiling-flamegraph-collapsed-frames',
-    true
-  );
   const canvasPoolManager = useMemo(() => new CanvasPoolManager(), []);
   const scheduler = useCanvasScheduler(canvasPoolManager);
 
@@ -96,15 +95,12 @@ export function AggregateFlamegraph(): ReactElement {
       inverted: view === 'bottom up',
       sort: sorting,
       configSpace: undefined,
-      filterFn: hideSystemFrames
-        ? (n: CallTreeNode) => n.frame.is_application
-        : undefined,
     });
 
     transaction.finish();
 
     return newFlamegraph;
-  }, [profile, sorting, threadId, hideSystemFrames, view]);
+  }, [profile, sorting, threadId, view]);
 
   const flamegraphCanvas = useMemo(() => {
     if (!flamegraphCanvasRef) {
@@ -294,8 +290,8 @@ export function AggregateFlamegraph(): ReactElement {
           <Flex align="center" gap={space(1)}>
             <span>{t('Hide System Frames')}</span>
             <SwitchButton
-              toggle={() => setHideSystemFrames(!hideSystemFrames)}
-              isActive={hideSystemFrames}
+              toggle={() => props.setHideSystemFrames(!props.hideSystemFrames)}
+              isActive={props.hideSystemFrames}
             />
           </Flex>
         </Flex>

+ 24 - 43
static/app/utils/profiling/flamegraph.ts

@@ -96,10 +96,8 @@ export class Flamegraph {
       inverted = false,
       sort = 'call order',
       configSpace,
-      filterFn,
     }: {
       configSpace?: Rect;
-      filterFn?: (node: CallTreeNode) => boolean;
       inverted?: boolean;
       sort?: 'left heavy' | 'alphabetical' | 'call order';
     } = {}
@@ -114,20 +112,20 @@ export class Flamegraph {
     // If a custom config space is provided, use it and draw the chart in it
     switch (this.sort) {
       case 'left heavy': {
-        this.frames = this.buildSortedChart(profile, leftHeavyTreeSort, filterFn);
+        this.frames = this.buildSortedChart(profile, leftHeavyTreeSort);
         break;
       }
       case 'alphabetical':
         if (this.profile.type === 'flamechart') {
           throw new TypeError('Flamechart does not support alphabetical sorting');
         }
-        this.frames = this.buildSortedChart(profile, alphabeticTreeSort, filterFn);
+        this.frames = this.buildSortedChart(profile, alphabeticTreeSort);
         break;
       case 'call order':
         if (this.profile.type === 'flamegraph') {
           throw new TypeError('Flamegraph does not support call order sorting');
         }
-        this.frames = this.buildCallOrderChart(profile, filterFn);
+        this.frames = this.buildCallOrderChart(profile);
         break;
       default:
         throw new TypeError(`Unknown flamechart sort type: ${this.sort}`);
@@ -136,44 +134,38 @@ export class Flamegraph {
     this.formatter = makeFormatter(profile.unit);
     this.timelineFormatter = makeTimelineFormatter(profile.unit);
 
-    if (this.profile.duration > 0) {
-      this.configSpace = new Rect(
-        0,
-        0,
-        configSpace ? configSpace.width : this.profile.duration,
-        this.depth
-      );
+    const weight = this.root.children.reduce(
+      (acc, frame) => acc + frame.node.totalWeight,
+      0
+    );
+
+    this.root.node.totalWeight += weight;
+    this.root.end = this.root.start + weight;
+    this.root.frame.totalWeight += weight;
+
+    let width = 0;
+
+    if (this.profile.type === 'flamegraph' && weight > 0) {
+      width = weight;
+    } else if (this.profile.duration > 0) {
+      width = configSpace ? configSpace.width : this.profile.duration;
     } else {
       // If the profile duration is 0, set the flamegraph duration
       // to 1 second so we can render a placeholder grid
-      this.configSpace = new Rect(
-        0,
-        0,
+      width =
         this.profile.unit === 'nanoseconds'
           ? 1e9
           : this.profile.unit === 'microseconds'
           ? 1e6
           : this.profile.unit === 'milliseconds'
           ? 1e3
-          : 1,
-        this.depth
-      );
+          : 1;
     }
 
-    const weight = this.root.children.reduce(
-      (acc, frame) => acc + frame.node.totalWeight,
-      0
-    );
-
-    this.root.node.totalWeight += weight;
-    this.root.end = this.root.start + weight;
-    this.root.frame.totalWeight += weight;
+    this.configSpace = new Rect(0, 0, width, this.depth);
   }
 
-  buildCallOrderChart(
-    profile: Profile,
-    filterFn?: (node: CallTreeNode) => boolean
-  ): FlamegraphFrame[] {
+  buildCallOrderChart(profile: Profile): FlamegraphFrame[] {
     const frames: FlamegraphFrame[] = [];
     const stack: FlamegraphFrame[] = [];
     let idx = 0;
@@ -221,14 +213,13 @@ export class Flamegraph {
       this.depth = Math.max(stackTop.depth, this.depth);
     };
 
-    profile.forEach(openFrame, closeFrame, filterFn);
+    profile.forEach(openFrame, closeFrame);
     return frames;
   }
 
   buildSortedChart(
     profile: Profile,
-    sortFn: (tree: CallTreeNode) => void,
-    filterFn?: (node: CallTreeNode) => boolean
+    sortFn: (tree: CallTreeNode) => void
   ): FlamegraphFrame[] {
     const frames: FlamegraphFrame[] = [];
     const stack: FlamegraphFrame[] = [];
@@ -292,16 +283,6 @@ export class Flamegraph {
     };
 
     function visit(node: CallTreeNode, start: number) {
-      // If the node should not be shown, skip it and just descend into its children
-      if (filterFn && !filterFn(node)) {
-        let childTime = 0;
-        node.children.forEach(child => {
-          visit(child, start + childTime);
-          childTime += child.totalWeight;
-        });
-        return;
-      }
-
       if (!node.frame.isRoot) {
         openFrame(node, start);
       }

+ 34 - 0
static/app/utils/profiling/profile/eventedProfile.spec.tsx

@@ -1,6 +1,8 @@
 import {EventedProfile} from 'sentry/utils/profiling/profile/eventedProfile';
 import {createFrameIndex} from 'sentry/utils/profiling/profile/utils';
 
+import {Frame} from '../frame';
+
 import {firstCallee, makeTestingBoilerplate} from './profile.spec';
 
 describe('EventedProfile', () => {
@@ -364,4 +366,36 @@ describe('EventedProfile - flamegraph', () => {
     expect(profile.callTree.children[0].count).toBe(3);
     expect(profile.callTree.children[0].children[0].count).toBe(1);
   });
+
+  it('filters frames', () => {
+    const trace: Profiling.EventedProfile = {
+      name: 'profile',
+      startValue: 0,
+      endValue: 1000,
+      unit: 'milliseconds',
+      threadID: 0,
+      type: 'evented',
+      events: [
+        {type: 'O', at: 0, frame: 0},
+        {type: 'O', at: 10, frame: 1},
+        {type: 'C', at: 20, frame: 1},
+        {type: 'C', at: 30, frame: 0},
+      ],
+    };
+
+    const profile = EventedProfile.FromProfile(
+      trace,
+      createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
+      {
+        type: 'flamegraph',
+        frameFilter: frame => frame.name === 'f0',
+      }
+    );
+
+    expect(profile.callTree.frame).toBe(Frame.Root);
+    expect(profile.callTree.children).toHaveLength(1);
+    expect(profile.callTree.children[0].frame.name).toEqual('f0');
+    // the f1 frame is filtered out, so the f0 frame has no children
+    expect(profile.callTree.children[0].children).toHaveLength(0);
+  });
 });

+ 8 - 1
static/app/utils/profiling/profile/eventedProfile.tsx

@@ -16,7 +16,10 @@ export class EventedProfile extends Profile {
   static FromProfile(
     eventedProfile: Profiling.EventedProfile,
     frameIndex: ReturnType<typeof createFrameIndex>,
-    options: {type: 'flamechart' | 'flamegraph'}
+    options: {
+      type: 'flamechart' | 'flamegraph';
+      frameFilter?: (frame: Frame) => boolean;
+    }
   ): EventedProfile {
     const profile = new EventedProfile({
       duration: eventedProfile.endValue - eventedProfile.startValue,
@@ -44,6 +47,10 @@ export class EventedProfile extends Profile {
         throw new Error(`Cannot retrieve event: ${event.frame} from frame index`);
       }
 
+      if (options.frameFilter && !options.frameFilter(frame)) {
+        continue;
+      }
+
       switch (event.type) {
         // Open a new frame
         case 'O': {

+ 21 - 9
static/app/utils/profiling/profile/importProfile.tsx

@@ -3,6 +3,7 @@ import {Transaction} from '@sentry/types';
 
 import {Image} from 'sentry/types/debugImage';
 
+import {Frame} from '../frame';
 import {
   isEventedProfile,
   isJSProfile,
@@ -25,6 +26,7 @@ import {
 export interface ImportOptions {
   transaction: Transaction | undefined;
   type: 'flamegraph' | 'flamechart';
+  frameFilter?: (frame: Frame) => boolean;
   profileIds?: Readonly<string[]>;
 }
 
@@ -42,7 +44,8 @@ export interface ProfileGroup {
 export function importProfile(
   input: Readonly<Profiling.ProfileInput>,
   traceID: string,
-  type: 'flamegraph' | 'flamechart'
+  type: 'flamegraph' | 'flamechart',
+  frameFilter?: (frame: Frame) => boolean
 ): ProfileGroup {
   const transaction = Sentry.startTransaction({
     op: 'import',
@@ -63,7 +66,7 @@ export function importProfile(
       if (transaction) {
         transaction.setTag('profile.type', 'sentry-sampled');
       }
-      return importSentrySampledProfile(input, {transaction, type});
+      return importSentrySampledProfile(input, {transaction, type, frameFilter});
     }
 
     if (isSchema(input)) {
@@ -71,7 +74,7 @@ export function importProfile(
       if (transaction) {
         transaction.setTag('profile.type', 'schema');
       }
-      return importSchema(input, traceID, {transaction, type});
+      return importSchema(input, traceID, {transaction, type, frameFilter});
     }
 
     throw new Error('Unsupported trace format');
@@ -152,7 +155,11 @@ function importSentrySampledProfile(
     profiles.push(
       wrapWithSpan(
         options.transaction,
-        () => SentrySampledProfile.FromProfile(profile, frameIndex, {type: options.type}),
+        () =>
+          SentrySampledProfile.FromProfile(profile, frameIndex, {
+            type: options.type,
+            frameFilter: options.frameFilter,
+          }),
         {
           op: 'profile.import',
           description: 'evented',
@@ -219,17 +226,17 @@ export function importSchema(
 function importSingleProfile(
   profile: Profiling.EventedProfile | Profiling.SampledProfile | JSSelfProfiling.Trace,
   frameIndex: ReturnType<typeof createFrameIndex>,
-  {transaction, type, profileIds}: ImportOptions
+  {transaction, type, frameFilter, profileIds}: ImportOptions
 ): Profile {
   if (isEventedProfile(profile)) {
     // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
     if (!transaction) {
-      return EventedProfile.FromProfile(profile, frameIndex, {type});
+      return EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter});
     }
 
     return wrapWithSpan(
       transaction,
-      () => EventedProfile.FromProfile(profile, frameIndex, {type}),
+      () => EventedProfile.FromProfile(profile, frameIndex, {type, frameFilter}),
       {
         op: 'profile.import',
         description: 'evented',
@@ -239,12 +246,17 @@ function importSingleProfile(
   if (isSampledProfile(profile)) {
     // In some cases, the SDK may return transaction as undefined and we dont want to throw there.
     if (!transaction) {
-      return SampledProfile.FromProfile(profile, frameIndex, {type, profileIds});
+      return SampledProfile.FromProfile(profile, frameIndex, {
+        type,
+        frameFilter,
+        profileIds,
+      });
     }
 
     return wrapWithSpan(
       transaction,
-      () => SampledProfile.FromProfile(profile, frameIndex, {type, profileIds}),
+      () =>
+        SampledProfile.FromProfile(profile, frameIndex, {type, frameFilter, profileIds}),
       {
         op: 'profile.import',
         description: 'sampled',

+ 0 - 38
static/app/utils/profiling/profile/profile.spec.tsx

@@ -224,42 +224,4 @@ describe('Profile', () => {
     expect(openSpy).toHaveBeenCalledTimes(2);
     expect(closeSpy).toHaveBeenCalledTimes(2);
   });
-
-  it('filter - removes system frames', () => {
-    const profile = new Profile({
-      duration: 1000,
-      startedAt: 0,
-      endedAt: 1000,
-      name: 'profile',
-      unit: 'ms',
-      threadId: 0,
-      type: 'flamechart',
-    });
-
-    // Frames
-    const f0 = f('f0', 0, false);
-    const f1 = f('f1', 1, true);
-    const f2 = f('f2', 1, false);
-
-    // Call tree nodes
-    const s0 = c(f0);
-    const s1 = c(f1);
-    const s2 = c(f2);
-
-    s1.parent = s0;
-    s2.parent = s1;
-
-    profile.samples = [s0, s1, s2];
-
-    const {open, close, openSpy, closeSpy, timings} = makeTestingBoilerplate();
-    profile.forEach(open, close, n => n.frame.is_application);
-
-    expect(timings).toEqual([
-      ['f1', 'open'],
-      ['f1', 'close'],
-    ]);
-
-    expect(openSpy).toHaveBeenCalledTimes(1);
-    expect(closeSpy).toHaveBeenCalledTimes(1);
-  });
 });

+ 1 - 9
static/app/utils/profiling/profile/profile.tsx

@@ -95,8 +95,7 @@ export class Profile {
 
   forEach(
     openFrame: (node: CallTreeNode, value: number) => void,
-    closeFrame: (node: CallTreeNode, value: number) => void,
-    filterFn?: (node: CallTreeNode) => boolean
+    closeFrame: (node: CallTreeNode, value: number) => void
   ): void {
     const prevStack: CallTreeNode[] = [];
     let value = 0;
@@ -119,18 +118,11 @@ export class Profile {
       let node: CallTreeNode | null = stackTop;
 
       while (node && !node.isRoot && node !== top) {
-        if (filterFn && !filterFn(node)) {
-          node = node.parent;
-          continue;
-        }
         toOpen.push(node);
         node = node.parent;
       }
 
       for (let i = toOpen.length - 1; i >= 0; i--) {
-        if (filterFn && !filterFn(toOpen[i])) {
-          continue;
-        }
         openFrame(toOpen[i], value);
         prevStack.push(toOpen[i]);
       }

+ 33 - 0
static/app/utils/profiling/profile/sampledProfile.spec.tsx

@@ -1,6 +1,8 @@
 import {SampledProfile} from 'sentry/utils/profiling/profile/sampledProfile';
 import {createFrameIndex} from 'sentry/utils/profiling/profile/utils';
 
+import {Frame} from '../frame';
+
 import {firstCallee, makeTestingBoilerplate} from './profile.spec';
 
 describe('SampledProfile', () => {
@@ -352,4 +354,35 @@ describe('SampledProfile', () => {
     expect(profile.callTree.children[0].count).toBe(3);
     expect(profile.callTree.children[0].children[0].count).toBe(1);
   });
+
+  it('filters frames', () => {
+    const trace: Profiling.SampledProfile = {
+      name: 'profile',
+      startValue: 0,
+      endValue: 1000,
+      unit: 'milliseconds',
+      threadID: 0,
+      type: 'sampled',
+      weights: [1, 1],
+      samples: [
+        [0, 1],
+        [0, 1],
+      ],
+    };
+
+    const profile = SampledProfile.FromProfile(
+      trace,
+      createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}]),
+      {
+        type: 'flamechart',
+        frameFilter: frame => frame.name === 'f0',
+      }
+    );
+
+    expect(profile.callTree.frame).toBe(Frame.Root);
+    expect(profile.callTree.children).toHaveLength(1);
+    expect(profile.callTree.children[0].frame.name).toEqual('f0');
+    // the f1 frame is filtered out, so the f0 frame has no children
+    expect(profile.callTree.children[0].children).toHaveLength(0);
+  });
 });

+ 51 - 35
static/app/utils/profiling/profile/sampledProfile.tsx

@@ -45,16 +45,16 @@ function sortSamples(
   return stacksWithWeights(profile, profileIds).sort(sortStacks);
 }
 
-function throwIfMissingFrame(index: number) {
-  throw new Error(`Could not resolve frame ${index} in frame index`);
-}
-
 // We should try and remove these as we adopt our own profile format and only rely on the sampled format.
 export class SampledProfile extends Profile {
   static FromProfile(
     sampledProfile: Profiling.SampledProfile,
     frameIndex: ReturnType<typeof createFrameIndex>,
-    options: {type: 'flamechart' | 'flamegraph'; profileIds?: Readonly<string[]>}
+    options: {
+      type: 'flamechart' | 'flamegraph';
+      frameFilter?: (frame: Frame) => boolean;
+      profileIds?: Readonly<string[]>;
+    }
   ): Profile {
     const profile = new SampledProfile({
       duration: sampledProfile.endValue - sampledProfile.startValue,
@@ -101,15 +101,26 @@ export class SampledProfile extends Profile {
     // and size of the stack to process. The size indicates how many items from the buffer we want
     // to process.
 
+    function resolveFrame(index) {
+      const resolvedFrame = frameIndex[index];
+      if (!resolvedFrame) {
+        throw new Error(`Could not resolve frame ${index} in frame index`);
+      }
+      if (options.frameFilter && !options.frameFilter(resolvedFrame)) {
+        return null;
+      }
+      return resolvedFrame;
+    }
+
     const resolvedStack: Frame[] = new Array(256); // stack size limit
+    let size = 0;
+    let frame: Frame | null = null;
 
     for (let i = 0; i < samples.length; i++) {
       const stack = samples[i].stack;
       let weight = samples[i].weight;
-      let size = samples[i].stack.length;
-      let useCurrentStack = true;
 
-      if (
+      const isGCStack =
         options.type === 'flamechart' &&
         i > 0 &&
         // We check for size <= 2 because we have so far only seen node profiles
@@ -118,37 +129,42 @@ export class SampledProfile extends Profile {
         // and when that happens, we do not want to enter this case as the GC will already
         // be placed at the top of the previous stack and the new stack length will be > 2
         stack.length <= 2 &&
-        frameIndex[stack[stack.length - 1]]?.name === '(garbage collector) [native code]'
-      ) {
-        // We have a GC frame, so we will use the previous stack
-        useCurrentStack = false;
+        frameIndex[stack[stack.length - 1]]?.name === '(garbage collector) [native code]';
+
+      if (isGCStack) {
         // The next stack we will process will be the previous stack + our new gc frame.
         // We write the GC frame on top of the previous stack and set the size to the new stack length.
-        resolvedStack[samples[i - 1].stack.length] = frameIndex[stack[stack.length - 1]];
-        // Size is not sample[i-1].size + our gc frame
-        size = samples[i - 1].stack.length + 1;
-
-        // Now collect all weights of all the consecutive gc frames and skip the samples
-        while (
-          samples[i + 1] &&
-          // We check for size <= 2 because we have so far only seen node profiles
-          // where GC is either marked as the root node or is directly under the root node.
-          // There is a good chance that this logic will at some point live on the backend
-          // and when that happens, we do not want to enter this case as the GC will already
-          // be placed at the top of the previous stack and the new stack length will be > 2
-          samples[i + 1].stack.length <= 2 &&
-          frameIndex[samples[i + 1].stack[samples[i + 1].stack.length - 1]]?.name ===
-            '(garbage collector) [native code]'
-        ) {
-          weight += samples[++i].weight;
+        frame = resolveFrame(stack[stack.length - 1]);
+        if (frame) {
+          resolvedStack[samples[i - 1].stack.length] =
+            frameIndex[stack[stack.length - 1]];
+          size += 1; // size of previous stack + new gc frame
+
+          // Now collect all weights of all the consecutive gc frames and skip the samples
+          while (
+            samples[i + 1] &&
+            // We check for size <= 2 because we have so far only seen node profiles
+            // where GC is either marked as the root node or is directly under the root node.
+            // There is a good chance that this logic will at some point live on the backend
+            // and when that happens, we do not want to enter this case as the GC will already
+            // be placed at the top of the previous stack and the new stack length will be > 2
+            samples[i + 1].stack.length <= 2 &&
+            frameIndex[samples[i + 1].stack[samples[i + 1].stack.length - 1]]?.name ===
+              '(garbage collector) [native code]'
+          ) {
+            weight += samples[++i].weight;
+          }
         }
-      }
-
-      // If we are using the current stack, then we need to resolve the frames,
-      // else the processed frames will be the frames that were previously resolved
-      if (useCurrentStack) {
+      } else {
+        size = 0;
+        // If we are using the current stack, then we need to resolve the frames,
+        // else the processed frames will be the frames that were previously resolved
         for (let j = 0; j < stack.length; j++) {
-          resolvedStack[j] = frameIndex[stack[j]] ?? throwIfMissingFrame(stack[j]);
+          frame = resolveFrame(stack[j]);
+          if (!frame) {
+            continue;
+          }
+          resolvedStack[size++] = frame;
         }
       }
 

Some files were not shown because too many files changed in this diff