Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): move flamegraph and differential flamegraph #30910

Merged
merged 10 commits into from
Jan 14, 2022
90 changes: 90 additions & 0 deletions static/app/utils/profiling/differentialFlamegraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {FlamegraphTheme} from './flamegraph/FlamegraphTheme';
import {relativeChange} from './units/units';
import {Flamegraph} from './flamegraph';
import {FlamegraphFrame} from './flamegraphFrame';

function countFrameOccurences(frames: FlamegraphFrame[]): Map<string, number> {
const counts = new Map<string, number>();

for (const frame of frames) {
const key = frame.frame.name + (frame.frame.file ? frame.frame.file : '');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a method on Frame?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zylphrex I thought about it before, but I haven't done it. The reason for that is that this is more the key that we use for color encoding which is not something that the Frame has to know or care about. Maybe a utility fn would be a good middle ground here, wdyt?


if (counts.has(key)) {
counts.set(key, counts.get(key)! + 1);
} else {
counts.set(key, 1);
}
}

return counts;
}

export class DifferentialFlamegraph extends Flamegraph {
fromToDiff: Map<string, number> = new Map();
toCount: Map<string, number> = new Map();
fromCount: Map<string, number> = new Map();

static Diff(
from: Flamegraph, // Reference chart is the one we compare the new flamegraph with
to: Flamegraph,
theme: FlamegraphTheme
): DifferentialFlamegraph {
const differentialFlamegraph = new DifferentialFlamegraph(
to.profile,
to.profileIndex,
from.inverted,
from.leftHeavy
);

const fromCounts = countFrameOccurences(from.frames);
const toCounts = countFrameOccurences(to.frames);

const countDiff: Map<string, number> = new Map();
const colorMap: Map<string | number, number[]> =
differentialFlamegraph.colors ?? new Map();

for (const frame of to.frames) {
const key = frame.frame.name + (frame.frame.file ? frame.frame.file : '');

// If we already diffed this frame, skip it
if (countDiff.has(key)) {
continue;
}

const fromCount = fromCounts.get(key);
const toCount = toCounts.get(key);

let diff = 0;
let color: number[] = [];

if (toCount === undefined) {
throw new Error(`Missing count for frame ${key}, this should never happen`);
}

if (fromCount === undefined) {
diff = 1;
color = [...theme.COLORS.DIFFERENTIAL_INCREASE, 1];
} else if (toCount > fromCount) {
diff = relativeChange(toCount, fromCount);
color = [...theme.COLORS.DIFFERENTIAL_INCREASE, diff];
} else if (fromCount > toCount) {
diff = relativeChange(toCount, fromCount);
color = [...theme.COLORS.DIFFERENTIAL_DECREASE, Math.abs(diff)];
} else {
countDiff.set(key, diff);
continue;
}

countDiff.set(key, diff);
colorMap.set(key, color);
}

differentialFlamegraph.fromToDiff = countDiff;
differentialFlamegraph.toCount = toCounts;
differentialFlamegraph.fromCount = fromCounts;

differentialFlamegraph.setColors(colorMap);

return differentialFlamegraph;
}
}
221 changes: 221 additions & 0 deletions static/app/utils/profiling/flamegraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import {lastOfArray} from 'sentry/utils';

import {Rect} from './gl/utils';
import {Profile} from './profile/profile';
import {CallTreeNode} from './callTreeNode';
import {FlamegraphFrame} from './flamegraphFrame';

