Skip to content

Commit b5a7109

Browse files
committed
feat: show a panel at the with stack frame info
1 parent ff995fa commit b5a7109

File tree

3 files changed

+188
-3
lines changed

3 files changed

+188
-3
lines changed

packages/vscode-js-profile-core/src/client/icons.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
* Copyright (C) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------*/
44

5-
import { h, FunctionComponent } from 'preact';
5+
import { FunctionComponent, h } from 'preact';
66

7-
export const Icon: FunctionComponent<{ i: string }> = ({ i }) => (
7+
export const Icon: FunctionComponent<{ i: string } & h.JSX.HTMLAttributes> = ({ i, ...props }) => (
88
<span
99
dangerouslySetInnerHTML={{ __html: i }}
1010
style={{ color: 'var(--vscode-icon-foreground)' }}
11+
{...props}
1112
/>
1213
);

packages/vscode-js-profile-flame/src/client/flame-graph.css

+61
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,67 @@
8686
background-color: var(--vscode-inputOption-activeBackground);
8787
}
8888

89+
.info {
90+
position: absolute;
91+
bottom: 0;
92+
left: 30px;
93+
right: 30px;
94+
background: var(--vscode-sideBar-background);
95+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
96+
padding: 1rem;
97+
display: flex;
98+
align-items: flex-start;
99+
}
100+
101+
.info dl {
102+
display: grid;
103+
align-items: flex-start;
104+
grid-template-columns: 80px 100px;
105+
grid-gap: 0.7rem 0.5rem;
106+
}
107+
108+
.info dt {
109+
opacity: 0.8;
110+
}
111+
112+
.info dt small {
113+
display: block;
114+
}
115+
116+
.info ol {
117+
padding-left: 1em;
118+
}
119+
120+
.info ol li a {
121+
padding: 0.1em 0;
122+
display: inline-block;
123+
cursor: pointer;
124+
color: var(--vscode-editor-foreground);
125+
}
126+
127+
.info ol li em {
128+
font-style: normal;
129+
}
130+
131+
.info ol li a em {
132+
color: var(--vscode-textLink-foreground);
133+
}
134+
135+
.info ol li a em:hover,
136+
.info ol li a em:focus {
137+
color: var(--vscode-textLink-activeForeground);
138+
}
139+
140+
.info .goToFile {
141+
width: 1em;
142+
display: inline-block;
143+
cursor: pointer;
144+
}
145+
146+
:global(.vscode-dark) .info {
147+
box-shadow: 0 0 15px rgba(0, 0, 0, 0.4);
148+
}
149+
89150
:global(.vscode-high-contrast) .handle {
90151
opacity: 1;
91152
}

packages/vscode-js-profile-flame/src/client/flame-graph.tsx

+124-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
import { Fragment, FunctionComponent, h } from 'preact';
66
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'preact/hooks';
7+
import * as GoToFileIcon from 'vscode-codicons/src/icons/go-to-file.svg';
78
import { binarySearch } from 'vscode-js-profile-core/out/esm/array';
9+
import { Icon } from 'vscode-js-profile-core/out/esm/client/icons';
810
import { MiddleOut } from 'vscode-js-profile-core/out/esm/client/middleOutCompression';
911
import { useCssVariables } from 'vscode-js-profile-core/out/esm/client/useCssVariables';
1012
import { usePersistedState } from 'vscode-js-profile-core/out/esm/client/usePersistedState';
@@ -26,7 +28,8 @@ export const enum Constants {
2628
TimelineHeight = 22,
2729
TimelineLabelSpacing = 200,
2830
MinWindow = 0.005,
29-
ExtraYBuffer = 30,
31+
ExtraYBuffer = 300,
32+
DefaultStackLimit = 7,
3033
}
3134

