Skip to content

Commit

Permalink
feat(profiling): track aggregate durations on flamegraph (#57983)
Browse files Browse the repository at this point in the history
Tracks (and accumulates) aggregate duration on flamegraph
  • Loading branch information
JonasBa authored Oct 12, 2023
1 parent 600b66f commit f0a558b
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 4 deletions.
1 change: 1 addition & 0 deletions static/app/types/profiling.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ declare namespace Profiling {
weights: number[];
samples: number[][];
samples_profiles?: number[][];
sample_durations_ns?: number[];
type: 'sampled';
}

Expand Down
2 changes: 2 additions & 0 deletions static/app/utils/profiling/callTreeNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/profiling/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class Frame {

totalWeight: number = 0;
selfWeight: number = 0;
aggregateDuration: number = 0;

static Root = new Frame({
key: ROOT_KEY,
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/profiling/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export class Profile {
minFrameDuration = Number.POSITIVE_INFINITY;

samples: CallTreeNode[] = [];
sample_durations_ns: number[] = [];
weights: number[] = [];
rawWeights: number[] = [];

Expand Down
33 changes: 33 additions & 0 deletions static/app/utils/profiling/profile/sampledProfile.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
19 changes: 15 additions & 4 deletions static/app/utils/profiling/profile/sampledProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};
});
Expand All @@ -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);
}

Expand Down Expand Up @@ -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' &&
Expand Down Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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];
Expand All @@ -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
Expand Down Expand Up @@ -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++;
}

Expand Down

0 comments on commit f0a558b

Please sign in to comment.