From f0a558b30af659091e2a42732b415159830ec62a Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 12 Oct 2023 17:05:46 -0400 Subject: [PATCH] feat(profiling): track aggregate durations on flamegraph (#57983) Tracks (and accumulates) aggregate duration on flamegraph --- static/app/types/profiling.d.ts | 1 + static/app/utils/profiling/callTreeNode.tsx | 2 ++ static/app/utils/profiling/frame.tsx | 1 + .../app/utils/profiling/profile/profile.tsx | 1 + .../profiling/profile/sampledProfile.spec.tsx | 33 +++++++++++++++++++ .../profiling/profile/sampledProfile.tsx | 19 ++++++++--- 6 files changed, 53 insertions(+), 4 deletions(-) diff --git a/static/app/types/profiling.d.ts b/static/app/types/profiling.d.ts index ebe33957923ea5..82794cd49e2539 100644 --- a/static/app/types/profiling.d.ts +++ b/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'; } diff --git a/static/app/utils/profiling/callTreeNode.tsx b/static/app/utils/profiling/callTreeNode.tsx index 13d2b45ad5b979..91f13ccddce4b9 100644 --- a/static/app/utils/profiling/callTreeNode.tsx +++ b/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) { diff --git a/static/app/utils/profiling/frame.tsx b/static/app/utils/profiling/frame.tsx index 2bde4cea9d20b1..734b105bb860b7 100644 --- a/static/app/utils/profiling/frame.tsx +++ b/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, diff --git a/static/app/utils/profiling/profile/profile.tsx b/static/app/utils/profiling/profile/profile.tsx index 3fbe53e6a3d1cc..291ec799d69569 100644 --- a/static/app/utils/profiling/profile/profile.tsx +++ b/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[] = []; diff --git a/static/app/utils/profiling/profile/sampledProfile.spec.tsx b/static/app/utils/profiling/profile/sampledProfile.spec.tsx index 0e4e4ef15dd50b..f0f6c50500469c 100644 --- a/static/app/utils/profiling/profile/sampledProfile.spec.tsx +++ b/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); + }); }); diff --git a/static/app/utils/profiling/profile/sampledProfile.tsx b/static/app/utils/profiling/profile/sampledProfile.tsx index 39074bb99e2f3d..78cdb0a51186ed 100644 --- a/static/app/utils/profiling/profile/sampledProfile.tsx +++ b/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, profileIds: Readonly = [], 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++; }