123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- import {lastOfArray} from 'sentry/utils';
- import {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame';
- import {Rect} from './gl/utils';
- import {Profile} from './profile/profile';
- import {SampledProfile} from './profile/sampledProfile';
- import {makeFormatter, makeTimelineFormatter} from './units/units';
- import {CallTreeNode} from './callTreeNode';
- import {Frame} from './frame';
- function sortByTotalWeight(a: CallTreeNode, b: CallTreeNode) {
- return b.totalWeight - a.totalWeight;
- }
- function sortAlphabetically(a: CallTreeNode, b: CallTreeNode) {
- return a.frame.name.localeCompare(b.frame.name);
- }
- function makeTreeSort(sortFn: (a: CallTreeNode, b: CallTreeNode) => number) {
- return (root: CallTreeNode) => {
- const queue: CallTreeNode[] = [root];
- while (queue.length > 0) {
- const next = queue.pop()!;
- next.children.sort(sortFn);
- for (let i = 0; i < next.children.length; i++) {
- queue.push(next.children[i]);
- }
- }
- };
- }
- const alphabeticTreeSort = makeTreeSort(sortAlphabetically);
- const leftHeavyTreeSort = makeTreeSort(sortByTotalWeight);
- // Intermediary flamegraph data structure for rendering a profile. Constructs a list of frames from a profile
- // and appends them to a virtual root. Taken mostly from speedscope with a few modifications. This should get
- // removed as we port to our own format for profiles. The general idea is to iterate over profiles while
- // keeping an intermediary stack so as to resemble the execution of the program.
- export class Flamegraph {
- profile: Profile;
- frames: ReadonlyArray<FlamegraphFrame> = [];
- profileIndex: number;
- inverted: boolean = false;
- sort: 'left heavy' | 'alphabetical' | 'call order' = 'call order';
- depth = 0;
- configSpace: Rect = Rect.Empty();
- root: FlamegraphFrame = {
- key: -1,
- parent: null,
- frame: new Frame({...Frame.Root}),
- node: new CallTreeNode(new Frame({...Frame.Root}), null),
- depth: -1,
- start: 0,
- end: 0,
- children: [],
- };
- formatter: (value: number) => string;
- timelineFormatter: (value: number) => string;
- static Empty(): Flamegraph {
- return new Flamegraph(Profile.Empty, 0, {
- inverted: false,
- sort: 'call order',
- });
- }
- static Example(): Flamegraph {
- return new Flamegraph(SampledProfile.Example, 0, {
- inverted: false,
- sort: 'call order',
- });
- }
- static From(
- from: Flamegraph,
- {
- inverted = false,
- sort = 'call order',
- }: {
- inverted?: Flamegraph['inverted'];
- sort?: Flamegraph['sort'];
- }
- ): Flamegraph {
- return new Flamegraph(from.profile, from.profileIndex, {
- inverted,
- sort,
- });
- }
- constructor(
- profile: Profile,
- profileIndex: number,
- {
- inverted = false,
- sort = 'call order',
- configSpace,
- }: {
- configSpace?: Rect;
- inverted?: boolean;
- sort?: 'left heavy' | 'alphabetical' | 'call order';
- } = {}
- ) {
- this.inverted = inverted;
- this.sort = sort;
- // @TODO check if we can get rid of this profile reference
- this.profile = profile;
- this.profileIndex = profileIndex;
- // If a custom config space is provided, use it and draw the chart in it
- switch (this.sort) {
- case 'left heavy': {
- this.frames = this.buildSortedChart(profile, leftHeavyTreeSort);
- break;
- }
- case 'alphabetical':
- if (this.profile.type === 'flamechart') {
- throw new TypeError('Flamechart does not support alphabetical sorting');
- }
- this.frames = this.buildSortedChart(profile, alphabeticTreeSort);
- break;
- case 'call order':
- if (this.profile.type === 'flamegraph') {
- throw new TypeError('Flamegraph does not support call order sorting');
- }
- this.frames = this.buildCallOrderChart(profile);
- break;
- default:
- throw new TypeError(`Unknown flamechart sort type: ${this.sort}`);
- }
- this.formatter = makeFormatter(profile.unit);
- this.timelineFormatter = makeTimelineFormatter(profile.unit);
- if (this.profile.duration > 0) {
- this.configSpace = new Rect(
- 0,
- 0,
- configSpace ? configSpace.width : this.profile.duration,
- this.depth
- );
- } else {
- // If the profile duration is 0, set the flamegraph duration
- // to 1 second so we can render a placeholder grid
- this.configSpace = new Rect(
- 0,
- 0,
- this.profile.unit === 'nanoseconds'
- ? 1e9
- : this.profile.unit === 'microseconds'
- ? 1e6
- : this.profile.unit === 'milliseconds'
- ? 1e3
- : 1,
- this.depth
- );
- }
- const weight = this.root.children.reduce(
- (acc, frame) => acc + frame.node.totalWeight,
- 0
- );
- this.root.node.addToTotalWeight(weight);
- this.root.end = this.root.start + weight;
- this.root.frame.addToTotalWeight(weight);
- }
- buildCallOrderChart(profile: Profile): FlamegraphFrame[] {
- const frames: FlamegraphFrame[] = [];
- const stack: FlamegraphFrame[] = [];
- let idx = 0;
- const openFrame = (node: CallTreeNode, value: number) => {
- const parent = lastOfArray(stack) ?? this.root;
- const frame: FlamegraphFrame = {
- key: idx,
- frame: node.frame,
- node,
- parent,
- children: [],
- depth: 0,
- start: value,
- end: value,
- };
- if (parent) {
- parent.children.push(frame);
- } else {
- this.root.children.push(frame);
- }
- stack.push(frame);
- idx++;
- };
- 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.push(stackTop);
- this.depth = Math.max(stackTop.depth, this.depth);
- };
- profile.forEach(openFrame, closeFrame);
- return frames;
- }
- buildSortedChart(
- profile: Profile,
- sortFn: (tree: CallTreeNode) => void
- ): FlamegraphFrame[] {
- const frames: FlamegraphFrame[] = [];
- const stack: FlamegraphFrame[] = [];
- sortFn(profile.callTree);
- const virtualRoot: FlamegraphFrame = {
- key: -1,
- frame: CallTreeNode.Root.frame,
- node: CallTreeNode.Root,
- parent: null,
- children: [],
- depth: 0,
- start: 0,
- end: 0,
- };
- this.root = virtualRoot;
- let idx = 0;
- const openFrame = (node: CallTreeNode, value: number) => {
- const parent = lastOfArray(stack) ?? this.root;
- const frame: FlamegraphFrame = {
- key: idx,
- frame: node.frame,
- node,
- parent,
- children: [],
- depth: 0,
- start: value,
- end: value,
- profileIds: profile.callTreeNodeProfileIdMap.get(node),
- };
- if (parent) {
- parent.children.push(frame);
- } else {
- this.root.children.push(frame);
- }
- stack.push(frame);
- idx++;
- };
- 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.push(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.callTree, 0);
- return frames;
- }
- findAllMatchingFramesBy(
- query: string,
- fields: (keyof FlamegraphFrame['frame'])[]
- ): FlamegraphFrame[] {
- const matches: FlamegraphFrame[] = [];
- if (!fields.length) {
- throw new Error('No fields provided');
- }
- if (fields.length === 1) {
- for (let i = 0; i < this.frames.length; i++) {
- if (this.frames[i].frame[fields[0]] === query) {
- matches.push(this.frames[i]);
- }
- }
- return matches;
- }
- for (let i = 0; i < this.frames.length; i++) {
- for (let j = fields.length; j--; ) {
- if (this.frames[i].frame[fields[j]] === query) {
- matches.push(this.frames[i]);
- }
- }
- }
- return matches;
- }
- findAllMatchingFrames(frameName?: string, framePackage?: string): FlamegraphFrame[] {
- const matches: FlamegraphFrame[] = [];
- for (let i = 0; i < this.frames.length; i++) {
- if (
- this.frames[i].frame.name === frameName &&
- // the framePackage can match either the package or the module
- // this is an artifact of how we previously used image
- (this.frames[i].frame.package === framePackage ||
- this.frames[i].frame.module === framePackage)
- ) {
- matches.push(this.frames[i]);
- }
- }
- return matches;
- }
- }
|