export class Flamegraph {
profile: Profile;
frames: FlamegraphFrame[] = [];

name: string;
profileIndex: number;
startedAt: number;
endedAt: number;

inverted?: boolean = false;
leftHeavy?: boolean = false;

colors: Map<string | number, number[]> | null = null;

depth = 0;
duration = 0;
configSpace: Rect = new Rect(0, 0, 0, 0);

constructor(
profile: Profile,
profileIndex: number,
inverted = false,
leftHeavy = false
) {
this.inverted = inverted;
this.leftHeavy = leftHeavy;

// @TODO check if we can not keep a reference to the profile
this.profile = profile;

this.duration = profile.duration;
this.profileIndex = profileIndex;
this.name = profile.name;

this.startedAt = profile.startedAt;
this.endedAt = profile.endedAt;

this.frames = leftHeavy
? this.buildLeftHeavyGraph(profile)
: this.buildCallOrderGraph(profile);

if (this.frames.length) {
this.configSpace = new Rect(0, 0, this.duration, this.depth);
} else {
// If we have no frames, set the trace duration to 1 second so that we can render a placeholder grid
this.configSpace = new Rect(
0,
0,
this.profile.unit === 'microseconds'
? 1e6
: this.profile.unit === 'milliseconds'
? 1e3
: 1,
0
);
}
}

static Empty(): Flamegraph {
return new Flamegraph(
new Profile(0, 0, 1_000_000, 'Profile', 'microseconds'),
0,
false,
false
);
}

static From(from: Flamegraph, inverted = false, leftHeavy = false): Flamegraph {
return new Flamegraph(from.profile, from.profileIndex, inverted, leftHeavy);
}

buildCallOrderGraph(profile: Profile): FlamegraphFrame[] {
const frames: FlamegraphFrame[] = [];
const stack: FlamegraphFrame[] = [];

const openFrame = (node: CallTreeNode, value: number) => {
const parent = lastOfArray(stack);

const frame: FlamegraphFrame = {
frame: node.frame,
node,
parent,
children: [],
depth: 0,
start: value,
end: value,
};

if (parent) {
parent.children.push(frame);
}

stack.push(frame);
};

const closeFrame = (_: CallTreeNode, value: number) => {
const stackTop = stack.pop();

if (!stackTop) {
// This is unreachable because the profile importing logic already checks this
throw new Error('Unbalanced stack');
}

stackTop.end = value;
stackTop.depth = stack.length;

if (stackTop.end - stackTop.start === 0) {
return;
}

frames.unshift(stackTop);
this.depth = Math.max(stackTop.depth, this.depth);
};

profile.forEach(openFrame, closeFrame);
return frames;
}

buildLeftHeavyGraph(profile: Profile): FlamegraphFrame[] {
const frames: FlamegraphFrame[] = [];
const stack: FlamegraphFrame[] = [];

const sortTree = (node: CallTreeNode) => {
node.children.sort((a, b) => -(a.totalWeight - b.totalWeight));
node.children.forEach(c => sortTree(c));
};

sortTree(profile.appendOrderTree);

const openFrame = (node: CallTreeNode, value: number) => {
const parent = lastOfArray(stack);
const frame: FlamegraphFrame = {
frame: node.frame,
node,
parent,
children: [],
depth: 0,
start: value,
end: value,
};

if (parent) {
parent.children.push(frame);
}

stack.push(frame);
};

const closeFrame = (_node: CallTreeNode, value: number) => {
const stackTop = stack.pop();

if (!stackTop) {
throw new Error('Unbalanced stack');
}

stackTop.end = value;
stackTop.depth = stack.length;

// Dont draw 0 width frames
if (stackTop.end - stackTop.start === 0) {
return;
}
frames.unshift(stackTop);
this.depth = Math.max(stackTop.depth, this.depth);
};

function visit(node: CallTreeNode, start: number) {
if (!node.frame.isRoot()) {
openFrame(node, start);
}

let childTime = 0;

node.children.forEach(child => {
visit(child, start + childTime);
childTime += child.totalWeight;
});

if (!node.frame.isRoot()) {
closeFrame(node, start + node.totalWeight);
}
}
visit(profile.appendOrderTree, 0);
return frames;
}

setColors(colors: Map<string | number, number[]> | null): void {
this.colors = colors;
}

withOffset(offset: number): Flamegraph {
const mutateFrame = (frame: FlamegraphFrame) => {
frame.start = offset + frame.start;
frame.end = offset + frame.end;

return frame;
};

const visit = (frame: FlamegraphFrame): void => {
mutateFrame(frame);
};

for (const frame of this.frames) {
visit(frame);
}

return this;
}

setConfigSpace(configSpace: Rect): Flamegraph {
this.configSpace = configSpace;
return this;
}
}
12 changes: 12 additions & 0 deletions static/app/utils/profiling/flamegraphFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {CallTreeNode} from './callTreeNode';
import {Frame} from './frame';

export interface FlamegraphFrame {
frame: Frame;
node: CallTreeNode;
start: number;
end: number;
depth: number;
parent: FlamegraphFrame | null;
children: FlamegraphFrame[];
}
4 changes: 4 additions & 0 deletions static/app/utils/profiling/frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,8 @@ export class Frame extends WeightedNode {
}
}
}

isRoot(): boolean {
return Frame.Root === this;
}
}
1 change: 0 additions & 1 deletion static/app/utils/profiling/gl/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export function createShader(
gl.shaderSource(shader, source);
gl.compileShader(shader);
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);

if (success) {
return shader;
}
Expand Down
3 changes: 3 additions & 0 deletions static/app/utils/profiling/units/units.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function relativeChange(final: number, initial: number): number {
return (final - initial) / initial;
}
Loading