Browse Source

feat(profiling): track aggregate durations on flamegraph (#57983)

Tracks (and accumulates) aggregate duration on flamegraph
Jonas 1 year ago
parent
commit
f0a558b30a

+ 1 - 0
static/app/types/profiling.d.ts

@@ -118,6 +118,7 @@ declare namespace Profiling {
     weights: number[];
     samples: number[][];
     samples_profiles?: number[][];
+    sample_durations_ns?: number[];
     type: 'sampled';
   }
 

+ 2 - 0
static/app/utils/profiling/callTreeNode.tsx

@@ -14,6 +14,8 @@ export class CallTreeNode {
   totalWeight: number = 0;
   selfWeight: number = 0;
 
+  aggregate_duration_ns = 0;
+
   static readonly Root = new CallTreeNode(Frame.Root, null);
 
   constructor(frame: Frame, parent: CallTreeNode | null) {

+ 1 - 0
static/app/utils/profiling/frame.tsx

@@ -24,6 +24,7 @@ export class Frame {
 
   totalWeight: number = 0;
   selfWeight: number = 0;
+  aggregateDuration: number = 0;
 
   static Root = new Frame({
     key: ROOT_KEY,

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

@@ -32,6 +32,7 @@ export class Profile {
   minFrameDuration = Number.POSITIVE_INFINITY;
 
   samples: CallTreeNode[] = [];
+  sample_durations_ns: number[] = [];
   weights: number[] = [];
   rawWeights: number[] = [];
 

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

@@ -387,4 +387,37 @@ describe('SampledProfile', () => {
     // the f1 frame is filtered out, so the f0 frame has no children
     expect(profile.callTree.children[0].children).toHaveLength(0);
   });
+
+  it('aggregates durations for flamegraph', () => {
+    const trace: Profiling.SampledProfile = {
+      name: 'profile',
+      startValue: 0,
+      endValue: 1000,
+      unit: 'milliseconds',
+      threadID: 0,
+      type: 'sampled',
+      weights: [1, 1],
+      sample_durations_ns: [10, 5],
+      samples: [
+        [0, 1],
+        [0, 2],
+      ],
+    };
+
+    const profile = SampledProfile.FromProfile(
+      trace,
+      createFrameIndex('mobile', [{name: 'f0'}, {name: 'f1'}, {name: 'f2'}]),
+      {
+        type: 'flamegraph',
+      }
+    );
+
+    expect(profile.callTree.children[0].frame.name).toBe('f0');
+    expect(profile.callTree.children[0].aggregate_duration_ns).toBe(15);
+    expect(profile.callTree.children[0].children[0].aggregate_duration_ns).toBe(10);
+    expect(profile.callTree.children[0].children[1].aggregate_duration_ns).toBe(5);
+    expect(profile.callTree.children[0].frame.aggregateDuration).toBe(15);
+    expect(profile.callTree.children[0].children[0].frame.aggregateDuration).toBe(10);
+    expect(profile.callTree.children[0].children[1].frame.aggregateDuration).toBe(5);
+  });
 });

+ 15 - 4
static/app/utils/profiling/profile/sampledProfile.tsx

@@ -34,6 +34,7 @@ function stacksWithWeights(
     return {
       stack: frameFilter ? stack.filter(frameFilter) : stack,
       weight: profile.weights[index],
+      aggregate_sample_duration: profile.sample_durations_ns?.[index] ?? 0,
       profileIds: profileIds[index],
     };
   });
@@ -43,7 +44,7 @@ function sortSamples(
   profile: Readonly<Profiling.SampledProfile>,
   profileIds: Readonly<string[][]> = [],
   frameFilter?: (i: number) => boolean
-): {stack: number[]; weight: number}[] {
+): {aggregate_sample_duration: number; stack: number[]; weight: number}[] {
   return stacksWithWeights(profile, profileIds, frameFilter).sort(sortStacks);
 }
 
@@ -122,6 +123,7 @@ export class SampledProfile extends Profile {
     for (let i = 0; i < samples.length; i++) {
       const stack = samples[i].stack;
       let weight = samples[i].weight;
+      let aggregate_duration_ns = samples[i].aggregate_sample_duration;
 
       const isGCStack =
         options.type === 'flamechart' &&
@@ -156,6 +158,7 @@ export class SampledProfile extends Profile {
               '(garbage collector) [native code]'
           ) {
             weight += samples[++i].weight;
+            aggregate_duration_ns += samples[i].aggregate_sample_duration;
           }
         }
       } else {
@@ -171,7 +174,13 @@ export class SampledProfile extends Profile {
         }
       }
 
-      profile.appendSampleWithWeight(resolvedStack, weight, size, resolvedProfileIds[i]);
+      profile.appendSampleWithWeight(
+        resolvedStack,
+        weight,
+        size,
+        resolvedProfileIds[i],
+        aggregate_duration_ns
+      );
     }
 
     return profile.build();
@@ -209,7 +218,8 @@ export class SampledProfile extends Profile {
     stack: Frame[],
     weight: number,
     end: number,
-    resolvedProfileIds?: string[]
+    resolvedProfileIds?: string[],
+    aggregate_duration_ns?: number
   ): void {
     // Keep track of discarded samples and ones that may have negative weights
     this.trackSampleStats(weight);
@@ -221,7 +231,6 @@ export class SampledProfile extends Profile {
 
     let node = this.callTree;
     const framesInStack: CallTreeNode[] = [];
-
     for (let i = 0; i < end; i++) {
       const frame = stack[i];
       const last = node.children[node.children.length - 1];
@@ -238,6 +247,7 @@ export class SampledProfile extends Profile {
       }
 
       node.totalWeight += weight;
+      node.aggregate_duration_ns += aggregate_duration_ns ?? 0;
 
       // TODO: This is On^2, because we iterate over all frames in the stack to check if our
       // frame is a recursive frame. We could do this in O(1) by keeping a map of frames in the stack
@@ -270,6 +280,7 @@ export class SampledProfile extends Profile {
 
     for (const stackNode of framesInStack) {
       stackNode.frame.totalWeight += weight;
+      stackNode.frame.aggregateDuration += aggregate_duration_ns ?? 0;
       stackNode.count++;
     }