3235
const pickColor = (location: IColumnLocation): number => {
@@ -683,6 +686,15 @@ export const FlameGraph: FunctionComponent<{
683686
location={hovered.box.loc}
684687
/>
685688
)}
689+
{focused && (
690+
<InfoBox
691+
columns={columns}
692+
boxes={rawBoxes.boxById}
693+
box={focused}
694+
model={model}
695+
setFocused={setFocused}
696+
/>
697+
)}
686698
</Fragment>
687699
);
688700
};
@@ -793,3 +805,114 @@ const Tooltip: FunctionComponent<{
793805
</div>
794806
);
795807
};
808+
809+
const InfoBox: FunctionComponent<{
810+
box: IBox;
811+
model: IProfileModel;
812+
columns: ReadonlyArray<IColumn>;
813+
boxes: ReadonlyMap<number, IBox>;
814+
setFocused(box: IBox): void;
815+
}> = ({ columns, boxes, box, model, setFocused }) => {
816+
const originalLocation = model.locations[box.loc.id];
817+
const localLocation = box.loc;
818+
const [limitedStack, setLimitedStack] = useState(true);
819+
820+
useEffect(() => setLimitedStack(true), [box]);
821+
822+
const stack = useMemo(() => {
823+
const stack: IBox[] = [box];
824+
for (let row = box.row - 1; row >= 0 && stack.length; row--) {
825+
const b = getBoxInRowColumn(columns, boxes, box.column, row);
826+
if (b) {
827+
stack.push(b);
828+
}
829+
}
830+
831+
return stack;
832+
}, [box, columns, boxes]);
833+
834+
const shouldTruncateStack = stack.length >= Constants.DefaultStackLimit + 3 && limitedStack;
835+
836+
return (
837+
<div className={styles.info}>
838+
<dl>
839+
<dt>Self Time</dt>
840+
<dd>{decimalFormat.format(localLocation.selfTime / 1000)}ms</dd>
841+
<dt>Total Time</dt>
842+
<dd>{decimalFormat.format(localLocation.aggregateTime / 1000)}ms</dd>
843+
<dt>
844+
Self Time<small>Entire Profile</small>
845+
</dt>
846+
<dd>{decimalFormat.format(originalLocation.selfTime / 1000)}ms</dd>
847+
<dt>
848+
Total Time<small>Entire Profile</small>
849+
</dt>
850+
<dd>{decimalFormat.format(originalLocation.aggregateTime / 1000)}ms</dd>
851+
</dl>
852+
<ol start={0}>
853+
{stack.map(
854+
(b, i) =>
855+
(!shouldTruncateStack || i < Constants.DefaultStackLimit) && (
856+
<li key={i}>
857+
<BoxLink box={b} onClick={setFocused} link={i > 0} />
858+
</li>
859+
),
860+
)}
861+
{shouldTruncateStack && (
862+
<li>
863+
<a onClick={() => setLimitedStack(false)}>
864+
<em>{stack.length - Constants.DefaultStackLimit} more...</em>
865+
</a>
866+
</li>
867+
)}
868+
</ol>
869+
</div>
870+
);
871+
};
872+
873+
const BoxLink: FunctionComponent<{ box: IBox; onClick(box: IBox): void; link?: boolean }> = ({
874+
box,
875+
onClick,
876+
link,
877+
}) => {
878+
const vscode = useContext(VsCodeApi) as IVscodeApi;
879+
const open = useCallback(
880+
(evt: { altKey: boolean }) => {
881+
const src = box.loc.src;
882+
if (!src?.source.path) {
883+
return;
884+
}
885+
886+
vscode.postMessage<IOpenDocumentMessage>({
887+
type: 'openDocument',
888+
location: src,
889+
callFrame: box.loc.callFrame,
890+
toSide: evt.altKey,
891+
});
892+
},
893+
[vscode, box],
894+
);
895+
896+
const click = useCallback(() => onClick(box), [box, onClick]);
897+
const locText = getLocationText(box.loc);
898+
const linkContent = (
899+
<Fragment>
900+
{box.loc.callFrame.functionName} <em>({locText})</em>
901+
</Fragment>
902+
);
903+
904+
return (
905+
<Fragment>
906+
{link ? <a onClick={click}>{linkContent}</a> : linkContent}
907+
{box.loc.src?.source.path && (
908+
<Icon
909+
i={GoToFileIcon}
910+
className={styles.goToFile}
911+
onClick={open}
912+
role="button"
913+
title="Go to File"
914+
/>
915+
)}
916+
</Fragment>
917+
);
918+
};

0 commit comments

Comments
 (0)