@kylincloud/flamegraph 0.35.20 → 0.35.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/FlameGraph/FlameGraphRenderer.d.ts.map +1 -1
- package/dist/ProfilerTable.d.ts +4 -0
- package/dist/ProfilerTable.d.ts.map +1 -1
- package/dist/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/i18n.d.ts +3 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.cjs.js +2 -2
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +4 -4
- package/dist/index.esm.js.map +1 -1
- package/dist/index.node.cjs.js +4 -4
- package/dist/index.node.cjs.js.map +1 -1
- package/dist/index.node.esm.js +4 -4
- package/dist/index.node.esm.js.map +1 -1
- package/dist/shims/Table.d.ts +4 -2
- package/dist/shims/Table.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/FlameGraph/FlameGraphRenderer.tsx +5 -1
- package/src/ProfilerTable.module.scss +43 -0
- package/src/ProfilerTable.tsx +184 -11
- package/src/Tooltip/Tooltip.module.scss +1 -1
- package/src/Tooltip/Tooltip.tsx +78 -9
- package/src/i18n.tsx +15 -0
- package/src/shims/Table.tsx +24 -9
package/dist/shims/Table.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ReactNode, CSSProperties, RefObject } from 'react';
|
|
1
|
+
import React, { ReactNode, CSSProperties, RefObject } from 'react';
|
|
2
2
|
interface CustomProp {
|
|
3
|
-
[k: string]: string | CSSProperties | ReactNode | number | undefined;
|
|
3
|
+
[k: string]: string | CSSProperties | ReactNode | number | undefined | ((e: React.MouseEvent) => void) | (() => void);
|
|
4
4
|
}
|
|
5
5
|
export interface Cell extends CustomProp {
|
|
6
6
|
value: ReactNode | string;
|
|
@@ -18,6 +18,8 @@ export interface BodyRow {
|
|
|
18
18
|
isRowDisabled?: boolean;
|
|
19
19
|
cells: Cell[];
|
|
20
20
|
onClick?: () => void;
|
|
21
|
+
onDoubleClick?: () => void;
|
|
22
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
21
23
|
className?: string;
|
|
22
24
|
}
|
|
23
25
|
export type TableBodyType = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Table.d.ts","sourceRoot":"","sources":["../../src/shims/Table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAY,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAU7E,UAAU,UAAU;IAClB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,CAAC;CACvH;AAED,MAAM,WAAW,IAAK,SAAQ,UAAU;IACtC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,UAAU,QAAS,SAAQ,UAAU;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GACrB;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB,CAAC;AAEN,KAAK,KAAK,GAAG,aAAa,GAAG;IAC3B,OAAO,EAAE,QAAQ,EAAE,CAAC;CACrB,CAAC;AAEF,UAAU,cAAc;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,eAAe,EAAE,MAAM,GAAG,KAAK,CAAC;CACjC;AAED,eAAO,MAAM,YAAY,YAAa,QAAQ,EAAE,KAAG,cAsBlD,CAAC;AAEF,UAAU,UAAU;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,CAAC,EAAE,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,iBAAS,KAAK,CAAC,EACb,eAAe,EACf,MAAM,EACN,gBAAgB,EAChB,KAAK,EACL,YAAY,EACZ,SAAS,EACT,SAAS,EACT,YAAY,GACb,EAAE,UAAU,2CAsGZ;AA2DD,eAAe,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -337,7 +337,7 @@ class FlameGraphRenderer extends Component<
|
|
|
337
337
|
return (
|
|
338
338
|
this.state.selectedItem.isJust ||
|
|
339
339
|
JSON.stringify(this.initialFlamegraphState) !==
|
|
340
|
-
|
|
340
|
+
JSON.stringify(this.state.flamegraphConfigs)
|
|
341
341
|
);
|
|
342
342
|
};
|
|
343
343
|
|
|
@@ -371,6 +371,10 @@ class FlameGraphRenderer extends Component<
|
|
|
371
371
|
selectedItem={this.state.selectedItem}
|
|
372
372
|
handleTableItemClick={this.setActiveItem}
|
|
373
373
|
palette={this.state.palette}
|
|
374
|
+
// 新增:传递聚焦和视图切换能力给表格
|
|
375
|
+
onFocusOnNode={this.onFocusOnNode}
|
|
376
|
+
updateView={this.props.onlyDisplay ? undefined : this.updateView}
|
|
377
|
+
enableSandwichView={this.props.enableSandwichView}
|
|
374
378
|
/>
|
|
375
379
|
</div>
|
|
376
380
|
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/ProfilerTable.module.scss
|
|
2
|
+
|
|
3
|
+
.tableContextMenu {
|
|
4
|
+
|
|
5
|
+
// 继承 react-menu 的基础样式
|
|
6
|
+
:global(.szh-menu) {
|
|
7
|
+
background-color: var(--ps-ui-background, #fff);
|
|
8
|
+
border: 1px solid var(--ps-ui-border, #ccc);
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
11
|
+
padding: 4px 0;
|
|
12
|
+
min-width: 180px;
|
|
13
|
+
z-index: 1000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:global(.szh-menu__item) {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
padding: 8px 12px;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
color: var(--ps-ui-foreground-text, #333);
|
|
23
|
+
font-size: 13px;
|
|
24
|
+
transition: background-color 0.15s;
|
|
25
|
+
|
|
26
|
+
&:hover {
|
|
27
|
+
background-color: var(--ps-ui-element-bg-highlight, #f0f0f0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
svg {
|
|
31
|
+
width: 14px;
|
|
32
|
+
height: 14px;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.sandwichItem {
|
|
39
|
+
svg {
|
|
40
|
+
width: 16px;
|
|
41
|
+
height: 16px;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/ProfilerTable.tsx
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/ProfilerTable.tsx
|
|
2
|
-
import React, { useRef, RefObject } from 'react';
|
|
2
|
+
import React, { useRef, RefObject, useCallback, useState } from 'react';
|
|
3
3
|
import type Color from 'color';
|
|
4
4
|
import cl from 'classnames';
|
|
5
5
|
import type { Maybe } from 'true-myth';
|
|
6
|
-
import {
|
|
6
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
7
|
+
import { faCrosshairs } from '@fortawesome/free-solid-svg-icons/faCrosshairs';
|
|
8
|
+
import { faCopy } from '@fortawesome/free-solid-svg-icons/faCopy';
|
|
9
|
+
import { faHighlighter } from '@fortawesome/free-solid-svg-icons/faHighlighter';
|
|
10
|
+
import { doubleFF, singleFF, Flamebearer, createFF } from './models';
|
|
7
11
|
// until ui is moved to its own package this should do it
|
|
8
12
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
9
13
|
import TableUI, {
|
|
@@ -11,6 +15,7 @@ import TableUI, {
|
|
|
11
15
|
BodyRow,
|
|
12
16
|
TableBodyType,
|
|
13
17
|
} from './shims/Table';
|
|
18
|
+
import { ControlledMenu, MenuItem, useMenuState } from './shims/Menu';
|
|
14
19
|
import TableTooltip from './Tooltip/TableTooltip';
|
|
15
20
|
import { getFormatter, ratioToPercent, diffPercent } from './format/format';
|
|
16
21
|
import {
|
|
@@ -20,10 +25,14 @@ import {
|
|
|
20
25
|
import { fitIntoTableCell, FitModes } from './fitMode/fitMode';
|
|
21
26
|
import { isMatch } from './search';
|
|
22
27
|
import type { FlamegraphPalette } from './FlameGraph/FlameGraphComponent/colorPalette';
|
|
28
|
+
import { SandwichIcon } from './Icons';
|
|
23
29
|
import {
|
|
24
30
|
useFlamegraphI18n,
|
|
25
31
|
type FlamegraphMessages,
|
|
26
32
|
} from './i18n';
|
|
33
|
+
import type { ViewTypes } from './FlameGraph/FlameGraphComponent/viewTypes';
|
|
34
|
+
|
|
35
|
+
import styles from './ProfilerTable.module.scss';
|
|
27
36
|
|
|
28
37
|
const zero = (v?: number) => v || 0;
|
|
29
38
|
|
|
@@ -138,6 +147,42 @@ function generateTable(
|
|
|
138
147
|
return Array.from(hash.values());
|
|
139
148
|
}
|
|
140
149
|
|
|
150
|
+
/**
|
|
151
|
+
* 根据函数名在 flamebearer 中查找 total 值最大的节点位置
|
|
152
|
+
* 返回 { i, j } 坐标,用于聚焦
|
|
153
|
+
*/
|
|
154
|
+
function findNodeByName(
|
|
155
|
+
flamebearer: Flamebearer,
|
|
156
|
+
targetName: string
|
|
157
|
+
): { i: number; j: number } | null {
|
|
158
|
+
if (!flamebearer || !flamebearer.levels) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const { names, levels, format } = flamebearer;
|
|
163
|
+
const ff = createFF(format);
|
|
164
|
+
|
|
165
|
+
let bestMatch: { i: number; j: number; total: number } | null = null;
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < levels.length; i++) {
|
|
168
|
+
const level = levels[i];
|
|
169
|
+
for (let j = 0; j < level.length; j += ff.jStep) {
|
|
170
|
+
const nameIndex = ff.getBarName(level, j);
|
|
171
|
+
const name = names[nameIndex];
|
|
172
|
+
|
|
173
|
+
if (name === targetName) {
|
|
174
|
+
const total = ff.getBarTotal(level, j);
|
|
175
|
+
// 找到 total 值最大的节点
|
|
176
|
+
if (!bestMatch || total > bestMatch.total) {
|
|
177
|
+
bestMatch = { i, j, total };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return bestMatch ? { i: bestMatch.i, j: bestMatch.j } : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
141
186
|
// the value must be negative or zero
|
|
142
187
|
function neg(v: number) {
|
|
143
188
|
return Math.min(0, v);
|
|
@@ -200,6 +245,8 @@ interface GetTableBodyRowsProps
|
|
|
200
245
|
sortBy: string;
|
|
201
246
|
sortByDirection: string;
|
|
202
247
|
messages: FlamegraphMessages;
|
|
248
|
+
onRowContextMenu: (e: React.MouseEvent, name: string) => void;
|
|
249
|
+
onRowDoubleClick: (name: string) => void;
|
|
203
250
|
}
|
|
204
251
|
|
|
205
252
|
const getTableBody = ({
|
|
@@ -212,6 +259,8 @@ const getTableBody = ({
|
|
|
212
259
|
palette,
|
|
213
260
|
selectedItem,
|
|
214
261
|
messages,
|
|
262
|
+
onRowContextMenu,
|
|
263
|
+
onRowDoubleClick,
|
|
215
264
|
}: GetTableBodyRowsProps): TableBodyType => {
|
|
216
265
|
const { numTicks, maxSelf, sampleRate, spyName, units } = flamebearer;
|
|
217
266
|
|
|
@@ -292,6 +341,8 @@ const getTableBody = ({
|
|
|
292
341
|
'data-row': `${x.type};${x.name};${x.self};${x.total}`,
|
|
293
342
|
isRowSelected: isRowSelected(x.name),
|
|
294
343
|
onClick: () => handleTableItemClick(x),
|
|
344
|
+
onContextMenu: (e: React.MouseEvent) => onRowContextMenu(e, x.name),
|
|
345
|
+
onDoubleClick: () => onRowDoubleClick(x.name),
|
|
295
346
|
cells: [
|
|
296
347
|
{ value: nameCell(x) },
|
|
297
348
|
{
|
|
@@ -337,6 +388,8 @@ const getTableBody = ({
|
|
|
337
388
|
'data-row': `${x.type};${x.name};${x.totalLeft};${x.leftTicks};${x.totalRght};${x.rightTicks}`,
|
|
338
389
|
isRowSelected: isRowSelected(x.name),
|
|
339
390
|
onClick: () => handleTableItemClick(x),
|
|
391
|
+
onContextMenu: (e: React.MouseEvent) => onRowContextMenu(e, x.name),
|
|
392
|
+
onDoubleClick: () => onRowDoubleClick(x.name),
|
|
340
393
|
cells: [
|
|
341
394
|
{ value: nameCell(x) },
|
|
342
395
|
{ value: `${leftPercent} %` },
|
|
@@ -389,6 +442,13 @@ export interface ProfilerTableProps {
|
|
|
389
442
|
selectedItem: Maybe<string>;
|
|
390
443
|
|
|
391
444
|
tableBodyRef: RefObject<HTMLTableSectionElement>;
|
|
445
|
+
|
|
446
|
+
// 新增:聚焦到火焰图节点
|
|
447
|
+
onFocusOnNode?: (i: number, j: number) => void;
|
|
448
|
+
// 新增:切换视图(用于 sandwich view)
|
|
449
|
+
updateView?: (v: ViewTypes) => void;
|
|
450
|
+
// 新增:是否启用 sandwich 视图菜单项
|
|
451
|
+
enableSandwichView?: boolean;
|
|
392
452
|
}
|
|
393
453
|
|
|
394
454
|
function Table({
|
|
@@ -400,8 +460,76 @@ function Table({
|
|
|
400
460
|
highlightQuery,
|
|
401
461
|
selectedItem,
|
|
402
462
|
palette,
|
|
463
|
+
onFocusOnNode,
|
|
464
|
+
updateView,
|
|
465
|
+
enableSandwichView = true,
|
|
403
466
|
}: ProfilerTableProps & { isDoubles: boolean }) {
|
|
404
467
|
const i18n = useFlamegraphI18n();
|
|
468
|
+
const [menuProps, toggleMenu] = useMenuState({ transition: true });
|
|
469
|
+
const [anchorPoint, setAnchorPoint] = useState({ x: 0, y: 0 });
|
|
470
|
+
const [contextMenuTarget, setContextMenuTarget] = useState<string | null>(null);
|
|
471
|
+
|
|
472
|
+
// 双击处理:聚焦到火焰图
|
|
473
|
+
const handleDoubleClick = useCallback(
|
|
474
|
+
(name: string) => {
|
|
475
|
+
if (!onFocusOnNode) return;
|
|
476
|
+
|
|
477
|
+
const node = findNodeByName(flamebearer, name);
|
|
478
|
+
if (node) {
|
|
479
|
+
onFocusOnNode(node.i, node.j);
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
[flamebearer, onFocusOnNode]
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// 右键菜单处理
|
|
486
|
+
const handleContextMenu = useCallback(
|
|
487
|
+
(e: React.MouseEvent, name: string) => {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
setAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
490
|
+
setContextMenuTarget(name);
|
|
491
|
+
toggleMenu(true);
|
|
492
|
+
},
|
|
493
|
+
[toggleMenu]
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const handleMenuClose = useCallback(() => {
|
|
497
|
+
toggleMenu(false);
|
|
498
|
+
setContextMenuTarget(null);
|
|
499
|
+
}, [toggleMenu]);
|
|
500
|
+
|
|
501
|
+
// 菜单项:聚焦到此函数
|
|
502
|
+
const handleFocusClick = useCallback(() => {
|
|
503
|
+
if (!contextMenuTarget || !onFocusOnNode) return;
|
|
504
|
+
|
|
505
|
+
const node = findNodeByName(flamebearer, contextMenuTarget);
|
|
506
|
+
if (node) {
|
|
507
|
+
onFocusOnNode(node.i, node.j);
|
|
508
|
+
}
|
|
509
|
+
handleMenuClose();
|
|
510
|
+
}, [contextMenuTarget, flamebearer, onFocusOnNode, handleMenuClose]);
|
|
511
|
+
|
|
512
|
+
// 菜单项:复制函数名
|
|
513
|
+
const handleCopyClick = useCallback(() => {
|
|
514
|
+
if (!contextMenuTarget || !navigator.clipboard) return;
|
|
515
|
+
navigator.clipboard.writeText(contextMenuTarget);
|
|
516
|
+
handleMenuClose();
|
|
517
|
+
}, [contextMenuTarget, handleMenuClose]);
|
|
518
|
+
|
|
519
|
+
// 菜单项:高亮相似节点
|
|
520
|
+
const handleHighlightClick = useCallback(() => {
|
|
521
|
+
if (!contextMenuTarget) return;
|
|
522
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
523
|
+
handleMenuClose();
|
|
524
|
+
}, [contextMenuTarget, handleTableItemClick, handleMenuClose]);
|
|
525
|
+
|
|
526
|
+
// 菜单项:在 sandwich 视图中打开
|
|
527
|
+
const handleSandwichClick = useCallback(() => {
|
|
528
|
+
if (!contextMenuTarget || !updateView) return;
|
|
529
|
+
updateView('sandwich');
|
|
530
|
+
handleTableItemClick({ name: contextMenuTarget });
|
|
531
|
+
handleMenuClose();
|
|
532
|
+
}, [contextMenuTarget, updateView, handleTableItemClick, handleMenuClose]);
|
|
405
533
|
|
|
406
534
|
const tableFormat = React.useMemo(
|
|
407
535
|
() =>
|
|
@@ -463,19 +591,58 @@ function Table({
|
|
|
463
591
|
palette,
|
|
464
592
|
selectedItem,
|
|
465
593
|
messages: i18n,
|
|
594
|
+
onRowContextMenu: handleContextMenu,
|
|
595
|
+
onRowDoubleClick: handleDoubleClick,
|
|
596
|
+
onFocusOnNode,
|
|
597
|
+
updateView,
|
|
598
|
+
enableSandwichView,
|
|
466
599
|
}),
|
|
467
600
|
};
|
|
468
601
|
|
|
602
|
+
const isHighlighted =
|
|
603
|
+
selectedItem.isJust && selectedItem.value === contextMenuTarget;
|
|
604
|
+
|
|
469
605
|
return (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
'flamegraph-table
|
|
477
|
-
|
|
478
|
-
|
|
606
|
+
<>
|
|
607
|
+
<TableUI
|
|
608
|
+
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
|
609
|
+
{...tableSortProps}
|
|
610
|
+
tableBodyRef={tableBodyRef}
|
|
611
|
+
table={table}
|
|
612
|
+
className={cl('flamegraph-table', {
|
|
613
|
+
'flamegraph-table-doubles': isDoubles,
|
|
614
|
+
})}
|
|
615
|
+
/>
|
|
616
|
+
|
|
617
|
+
{/* 右键菜单 */}
|
|
618
|
+
<ControlledMenu
|
|
619
|
+
{...menuProps}
|
|
620
|
+
anchorPoint={anchorPoint}
|
|
621
|
+
onClose={handleMenuClose}
|
|
622
|
+
className={styles.tableContextMenu}
|
|
623
|
+
>
|
|
624
|
+
{onFocusOnNode && (
|
|
625
|
+
<MenuItem onClick={handleFocusClick}>
|
|
626
|
+
<FontAwesomeIcon icon={faCrosshairs} />
|
|
627
|
+
{i18n.focusOnThisFunction}
|
|
628
|
+
</MenuItem>
|
|
629
|
+
)}
|
|
630
|
+
<MenuItem onClick={handleCopyClick}>
|
|
631
|
+
<FontAwesomeIcon icon={faCopy} />
|
|
632
|
+
{i18n.copyFunctionName}
|
|
633
|
+
</MenuItem>
|
|
634
|
+
<MenuItem onClick={handleHighlightClick}>
|
|
635
|
+
<FontAwesomeIcon icon={faHighlighter} />
|
|
636
|
+
{isHighlighted ? i18n.clearHighlight : i18n.highlightSimilarNodes}
|
|
637
|
+
</MenuItem>
|
|
638
|
+
{updateView && enableSandwichView && (
|
|
639
|
+
<MenuItem onClick={handleSandwichClick} className={styles.sandwichItem}>
|
|
640
|
+
<SandwichIcon fill="currentColor" />
|
|
641
|
+
{i18n.openInSandwichView}
|
|
642
|
+
</MenuItem>
|
|
643
|
+
)}
|
|
644
|
+
</ControlledMenu>
|
|
645
|
+
</>
|
|
479
646
|
);
|
|
480
647
|
}
|
|
481
648
|
|
|
@@ -486,6 +653,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
486
653
|
highlightQuery,
|
|
487
654
|
palette,
|
|
488
655
|
selectedItem,
|
|
656
|
+
onFocusOnNode,
|
|
657
|
+
updateView,
|
|
658
|
+
enableSandwichView,
|
|
489
659
|
}: Omit<ProfilerTableProps, 'tableBodyRef'>) {
|
|
490
660
|
const tableBodyRef = useRef<HTMLTableSectionElement>(null);
|
|
491
661
|
const isDoubles = flamebearer.format === 'double';
|
|
@@ -510,6 +680,9 @@ const ProfilerTable = React.memo(function ProfilerTable({
|
|
|
510
680
|
handleTableItemClick={handleTableItemClick}
|
|
511
681
|
palette={palette}
|
|
512
682
|
selectedItem={selectedItem}
|
|
683
|
+
onFocusOnNode={onFocusOnNode}
|
|
684
|
+
updateView={updateView}
|
|
685
|
+
enableSandwichView={enableSandwichView}
|
|
513
686
|
/>
|
|
514
687
|
</div>
|
|
515
688
|
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
useState,
|
|
8
8
|
useRef,
|
|
9
9
|
useCallback,
|
|
10
|
+
useLayoutEffect,
|
|
10
11
|
Dispatch,
|
|
11
12
|
SetStateAction,
|
|
12
13
|
} from 'react';
|
|
@@ -210,6 +211,52 @@ function getDiffColorByText(
|
|
|
210
211
|
return undefined;
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Tooltip 位置计算:默认右下;若越界则翻到左/上;最终 clamp 到视口内。
|
|
216
|
+
* 注意:Tooltip 使用 position: fixed,因此用 innerWidth/innerHeight 做视口碰撞检测最直接。
|
|
217
|
+
*/
|
|
218
|
+
const TOOLTIP_GAP_X = 12;
|
|
219
|
+
const TOOLTIP_GAP_Y = 20;
|
|
220
|
+
const TOOLTIP_PAD = 8;
|
|
221
|
+
|
|
222
|
+
function clamp(v: number, min: number, max: number) {
|
|
223
|
+
if (max < min) return min;
|
|
224
|
+
return Math.max(min, Math.min(max, v));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function computeSafeTooltipPos(
|
|
228
|
+
x: number,
|
|
229
|
+
y: number,
|
|
230
|
+
tipW: number,
|
|
231
|
+
tipH: number
|
|
232
|
+
) {
|
|
233
|
+
const vw = window.innerWidth;
|
|
234
|
+
const vh = window.innerHeight;
|
|
235
|
+
|
|
236
|
+
const w = Math.max(0, tipW || 0);
|
|
237
|
+
const h = Math.max(0, tipH || 0);
|
|
238
|
+
|
|
239
|
+
// 默认:右下
|
|
240
|
+
let left = x + TOOLTIP_GAP_X;
|
|
241
|
+
let top = y + TOOLTIP_GAP_Y;
|
|
242
|
+
|
|
243
|
+
// 右侧溢出:翻到左边
|
|
244
|
+
if (left + w + TOOLTIP_PAD > vw) {
|
|
245
|
+
left = x - w - TOOLTIP_GAP_X;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 底部溢出:翻到上边
|
|
249
|
+
if (top + h + TOOLTIP_PAD > vh) {
|
|
250
|
+
top = y - h - TOOLTIP_GAP_Y;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 最终夹取到视口内
|
|
254
|
+
left = clamp(left, TOOLTIP_PAD, vw - w - TOOLTIP_PAD);
|
|
255
|
+
top = clamp(top, TOOLTIP_PAD, vh - h - TOOLTIP_PAD);
|
|
256
|
+
|
|
257
|
+
return { left, top };
|
|
258
|
+
}
|
|
259
|
+
|
|
213
260
|
export function Tooltip({
|
|
214
261
|
shouldShowFooter = true,
|
|
215
262
|
shouldShowTitle = true,
|
|
@@ -219,6 +266,8 @@ export function Tooltip({
|
|
|
219
266
|
palette,
|
|
220
267
|
}: TooltipProps) {
|
|
221
268
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
269
|
+
const lastPosRef = useRef<{ x: number; y: number } | null>(null);
|
|
270
|
+
|
|
222
271
|
const [content, setContent] = React.useState({
|
|
223
272
|
title: {
|
|
224
273
|
text: '',
|
|
@@ -245,13 +294,17 @@ export function Tooltip({
|
|
|
245
294
|
throw new Error('Missing tooltipElement');
|
|
246
295
|
}
|
|
247
296
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
window.innerWidth - tooltipRef.current.clientWidth - 20
|
|
251
|
-
);
|
|
252
|
-
const top = e.clientY + 20;
|
|
297
|
+
// 记录最后鼠标位置(给 useLayoutEffect 二次校正用)
|
|
298
|
+
lastPosRef.current = { x: e.clientX, y: e.clientY };
|
|
253
299
|
|
|
300
|
+
// 先更新内容(tooltip 高度可能因此变化)
|
|
254
301
|
setTooltipContent(setContent, onMouseOut, e);
|
|
302
|
+
|
|
303
|
+
// 用当前已渲染的尺寸先算一次位置(内容变化后的真实尺寸会在 useLayoutEffect 再校正)
|
|
304
|
+
const w = tooltipRef.current.clientWidth || 0;
|
|
305
|
+
const h = tooltipRef.current.clientHeight || 0;
|
|
306
|
+
|
|
307
|
+
const { left, top } = computeSafeTooltipPos(e.clientX, e.clientY, w, h);
|
|
255
308
|
setStyle({ top, left, visibility: 'visible' });
|
|
256
309
|
},
|
|
257
310
|
[setTooltipContent]
|
|
@@ -278,6 +331,24 @@ export function Tooltip({
|
|
|
278
331
|
};
|
|
279
332
|
}, [dataSourceRef.current, memoizedOnMouseMove]);
|
|
280
333
|
|
|
334
|
+
// 内容变化后用真实 DOM 尺寸做二次校正,避免“首次显示尺寸尚未更新导致仍越界”
|
|
335
|
+
useLayoutEffect(() => {
|
|
336
|
+
if (!tooltipRef.current) return;
|
|
337
|
+
if (!lastPosRef.current) return;
|
|
338
|
+
if (content.tooltipData.length === 0) return;
|
|
339
|
+
|
|
340
|
+
const { x, y } = lastPosRef.current;
|
|
341
|
+
const rect = tooltipRef.current.getBoundingClientRect();
|
|
342
|
+
const { left, top } = computeSafeTooltipPos(x, y, rect.width, rect.height);
|
|
343
|
+
|
|
344
|
+
setStyle((prev) => {
|
|
345
|
+
if (prev && prev.left === left && prev.top === top && prev.visibility === 'visible') {
|
|
346
|
+
return prev;
|
|
347
|
+
}
|
|
348
|
+
return { ...(prev || {}), left, top, visibility: 'visible' };
|
|
349
|
+
});
|
|
350
|
+
}, [content.tooltipData.length, content.title.text]);
|
|
351
|
+
|
|
281
352
|
return (
|
|
282
353
|
<div
|
|
283
354
|
data-testid="tooltip"
|
|
@@ -460,12 +531,10 @@ function TooltipTable({
|
|
|
460
531
|
<tr>
|
|
461
532
|
<td />
|
|
462
533
|
<td>
|
|
463
|
-
{i18n.self} (
|
|
464
|
-
{i18n.tooltipUnitTitles[baselineData.units].total})
|
|
534
|
+
{i18n.self} ({i18n.tooltipUnitTitles[baselineData.units].total})
|
|
465
535
|
</td>
|
|
466
536
|
<td>
|
|
467
|
-
{i18n.total} (
|
|
468
|
-
{i18n.tooltipUnitTitles[baselineData.units].total})
|
|
537
|
+
{i18n.total} ({i18n.tooltipUnitTitles[baselineData.units].total})
|
|
469
538
|
</td>
|
|
470
539
|
</tr>
|
|
471
540
|
</thead>
|
package/src/i18n.tsx
CHANGED
|
@@ -68,6 +68,11 @@ export type FlamegraphMessages = {
|
|
|
68
68
|
searchPlaceholder: string;
|
|
69
69
|
syncSearchBars: string;
|
|
70
70
|
unsyncSearchBars: string;
|
|
71
|
+
|
|
72
|
+
// 表格右键菜单(新增)
|
|
73
|
+
focusOnThisFunction: string;
|
|
74
|
+
tableDoubleClickToFocus: string;
|
|
75
|
+
tableRightClickForOptions: string;
|
|
71
76
|
};
|
|
72
77
|
|
|
73
78
|
const defaultTooltipUnitTitles: Record<Units, TooltipUnitMessages> = {
|
|
@@ -217,6 +222,11 @@ export const defaultMessages: FlamegraphMessages = {
|
|
|
217
222
|
searchPlaceholder: 'Search...',
|
|
218
223
|
syncSearchBars: 'Sync Search Bars',
|
|
219
224
|
unsyncSearchBars: 'Unsync Search Bars',
|
|
225
|
+
|
|
226
|
+
// 表格右键菜单(新增)
|
|
227
|
+
focusOnThisFunction: 'Focus on this function',
|
|
228
|
+
tableDoubleClickToFocus: 'Double-click to focus in flamegraph',
|
|
229
|
+
tableRightClickForOptions: 'Right-click for more options',
|
|
220
230
|
};
|
|
221
231
|
|
|
222
232
|
export const zhCNMessages: FlamegraphMessages = {
|
|
@@ -271,6 +281,11 @@ export const zhCNMessages: FlamegraphMessages = {
|
|
|
271
281
|
searchPlaceholder: '搜索...',
|
|
272
282
|
syncSearchBars: '同步搜索栏',
|
|
273
283
|
unsyncSearchBars: '取消同步搜索栏',
|
|
284
|
+
|
|
285
|
+
// 表格右键菜单(新增)
|
|
286
|
+
focusOnThisFunction: '聚焦到此函数',
|
|
287
|
+
tableDoubleClickToFocus: '双击可聚焦到火焰图',
|
|
288
|
+
tableRightClickForOptions: '右键查看更多选项',
|
|
274
289
|
};
|
|
275
290
|
|
|
276
291
|
const I18nContext = createContext<FlamegraphMessages>(defaultMessages);
|
package/src/shims/Table.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// src/shims/Table.tsx
|
|
1
2
|
/* eslint-disable react/jsx-props-no-spreading */
|
|
2
3
|
import React, { useState, ReactNode, CSSProperties, RefObject } from 'react';
|
|
3
4
|
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft';
|
|
@@ -10,7 +11,7 @@ import styles from './Table.module.scss';
|
|
|
10
11
|
import Button from './Button';
|
|
11
12
|
|
|
12
13
|
interface CustomProp {
|
|
13
|
-
[k: string]: string | CSSProperties | ReactNode | number | undefined;
|
|
14
|
+
[k: string]: string | CSSProperties | ReactNode | number | undefined | ((e: React.MouseEvent) => void) | (() => void);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface Cell extends CustomProp {
|
|
@@ -31,19 +32,21 @@ export interface BodyRow {
|
|
|
31
32
|
isRowDisabled?: boolean;
|
|
32
33
|
cells: Cell[];
|
|
33
34
|
onClick?: () => void;
|
|
35
|
+
onDoubleClick?: () => void;
|
|
36
|
+
onContextMenu?: (e: React.MouseEvent) => void;
|
|
34
37
|
className?: string;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export type TableBodyType =
|
|
38
41
|
| {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
type: 'not-filled';
|
|
43
|
+
value: string | ReactNode;
|
|
44
|
+
bodyClassName?: string;
|
|
45
|
+
}
|
|
43
46
|
| {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
type: 'filled';
|
|
48
|
+
bodyRows: BodyRow[];
|
|
49
|
+
};
|
|
47
50
|
|
|
48
51
|
type Table = TableBodyType & {
|
|
49
52
|
headRow: HeadCell[];
|
|
@@ -151,7 +154,16 @@ function Table({
|
|
|
151
154
|
</tr>
|
|
152
155
|
) : (
|
|
153
156
|
paginate(table.bodyRows, currPage, itemsPerPage).map(
|
|
154
|
-
({
|
|
157
|
+
({
|
|
158
|
+
cells,
|
|
159
|
+
isRowSelected,
|
|
160
|
+
isRowDisabled,
|
|
161
|
+
className,
|
|
162
|
+
onClick,
|
|
163
|
+
onDoubleClick,
|
|
164
|
+
onContextMenu,
|
|
165
|
+
...rest
|
|
166
|
+
}) => {
|
|
155
167
|
// The problem is that when you switch apps or time-range and the function
|
|
156
168
|
// names stay the same it leads to an issue where rows don't get re-rendered
|
|
157
169
|
// So we force a rerender each time.
|
|
@@ -161,6 +173,9 @@ function Table({
|
|
|
161
173
|
<tr
|
|
162
174
|
key={renderID}
|
|
163
175
|
{...rest}
|
|
176
|
+
onClick={onClick}
|
|
177
|
+
onDoubleClick={onDoubleClick}
|
|
178
|
+
onContextMenu={onContextMenu}
|
|
164
179
|
className={clsx(className, {
|
|
165
180
|
[styles.isRowSelected]: isRowSelected,
|
|
166
181
|
[styles.isRowDisabled]: isRowDisabled,
|