@parca/profile 0.19.133 → 0.19.135
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 +7 -7
- 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
|
@@ -43,6 +43,9 @@ interface MiniMapProps {
|
|
|
43
43
|
profileSource: ProfileSource;
|
|
44
44
|
isDarkMode: boolean;
|
|
45
45
|
scrollLeft: number;
|
|
46
|
+
scrollLeftRef: React.RefObject<number>;
|
|
47
|
+
onZoomToPosition?: (normalizedX: number, targetZoom: number) => void;
|
|
48
|
+
onSetZoomWithScroll?: (zoom: number, scrollLeft: number) => void;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export const MiniMap = React.memo(function MiniMap({
|
|
@@ -56,7 +59,10 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
56
59
|
colorBy,
|
|
57
60
|
profileSource,
|
|
58
61
|
isDarkMode,
|
|
59
|
-
scrollLeft,
|
|
62
|
+
scrollLeft: _scrollLeft,
|
|
63
|
+
scrollLeftRef,
|
|
64
|
+
onZoomToPosition,
|
|
65
|
+
onSetZoomWithScroll,
|
|
60
66
|
}: MiniMapProps): React.JSX.Element | null {
|
|
61
67
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
62
68
|
const containerElRef = useRef<HTMLDivElement>(null);
|
|
@@ -83,7 +89,6 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
83
89
|
ctx.fillStyle = isDarkMode ? '#374151' : '#f3f4f6';
|
|
84
90
|
ctx.fillRect(0, 0, width, MINIMAP_HEIGHT);
|
|
85
91
|
|
|
86
|
-
const xScale = width / zoomedWidth;
|
|
87
92
|
const yScale = MINIMAP_HEIGHT / totalHeight;
|
|
88
93
|
|
|
89
94
|
const tsBounds = boundsFromProfileSource(profileSource);
|
|
@@ -109,11 +114,11 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
109
114
|
const cumulative = Number(cumulativeCol.get(row) ?? 0n);
|
|
110
115
|
if (cumulative <= 0) continue;
|
|
111
116
|
|
|
112
|
-
const nodeWidth = (cumulative / tsRange) *
|
|
117
|
+
const nodeWidth = (cumulative / tsRange) * width;
|
|
113
118
|
if (nodeWidth < 0.5) continue;
|
|
114
119
|
|
|
115
120
|
const ts = tsCol != null ? Number(tsCol.get(row)) : 0;
|
|
116
|
-
const x = ((ts - Number(tsBounds[0])) / tsRange) *
|
|
121
|
+
const x = ((ts - Number(tsBounds[0])) / tsRange) * width;
|
|
117
122
|
const y = (depth - 1) * RowHeight * yScale;
|
|
118
123
|
const h = Math.max(1, RowHeight * yScale);
|
|
119
124
|
|
|
@@ -129,21 +134,17 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
129
134
|
ctx.fillStyle = color ?? (isDarkMode ? '#6b7280' : '#9ca3af');
|
|
130
135
|
ctx.fillRect(x, y, Math.max(0.5, nodeWidth), h);
|
|
131
136
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
width,
|
|
135
|
-
zoomedWidth,
|
|
136
|
-
totalHeight,
|
|
137
|
-
maxDepth,
|
|
138
|
-
colorBy,
|
|
139
|
-
colors,
|
|
140
|
-
isDarkMode,
|
|
141
|
-
profileSource,
|
|
142
|
-
]);
|
|
137
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- zoomedWidth intentionally excluded: canvas is zoom-independent
|
|
138
|
+
}, [table, width, totalHeight, maxDepth, colorBy, colors, isDarkMode, profileSource]);
|
|
143
139
|
|
|
144
140
|
const isZoomed = zoomedWidth > width;
|
|
145
141
|
const sliderWidth = Math.max(20, (width / zoomedWidth) * width);
|
|
146
|
-
|
|
142
|
+
// Use scrollLeftRef for positioning — it's pre-set before flushSync during zoom changes,
|
|
143
|
+
// avoiding the 1-frame lag where viewport.scrollLeft is stale but zoomedWidth is already updated.
|
|
144
|
+
const currentScrollLeft = scrollLeftRef.current ?? 0;
|
|
145
|
+
const sliderLeft = Math.min((currentScrollLeft / zoomedWidth) * width, width - sliderWidth);
|
|
146
|
+
|
|
147
|
+
const EDGE_HIT_ZONE = 6;
|
|
147
148
|
|
|
148
149
|
const handleMouseDown = useCallback(
|
|
149
150
|
(e: React.MouseEvent) => {
|
|
@@ -153,12 +154,86 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
153
154
|
|
|
154
155
|
const clickX = e.clientX - rect.left;
|
|
155
156
|
|
|
156
|
-
//
|
|
157
|
-
if (
|
|
158
|
-
//
|
|
157
|
+
// When not zoomed, clicking the minimap zooms into a +-50px region
|
|
158
|
+
if (!isZoomed) {
|
|
159
|
+
const regionPx = 100; // 50px on each side of the click
|
|
160
|
+
const targetZoom = width / regionPx;
|
|
161
|
+
onZoomToPosition?.(clickX / width, targetZoom);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sliderRight = sliderLeft + sliderWidth;
|
|
166
|
+
const isNearLeftEdge =
|
|
167
|
+
Math.abs(clickX - sliderLeft) <= EDGE_HIT_ZONE && clickX <= sliderLeft + EDGE_HIT_ZONE;
|
|
168
|
+
const isNearRightEdge =
|
|
169
|
+
Math.abs(clickX - sliderRight) <= EDGE_HIT_ZONE && clickX >= sliderRight - EDGE_HIT_ZONE;
|
|
170
|
+
|
|
171
|
+
// Edge drag: resize the zoomed region by dragging one bound
|
|
172
|
+
if (isNearLeftEdge || isNearRightEdge) {
|
|
173
|
+
const edge = isNearLeftEdge ? 'left' : 'right';
|
|
174
|
+
// The opposite edge stays fixed in minimap coordinates
|
|
175
|
+
const anchorPx = edge === 'left' ? sliderRight : sliderLeft;
|
|
176
|
+
const MIN_SLIDER_PX = 10;
|
|
177
|
+
|
|
178
|
+
let edgeRafId: number | null = null;
|
|
179
|
+
let pendingEdgeEvent: MouseEvent | null = null;
|
|
180
|
+
|
|
181
|
+
const applyEdgeMove = (): void => {
|
|
182
|
+
edgeRafId = null;
|
|
183
|
+
const moveEvent = pendingEdgeEvent;
|
|
184
|
+
if (moveEvent == null) return;
|
|
185
|
+
pendingEdgeEvent = null;
|
|
186
|
+
|
|
187
|
+
const moveRect = containerElRef.current?.getBoundingClientRect();
|
|
188
|
+
if (moveRect == null) return;
|
|
189
|
+
|
|
190
|
+
let edgePx = moveEvent.clientX - moveRect.left;
|
|
191
|
+
edgePx = Math.max(0, Math.min(edgePx, width));
|
|
192
|
+
|
|
193
|
+
let newLeft: number;
|
|
194
|
+
let newRight: number;
|
|
195
|
+
|
|
196
|
+
if (edge === 'left') {
|
|
197
|
+
newLeft = Math.min(edgePx, anchorPx - MIN_SLIDER_PX);
|
|
198
|
+
newRight = anchorPx;
|
|
199
|
+
} else {
|
|
200
|
+
newLeft = anchorPx;
|
|
201
|
+
newRight = Math.max(edgePx, anchorPx + MIN_SLIDER_PX);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const newSliderWidth = newRight - newLeft;
|
|
205
|
+
const newZoom = width / newSliderWidth;
|
|
206
|
+
const newScrollLeft = newLeft * newZoom;
|
|
207
|
+
onSetZoomWithScroll?.(newZoom, newScrollLeft);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const handleEdgeMove = (moveEvent: MouseEvent): void => {
|
|
211
|
+
pendingEdgeEvent = moveEvent;
|
|
212
|
+
if (edgeRafId === null) {
|
|
213
|
+
edgeRafId = requestAnimationFrame(applyEdgeMove);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const handleEdgeUp = (): void => {
|
|
218
|
+
if (edgeRafId !== null) {
|
|
219
|
+
cancelAnimationFrame(edgeRafId);
|
|
220
|
+
// Apply final position immediately on mouse up
|
|
221
|
+
applyEdgeMove();
|
|
222
|
+
}
|
|
223
|
+
document.removeEventListener('mousemove', handleEdgeMove);
|
|
224
|
+
document.removeEventListener('mouseup', handleEdgeUp);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
document.addEventListener('mousemove', handleEdgeMove);
|
|
228
|
+
document.addEventListener('mouseup', handleEdgeUp);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check if clicking inside the slider — start pan drag
|
|
233
|
+
if (clickX >= sliderLeft && clickX <= sliderRight) {
|
|
159
234
|
isDragging.current = true;
|
|
160
235
|
dragStartX.current = e.clientX;
|
|
161
|
-
dragStartScrollLeft.current =
|
|
236
|
+
dragStartScrollLeft.current = currentScrollLeft;
|
|
162
237
|
} else {
|
|
163
238
|
// Click-to-jump: center viewport at click position
|
|
164
239
|
const targetCenter = (clickX / width) * zoomedWidth;
|
|
@@ -198,7 +273,17 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
198
273
|
document.addEventListener('mousemove', handleMouseMove);
|
|
199
274
|
document.addEventListener('mouseup', handleMouseUp);
|
|
200
275
|
},
|
|
201
|
-
[
|
|
276
|
+
[
|
|
277
|
+
sliderLeft,
|
|
278
|
+
sliderWidth,
|
|
279
|
+
currentScrollLeft,
|
|
280
|
+
width,
|
|
281
|
+
zoomedWidth,
|
|
282
|
+
containerRef,
|
|
283
|
+
isZoomed,
|
|
284
|
+
onZoomToPosition,
|
|
285
|
+
onSetZoomWithScroll,
|
|
286
|
+
]
|
|
202
287
|
);
|
|
203
288
|
|
|
204
289
|
// Forward wheel events to the container so zoom (Ctrl+scroll) works on the minimap
|
|
@@ -233,9 +318,9 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
233
318
|
return (
|
|
234
319
|
<div
|
|
235
320
|
ref={containerElRef}
|
|
236
|
-
className="relative select-none"
|
|
237
|
-
style={{width, height: MINIMAP_HEIGHT
|
|
238
|
-
onMouseDown={
|
|
321
|
+
className="relative select-none cursor-pointer"
|
|
322
|
+
style={{width, height: MINIMAP_HEIGHT}}
|
|
323
|
+
onMouseDown={handleMouseDown}
|
|
239
324
|
>
|
|
240
325
|
<canvas
|
|
241
326
|
ref={canvasRef}
|
|
@@ -243,7 +328,6 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
243
328
|
width,
|
|
244
329
|
height: MINIMAP_HEIGHT,
|
|
245
330
|
display: 'block',
|
|
246
|
-
visibility: isZoomed ? 'visible' : 'hidden',
|
|
247
331
|
}}
|
|
248
332
|
/>
|
|
249
333
|
{isZoomed && (
|
|
@@ -258,6 +342,16 @@ export const MiniMap = React.memo(function MiniMap({
|
|
|
258
342
|
className="absolute top-0 bottom-0 border-x-2 border-gray-500"
|
|
259
343
|
style={{left: sliderLeft, width: sliderWidth}}
|
|
260
344
|
/>
|
|
345
|
+
{/* Left edge drag handle */}
|
|
346
|
+
<div
|
|
347
|
+
className="absolute top-0 bottom-0 cursor-col-resize"
|
|
348
|
+
style={{left: sliderLeft - EDGE_HIT_ZONE, width: EDGE_HIT_ZONE * 2}}
|
|
349
|
+
/>
|
|
350
|
+
{/* Right edge drag handle */}
|
|
351
|
+
<div
|
|
352
|
+
className="absolute top-0 bottom-0 cursor-col-resize"
|
|
353
|
+
style={{left: sliderLeft + sliderWidth - EDGE_HIT_ZONE, width: EDGE_HIT_ZONE * 2}}
|
|
354
|
+
/>
|
|
261
355
|
{/* Right overlay */}
|
|
262
356
|
<div
|
|
263
357
|
className="absolute top-0 bottom-0 bg-black/30 dark:bg-black/50"
|
|
@@ -16,6 +16,8 @@ import React from 'react';
|
|
|
16
16
|
import {Icon} from '@iconify/react';
|
|
17
17
|
import {createPortal} from 'react-dom';
|
|
18
18
|
|
|
19
|
+
import {MAX_ZOOM} from './useZoom';
|
|
20
|
+
|
|
19
21
|
interface ZoomControlsProps {
|
|
20
22
|
zoomLevel: number;
|
|
21
23
|
zoomIn: () => void;
|
|
@@ -50,7 +52,7 @@ export const ZoomControls = ({
|
|
|
50
52
|
</button>
|
|
51
53
|
<button
|
|
52
54
|
onClick={zoomIn}
|
|
53
|
-
disabled={zoomLevel >=
|
|
55
|
+
disabled={zoomLevel >= MAX_ZOOM}
|
|
54
56
|
className="rounded p-1 text-gray-600 hover:bg-gray-100 disabled:opacity-30 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
55
57
|
title="Zoom in"
|
|
56
58
|
>
|
|
@@ -273,9 +273,8 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
273
273
|
|
|
274
274
|
const isZoomEnabled = isFlameChart;
|
|
275
275
|
|
|
276
|
-
const {zoomLevel, zoomIn, zoomOut, resetZoom} =
|
|
277
|
-
isZoomEnabled ? containerRef : {current: null}
|
|
278
|
-
);
|
|
276
|
+
const {zoomLevel, zoomIn, zoomOut, resetZoom, zoomToPosition, setZoomWithScroll, scrollLeftRef} =
|
|
277
|
+
useZoom(isZoomEnabled ? containerRef : {current: null});
|
|
279
278
|
const zoomedWidth = isZoomEnabled ? Math.round((width ?? 1) * zoomLevel) : width ?? 0;
|
|
280
279
|
|
|
281
280
|
// Reset zoom when the data changes (e.g. new query, different time range)
|
|
@@ -400,6 +399,9 @@ export const FlameGraphArrow = memo(function FlameGraphArrow({
|
|
|
400
399
|
profileSource={profileSource}
|
|
401
400
|
isDarkMode={isDarkMode}
|
|
402
401
|
scrollLeft={viewport.scrollLeft}
|
|
402
|
+
scrollLeftRef={scrollLeftRef}
|
|
403
|
+
onZoomToPosition={zoomToPosition}
|
|
404
|
+
onSetZoomWithScroll={setZoomWithScroll}
|
|
403
405
|
/>
|
|
404
406
|
)}
|
|
405
407
|
<div
|
|
@@ -16,7 +16,7 @@ import {useCallback, useEffect, useRef, useState} from 'react';
|
|
|
16
16
|
import {flushSync} from 'react-dom';
|
|
17
17
|
|
|
18
18
|
const MIN_ZOOM = 1.0;
|
|
19
|
-
const MAX_ZOOM =
|
|
19
|
+
export const MAX_ZOOM = 100.0;
|
|
20
20
|
const BUTTON_ZOOM_STEP = 1.5;
|
|
21
21
|
// Sensitivity for trackpad/wheel zoom - smaller = smoother
|
|
22
22
|
const WHEEL_ZOOM_SENSITIVITY = 0.01;
|
|
@@ -26,6 +26,9 @@ interface UseZoomResult {
|
|
|
26
26
|
zoomIn: () => void;
|
|
27
27
|
zoomOut: () => void;
|
|
28
28
|
resetZoom: () => void;
|
|
29
|
+
zoomToPosition: (normalizedX: number, targetZoom: number) => void;
|
|
30
|
+
setZoomWithScroll: (zoom: number, scrollLeft: number) => void;
|
|
31
|
+
scrollLeftRef: React.RefObject<number>;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
const clampZoom = (zoom: number): number => {
|
|
@@ -35,32 +38,42 @@ const clampZoom = (zoom: number): number => {
|
|
|
35
38
|
export const useZoom = (containerRef: React.RefObject<HTMLDivElement | null>): UseZoomResult => {
|
|
36
39
|
const [zoomLevel, setZoomLevel] = useState(MIN_ZOOM);
|
|
37
40
|
const zoomLevelRef = useRef(MIN_ZOOM);
|
|
41
|
+
const scrollLeftRef = useRef(0);
|
|
38
42
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (container === null) return;
|
|
43
|
+
// Keep scrollLeftRef in sync with actual scroll position during regular scrolling
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const container = containerRef.current;
|
|
46
|
+
if (container === null) return;
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
);
|
|
48
|
+
const onScroll = (): void => {
|
|
49
|
+
scrollLeftRef.current = container.scrollLeft;
|
|
50
|
+
};
|
|
51
|
+
container.addEventListener('scroll', onScroll, {passive: true});
|
|
52
|
+
return () => container.removeEventListener('scroll', onScroll);
|
|
53
|
+
}, [containerRef]);
|
|
51
54
|
|
|
52
55
|
// Apply a new zoom level around a focal point
|
|
53
56
|
const applyZoom = useCallback(
|
|
54
57
|
(newZoom: number, focalX: number) => {
|
|
58
|
+
const container = containerRef.current;
|
|
59
|
+
if (container === null) return;
|
|
60
|
+
|
|
55
61
|
const oldZoom = zoomLevelRef.current;
|
|
56
62
|
if (newZoom === oldZoom) return;
|
|
57
|
-
zoomLevelRef.current = newZoom;
|
|
58
63
|
|
|
59
|
-
//
|
|
64
|
+
// Pre-compute intended scrollLeft BEFORE flushSync so MiniMap reads correct value during render
|
|
65
|
+
const contentX = container.scrollLeft + focalX;
|
|
66
|
+
const ratio = contentX / oldZoom;
|
|
67
|
+
const newScrollLeft = ratio * newZoom - focalX;
|
|
68
|
+
scrollLeftRef.current = newScrollLeft;
|
|
69
|
+
|
|
70
|
+
zoomLevelRef.current = newZoom;
|
|
60
71
|
flushSync(() => setZoomLevel(newZoom));
|
|
61
|
-
|
|
72
|
+
|
|
73
|
+
// Apply scroll to DOM after render (content is now wide enough)
|
|
74
|
+
container.scrollLeft = newScrollLeft;
|
|
62
75
|
},
|
|
63
|
-
[
|
|
76
|
+
[containerRef]
|
|
64
77
|
);
|
|
65
78
|
|
|
66
79
|
const zoomIn = useCallback(() => {
|
|
@@ -77,6 +90,7 @@ export const useZoom = (containerRef: React.RefObject<HTMLDivElement | null>): U
|
|
|
77
90
|
|
|
78
91
|
const resetZoom = useCallback(() => {
|
|
79
92
|
zoomLevelRef.current = MIN_ZOOM;
|
|
93
|
+
scrollLeftRef.current = 0;
|
|
80
94
|
setZoomLevel(MIN_ZOOM);
|
|
81
95
|
const container = containerRef.current;
|
|
82
96
|
if (container !== null) {
|
|
@@ -112,5 +126,52 @@ export const useZoom = (containerRef: React.RefObject<HTMLDivElement | null>): U
|
|
|
112
126
|
};
|
|
113
127
|
}, [containerRef, applyZoom]);
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
const zoomToPosition = useCallback(
|
|
130
|
+
(normalizedX: number, targetZoom: number) => {
|
|
131
|
+
const container = containerRef.current;
|
|
132
|
+
if (container === null) return;
|
|
133
|
+
|
|
134
|
+
const newZoom = clampZoom(targetZoom);
|
|
135
|
+
if (newZoom === zoomLevelRef.current) return;
|
|
136
|
+
|
|
137
|
+
const containerWidth = container.clientWidth;
|
|
138
|
+
const contentWidth = containerWidth * newZoom;
|
|
139
|
+
const targetScrollLeft = Math.max(
|
|
140
|
+
0,
|
|
141
|
+
Math.min(normalizedX * contentWidth - containerWidth / 2, contentWidth - containerWidth)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Pre-set scrollLeftRef before flushSync so MiniMap reads correct value during render
|
|
145
|
+
scrollLeftRef.current = targetScrollLeft;
|
|
146
|
+
zoomLevelRef.current = newZoom;
|
|
147
|
+
flushSync(() => setZoomLevel(newZoom));
|
|
148
|
+
|
|
149
|
+
container.scrollLeft = targetScrollLeft;
|
|
150
|
+
},
|
|
151
|
+
[containerRef]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const setZoomWithScroll = useCallback(
|
|
155
|
+
(zoom: number, newScrollLeft: number) => {
|
|
156
|
+
const container = containerRef.current;
|
|
157
|
+
if (container === null) return;
|
|
158
|
+
|
|
159
|
+
const clamped = clampZoom(zoom);
|
|
160
|
+
const contentWidth = container.clientWidth * clamped;
|
|
161
|
+
const clampedScroll = Math.max(
|
|
162
|
+
0,
|
|
163
|
+
Math.min(newScrollLeft, contentWidth - container.clientWidth)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Pre-set scrollLeftRef before flushSync so MiniMap reads correct value during render
|
|
167
|
+
scrollLeftRef.current = clampedScroll;
|
|
168
|
+
zoomLevelRef.current = clamped;
|
|
169
|
+
flushSync(() => setZoomLevel(clamped));
|
|
170
|
+
|
|
171
|
+
container.scrollLeft = clampedScroll;
|
|
172
|
+
},
|
|
173
|
+
[containerRef]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {zoomLevel, zoomIn, zoomOut, resetZoom, zoomToPosition, setZoomWithScroll, scrollLeftRef};
|
|
116
177
|
};
|
|
@@ -74,11 +74,9 @@ const ErrorContent = ({errorMessage}: {errorMessage: string | ReactNode}): JSX.E
|
|
|
74
74
|
|
|
75
75
|
export const validateFlameChartQuery = (
|
|
76
76
|
profileSource: MergedProfileSource
|
|
77
|
-
): {isValid: boolean; isNonDelta: boolean
|
|
77
|
+
): {isValid: boolean; isNonDelta: boolean} => {
|
|
78
78
|
const isNonDelta = !profileSource.ProfileType().delta;
|
|
79
|
-
|
|
80
|
-
const isDurationTooLong = duration > 60_000_000_000n; // 60 seconds in nanoseconds
|
|
81
|
-
return {isValid: !isNonDelta && !isDurationTooLong, isNonDelta, isDurationTooLong};
|
|
79
|
+
return {isValid: !isNonDelta, isNonDelta};
|
|
82
80
|
};
|
|
83
81
|
|
|
84
82
|
const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
|
|
@@ -194,13 +192,9 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
|
|
|
194
192
|
}, [loadingState]);
|
|
195
193
|
|
|
196
194
|
const flameGraph = useMemo(() => {
|
|
197
|
-
const {
|
|
198
|
-
isValid: isFlameChartValid,
|
|
199
|
-
isNonDelta,
|
|
200
|
-
isDurationTooLong,
|
|
201
|
-
} = isFlameChart
|
|
195
|
+
const {isValid: isFlameChartValid, isNonDelta} = isFlameChart
|
|
202
196
|
? validateFlameChartQuery(profileSource as MergedProfileSource)
|
|
203
|
-
: {isValid: true, isNonDelta: false
|
|
197
|
+
: {isValid: true, isNonDelta: false};
|
|
204
198
|
const isInvalidFlameChartQuery = isFlameChart && !isFlameChartValid;
|
|
205
199
|
|
|
206
200
|
if (isLoading && !isInvalidFlameChartQuery) {
|
|
@@ -228,20 +222,6 @@ const ProfileFlameGraph = function ProfileFlameGraphNonMemo({
|
|
|
228
222
|
}
|
|
229
223
|
/>
|
|
230
224
|
);
|
|
231
|
-
} else if (isDurationTooLong) {
|
|
232
|
-
return (
|
|
233
|
-
<ErrorContent
|
|
234
|
-
errorMessage={
|
|
235
|
-
<>
|
|
236
|
-
<span>
|
|
237
|
-
Flame chart is unavailable for queries longer than one minute. Please select a
|
|
238
|
-
point in the metrics graph to continue.
|
|
239
|
-
</span>
|
|
240
|
-
{flamechartHelpText ?? null}
|
|
241
|
-
</>
|
|
242
|
-
}
|
|
243
|
-
/>
|
|
244
|
-
);
|
|
245
225
|
} else {
|
|
246
226
|
return (
|
|
247
227
|
<ErrorContent
|
|
@@ -50,7 +50,6 @@ interface GetDashboardItemProps {
|
|
|
50
50
|
onRender?: ProfilerOnRenderCallback;
|
|
51
51
|
};
|
|
52
52
|
queryClient: QueryServiceClient;
|
|
53
|
-
onSwitchToOneMinute?: () => void;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export const getDashboardItem = ({
|
|
@@ -69,7 +68,6 @@ export const getDashboardItem = ({
|
|
|
69
68
|
setNewCurPathArrow,
|
|
70
69
|
perf,
|
|
71
70
|
queryClient,
|
|
72
|
-
onSwitchToOneMinute,
|
|
73
71
|
}: GetDashboardItemProps): JSX.Element => {
|
|
74
72
|
switch (type) {
|
|
75
73
|
case 'flamegraph':
|
|
@@ -124,7 +122,6 @@ export const getDashboardItem = ({
|
|
|
124
122
|
isHalfScreen={isHalfScreen}
|
|
125
123
|
metadataMappingFiles={flamegraphData.metadataMappingFiles}
|
|
126
124
|
metadataLoading={flamegraphData.metadataLoading}
|
|
127
|
-
onSwitchToOneMinute={onSwitchToOneMinute}
|
|
128
125
|
/>
|
|
129
126
|
);
|
|
130
127
|
case 'table':
|
|
@@ -46,7 +46,6 @@ export const ProfileView = ({
|
|
|
46
46
|
compare,
|
|
47
47
|
showVisualizationSelector,
|
|
48
48
|
sandwichData,
|
|
49
|
-
onSwitchToOneMinute,
|
|
50
49
|
}: ProfileViewProps): JSX.Element => {
|
|
51
50
|
const {
|
|
52
51
|
timezone,
|
|
@@ -116,7 +115,6 @@ export const ProfileView = ({
|
|
|
116
115
|
setNewCurPathArrow: setCurPathArrow,
|
|
117
116
|
perf,
|
|
118
117
|
queryClient,
|
|
119
|
-
onSwitchToOneMinute,
|
|
120
118
|
});
|
|
121
119
|
};
|
|
122
120
|
|
|
@@ -43,14 +43,12 @@ interface ProfileViewWithDataProps {
|
|
|
43
43
|
profileSource: ProfileSource;
|
|
44
44
|
compare?: boolean;
|
|
45
45
|
showVisualizationSelector?: boolean;
|
|
46
|
-
onSwitchToOneMinute?: () => void;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export const ProfileViewWithData = ({
|
|
50
49
|
queryClient,
|
|
51
50
|
profileSource,
|
|
52
51
|
showVisualizationSelector,
|
|
53
|
-
onSwitchToOneMinute,
|
|
54
52
|
}: ProfileViewWithDataProps): JSX.Element => {
|
|
55
53
|
const metadata = useGrpcMetadata();
|
|
56
54
|
const [dashboardItems, setDashboardItems] = useURLState<string[]>('dashboard_items', {
|
|
@@ -380,7 +378,6 @@ export const ProfileViewWithData = ({
|
|
|
380
378
|
onDownloadPProf={() => void downloadPProfClick()}
|
|
381
379
|
pprofDownloading={pprofDownloading}
|
|
382
380
|
showVisualizationSelector={showVisualizationSelector}
|
|
383
|
-
onSwitchToOneMinute={onSwitchToOneMinute}
|
|
384
381
|
/>
|
|
385
382
|
);
|
|
386
383
|
};
|
|
@@ -48,7 +48,7 @@ export const TimelineGuide = ({
|
|
|
48
48
|
const xScale = scaleLinear(bounds, [0, width]);
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
<div className="relative h-5">
|
|
51
|
+
<div className="relative h-5 z-40">
|
|
52
52
|
<div className="pointer-events-none absolute" style={{width, height}}>
|
|
53
53
|
<svg style={{width: '100%', height: '100%'}}>
|
|
54
54
|
<g
|