@parca/profile 0.16.227 → 0.16.228
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 +4 -0
- package/dist/GraphTooltipArrow/Content.d.ts +3 -1
- package/dist/GraphTooltipArrow/Content.js +44 -6
- package/dist/GraphTooltipArrow/ExpandOnHoverValue.js +1 -1
- package/dist/MetricsGraph/useMetricsGraphDimensions.js +9 -0
- package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +1 -1
- package/dist/ProfileView/ProfileViewContext.d.ts +14 -0
- package/dist/ProfileView/ProfileViewContext.js +30 -0
- package/dist/ProfileView/ViewSelector.js +1 -0
- package/dist/ProfileView/index.d.ts +8 -2
- package/dist/ProfileView/index.js +19 -10
- package/dist/ProfileViewWithData.js +22 -0
- package/dist/SourceView/Highlighter.d.ts +16 -0
- package/dist/SourceView/Highlighter.js +80 -0
- package/dist/SourceView/LineNo.d.ts +6 -0
- package/dist/SourceView/LineNo.js +23 -0
- package/dist/SourceView/index.d.ts +11 -0
- package/dist/SourceView/index.js +37 -0
- package/dist/styles.css +1 -1
- package/dist/useQuery.d.ts +3 -0
- package/dist/useQuery.js +17 -1
- package/package.json +10 -8
- package/src/GraphTooltipArrow/Content.tsx +88 -9
- package/src/GraphTooltipArrow/ExpandOnHoverValue.tsx +1 -1
- package/src/MetricsGraph/useMetricsGraphDimensions.ts +9 -0
- package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +1 -0
- package/src/ProfileView/ProfileViewContext.tsx +52 -0
- package/src/ProfileView/ViewSelector.tsx +1 -0
- package/src/ProfileView/index.tsx +128 -93
- package/src/ProfileViewWithData.tsx +31 -0
- package/src/SourceView/Highlighter.tsx +196 -0
- package/src/SourceView/LineNo.tsx +31 -0
- package/src/SourceView/index.tsx +74 -0
- package/src/useQuery.tsx +20 -1
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
type DropResult,
|
|
26
26
|
} from 'react-beautiful-dnd';
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
Callgraph as CallgraphType,
|
|
30
|
+
Flamegraph,
|
|
31
|
+
QueryServiceClient,
|
|
32
|
+
Source,
|
|
33
|
+
Top,
|
|
34
|
+
} from '@parca/client';
|
|
29
35
|
import {
|
|
30
36
|
Button,
|
|
31
37
|
Card,
|
|
@@ -42,10 +48,12 @@ import {Callgraph} from '../';
|
|
|
42
48
|
import {jsonToDot} from '../Callgraph/utils';
|
|
43
49
|
import ProfileIcicleGraph from '../ProfileIcicleGraph';
|
|
44
50
|
import {ProfileSource} from '../ProfileSource';
|
|
51
|
+
import {SourceView} from '../SourceView';
|
|
45
52
|
import {TopTable} from '../TopTable';
|
|
46
53
|
import ProfileShareButton from '../components/ProfileShareButton';
|
|
47
54
|
import useDelayedLoader from '../useDelayedLoader';
|
|
48
55
|
import FilterByFunctionButton from './FilterByFunctionButton';
|
|
56
|
+
import {ProfileViewContextProvider} from './ProfileViewContext';
|
|
49
57
|
import ViewSelector from './ViewSelector';
|
|
50
58
|
import {VisualizationPanel} from './VisualizationPanel';
|
|
51
59
|
|
|
@@ -76,12 +84,19 @@ interface CallgraphData {
|
|
|
76
84
|
error?: any;
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
interface SourceData {
|
|
88
|
+
loading: boolean;
|
|
89
|
+
data?: Source;
|
|
90
|
+
error?: any;
|
|
91
|
+
}
|
|
92
|
+
|
|
79
93
|
export interface ProfileViewProps {
|
|
80
94
|
total: bigint;
|
|
81
95
|
filtered: bigint;
|
|
82
96
|
flamegraphData?: FlamegraphData;
|
|
83
97
|
topTableData?: TopTableData;
|
|
84
98
|
callgraphData?: CallgraphData;
|
|
99
|
+
sourceData?: SourceData;
|
|
85
100
|
sampleUnit: string;
|
|
86
101
|
profileSource?: ProfileSource;
|
|
87
102
|
queryClient?: QueryServiceClient;
|
|
@@ -106,6 +121,7 @@ export const ProfileView = ({
|
|
|
106
121
|
flamegraphData,
|
|
107
122
|
topTableData,
|
|
108
123
|
callgraphData,
|
|
124
|
+
sourceData,
|
|
109
125
|
sampleUnit,
|
|
110
126
|
profileSource,
|
|
111
127
|
queryClient,
|
|
@@ -158,12 +174,16 @@ export const ProfileView = ({
|
|
|
158
174
|
if (dashboardItems.includes('table')) {
|
|
159
175
|
return Boolean(topTableData?.loading);
|
|
160
176
|
}
|
|
177
|
+
if (dashboardItems.includes('source')) {
|
|
178
|
+
return Boolean(sourceData?.loading);
|
|
179
|
+
}
|
|
161
180
|
return false;
|
|
162
181
|
}, [
|
|
163
182
|
dashboardItems,
|
|
164
183
|
callgraphData?.loading,
|
|
165
184
|
flamegraphData?.loading,
|
|
166
185
|
topTableData?.loading,
|
|
186
|
+
sourceData?.loading,
|
|
167
187
|
callgraphSVG,
|
|
168
188
|
]);
|
|
169
189
|
|
|
@@ -286,6 +306,19 @@ export const ProfileView = ({
|
|
|
286
306
|
<></>
|
|
287
307
|
);
|
|
288
308
|
}
|
|
309
|
+
case 'source': {
|
|
310
|
+
return sourceData != null ? (
|
|
311
|
+
<SourceView
|
|
312
|
+
loading={sourceData.loading}
|
|
313
|
+
data={sourceData.data}
|
|
314
|
+
total={total}
|
|
315
|
+
filtered={filtered}
|
|
316
|
+
setActionButtons={setActionButtons}
|
|
317
|
+
/>
|
|
318
|
+
) : (
|
|
319
|
+
<></>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
289
322
|
default: {
|
|
290
323
|
return <></>;
|
|
291
324
|
}
|
|
@@ -314,102 +347,104 @@ export const ProfileView = ({
|
|
|
314
347
|
|
|
315
348
|
return (
|
|
316
349
|
<KeyDownProvider>
|
|
317
|
-
<
|
|
318
|
-
<
|
|
319
|
-
<Card
|
|
320
|
-
<
|
|
321
|
-
<div className="flex
|
|
322
|
-
<div className="flex space-x-1">
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
e
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
350
|
+
<ProfileViewContextProvider value={{profileSource, sampleUnit}}>
|
|
351
|
+
<div className="py-3">
|
|
352
|
+
<Card>
|
|
353
|
+
<Card.Body>
|
|
354
|
+
<div className="flex w-full py-3">
|
|
355
|
+
<div className="flex space-x-4 lg:w-1/2">
|
|
356
|
+
<div className="flex space-x-1">
|
|
357
|
+
{profileSource !== undefined && queryClient !== undefined ? (
|
|
358
|
+
<ProfileShareButton
|
|
359
|
+
queryRequest={profileSource.QueryRequest()}
|
|
360
|
+
queryClient={queryClient}
|
|
361
|
+
/>
|
|
362
|
+
) : null}
|
|
363
|
+
|
|
364
|
+
<Button
|
|
365
|
+
color="neutral"
|
|
366
|
+
onClick={e => {
|
|
367
|
+
e.preventDefault();
|
|
368
|
+
onDownloadPProf();
|
|
369
|
+
}}
|
|
370
|
+
disabled={pprofDownloading}
|
|
371
|
+
>
|
|
372
|
+
{pprofDownloading != null && pprofDownloading
|
|
373
|
+
? 'Downloading'
|
|
374
|
+
: 'Download pprof'}
|
|
375
|
+
</Button>
|
|
376
|
+
</div>
|
|
377
|
+
<FilterByFunctionButton navigateTo={navigateTo} />
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div className="ml-auto flex gap-2">
|
|
381
|
+
<ViewSelector
|
|
382
|
+
defaultValue=""
|
|
383
|
+
navigateTo={navigateTo}
|
|
384
|
+
position={-1}
|
|
385
|
+
placeholderText="Add panel..."
|
|
386
|
+
primary
|
|
387
|
+
addView={true}
|
|
388
|
+
disabled={isMultiPanelView || dashboardItems.length < 1}
|
|
389
|
+
/>
|
|
342
390
|
</div>
|
|
343
|
-
<FilterByFunctionButton navigateTo={navigateTo} />
|
|
344
391
|
</div>
|
|
345
392
|
|
|
346
|
-
<div className="
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
<div className="w-full" ref={ref}>
|
|
394
|
+
{isLoaderVisible ? (
|
|
395
|
+
<>{loader}</>
|
|
396
|
+
) : (
|
|
397
|
+
<DragDropContext onDragEnd={onDragEnd}>
|
|
398
|
+
<Droppable droppableId="droppable" direction="horizontal">
|
|
399
|
+
{provided => (
|
|
400
|
+
<div
|
|
401
|
+
ref={provided.innerRef}
|
|
402
|
+
className="flex w-full justify-between space-x-4"
|
|
403
|
+
{...provided.droppableProps}
|
|
404
|
+
>
|
|
405
|
+
{dashboardItems.map((dashboardItem, index) => {
|
|
406
|
+
return (
|
|
407
|
+
<Draggable
|
|
408
|
+
key={dashboardItem}
|
|
409
|
+
draggableId={dashboardItem}
|
|
410
|
+
index={index}
|
|
411
|
+
isDragDisabled={!isMultiPanelView}
|
|
412
|
+
>
|
|
413
|
+
{(provided, snapshot: {isDragging: boolean}) => (
|
|
414
|
+
<div
|
|
415
|
+
ref={provided.innerRef}
|
|
416
|
+
{...provided.draggableProps}
|
|
417
|
+
key={dashboardItem}
|
|
418
|
+
className={cx(
|
|
419
|
+
'rounded border border-gray-300 p-3 dark:border-gray-500 dark:bg-gray-700',
|
|
420
|
+
isMultiPanelView ? 'w-1/2' : 'w-full',
|
|
421
|
+
snapshot.isDragging ? 'bg-gray-200' : 'bg-white'
|
|
422
|
+
)}
|
|
423
|
+
>
|
|
424
|
+
<VisualizationPanel
|
|
425
|
+
handleClosePanel={handleClosePanel}
|
|
426
|
+
isMultiPanelView={isMultiPanelView}
|
|
427
|
+
dashboardItem={dashboardItem}
|
|
428
|
+
getDashboardItemByType={getDashboardItemByType}
|
|
429
|
+
dragHandleProps={provided.dragHandleProps}
|
|
430
|
+
navigateTo={navigateTo}
|
|
431
|
+
index={index}
|
|
432
|
+
/>
|
|
433
|
+
</div>
|
|
434
|
+
)}
|
|
435
|
+
</Draggable>
|
|
436
|
+
);
|
|
437
|
+
})}
|
|
438
|
+
</div>
|
|
439
|
+
)}
|
|
440
|
+
</Droppable>
|
|
441
|
+
</DragDropContext>
|
|
442
|
+
)}
|
|
356
443
|
</div>
|
|
357
|
-
</
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<>{loader}</>
|
|
362
|
-
) : (
|
|
363
|
-
<DragDropContext onDragEnd={onDragEnd}>
|
|
364
|
-
<Droppable droppableId="droppable" direction="horizontal">
|
|
365
|
-
{provided => (
|
|
366
|
-
<div
|
|
367
|
-
ref={provided.innerRef}
|
|
368
|
-
className="flex w-full justify-between space-x-4"
|
|
369
|
-
{...provided.droppableProps}
|
|
370
|
-
>
|
|
371
|
-
{dashboardItems.map((dashboardItem, index) => {
|
|
372
|
-
return (
|
|
373
|
-
<Draggable
|
|
374
|
-
key={dashboardItem}
|
|
375
|
-
draggableId={dashboardItem}
|
|
376
|
-
index={index}
|
|
377
|
-
isDragDisabled={!isMultiPanelView}
|
|
378
|
-
>
|
|
379
|
-
{(provided, snapshot: {isDragging: boolean}) => (
|
|
380
|
-
<div
|
|
381
|
-
ref={provided.innerRef}
|
|
382
|
-
{...provided.draggableProps}
|
|
383
|
-
key={dashboardItem}
|
|
384
|
-
className={cx(
|
|
385
|
-
'rounded border border-gray-300 p-3 dark:border-gray-500 dark:bg-gray-700',
|
|
386
|
-
isMultiPanelView ? 'w-1/2' : 'w-full',
|
|
387
|
-
snapshot.isDragging ? 'bg-gray-200' : 'bg-white'
|
|
388
|
-
)}
|
|
389
|
-
>
|
|
390
|
-
<VisualizationPanel
|
|
391
|
-
handleClosePanel={handleClosePanel}
|
|
392
|
-
isMultiPanelView={isMultiPanelView}
|
|
393
|
-
dashboardItem={dashboardItem}
|
|
394
|
-
getDashboardItemByType={getDashboardItemByType}
|
|
395
|
-
dragHandleProps={provided.dragHandleProps}
|
|
396
|
-
navigateTo={navigateTo}
|
|
397
|
-
index={index}
|
|
398
|
-
/>
|
|
399
|
-
</div>
|
|
400
|
-
)}
|
|
401
|
-
</Draggable>
|
|
402
|
-
);
|
|
403
|
-
})}
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
406
|
-
</Droppable>
|
|
407
|
-
</DragDropContext>
|
|
408
|
-
)}
|
|
409
|
-
</div>
|
|
410
|
-
</Card.Body>
|
|
411
|
-
</Card>
|
|
412
|
-
</div>
|
|
444
|
+
</Card.Body>
|
|
445
|
+
</Card>
|
|
446
|
+
</div>
|
|
447
|
+
</ProfileViewContextProvider>
|
|
413
448
|
</KeyDownProvider>
|
|
414
449
|
);
|
|
415
450
|
};
|
|
@@ -40,6 +40,10 @@ export const ProfileViewWithData = ({
|
|
|
40
40
|
}: ProfileViewWithDataProps): JSX.Element => {
|
|
41
41
|
const metadata = useGrpcMetadata();
|
|
42
42
|
const [dashboardItems = ['icicle']] = useURLState({param: 'dashboard_items', navigateTo});
|
|
43
|
+
const [sourceBuildID] = useURLState({param: 'source_buildid', navigateTo}) as unknown as [string];
|
|
44
|
+
const [sourceFilename] = useURLState({param: 'source_filename', navigateTo}) as unknown as [
|
|
45
|
+
string
|
|
46
|
+
];
|
|
43
47
|
const [groupBy = [FIELD_FUNCTION_NAME]] = useURLState({param: 'group_by', navigateTo});
|
|
44
48
|
|
|
45
49
|
const [enableTrimming] = useUserPreference<boolean>(USER_PREFERENCES.ENABLE_GRAPH_TRIMMING.key);
|
|
@@ -93,6 +97,16 @@ export const ProfileViewWithData = ({
|
|
|
93
97
|
skip: !dashboardItems.includes('callgraph'),
|
|
94
98
|
});
|
|
95
99
|
|
|
100
|
+
const {
|
|
101
|
+
isLoading: sourceLoading,
|
|
102
|
+
response: sourceResponse,
|
|
103
|
+
error: sourceError,
|
|
104
|
+
} = useQuery(queryClient, profileSource, QueryRequest_ReportType.SOURCE, {
|
|
105
|
+
skip: !dashboardItems.includes('source'),
|
|
106
|
+
sourceBuildID,
|
|
107
|
+
sourceFilename,
|
|
108
|
+
});
|
|
109
|
+
|
|
96
110
|
useEffect(() => {
|
|
97
111
|
if (
|
|
98
112
|
(!flamegraphLoading && flamegraphResponse?.report.oneofKind === 'flamegraph') ||
|
|
@@ -108,6 +122,10 @@ export const ProfileViewWithData = ({
|
|
|
108
122
|
if (!callgraphLoading && callgraphResponse?.report.oneofKind === 'callgraph') {
|
|
109
123
|
perf?.markInteraction('Callgraph render', callgraphResponse.total);
|
|
110
124
|
}
|
|
125
|
+
|
|
126
|
+
if (!sourceLoading && sourceResponse?.report.oneofKind === 'source') {
|
|
127
|
+
perf?.markInteraction('Source render', sourceResponse.total);
|
|
128
|
+
}
|
|
111
129
|
}, [
|
|
112
130
|
flamegraphLoading,
|
|
113
131
|
flamegraphResponse,
|
|
@@ -115,6 +133,8 @@ export const ProfileViewWithData = ({
|
|
|
115
133
|
callgraphLoading,
|
|
116
134
|
topTableLoading,
|
|
117
135
|
topTableResponse,
|
|
136
|
+
sourceLoading,
|
|
137
|
+
sourceResponse,
|
|
118
138
|
perf,
|
|
119
139
|
]);
|
|
120
140
|
|
|
@@ -149,6 +169,9 @@ export const ProfileViewWithData = ({
|
|
|
149
169
|
} else if (callgraphResponse !== null) {
|
|
150
170
|
total = BigInt(callgraphResponse.total);
|
|
151
171
|
filtered = BigInt(callgraphResponse.filtered);
|
|
172
|
+
} else if (sourceResponse !== null) {
|
|
173
|
+
total = BigInt(sourceResponse.total);
|
|
174
|
+
filtered = BigInt(sourceResponse.filtered);
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
return (
|
|
@@ -183,6 +206,14 @@ export const ProfileViewWithData = ({
|
|
|
183
206
|
: undefined,
|
|
184
207
|
error: callgraphError,
|
|
185
208
|
}}
|
|
209
|
+
sourceData={{
|
|
210
|
+
loading: sourceLoading,
|
|
211
|
+
data:
|
|
212
|
+
sourceResponse?.report.oneofKind === 'source'
|
|
213
|
+
? sourceResponse?.report?.source
|
|
214
|
+
: undefined,
|
|
215
|
+
error: sourceError,
|
|
216
|
+
}}
|
|
186
217
|
sampleUnit={sampleUnit}
|
|
187
218
|
profileSource={profileSource}
|
|
188
219
|
queryClient={queryClient}
|
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
|
|
14
|
+
import {useId} from 'react';
|
|
15
|
+
|
|
16
|
+
import {Vector} from 'apache-arrow';
|
|
17
|
+
import cx from 'classnames';
|
|
18
|
+
import {scaleLinear} from 'd3-scale';
|
|
19
|
+
import SyntaxHighlighter, {createElement, type createElementProps} from 'react-syntax-highlighter';
|
|
20
|
+
import {atomOneDark, atomOneLight} from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
|
21
|
+
import {Tooltip} from 'react-tooltip';
|
|
22
|
+
|
|
23
|
+
import {useURLState} from '@parca/components';
|
|
24
|
+
import {selectDarkMode, useAppSelector} from '@parca/store';
|
|
25
|
+
|
|
26
|
+
import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
|
|
27
|
+
import {LineNo} from './LineNo';
|
|
28
|
+
|
|
29
|
+
interface RendererProps {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
rows: any[];
|
|
32
|
+
stylesheet: createElementProps['stylesheet'];
|
|
33
|
+
useInlineStyles: createElementProps['useInlineStyles'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type Renderer = ({rows, stylesheet, useInlineStyles}: RendererProps) => JSX.Element;
|
|
37
|
+
|
|
38
|
+
interface HighlighterProps {
|
|
39
|
+
content: string;
|
|
40
|
+
language?: string;
|
|
41
|
+
renderer?: Renderer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// cannot make this a function on the number as we need the classes to be static for tailwind
|
|
45
|
+
const charsToWidthMap: {[key: number]: string} = {
|
|
46
|
+
1: 'w-3',
|
|
47
|
+
2: 'w-5',
|
|
48
|
+
3: 'w-7',
|
|
49
|
+
4: 'w-9',
|
|
50
|
+
5: 'w-11',
|
|
51
|
+
6: 'w-[52px]',
|
|
52
|
+
7: 'w-[60px]]',
|
|
53
|
+
8: 'w-[68px]',
|
|
54
|
+
9: 'w-[76px]',
|
|
55
|
+
10: 'w-[84px]',
|
|
56
|
+
11: 'w-[92px]',
|
|
57
|
+
12: 'w-[100px]',
|
|
58
|
+
13: 'w-[108px]',
|
|
59
|
+
14: 'w-[116px]',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const intensityScale = scaleLinear().domain([0, 99]).range([0.05, 0.75]);
|
|
63
|
+
|
|
64
|
+
const LineProfileMetadata = ({
|
|
65
|
+
value,
|
|
66
|
+
total,
|
|
67
|
+
filtered,
|
|
68
|
+
}: {
|
|
69
|
+
value: bigint;
|
|
70
|
+
total: bigint;
|
|
71
|
+
filtered: bigint;
|
|
72
|
+
}): JSX.Element => {
|
|
73
|
+
const commonClasses = 'w-[52px] shrink-0';
|
|
74
|
+
const id = useId();
|
|
75
|
+
const {sampleUnit} = useProfileViewContext();
|
|
76
|
+
if (value === 0n) {
|
|
77
|
+
return <div className={cx(commonClasses)} />;
|
|
78
|
+
}
|
|
79
|
+
const unfilteredPercent = (Number(value) / Number(total + filtered)) * 100;
|
|
80
|
+
const filteredPercent = (Number(value) / Number(total)) * 100;
|
|
81
|
+
|
|
82
|
+
const valueString = value.toString();
|
|
83
|
+
const valueWithUnit = `${valueString}${sampleUnit}`;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<p
|
|
88
|
+
className={cx(
|
|
89
|
+
'w- flex justify-end overflow-hidden text-ellipsis whitespace-nowrap',
|
|
90
|
+
commonClasses
|
|
91
|
+
)}
|
|
92
|
+
style={{backgroundColor: `rgba(236, 151, 6, ${intensityScale(unfilteredPercent)})`}}
|
|
93
|
+
data-tooltip-id={id}
|
|
94
|
+
data-tooltip-content={`${valueWithUnit} (${unfilteredPercent.toFixed(2)}%${
|
|
95
|
+
filtered > 0n ? ` / ${filteredPercent.toFixed(2)}%` : ''
|
|
96
|
+
})`}
|
|
97
|
+
>
|
|
98
|
+
{valueString}
|
|
99
|
+
</p>
|
|
100
|
+
<Tooltip id={id} />
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const charsToWidth = (chars: number): string => {
|
|
106
|
+
return charsToWidthMap[chars];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const profileAwareRenderer = (
|
|
110
|
+
cumulative: Vector | null,
|
|
111
|
+
flat: Vector | null,
|
|
112
|
+
total: bigint,
|
|
113
|
+
filtered: bigint
|
|
114
|
+
): Renderer => {
|
|
115
|
+
return function ProfileAwareRenderer({
|
|
116
|
+
rows,
|
|
117
|
+
stylesheet,
|
|
118
|
+
useInlineStyles,
|
|
119
|
+
}: RendererProps): JSX.Element {
|
|
120
|
+
const lineNumberWidth = charsToWidth(rows.length.toString().length);
|
|
121
|
+
const [sourceLine] = useURLState({param: 'source_line', navigateTo: () => {}});
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
{rows.map((node, i) => {
|
|
125
|
+
const lineNumber: number = node.children[0].children[0].value as number;
|
|
126
|
+
const isCurrentLine = sourceLine === lineNumber.toString();
|
|
127
|
+
node.children = node.children.slice(1);
|
|
128
|
+
return (
|
|
129
|
+
<div className="flex gap-1" key={`${i}`}>
|
|
130
|
+
<div
|
|
131
|
+
className={cx(
|
|
132
|
+
'shrink-0 border-r border-gray-200 pr-1 text-right dark:border-gray-700',
|
|
133
|
+
lineNumberWidth
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
<LineNo value={lineNumber} isCurrent={isCurrentLine} />
|
|
137
|
+
</div>
|
|
138
|
+
<LineProfileMetadata
|
|
139
|
+
value={cumulative?.get(i) ?? 0n}
|
|
140
|
+
total={total}
|
|
141
|
+
filtered={filtered}
|
|
142
|
+
/>
|
|
143
|
+
<LineProfileMetadata value={flat?.get(i) ?? 0n} total={total} filtered={filtered} />
|
|
144
|
+
<div
|
|
145
|
+
className={cx(
|
|
146
|
+
'w-11/12 flex-grow-0 border-l border-gray-200 pl-1 dark:border-gray-700',
|
|
147
|
+
{
|
|
148
|
+
'bg-yellow-200 dark:bg-yellow-700': isCurrentLine,
|
|
149
|
+
}
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
152
|
+
{createElement({
|
|
153
|
+
key: `source-line-${i}`,
|
|
154
|
+
node,
|
|
155
|
+
stylesheet,
|
|
156
|
+
useInlineStyles,
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export const Highlighter = ({content, language, renderer}: HighlighterProps): JSX.Element => {
|
|
168
|
+
const isDarkMode = useAppSelector(selectDarkMode);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="relative">
|
|
172
|
+
<div className="flex gap-2 text-xs">
|
|
173
|
+
<div
|
|
174
|
+
className={cx('text-right', charsToWidth(content.split('\n').length.toString().length))}
|
|
175
|
+
>
|
|
176
|
+
Line
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex gap-3">
|
|
179
|
+
<div>Cumulative</div>
|
|
180
|
+
<div>Flat</div>
|
|
181
|
+
<div>Source</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="h-[80vh] overflow-y-auto text-xs">
|
|
185
|
+
<SyntaxHighlighter
|
|
186
|
+
language={language}
|
|
187
|
+
style={isDarkMode ? atomOneDark : atomOneLight}
|
|
188
|
+
showLineNumbers
|
|
189
|
+
renderer={renderer}
|
|
190
|
+
>
|
|
191
|
+
{content}
|
|
192
|
+
</SyntaxHighlighter>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
|
|
14
|
+
import {useEffect, useRef} from 'react';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
value: number;
|
|
18
|
+
isCurrent?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const LineNo = ({value, isCurrent = false}: Props): JSX.Element => {
|
|
22
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (isCurrent) {
|
|
26
|
+
ref.current?.scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
27
|
+
}
|
|
28
|
+
}, [isCurrent]);
|
|
29
|
+
|
|
30
|
+
return <code ref={ref}>{value.toString() + '\n'}</code>;
|
|
31
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
|
|
14
|
+
import React, {useEffect} from 'react';
|
|
15
|
+
|
|
16
|
+
import {tableFromIPC} from 'apache-arrow';
|
|
17
|
+
|
|
18
|
+
import {Source} from '@parca/client';
|
|
19
|
+
import {useParcaContext, useURLState} from '@parca/components';
|
|
20
|
+
|
|
21
|
+
import {ExpandOnHover} from '../GraphTooltipArrow/ExpandOnHoverValue';
|
|
22
|
+
import {truncateStringReverse} from '../utils';
|
|
23
|
+
import {Highlighter, profileAwareRenderer} from './Highlighter';
|
|
24
|
+
|
|
25
|
+
interface SourceViewProps {
|
|
26
|
+
loading: boolean;
|
|
27
|
+
data?: Source;
|
|
28
|
+
total: bigint;
|
|
29
|
+
filtered: bigint;
|
|
30
|
+
setActionButtons?: (buttons: JSX.Element) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const SourceView = React.memo(function SourceView({
|
|
34
|
+
data,
|
|
35
|
+
loading,
|
|
36
|
+
total,
|
|
37
|
+
filtered,
|
|
38
|
+
setActionButtons,
|
|
39
|
+
}: SourceViewProps): JSX.Element {
|
|
40
|
+
const [sourceFileName] = useURLState({param: 'source_filename', navigateTo: () => {}});
|
|
41
|
+
const {loader} = useParcaContext();
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setActionButtons?.(
|
|
45
|
+
<div className="px-2">
|
|
46
|
+
<ExpandOnHover
|
|
47
|
+
value={sourceFileName as string}
|
|
48
|
+
displayValue={truncateStringReverse(sourceFileName as string, 50)}
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}, [sourceFileName, setActionButtons]);
|
|
53
|
+
|
|
54
|
+
if (loading) {
|
|
55
|
+
return <>{loader}</>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (data === undefined) {
|
|
59
|
+
return <>Source code not uploaded for this build.</>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const table = tableFromIPC(data.record);
|
|
63
|
+
const cumulative = table.getChild('cumulative');
|
|
64
|
+
const flat = table.getChild('flat');
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Highlighter
|
|
68
|
+
content={data.source}
|
|
69
|
+
renderer={profileAwareRenderer(cumulative, flat, total, filtered)}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default SourceView;
|