@parca/profile 0.19.132 → 0.19.134
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 +8 -0
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +3 -9
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
- package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
- package/dist/ProfileFlameChart/SamplesStrips/index.js +61 -39
- package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.d.ts +7 -0
- package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.d.ts.map +1 -0
- package/dist/ProfileFlameChart/SamplesStrips/labelSetUtils.js +79 -0
- package/dist/ProfileFlameChart/index.d.ts +1 -2
- package/dist/ProfileFlameChart/index.d.ts.map +1 -1
- package/dist/ProfileFlameChart/index.js +14 -21
- package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts +3 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/MiniMap.js +89 -24
- package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/ZoomControls.js +2 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/index.js +2 -2
- package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts +4 -0
- package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/FlameGraphArrow/useZoom.js +51 -10
- package/dist/ProfileFlameGraph/index.d.ts +0 -1
- package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
- package/dist/ProfileFlameGraph/index.js +3 -8
- package/dist/ProfileView/components/DashboardItems/index.d.ts +1 -2
- package/dist/ProfileView/components/DashboardItems/index.d.ts.map +1 -1
- package/dist/ProfileView/components/DashboardItems/index.js +2 -2
- package/dist/ProfileView/index.d.ts +1 -1
- package/dist/ProfileView/index.d.ts.map +1 -1
- package/dist/ProfileView/index.js +1 -2
- package/dist/ProfileView/types/visualization.d.ts +0 -1
- package/dist/ProfileView/types/visualization.d.ts.map +1 -1
- package/dist/ProfileViewWithData.d.ts +1 -2
- package/dist/ProfileViewWithData.d.ts.map +1 -1
- package/dist/ProfileViewWithData.js +2 -2
- package/dist/TimelineGuide/index.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +4 -4
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +3 -14
- package/src/ProfileFlameChart/SamplesStrips/index.tsx +90 -49
- package/src/ProfileFlameChart/SamplesStrips/labelSetUtils.test.ts +73 -0
- package/src/ProfileFlameChart/SamplesStrips/labelSetUtils.ts +86 -0
- package/src/ProfileFlameChart/index.tsx +16 -45
- package/src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx +119 -25
- package/src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx +3 -1
- package/src/ProfileFlameGraph/FlameGraphArrow/index.tsx +5 -3
- package/src/ProfileFlameGraph/FlameGraphArrow/useZoom.ts +78 -17
- package/src/ProfileFlameGraph/index.tsx +4 -24
- package/src/ProfileView/components/DashboardItems/index.tsx +0 -3
- package/src/ProfileView/index.tsx +0 -2
- package/src/ProfileView/types/visualization.ts +0 -1
- package/src/ProfileViewWithData.tsx +0 -3
- package/src/TimelineGuide/index.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.19.134](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.133...@parca/profile@0.19.134) (2026-03-11)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @parca/profile
|
|
9
|
+
|
|
10
|
+
## [0.19.133](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.132...@parca/profile@0.19.133) (2026-03-04)
|
|
11
|
+
|
|
12
|
+
**Note:** Version bump only for package @parca/profile
|
|
13
|
+
|
|
6
14
|
## [0.19.132](https://github.com/parca-dev/parca/compare/@parca/profile@0.19.131...@parca/profile@0.19.132) (2026-03-03)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @parca/profile
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProfileExplorerSingle.d.ts","sourceRoot":"","sources":["../../src/ProfileExplorer/ProfileExplorerSingle.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAMvD,UAAU,0BAA0B;IAClC,WAAW,EAAE,kBAAkB,CAAC;IAChC,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,QAAA,MAAM,qBAAqB,GAAI,8BAG5B,0BAA0B,KAAG,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"ProfileExplorerSingle.d.ts","sourceRoot":"","sources":["../../src/ProfileExplorer/ProfileExplorerSingle.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAMvD,UAAU,0BAA0B;IAClC,WAAW,EAAE,kBAAkB,CAAC;IAChC,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAED,QAAA,MAAM,qBAAqB,GAAI,8BAG5B,0BAA0B,KAAG,GAAG,CAAC,OAwBnC,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -11,19 +11,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
11
11
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
|
-
import {
|
|
14
|
+
import { useState } from 'react';
|
|
15
15
|
import { ProfileViewWithData } from '..';
|
|
16
16
|
import ProfileSelector from '../ProfileSelector';
|
|
17
17
|
import { useQueryState } from '../hooks/useQueryState';
|
|
18
18
|
const ProfileExplorerSingle = ({ queryClient, navigateTo, }) => {
|
|
19
19
|
const [showMetricsGraph, setShowMetricsGraph] = useState(true);
|
|
20
|
-
const { profileSource
|
|
21
|
-
|
|
22
|
-
const now = Date.now();
|
|
23
|
-
const from = now - 60000; // 1 minute ago
|
|
24
|
-
setDraftTimeRange(from, now, 'relative:minute|1');
|
|
25
|
-
commitDraft({ from, to: now, timeSelection: 'relative:minute|1' });
|
|
26
|
-
}, [setDraftTimeRange, commitDraft]);
|
|
27
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "relative", children: _jsx(ProfileSelector, { queryClient: queryClient, closeProfile: () => { }, comparing: false, enforcedProfileName: '', navigateTo: navigateTo, suffix: "_a", showMetricsGraph: showMetricsGraph, setDisplayHideMetricsGraphButton: setShowMetricsGraph }) }), profileSource != null && (_jsx(ProfileViewWithData, { queryClient: queryClient, profileSource: profileSource, onSwitchToOneMinute: handleSwitchToOneMinute }))] }));
|
|
20
|
+
const { profileSource } = useQueryState({ suffix: '_a' });
|
|
21
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "relative", children: _jsx(ProfileSelector, { queryClient: queryClient, closeProfile: () => { }, comparing: false, enforcedProfileName: '', navigateTo: navigateTo, suffix: "_a", showMetricsGraph: showMetricsGraph, setDisplayHideMetricsGraphButton: setShowMetricsGraph }) }), profileSource != null && (_jsx(ProfileViewWithData, { queryClient: queryClient, profileSource: profileSource }))] }));
|
|
28
22
|
};
|
|
29
23
|
export default ProfileExplorerSingle;
|
|
@@ -3,6 +3,7 @@ import { NumberDuo } from '../../utils';
|
|
|
3
3
|
import { DataPoint } from './SamplesGraph';
|
|
4
4
|
export type { DataPoint } from './SamplesGraph';
|
|
5
5
|
interface Props {
|
|
6
|
+
loading?: boolean;
|
|
6
7
|
cpus: LabelSet[];
|
|
7
8
|
data: DataPoint[][];
|
|
8
9
|
selectedTimeframe?: {
|
|
@@ -14,6 +15,5 @@ interface Props {
|
|
|
14
15
|
bounds: NumberDuo;
|
|
15
16
|
stepMs: number;
|
|
16
17
|
}
|
|
17
|
-
export declare const
|
|
18
|
-
export declare const SamplesStrip: ({ cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }: Props) => JSX.Element;
|
|
18
|
+
export declare const SamplesStrip: ({ loading, cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }: Props) => JSX.Element;
|
|
19
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameChart/SamplesStrips/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AAIvC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,SAAS,EAAe,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameChart/SamplesStrips/index.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AAIvC,OAAO,EAAC,SAAS,EAAC,MAAM,aAAa,CAAC;AACtC,OAAO,EAAC,SAAS,EAAe,MAAM,gBAAgB,CAAC;AAGvD,YAAY,EAAC,SAAS,EAAC,MAAM,gBAAgB,CAAC;AAQ9C,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,IAAI,EAAE,SAAS,EAAE,EAAE,CAAC;IACpB,iBAAiB,CAAC,EAAE;QAClB,MAAM,EAAE,QAAQ,CAAC;QACjB,MAAM,EAAE,SAAS,CAAC;KACnB,CAAC;IACF,mBAAmB,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAmID,eAAO,MAAM,YAAY,GAAI,yFAS1B,KAAK,KAAG,GAAG,CAAC,OAwKd,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Copyright 2022 The Parca Authors
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
// you may not use this file except in compliance with the License.
|
|
@@ -17,35 +17,46 @@ import cx from 'classnames';
|
|
|
17
17
|
import * as d3 from 'd3';
|
|
18
18
|
import isEqual from 'fast-deep-equal';
|
|
19
19
|
import { useIntersectionObserver } from 'usehooks-ts';
|
|
20
|
-
import { Button } from '@parca/components';
|
|
20
|
+
import { Button, useParcaContext } from '@parca/components';
|
|
21
21
|
import { TimelineGuide } from '../../TimelineGuide';
|
|
22
22
|
import { SamplesGraph } from './SamplesGraph';
|
|
23
|
-
|
|
24
|
-
if (labelSet === undefined) {
|
|
25
|
-
return '{}';
|
|
26
|
-
}
|
|
27
|
-
let str = '{';
|
|
28
|
-
let isFirst = true;
|
|
29
|
-
for (const label of labelSet.labels) {
|
|
30
|
-
if (!isFirst) {
|
|
31
|
-
str += ', ';
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
isFirst = false;
|
|
35
|
-
}
|
|
36
|
-
str += `${label.name}: ${label.value}`;
|
|
37
|
-
}
|
|
38
|
-
str += '}';
|
|
39
|
-
return str;
|
|
40
|
-
};
|
|
23
|
+
import { createLabelSetComparator, labelSetToString } from './labelSetUtils';
|
|
41
24
|
const STRIP_HEIGHT = 24;
|
|
25
|
+
const LABEL_ROW_HEIGHT = 16; // text-xs label row above each strip
|
|
26
|
+
const GAP = 4; // gap-1 between flex children
|
|
42
27
|
const MAX_VISIBLE_STRIPS = 20;
|
|
28
|
+
const LOADING_STRIP_COUNT = 8;
|
|
29
|
+
const generateMockStripData = (bounds) => {
|
|
30
|
+
const stepMs = Math.max(Math.floor((bounds[1] - bounds[0]) / 240), 100);
|
|
31
|
+
const cpus = Array.from({ length: LOADING_STRIP_COUNT }, (_, i) => ({
|
|
32
|
+
labels: [{ name: 'cpu', value: String(i) }],
|
|
33
|
+
}));
|
|
34
|
+
let seed = 42;
|
|
35
|
+
const data = cpus.map(() => {
|
|
36
|
+
const points = [];
|
|
37
|
+
for (let ts = bounds[0]; ts < bounds[1]; ts += stepMs) {
|
|
38
|
+
seed = (seed * 16807 + 11) % 2147483647;
|
|
39
|
+
const value = (seed % 80) + 10;
|
|
40
|
+
seed = (seed * 16807 + 11) % 2147483647;
|
|
41
|
+
const sampleCount = (seed % 50) + 1;
|
|
42
|
+
points.push({ timestamp: ts, value, sampleCount });
|
|
43
|
+
}
|
|
44
|
+
return points;
|
|
45
|
+
});
|
|
46
|
+
return { cpus, data, stepMs };
|
|
47
|
+
};
|
|
43
48
|
const getTimelineGuideHeight = (cpusCount, collapsedCount) => {
|
|
44
|
-
|
|
49
|
+
const expandedCount = cpusCount - collapsedCount;
|
|
50
|
+
// Each expanded strip: label row + graph height
|
|
51
|
+
// Each collapsed strip: min-h-5 (20px)
|
|
52
|
+
// Gaps between strips (gap-1 = 4px)
|
|
53
|
+
const expandedTotal = expandedCount * (LABEL_ROW_HEIGHT + STRIP_HEIGHT);
|
|
54
|
+
const collapsedTotal = collapsedCount * 20; // min-h-5
|
|
55
|
+
const gaps = cpusCount * GAP + 20; // timeline header
|
|
56
|
+
return expandedTotal + collapsedTotal + gaps;
|
|
45
57
|
};
|
|
46
58
|
const stickyPx = 0;
|
|
47
|
-
const SamplesGraphContainer = ({ isSelected, isCollapsed,
|
|
48
|
-
const labelStr = labelSetToString(cpu);
|
|
59
|
+
const SamplesGraphContainer = ({ isSelected, isCollapsed, label: labelStr, width, onToggleCollapse, data, selectionBounds, setSelectionBounds, color, stepMs, onDragStart, dragState, stripIndex, isAnyDragActive, timeBounds, loading, }) => {
|
|
49
60
|
const { isIntersecting, ref } = useIntersectionObserver({
|
|
50
61
|
rootMargin: `${stickyPx}px 0px 0px 0px`,
|
|
51
62
|
});
|
|
@@ -56,30 +67,38 @@ const SamplesGraphContainer = ({ isSelected, isCollapsed, cpu, width, onToggleCo
|
|
|
56
67
|
relative: !isSelected,
|
|
57
68
|
'sticky z-30 bg-white dark:bg-black bg-opacity-75': isSelected,
|
|
58
69
|
'!bg-opacity-100': isSticky,
|
|
59
|
-
}), style: { width: width ?? 1468, top: isSelected ? stickyPx : undefined }, ref: ref, children: [
|
|
60
|
-
zIndex: 15,
|
|
61
|
-
}, onClick: onToggleCollapse, children: [_jsx(Icon, { icon: isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow' }), labelStr] }), !isCollapsed ? (_jsx(SamplesGraph, { data: data, height: STRIP_HEIGHT, width: width ?? 1468, fill: color(labelStr), selectionBounds: selectionBounds, setSelectionBounds: setSelectionBounds, stepMs: stepMs, onDragStart: (startX) => onDragStart(stripIndex, startX), dragState: dragState?.stripIndex === stripIndex ? dragState : undefined, isAnyDragActive: isAnyDragActive, timeBounds: timeBounds })) : null] }, labelStr));
|
|
70
|
+
}), style: { width: width ?? 1468, top: isSelected ? stickyPx : undefined }, ref: ref, children: [_jsx("div", { className: "text-xs flex gap-[2px] items-center px-1 cursor-pointer text-gray-600 dark:text-gray-400", onClick: loading === true ? undefined : onToggleCollapse, children: loading === true ? (_jsx("div", { className: "h-3 w-24 rounded bg-gray-200 dark:bg-gray-700 mb-1" })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { icon: isCollapsed ? 'bxs:right-arrow' : 'bxs:down-arrow', className: "shrink-0" }), labelStr] })) }), !isCollapsed ? (_jsx(SamplesGraph, { data: data, height: STRIP_HEIGHT, width: width ?? 1468, fill: color(labelStr), selectionBounds: selectionBounds, setSelectionBounds: setSelectionBounds, stepMs: stepMs, onDragStart: (startX) => onDragStart(stripIndex, startX), dragState: dragState?.stripIndex === stripIndex ? dragState : undefined, isAnyDragActive: isAnyDragActive, timeBounds: timeBounds })) : null] }, labelStr));
|
|
62
71
|
};
|
|
63
|
-
export const SamplesStrip = ({ cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }) => {
|
|
72
|
+
export const SamplesStrip = ({ loading, cpus, data, selectedTimeframe, onSelectedTimeframe, width, bounds, stepMs, }) => {
|
|
73
|
+
const { isDarkMode } = useParcaContext();
|
|
74
|
+
const effectiveLoading = loading === true;
|
|
75
|
+
// When loading, use mock data to render a pixel-perfect skeleton
|
|
76
|
+
const mockData = useMemo(() => (effectiveLoading && bounds[0] !== bounds[1] ? generateMockStripData(bounds) : null), [effectiveLoading, bounds]);
|
|
77
|
+
const effectiveCpus = mockData?.cpus ?? cpus;
|
|
78
|
+
const effectiveData = mockData?.data ?? data;
|
|
79
|
+
const effectiveStepMs = mockData?.stepMs ?? stepMs;
|
|
64
80
|
const [collapsedLabels, setCollapsedLabels] = useState(new Set());
|
|
65
81
|
const [showAll, setShowAll] = useState(false);
|
|
66
82
|
const [dragState, setDragState] = useState(undefined);
|
|
67
83
|
const containerRef = useRef(null);
|
|
68
84
|
const isDragging = dragState !== undefined;
|
|
69
|
-
|
|
85
|
+
const { compare, keyOrder } = useMemo(() => createLabelSetComparator(effectiveCpus), [effectiveCpus]);
|
|
70
86
|
const sortedItems = useMemo(() => {
|
|
71
|
-
const items =
|
|
87
|
+
const items = effectiveCpus.map((cpu, i) => ({
|
|
72
88
|
cpu,
|
|
73
|
-
data:
|
|
74
|
-
label: labelSetToString(cpu),
|
|
89
|
+
data: effectiveData[i],
|
|
90
|
+
label: labelSetToString(cpu, keyOrder),
|
|
75
91
|
}));
|
|
76
|
-
return items.sort((a, b) => a.
|
|
77
|
-
}, [
|
|
92
|
+
return items.sort((a, b) => compare(a.cpu, b.cpu));
|
|
93
|
+
}, [effectiveCpus, effectiveData, compare, keyOrder]);
|
|
78
94
|
const hasMore = useMemo(() => sortedItems.length > MAX_VISIBLE_STRIPS, [sortedItems]);
|
|
79
95
|
const visibleItems = useMemo(() => (showAll || !hasMore ? sortedItems : sortedItems.slice(0, MAX_VISIBLE_STRIPS)), [sortedItems, showAll, hasMore]);
|
|
80
96
|
// Deterministic color: hash the label string so the same label always gets the same color
|
|
81
|
-
// regardless of render order.
|
|
97
|
+
// regardless of render order. When loading, use muted gray.
|
|
82
98
|
const color = useMemo(() => {
|
|
99
|
+
if (effectiveLoading) {
|
|
100
|
+
return (_label) => (isDarkMode ? '#374151' : '#d1d5db');
|
|
101
|
+
}
|
|
83
102
|
const palette = d3.schemeObservable10;
|
|
84
103
|
const hashStr = (s) => {
|
|
85
104
|
let h = 0;
|
|
@@ -89,7 +108,7 @@ export const SamplesStrip = ({ cpus, data, selectedTimeframe, onSelectedTimefram
|
|
|
89
108
|
return Math.abs(h);
|
|
90
109
|
};
|
|
91
110
|
return (label) => palette[hashStr(label) % palette.length];
|
|
92
|
-
}, []);
|
|
111
|
+
}, [effectiveLoading, isDarkMode]);
|
|
93
112
|
const handleDragStart = (stripIndex, startX) => {
|
|
94
113
|
setDragState({ stripIndex, startX, currentX: startX });
|
|
95
114
|
};
|
|
@@ -123,13 +142,16 @@ export const SamplesStrip = ({ cpus, data, selectedTimeframe, onSelectedTimefram
|
|
|
123
142
|
const handleMouseLeave = () => {
|
|
124
143
|
setDragState(undefined);
|
|
125
144
|
};
|
|
126
|
-
if (
|
|
145
|
+
if (!effectiveLoading && effectiveData.length === 0) {
|
|
127
146
|
return (_jsx("span", { className: "flex justify-center my-10", children: "There is no data matching your filter criteria, please try changing the filter." }));
|
|
128
147
|
}
|
|
129
|
-
return (_jsxs("div", { ref: containerRef, className: cx('flex flex-col gap-1 relative my-0', {
|
|
148
|
+
return (_jsxs("div", { ref: containerRef, className: cx('flex flex-col gap-1 relative my-0', {
|
|
149
|
+
'cursor-ew-resize': isDragging,
|
|
150
|
+
'animate-pulse pointer-events-none': effectiveLoading,
|
|
151
|
+
}), style: { width: width ?? '100%' }, onMouseMove: effectiveLoading ? undefined : handleMouseMove, onMouseUp: effectiveLoading ? undefined : handleMouseUp, onMouseLeave: effectiveLoading ? undefined : handleMouseLeave, children: [_jsx(TimelineGuide, { bounds: [BigInt(0), BigInt(bounds[1] - bounds[0])], width: width ?? 1468, height: getTimelineGuideHeight(visibleItems.length, [...collapsedLabels].filter(l => visibleItems.some(item => item.label === l)).length), margin: 1 }), visibleItems.map((item, i) => {
|
|
130
152
|
const isCollapsed = collapsedLabels.has(item.label);
|
|
131
153
|
const isSelected = isEqual(item.cpu, selectedTimeframe?.labels);
|
|
132
|
-
return (_jsx(SamplesGraphContainer, { isSelected: isSelected, isCollapsed: isCollapsed,
|
|
154
|
+
return (_jsx(SamplesGraphContainer, { isSelected: isSelected, isCollapsed: isCollapsed, label: item.label, width: width, data: item.data, onToggleCollapse: () => {
|
|
133
155
|
const newCollapsedLabels = new Set(collapsedLabels);
|
|
134
156
|
if (collapsedLabels.has(item.label)) {
|
|
135
157
|
newCollapsedLabels.delete(item.label);
|
|
@@ -140,6 +162,6 @@ export const SamplesStrip = ({ cpus, data, selectedTimeframe, onSelectedTimefram
|
|
|
140
162
|
setCollapsedLabels(newCollapsedLabels);
|
|
141
163
|
}, selectionBounds: isSelected ? selectedTimeframe?.bounds : undefined, setSelectionBounds: newBounds => {
|
|
142
164
|
onSelectedTimeframe(item.cpu, newBounds);
|
|
143
|
-
}, color: color, stepMs:
|
|
165
|
+
}, color: color, stepMs: effectiveStepMs, onDragStart: handleDragStart, dragState: dragState, stripIndex: i, isAnyDragActive: isDragging, timeBounds: bounds, loading: effectiveLoading }, item.label));
|
|
144
166
|
}), hasMore && !showAll && (_jsxs(Button, { variant: "secondary", onClick: () => setShowAll(true), className: "w-fit mx-auto mt-2", children: ["Show all ", sortedItems.length, " rows"] }))] }));
|
|
145
167
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { LabelSet } from '@parca/client';
|
|
2
|
+
export declare const labelSetToString: (labelSet: LabelSet | undefined, keyOrder?: string[]) => string;
|
|
3
|
+
export declare const createLabelSetComparator: (labelSets: LabelSet[]) => {
|
|
4
|
+
compare: (a: LabelSet, b: LabelSet) => number;
|
|
5
|
+
keyOrder: string[];
|
|
6
|
+
};
|
|
7
|
+
//# sourceMappingURL=labelSetUtils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labelSetUtils.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameChart/SamplesStrips/labelSetUtils.ts"],"names":[],"mappings":"AAaA,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AA+BvC,eAAO,MAAM,gBAAgB,GAAI,UAAU,QAAQ,GAAG,SAAS,EAAE,WAAW,MAAM,EAAE,KAAG,MAatF,CAAC;AAIF,eAAO,MAAM,wBAAwB,GACnC,WAAW,QAAQ,EAAE,KACpB;IAAC,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,KAAK,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAsBpE,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
// Determine which label keys have all-numeric values across every label set.
|
|
14
|
+
const getNumericKeys = (labelSets) => {
|
|
15
|
+
const numericKeys = new Set();
|
|
16
|
+
if (labelSets.length === 0)
|
|
17
|
+
return numericKeys;
|
|
18
|
+
const keyCandidates = new Set(labelSets[0].labels.map(l => l.name));
|
|
19
|
+
for (const key of keyCandidates) {
|
|
20
|
+
const allNumeric = labelSets.every(ls => {
|
|
21
|
+
const label = ls.labels.find(l => l.name === key);
|
|
22
|
+
return label != null && label.value !== '' && !isNaN(Number(label.value));
|
|
23
|
+
});
|
|
24
|
+
if (allNumeric)
|
|
25
|
+
numericKeys.add(key);
|
|
26
|
+
}
|
|
27
|
+
return numericKeys;
|
|
28
|
+
};
|
|
29
|
+
// Get key order: text keys first (sorted), then numeric keys (sorted).
|
|
30
|
+
const getSortedKeys = (labelSets, numericKeys) => {
|
|
31
|
+
const allKeys = new Set();
|
|
32
|
+
for (const ls of labelSets) {
|
|
33
|
+
for (const l of ls.labels)
|
|
34
|
+
allKeys.add(l.name);
|
|
35
|
+
}
|
|
36
|
+
return [...allKeys]
|
|
37
|
+
.filter(k => !numericKeys.has(k))
|
|
38
|
+
.sort()
|
|
39
|
+
.concat([...numericKeys].sort());
|
|
40
|
+
};
|
|
41
|
+
// Format a LabelSet as a string with keys ordered: text first, then numeric.
|
|
42
|
+
export const labelSetToString = (labelSet, keyOrder) => {
|
|
43
|
+
if (labelSet === undefined)
|
|
44
|
+
return '{}';
|
|
45
|
+
const labels = keyOrder != null
|
|
46
|
+
? keyOrder
|
|
47
|
+
.map(key => labelSet.labels.find(l => l.name === key))
|
|
48
|
+
.filter((l) => l != null)
|
|
49
|
+
: labelSet.labels;
|
|
50
|
+
if (labels.length === 0)
|
|
51
|
+
return '{}';
|
|
52
|
+
return '{' + labels.map(l => `${l.name}: ${l.value}`).join(', ') + '}';
|
|
53
|
+
};
|
|
54
|
+
// Build a comparator for LabelSets: text keys first (for grouping), then numeric keys.
|
|
55
|
+
// Also returns the key order so labelSetToString can use the same ordering.
|
|
56
|
+
export const createLabelSetComparator = (labelSets) => {
|
|
57
|
+
const numericKeys = getNumericKeys(labelSets);
|
|
58
|
+
const keyOrder = getSortedKeys(labelSets, numericKeys);
|
|
59
|
+
const compare = (a, b) => {
|
|
60
|
+
const aMap = new Map(a.labels.map(l => [l.name, l.value]));
|
|
61
|
+
const bMap = new Map(b.labels.map(l => [l.name, l.value]));
|
|
62
|
+
for (const key of keyOrder) {
|
|
63
|
+
const aVal = aMap.get(key) ?? '';
|
|
64
|
+
const bVal = bMap.get(key) ?? '';
|
|
65
|
+
if (numericKeys.has(key)) {
|
|
66
|
+
const diff = Number(aVal) - Number(bVal);
|
|
67
|
+
if (diff !== 0)
|
|
68
|
+
return diff;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const cmp = aVal.localeCompare(bVal);
|
|
72
|
+
if (cmp !== 0)
|
|
73
|
+
return cmp;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
};
|
|
78
|
+
return { compare, keyOrder };
|
|
79
|
+
};
|
|
@@ -13,8 +13,7 @@ interface ProfileFlameChartProps {
|
|
|
13
13
|
isHalfScreen: boolean;
|
|
14
14
|
metadataMappingFiles?: string[];
|
|
15
15
|
metadataLoading?: boolean;
|
|
16
|
-
onSwitchToOneMinute?: () => void;
|
|
17
16
|
}
|
|
18
|
-
export declare const ProfileFlameChart: ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading,
|
|
17
|
+
export declare const ProfileFlameChart: ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading, }: ProfileFlameChartProps) => JSX.Element;
|
|
19
18
|
export default ProfileFlameChart;
|
|
20
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameChart/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAoC,kBAAkB,EAAC,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ProfileFlameChart/index.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAoC,kBAAkB,EAAC,MAAM,eAAe,CAAC;AAEpF,OAAO,EAAwB,WAAW,EAAQ,MAAM,eAAe,CAAC;AAKxE,OAAO,EAAsB,aAAa,EAAa,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oCAAoC,CAAC;AA4CpE,UAAU,sBAAsB;IAC9B,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,EAAE,kBAAkB,CAAC;IAChC,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AA+BD,eAAO,MAAM,iBAAiB,GAAI,wIAW/B,sBAAsB,KAAG,GAAG,CAAC,OAgL/B,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Copyright 2022 The Parca Authors
|
|
3
3
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
// you may not use this file except in compliance with the License.
|
|
@@ -13,12 +13,12 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
import { useEffect, useMemo, useRef } from 'react';
|
|
15
15
|
import { QueryRequest_ReportType } from '@parca/client';
|
|
16
|
-
import {
|
|
16
|
+
import { useURLState, useURLStateCustom } from '@parca/components';
|
|
17
17
|
import { Matcher, MatcherTypes, Query } from '@parca/parser';
|
|
18
|
-
import { TimeUnits,
|
|
18
|
+
import { TimeUnits, formatDate, formatDuration } from '@parca/utilities';
|
|
19
19
|
import ProfileFlameGraph, { validateFlameChartQuery } from '../ProfileFlameGraph';
|
|
20
20
|
import { boundsFromProfileSource } from '../ProfileFlameGraph/FlameGraphArrow/utils';
|
|
21
|
-
import { MergedProfileSource } from '../ProfileSource';
|
|
21
|
+
import { MergedProfileSource, timeFormat } from '../ProfileSource';
|
|
22
22
|
import { useQuery } from '../useQuery';
|
|
23
23
|
import { SamplesStrip } from './SamplesStrips';
|
|
24
24
|
const TimeframeStateSerializer = {
|
|
@@ -70,8 +70,7 @@ const createFilteredProfileSource = (profileSource, selectedTimeframe) => {
|
|
|
70
70
|
const query = new Query(profileSource.query.profType, [...profileSource.query.matchers, ...dimensionMatchers], '');
|
|
71
71
|
return new MergedProfileSource(mergeFrom, mergeTo, query);
|
|
72
72
|
};
|
|
73
|
-
export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading,
|
|
74
|
-
const { loader } = useParcaContext();
|
|
73
|
+
export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, width, total, filtered, profileType, isHalfScreen, metadataMappingFiles, metadataLoading, }) => {
|
|
75
74
|
const zoomControlsRef = useRef(null);
|
|
76
75
|
const [selectedTimeframe, setSelectedTimeframe] = useURLStateCustom('flamechart_timeframe', TimeframeStateSerializer);
|
|
77
76
|
// Read flamechart dimension from URL state to detect changes
|
|
@@ -131,35 +130,29 @@ export const ProfileFlameChart = ({ samplesData, queryClient, profileSource, wid
|
|
|
131
130
|
const stepMs = samplesData.stepMs ?? 0;
|
|
132
131
|
return { cpus, data, stepMs };
|
|
133
132
|
}, [samplesData?.series, samplesData?.stepMs]);
|
|
134
|
-
const { isValid, isNonDelta
|
|
133
|
+
const { isValid, isNonDelta } = validateFlameChartQuery(profileSource);
|
|
135
134
|
if (!isValid) {
|
|
136
|
-
if (isDurationTooLong) {
|
|
137
|
-
return (_jsxs("div", { className: "flex flex-col justify-center items-center p-10 text-center gap-4 text-sm", children: [_jsx("span", { children: "Flame chart is unavailable for queries longer than one minute. Try reducing the time range to one minute or selecting a point in the metrics graph." }), onSwitchToOneMinute != null && (_jsx(Button, { variant: "primary", onClick: onSwitchToOneMinute, children: "Switch to last 1 minute" }))] }));
|
|
138
|
-
}
|
|
139
135
|
const message = isNonDelta
|
|
140
136
|
? 'To use the Flame chart, please switch to a Delta profile.'
|
|
141
137
|
: 'Flame chart is unavailable for this query.';
|
|
142
138
|
return (_jsx("div", { className: "flex flex-col justify-center p-10 text-center gap-6 text-sm", children: message }));
|
|
143
139
|
}
|
|
144
140
|
const hasDimension = (flamechartDimension ?? []).length > 0;
|
|
145
|
-
|
|
146
|
-
if (metadataLoading
|
|
147
|
-
return _jsx(_Fragment, { children: loader });
|
|
148
|
-
}
|
|
149
|
-
if (!hasDimension) {
|
|
141
|
+
const isStripsLoading = metadataLoading === true || !hasDimension || samplesData?.loading === true;
|
|
142
|
+
if (!hasDimension && metadataLoading !== true) {
|
|
150
143
|
return (_jsx("div", { className: "flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm", children: "Select a label in the \"Samples group by\" dropdown above to view the samples strips." }));
|
|
151
144
|
}
|
|
152
|
-
|
|
153
|
-
return _jsx(_Fragment, { children: loader });
|
|
154
|
-
}
|
|
155
|
-
return (_jsxs("div", { children: [stripsData.cpus.length > 0 && stripsData.data.length > 0 && (_jsx("div", { className: "mb-2", children: _jsx(SamplesStrip, { cpus: stripsData.cpus, data: stripsData.data, selectedTimeframe: selectedTimeframe, onSelectedTimeframe: handleSelectedTimeframe, width: width, bounds: [Number(timeBounds[0] / 1000000n), Number(timeBounds[1] / 1000000n)], stepMs: stripsData.stepMs }) })), selectedTimeframe != null &&
|
|
145
|
+
return (_jsxs("div", { children: [(isStripsLoading || (stripsData.cpus.length > 0 && stripsData.data.length > 0)) && (_jsx("div", { className: "mb-2", children: _jsx(SamplesStrip, { loading: isStripsLoading, cpus: stripsData.cpus, data: stripsData.data, selectedTimeframe: selectedTimeframe, onSelectedTimeframe: handleSelectedTimeframe, width: width, bounds: [Number(timeBounds[0] / 1000000n), Number(timeBounds[1] / 1000000n)], stepMs: stripsData.stepMs }) })), selectedTimeframe != null &&
|
|
156
146
|
(() => {
|
|
157
147
|
const labels = selectedTimeframe.labels.labels
|
|
158
148
|
.map(l => `${l.name} = ${l.value}`)
|
|
159
149
|
.join(', ');
|
|
160
150
|
const durationMs = selectedTimeframe.bounds[1] - selectedTimeframe.bounds[0];
|
|
161
|
-
const duration =
|
|
162
|
-
|
|
151
|
+
const duration = durationMs < 5000
|
|
152
|
+
? `${(durationMs / 1000).toFixed(1)}s`
|
|
153
|
+
: formatDuration({ [TimeUnits.Milliseconds]: durationMs });
|
|
154
|
+
const fmt = durationMs < 5000 ? "yyyy-MM-dd HH:mm:ss.SSS '(UTC)'" : timeFormat();
|
|
155
|
+
return (_jsxs("div", { className: "flex items-center justify-between px-2 py-1", children: [_jsxs("div", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: ["Samples matching ", labels, " over ", duration, " from", ' ', formatDate(new Date(selectedTimeframe.bounds[0]), fmt), " to", ' ', formatDate(new Date(selectedTimeframe.bounds[1]), fmt)] }), _jsx("div", { ref: zoomControlsRef })] }));
|
|
163
156
|
})(), selectedTimeframe != null && filteredProfileSource != null ? (_jsx(ProfileFlameGraph, { arrow: flamechartArrow, loading: flamechartLoading, error: flamechartError, profileSource: filteredProfileSource, width: width, total: flamechartTotal, filtered: flamechartFiltered, profileType: profileType, isHalfScreen: isHalfScreen, metadataMappingFiles: metadataMappingFiles, metadataLoading: metadataLoading, isFlameChart: true, curPathArrow: [], setNewCurPathArrow: () => { }, zoomControlsRef: zoomControlsRef })) : (_jsx("div", { className: "flex justify-center items-center py-10 text-gray-500 dark:text-gray-400 text-sm", children: "Select a time range in the samples strips above to view the flamechart." }))] }));
|
|
164
157
|
};
|
|
165
158
|
export default ProfileFlameChart;
|
|
@@ -14,6 +14,9 @@ interface MiniMapProps {
|
|
|
14
14
|
profileSource: ProfileSource;
|
|
15
15
|
isDarkMode: boolean;
|
|
16
16
|
scrollLeft: number;
|
|
17
|
+
scrollLeftRef: React.RefObject<number>;
|
|
18
|
+
onZoomToPosition?: (normalizedX: number, targetZoom: number) => void;
|
|
19
|
+
onSetZoomWithScroll?: (zoom: number, scrollLeft: number) => void;
|
|
17
20
|
}
|
|
18
21
|
export declare const MiniMap: React.NamedExoticComponent<MiniMapProps>;
|
|
19
22
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MiniMap.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAE5D,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AAKxC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAY,KAAK,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAYhE,UAAU,YAAY;IACpB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"MiniMap.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/MiniMap.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAE5D,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AAKxC,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAClD,OAAO,EAAY,KAAK,aAAa,EAAC,MAAM,mBAAmB,CAAC;AAYhE,UAAU,YAAY;IACpB,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACrD,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,aAAa,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACvC,gBAAgB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACrE,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE;AAED,eAAO,MAAM,OAAO,0CAyTlB,CAAC"}
|
|
@@ -18,7 +18,7 @@ import { RowHeight } from './FlameGraphNodes';
|
|
|
18
18
|
import { FIELD_CUMULATIVE, FIELD_DEPTH, FIELD_FUNCTION_FILE_NAME, FIELD_MAPPING_FILE, FIELD_TIMESTAMP, } from './index';
|
|
19
19
|
import { arrowToString, boundsFromProfileSource } from './utils';
|
|
20
20
|
const MINIMAP_HEIGHT = 20;
|
|
21
|
-
export const MiniMap = React.memo(function MiniMap({ containerRef, table, width, zoomedWidth, totalHeight, maxDepth, colorByColors: colors, colorBy, profileSource, isDarkMode, scrollLeft, }) {
|
|
21
|
+
export const MiniMap = React.memo(function MiniMap({ containerRef, table, width, zoomedWidth, totalHeight, maxDepth, colorByColors: colors, colorBy, profileSource, isDarkMode, scrollLeft: _scrollLeft, scrollLeftRef, onZoomToPosition, onSetZoomWithScroll, }) {
|
|
22
22
|
const canvasRef = useRef(null);
|
|
23
23
|
const containerElRef = useRef(null);
|
|
24
24
|
const isDragging = useRef(false);
|
|
@@ -40,7 +40,6 @@ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width,
|
|
|
40
40
|
// Background
|
|
41
41
|
ctx.fillStyle = isDarkMode ? '#374151' : '#f3f4f6';
|
|
42
42
|
ctx.fillRect(0, 0, width, MINIMAP_HEIGHT);
|
|
43
|
-
const xScale = width / zoomedWidth;
|
|
44
43
|
const yScale = MINIMAP_HEIGHT / totalHeight;
|
|
45
44
|
const tsBounds = boundsFromProfileSource(profileSource);
|
|
46
45
|
const tsRange = Number(tsBounds[1]) - Number(tsBounds[0]);
|
|
@@ -63,11 +62,11 @@ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width,
|
|
|
63
62
|
const cumulative = Number(cumulativeCol.get(row) ?? 0n);
|
|
64
63
|
if (cumulative <= 0)
|
|
65
64
|
continue;
|
|
66
|
-
const nodeWidth = (cumulative / tsRange) *
|
|
65
|
+
const nodeWidth = (cumulative / tsRange) * width;
|
|
67
66
|
if (nodeWidth < 0.5)
|
|
68
67
|
continue;
|
|
69
68
|
const ts = tsCol != null ? Number(tsCol.get(row)) : 0;
|
|
70
|
-
const x = ((ts - Number(tsBounds[0])) / tsRange) *
|
|
69
|
+
const x = ((ts - Number(tsBounds[0])) / tsRange) * width;
|
|
71
70
|
const y = (depth - 1) * RowHeight * yScale;
|
|
72
71
|
const h = Math.max(1, RowHeight * yScale);
|
|
73
72
|
// Get color using same logic as useNodeColor
|
|
@@ -80,32 +79,89 @@ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width,
|
|
|
80
79
|
ctx.fillStyle = color ?? (isDarkMode ? '#6b7280' : '#9ca3af');
|
|
81
80
|
ctx.fillRect(x, y, Math.max(0.5, nodeWidth), h);
|
|
82
81
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
width,
|
|
86
|
-
zoomedWidth,
|
|
87
|
-
totalHeight,
|
|
88
|
-
maxDepth,
|
|
89
|
-
colorBy,
|
|
90
|
-
colors,
|
|
91
|
-
isDarkMode,
|
|
92
|
-
profileSource,
|
|
93
|
-
]);
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- zoomedWidth intentionally excluded: canvas is zoom-independent
|
|
83
|
+
}, [table, width, totalHeight, maxDepth, colorBy, colors, isDarkMode, profileSource]);
|
|
94
84
|
const isZoomed = zoomedWidth > width;
|
|
95
85
|
const sliderWidth = Math.max(20, (width / zoomedWidth) * width);
|
|
96
|
-
|
|
86
|
+
// Use scrollLeftRef for positioning — it's pre-set before flushSync during zoom changes,
|
|
87
|
+
// avoiding the 1-frame lag where viewport.scrollLeft is stale but zoomedWidth is already updated.
|
|
88
|
+
const currentScrollLeft = scrollLeftRef.current ?? 0;
|
|
89
|
+
const sliderLeft = Math.min((currentScrollLeft / zoomedWidth) * width, width - sliderWidth);
|
|
90
|
+
const EDGE_HIT_ZONE = 6;
|
|
97
91
|
const handleMouseDown = useCallback((e) => {
|
|
98
92
|
e.preventDefault();
|
|
99
93
|
const rect = containerElRef.current?.getBoundingClientRect();
|
|
100
94
|
if (rect == null)
|
|
101
95
|
return;
|
|
102
96
|
const clickX = e.clientX - rect.left;
|
|
103
|
-
//
|
|
104
|
-
if (
|
|
105
|
-
//
|
|
97
|
+
// When not zoomed, clicking the minimap zooms into a +-50px region
|
|
98
|
+
if (!isZoomed) {
|
|
99
|
+
const regionPx = 100; // 50px on each side of the click
|
|
100
|
+
const targetZoom = width / regionPx;
|
|
101
|
+
onZoomToPosition?.(clickX / width, targetZoom);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const sliderRight = sliderLeft + sliderWidth;
|
|
105
|
+
const isNearLeftEdge = Math.abs(clickX - sliderLeft) <= EDGE_HIT_ZONE && clickX <= sliderLeft + EDGE_HIT_ZONE;
|
|
106
|
+
const isNearRightEdge = Math.abs(clickX - sliderRight) <= EDGE_HIT_ZONE && clickX >= sliderRight - EDGE_HIT_ZONE;
|
|
107
|
+
// Edge drag: resize the zoomed region by dragging one bound
|
|
108
|
+
if (isNearLeftEdge || isNearRightEdge) {
|
|
109
|
+
const edge = isNearLeftEdge ? 'left' : 'right';
|
|
110
|
+
// The opposite edge stays fixed in minimap coordinates
|
|
111
|
+
const anchorPx = edge === 'left' ? sliderRight : sliderLeft;
|
|
112
|
+
const MIN_SLIDER_PX = 10;
|
|
113
|
+
let edgeRafId = null;
|
|
114
|
+
let pendingEdgeEvent = null;
|
|
115
|
+
const applyEdgeMove = () => {
|
|
116
|
+
edgeRafId = null;
|
|
117
|
+
const moveEvent = pendingEdgeEvent;
|
|
118
|
+
if (moveEvent == null)
|
|
119
|
+
return;
|
|
120
|
+
pendingEdgeEvent = null;
|
|
121
|
+
const moveRect = containerElRef.current?.getBoundingClientRect();
|
|
122
|
+
if (moveRect == null)
|
|
123
|
+
return;
|
|
124
|
+
let edgePx = moveEvent.clientX - moveRect.left;
|
|
125
|
+
edgePx = Math.max(0, Math.min(edgePx, width));
|
|
126
|
+
let newLeft;
|
|
127
|
+
let newRight;
|
|
128
|
+
if (edge === 'left') {
|
|
129
|
+
newLeft = Math.min(edgePx, anchorPx - MIN_SLIDER_PX);
|
|
130
|
+
newRight = anchorPx;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
newLeft = anchorPx;
|
|
134
|
+
newRight = Math.max(edgePx, anchorPx + MIN_SLIDER_PX);
|
|
135
|
+
}
|
|
136
|
+
const newSliderWidth = newRight - newLeft;
|
|
137
|
+
const newZoom = width / newSliderWidth;
|
|
138
|
+
const newScrollLeft = newLeft * newZoom;
|
|
139
|
+
onSetZoomWithScroll?.(newZoom, newScrollLeft);
|
|
140
|
+
};
|
|
141
|
+
const handleEdgeMove = (moveEvent) => {
|
|
142
|
+
pendingEdgeEvent = moveEvent;
|
|
143
|
+
if (edgeRafId === null) {
|
|
144
|
+
edgeRafId = requestAnimationFrame(applyEdgeMove);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const handleEdgeUp = () => {
|
|
148
|
+
if (edgeRafId !== null) {
|
|
149
|
+
cancelAnimationFrame(edgeRafId);
|
|
150
|
+
// Apply final position immediately on mouse up
|
|
151
|
+
applyEdgeMove();
|
|
152
|
+
}
|
|
153
|
+
document.removeEventListener('mousemove', handleEdgeMove);
|
|
154
|
+
document.removeEventListener('mouseup', handleEdgeUp);
|
|
155
|
+
};
|
|
156
|
+
document.addEventListener('mousemove', handleEdgeMove);
|
|
157
|
+
document.addEventListener('mouseup', handleEdgeUp);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Check if clicking inside the slider — start pan drag
|
|
161
|
+
if (clickX >= sliderLeft && clickX <= sliderRight) {
|
|
106
162
|
isDragging.current = true;
|
|
107
163
|
dragStartX.current = e.clientX;
|
|
108
|
-
dragStartScrollLeft.current =
|
|
164
|
+
dragStartScrollLeft.current = currentScrollLeft;
|
|
109
165
|
}
|
|
110
166
|
else {
|
|
111
167
|
// Click-to-jump: center viewport at click position
|
|
@@ -137,7 +193,17 @@ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width,
|
|
|
137
193
|
};
|
|
138
194
|
document.addEventListener('mousemove', handleMouseMove);
|
|
139
195
|
document.addEventListener('mouseup', handleMouseUp);
|
|
140
|
-
}, [
|
|
196
|
+
}, [
|
|
197
|
+
sliderLeft,
|
|
198
|
+
sliderWidth,
|
|
199
|
+
currentScrollLeft,
|
|
200
|
+
width,
|
|
201
|
+
zoomedWidth,
|
|
202
|
+
containerRef,
|
|
203
|
+
isZoomed,
|
|
204
|
+
onZoomToPosition,
|
|
205
|
+
onSetZoomWithScroll,
|
|
206
|
+
]);
|
|
141
207
|
// Forward wheel events to the container so zoom (Ctrl+scroll) works on the minimap
|
|
142
208
|
useEffect(() => {
|
|
143
209
|
const el = containerElRef.current;
|
|
@@ -164,10 +230,9 @@ export const MiniMap = React.memo(function MiniMap({ containerRef, table, width,
|
|
|
164
230
|
}, [containerRef]);
|
|
165
231
|
if (width <= 0)
|
|
166
232
|
return null;
|
|
167
|
-
return (_jsxs("div", { ref: containerElRef, className: "relative select-none", style: { width, height: MINIMAP_HEIGHT
|
|
233
|
+
return (_jsxs("div", { ref: containerElRef, className: "relative select-none cursor-pointer", style: { width, height: MINIMAP_HEIGHT }, onMouseDown: handleMouseDown, children: [_jsx("canvas", { ref: canvasRef, style: {
|
|
168
234
|
width,
|
|
169
235
|
height: MINIMAP_HEIGHT,
|
|
170
236
|
display: 'block',
|
|
171
|
-
|
|
172
|
-
} }), isZoomed && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: 0, width: Math.max(0, sliderLeft) } }), _jsx("div", { className: "absolute top-0 bottom-0 border-x-2 border-gray-500", style: { left: sliderLeft, width: sliderWidth } }), _jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: sliderLeft + sliderWidth, right: 0 } })] }))] }));
|
|
237
|
+
} }), isZoomed && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: 0, width: Math.max(0, sliderLeft) } }), _jsx("div", { className: "absolute top-0 bottom-0 border-x-2 border-gray-500", style: { left: sliderLeft, width: sliderWidth } }), _jsx("div", { className: "absolute top-0 bottom-0 cursor-col-resize", style: { left: sliderLeft - EDGE_HIT_ZONE, width: EDGE_HIT_ZONE * 2 } }), _jsx("div", { className: "absolute top-0 bottom-0 cursor-col-resize", style: { left: sliderLeft + sliderWidth - EDGE_HIT_ZONE, width: EDGE_HIT_ZONE * 2 } }), _jsx("div", { className: "absolute top-0 bottom-0 bg-black/30 dark:bg-black/50", style: { left: sliderLeft + sliderWidth, right: 0 } })] }))] }));
|
|
173
238
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ZoomControls.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"ZoomControls.d.ts","sourceRoot":"","sources":["../../../src/ProfileFlameGraph/FlameGraphArrow/ZoomControls.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACpD;AAED,eAAO,MAAM,YAAY,GAAI,uDAM1B,iBAAiB,KAAG,KAAK,CAAC,GAAG,CAAC,OAkChC,CAAC"}
|