-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
ba0d538
feat(profiling): add gl utils and gl-matrix lib
JonasBa 14acca6
test(utils): add some test coverage
JonasBa cd7541f
feat(profiling): move flamegraph and differential flamegraph
JonasBa 2113c3f
style(lint): Auto commit lint changes
getsantry[bot] 073a4fd
style(lint): Auto commit lint changes
getsantry[bot] 06f02aa
test(flamegraph): add flamegraph tests
JonasBa 197c99d
test(differentialflamegraph): add tests
JonasBa a5b1546
fix(flamegraph): fix imports
JonasBa fe6995c
test(flamegraph): test updated timings
JonasBa 24788b6
fix(flamegraph): strongly typed unit
JonasBa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 : ''); | ||
|
||
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,4 +49,8 @@ export class Frame extends WeightedNode { | |
} | ||
} | ||
} | ||
|
||
isRoot(): boolean { | ||
return Frame.Root === this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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?