@parca/profile 0.16.251 → 0.16.252

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.
@@ -18,27 +18,14 @@ import cx from 'classnames';
18
18
  import {CopyToClipboard} from 'react-copy-to-clipboard';
19
19
  import {Tooltip} from 'react-tooltip';
20
20
 
21
- import {QueryRequest_ReportType} from '@parca/client';
22
- import {Button, useParcaContext, useURLState} from '@parca/components';
23
- import {divide, getLastItem, valueFormatter, type NavigateFunction} from '@parca/utilities';
21
+ import {Button, IconButton, useParcaContext} from '@parca/components';
22
+ import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
23
+ import {getLastItem, type NavigateFunction} from '@parca/utilities';
24
24
 
25
- import {
26
- FIELD_CUMULATIVE,
27
- FIELD_DIFF,
28
- FIELD_FUNCTION_FILE_NAME,
29
- FIELD_FUNCTION_START_LINE,
30
- FIELD_LABELS,
31
- FIELD_LOCATION_ADDRESS,
32
- FIELD_LOCATION_LINE,
33
- FIELD_MAPPING_BUILD_ID,
34
- FIELD_MAPPING_FILE,
35
- } from '../ProfileIcicleGraph/IcicleGraphArrow';
36
- import {arrowToString, nodeLabel} from '../ProfileIcicleGraph/IcicleGraphArrow/utils';
37
- import {ProfileSource} from '../ProfileSource';
38
- import {useProfileViewContext} from '../ProfileView/ProfileViewContext';
39
- import {useQuery} from '../useQuery';
40
25
  import {hexifyAddress, truncateString, truncateStringReverse} from '../utils';
41
26
  import {ExpandOnHover} from './ExpandOnHoverValue';
27
+ import {useGraphTooltip} from './useGraphTooltip';
28
+ import {useGraphTooltipMetaInfo} from './useGraphTooltipMetaInfo';
42
29
 
43
30
  let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
44
31
 
@@ -69,14 +56,20 @@ const GraphTooltipArrowContent = ({
69
56
  }: GraphTooltipArrowContentProps): React.JSX.Element => {
70
57
  const [isCopied, setIsCopied] = useState<boolean>(false);
71
58
 
72
- if (row === null) {
59
+ const graphTooltipData = useGraphTooltip({
60
+ table,
61
+ unit,
62
+ total,
63
+ totalUnfiltered,
64
+ row,
65
+ level,
66
+ });
67
+ const [_, setIsDocked] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
68
+
69
+ if (graphTooltipData === null) {
73
70
  return <></>;
74
71
  }
75
72
 
76
- const locationAddress: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0n;
77
- const cumulative: bigint = BigInt(table.getChild(FIELD_CUMULATIVE)?.get(row)) ?? 0n;
78
- const diff: bigint = BigInt(table.getChild(FIELD_DIFF)?.get(row)) ?? 0n;
79
-
80
73
  const onCopy = (): void => {
81
74
  setIsCopied(true);
82
75
 
@@ -86,23 +79,7 @@ const GraphTooltipArrowContent = ({
86
79
  timeoutHandle = setTimeout(() => setIsCopied(false), 3000);
87
80
  };
88
81
 
89
- const prevValue = cumulative - diff;
90
- const diffRatio = diff !== 0n ? divide(diff, prevValue) : 0;
91
- const diffSign = diff > 0 ? '+' : '';
92
- const diffValueText = diffSign + valueFormatter(diff, unit, 1);
93
- const diffPercentageText = diffSign + (diffRatio * 100).toFixed(2) + '%';
94
- const diffText = `${diffValueText} (${diffPercentageText})`;
95
-
96
- const name = nodeLabel(table, row, level, false);
97
-
98
- const getTextForCumulative = (hoveringNodeCumulative: bigint): string => {
99
- const filtered =
100
- totalUnfiltered > total
101
- ? ` / ${(100 * divide(hoveringNodeCumulative, total)).toFixed(2)}% of filtered`
102
- : '';
103
- return `${valueFormatter(hoveringNodeCumulative, unit, 2)}
104
- (${(100 * divide(hoveringNodeCumulative, totalUnfiltered)).toFixed(2)}%${filtered})`;
105
- };
82
+ const {name, locationAddress, cumulativeText, diffText, diff, row: rowNumber} = graphTooltipData;
106
83
 
107
84
  return (
108
85
  <div className={`flex text-sm ${isFixed ? 'w-full' : ''}`}>
@@ -110,7 +87,7 @@ const GraphTooltipArrowContent = ({
110
87
  <div className="min-h-52 flex w-[500px] flex-col justify-between rounded-lg border border-gray-300 bg-gray-50 p-3 shadow-lg dark:border-gray-500 dark:bg-gray-900">
111
88
  <div className="flex flex-row">
112
89
  <div className="mx-2">
113
- <div className="flex h-10 items-center break-all font-semibold">
90
+ <div className="flex h-10 items-start justify-between gap-4 break-all font-semibold">
114
91
  {row === 0 ? (
115
92
  <p>root</p>
116
93
  ) : (
@@ -134,6 +111,11 @@ const GraphTooltipArrowContent = ({
134
111
  )}
135
112
  </>
136
113
  )}
114
+ <IconButton
115
+ onClick={() => setIsDocked(true)}
116
+ icon="mdi:dock-bottom"
117
+ title="Dock MetaInfo Panel"
118
+ />
137
119
  </div>
138
120
  <table className="my-2 w-full table-fixed pr-0 text-gray-700 dark:text-gray-300">
139
121
  <tbody>
@@ -141,10 +123,8 @@ const GraphTooltipArrowContent = ({
141
123
  <td className="w-1/4">Cumulative</td>
142
124
 
143
125
  <td className="w-3/4">
144
- <CopyToClipboard onCopy={onCopy} text={getTextForCumulative(cumulative)}>
145
- <button className="cursor-pointer">
146
- {getTextForCumulative(cumulative)}
147
- </button>
126
+ <CopyToClipboard onCopy={onCopy} text={cumulativeText}>
127
+ <button className="cursor-pointer">{cumulativeText}</button>
148
128
  </CopyToClipboard>
149
129
  </td>
150
130
  </tr>
@@ -160,7 +140,7 @@ const GraphTooltipArrowContent = ({
160
140
  )}
161
141
  <TooltipMetaInfo
162
142
  table={table}
163
- row={row}
143
+ row={rowNumber}
164
144
  onCopy={onCopy}
165
145
  navigateTo={navigateTo}
166
146
  />
@@ -190,57 +170,18 @@ const TooltipMetaInfo = ({
190
170
  onCopy: () => void;
191
171
  navigateTo: NavigateFunction;
192
172
  }): React.JSX.Element => {
193
- const mappingFile: string = arrowToString(table.getChild(FIELD_MAPPING_FILE)?.get(row)) ?? '';
194
- const mappingBuildID: string =
195
- arrowToString(table.getChild(FIELD_MAPPING_BUILD_ID)?.get(row)) ?? '';
196
- const locationAddress: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0n;
197
- const locationLine: bigint = table.getChild(FIELD_LOCATION_LINE)?.get(row) ?? 0n;
198
- const functionFilename: string =
199
- arrowToString(table.getChild(FIELD_FUNCTION_FILE_NAME)?.get(row)) ?? '';
200
- const functionStartLine: bigint = table.getChild(FIELD_FUNCTION_START_LINE)?.get(row) ?? 0n;
201
- const lineNumber =
202
- locationLine !== 0n ? locationLine : functionStartLine !== 0n ? functionStartLine : undefined;
203
- const pprofLabelPrefix = 'pprof_labels.';
204
- const labelColumnNames = table.schema.fields.filter(field =>
205
- field.name.startsWith(pprofLabelPrefix)
206
- );
207
-
208
- const {queryServiceClient, enableSourcesView} = useParcaContext();
209
- const {profileSource} = useProfileViewContext();
210
-
211
- const {isLoading: sourceLoading, response: sourceResponse} = useQuery(
212
- queryServiceClient,
213
- profileSource as ProfileSource,
214
- QueryRequest_ReportType.SOURCE,
215
- {
216
- skip:
217
- enableSourcesView === false ||
218
- profileSource === undefined ||
219
- // eslint-disable-next-line no-extra-boolean-cast
220
- !Boolean(mappingBuildID) ||
221
- // eslint-disable-next-line no-extra-boolean-cast
222
- !Boolean(functionFilename),
223
- sourceBuildID: mappingBuildID,
224
- sourceFilename: functionFilename,
225
- sourceOnly: true,
226
- }
227
- );
228
-
229
- const isSourceAvailable = !sourceLoading && sourceResponse?.report != null;
230
-
231
- const getTextForFile = (): string => {
232
- if (functionFilename === '') return '<unknown>';
173
+ const {
174
+ labelPairs,
175
+ functionFilename,
176
+ file,
177
+ openFile,
178
+ isSourceAvailable,
179
+ locationAddress,
180
+ mappingFile,
181
+ mappingBuildID,
182
+ } = useGraphTooltipMetaInfo({table, row, navigateTo});
183
+ const {enableSourcesView} = useParcaContext();
233
184
 
234
- return `${functionFilename} ${lineNumber !== undefined ? ` +${lineNumber.toString()}` : ''}`;
235
- };
236
- const file = getTextForFile();
237
-
238
- const labelPairs = labelColumnNames
239
- .map((field, i) => [
240
- labelColumnNames[i].name.slice(pprofLabelPrefix.length),
241
- arrowToString(table.getChild(field.name)?.get(row)) ?? '',
242
- ])
243
- .filter(value => value[1] !== '');
244
185
  const labels = labelPairs.map(
245
186
  (l): React.JSX.Element => (
246
187
  <span
@@ -252,38 +193,6 @@ const TooltipMetaInfo = ({
252
193
  )
253
194
  );
254
195
 
255
- const [dashboardItems, setDashboardItems] = useURLState({
256
- param: 'dashboard_items',
257
- navigateTo,
258
- });
259
-
260
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
261
- const [unusedBuildId, setSourceBuildId] = useURLState({
262
- param: 'source_buildid',
263
- navigateTo,
264
- });
265
-
266
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
267
- const [unusedFilename, setSourceFilename] = useURLState({
268
- param: 'source_filename',
269
- navigateTo,
270
- });
271
-
272
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
273
- const [unusedLine, setSourceLine] = useURLState({
274
- param: 'source_line',
275
- navigateTo,
276
- });
277
-
278
- const openFile = (): void => {
279
- setDashboardItems([dashboardItems[0], 'source']);
280
- setSourceBuildId(mappingBuildID);
281
- setSourceFilename(functionFilename);
282
- if (lineNumber !== undefined) {
283
- setSourceLine(lineNumber.toString());
284
- }
285
- };
286
-
287
196
  return (
288
197
  <>
289
198
  <tr>
@@ -333,7 +242,7 @@ const TooltipMetaInfo = ({
333
242
  <tr>
334
243
  <td className="w-1/4">Binary</td>
335
244
  <td className="w-3/4 break-all">
336
- {mappingFile === '' ? (
245
+ {mappingFile === null ? (
337
246
  <NoData />
338
247
  ) : (
339
248
  <CopyToClipboard onCopy={onCopy} text={mappingFile}>
@@ -345,7 +254,7 @@ const TooltipMetaInfo = ({
345
254
  <tr>
346
255
  <td className="w-1/4">Build Id</td>
347
256
  <td className="w-3/4 break-all">
348
- {mappingBuildID === '' ? (
257
+ {mappingBuildID === null ? (
349
258
  <NoData />
350
259
  ) : (
351
260
  <CopyToClipboard onCopy={onCopy} text={mappingBuildID}>
@@ -0,0 +1,249 @@
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 {useState} from 'react';
15
+
16
+ import {Table} from 'apache-arrow';
17
+ import cx from 'classnames';
18
+ import {CopyToClipboard} from 'react-copy-to-clipboard';
19
+ import {Tooltip} from 'react-tooltip';
20
+ import {useWindowSize} from 'react-use';
21
+
22
+ import {Button, IconButton, useParcaContext} from '@parca/components';
23
+ import {USER_PREFERENCES, useUserPreference} from '@parca/hooks';
24
+ import {getLastItem} from '@parca/utilities';
25
+
26
+ import {hexifyAddress, truncateString, truncateStringReverse} from '../../utils';
27
+ import {ExpandOnHover} from '../ExpandOnHoverValue';
28
+ import {useGraphTooltip} from '../useGraphTooltip';
29
+ import {useGraphTooltipMetaInfo} from '../useGraphTooltipMetaInfo';
30
+
31
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
32
+
33
+ interface Props {
34
+ table: Table<any>;
35
+ unit: string;
36
+ total: bigint;
37
+ totalUnfiltered: bigint;
38
+ row: number | null;
39
+ level: number;
40
+ }
41
+
42
+ const InfoSection = ({
43
+ title,
44
+ value,
45
+ onCopy,
46
+ copyText,
47
+ minWidth = '',
48
+ }: {
49
+ title: string;
50
+ value: string | JSX.Element;
51
+ copyText: string;
52
+ onCopy: () => void;
53
+ minWidth?: string;
54
+ }): JSX.Element => {
55
+ return (
56
+ <div className={cx('flex shrink-0 flex-col gap-1 p-2', {[minWidth]: minWidth != null})}>
57
+ <p className="text-sm font-medium leading-5 text-gray-500 dark:text-gray-400">{title}</p>
58
+ <div className="text-lg font-normal text-gray-900 dark:text-gray-50">
59
+ <CopyToClipboard onCopy={onCopy} text={copyText}>
60
+ <button>{value}</button>
61
+ </CopyToClipboard>
62
+ </div>
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export const DockedGraphTooltip = ({
68
+ table,
69
+ unit,
70
+ total,
71
+ totalUnfiltered,
72
+ row,
73
+ level,
74
+ }: Props): JSX.Element => {
75
+ let {width} = useWindowSize();
76
+ const {profileExplorer, navigateTo, enableSourcesView} = useParcaContext();
77
+ const {PaddingX} = profileExplorer ?? {PaddingX: 0};
78
+ width = width - PaddingX - 24;
79
+ const [isCopied, setIsCopied] = useState<boolean>(false);
80
+
81
+ const onCopy = (): void => {
82
+ setIsCopied(true);
83
+
84
+ if (timeoutHandle !== null) {
85
+ clearTimeout(timeoutHandle);
86
+ }
87
+ timeoutHandle = setTimeout(() => setIsCopied(false), 3000);
88
+ };
89
+
90
+ const graphTooltipData = useGraphTooltip({
91
+ table,
92
+ unit,
93
+ total,
94
+ totalUnfiltered,
95
+ row,
96
+ level,
97
+ });
98
+
99
+ const {
100
+ labelPairs,
101
+ functionFilename,
102
+ file,
103
+ openFile,
104
+ isSourceAvailable,
105
+ locationAddress,
106
+ mappingFile,
107
+ mappingBuildID,
108
+ } = useGraphTooltipMetaInfo({table, row: row ?? 0, navigateTo});
109
+
110
+ const [_, setIsDocked] = useUserPreference(USER_PREFERENCES.GRAPH_METAINFO_DOCKED.key);
111
+
112
+ if (graphTooltipData === null) {
113
+ return <></>;
114
+ }
115
+
116
+ const {name, cumulativeText, diffText, diff} = graphTooltipData;
117
+
118
+ const labels = labelPairs.map(
119
+ (l): React.JSX.Element => (
120
+ <span
121
+ key={l[0]}
122
+ className="mr-3 inline-block rounded-lg bg-gray-200 px-2 py-1 text-xs font-bold text-gray-700 dark:bg-gray-700 dark:text-gray-400"
123
+ >
124
+ {`${l[0]}="${l[1]}"`}
125
+ </span>
126
+ )
127
+ );
128
+
129
+ const addressText = locationAddress !== 0n ? hexifyAddress(locationAddress) : 'unknown';
130
+ const fileText = functionFilename !== '' ? file : 'Not available';
131
+
132
+ return (
133
+ <div
134
+ className="fixed bottom-0 z-20 overflow-hidden rounded-t-lg border-l border-r border-t border-gray-400 bg-white bg-opacity-90 px-8 py-3 dark:border-gray-600 dark:bg-black dark:bg-opacity-80"
135
+ style={{width}}
136
+ >
137
+ <div className="flex flex-col gap-4">
138
+ <div className="flex justify-between gap-4">
139
+ {row === 0 ? (
140
+ <p>root</p>
141
+ ) : (
142
+ <>
143
+ {name !== '' ? (
144
+ <CopyToClipboard onCopy={onCopy} text={name}>
145
+ <button className="cursor-pointer text-left">{name}</button>
146
+ </CopyToClipboard>
147
+ ) : (
148
+ <>
149
+ {locationAddress !== 0n ? (
150
+ <CopyToClipboard onCopy={onCopy} text={hexifyAddress(locationAddress)}>
151
+ <button className="cursor-pointer text-left">
152
+ {hexifyAddress(locationAddress)}
153
+ </button>
154
+ </CopyToClipboard>
155
+ ) : (
156
+ <p>unknown</p>
157
+ )}
158
+ </>
159
+ )}
160
+ </>
161
+ )}
162
+ <IconButton
163
+ onClick={() => setIsDocked(false)}
164
+ icon="mdi:dock-window"
165
+ title="Undock MetaInfo Panel"
166
+ />
167
+ </div>
168
+ <div className="flex justify-between gap-3">
169
+ <InfoSection
170
+ title="Cumulative"
171
+ value={cumulativeText}
172
+ onCopy={onCopy}
173
+ copyText={cumulativeText}
174
+ minWidth="w-44"
175
+ />
176
+ {diff !== 0n ? (
177
+ <InfoSection
178
+ title="Diff"
179
+ value={diffText}
180
+ onCopy={onCopy}
181
+ copyText={diffText}
182
+ minWidth="w-44"
183
+ />
184
+ ) : null}
185
+ <InfoSection
186
+ title="File"
187
+ value={
188
+ <div className="flex gap-2">
189
+ <ExpandOnHover
190
+ value={fileText}
191
+ displayValue={truncateStringReverse(fileText, 45)}
192
+ />
193
+ <div
194
+ className={cx('flex items-center gap-2', {
195
+ hidden: enableSourcesView === false || functionFilename === '',
196
+ })}
197
+ >
198
+ <div
199
+ data-tooltip-id="open-source-button-help"
200
+ data-tooltip-content="There is no source code uploaded for this build"
201
+ >
202
+ <Button
203
+ variant={'neutral'}
204
+ onClick={() => openFile()}
205
+ className="shrink-0"
206
+ disabled={!isSourceAvailable}
207
+ >
208
+ open
209
+ </Button>
210
+ </div>
211
+ {!isSourceAvailable ? <Tooltip id="open-source-button-help" /> : null}
212
+ </div>
213
+ </div>
214
+ }
215
+ onCopy={onCopy}
216
+ copyText={file}
217
+ minWidth={'w-[460px]'}
218
+ />
219
+ <InfoSection
220
+ title="Address"
221
+ value={addressText}
222
+ onCopy={onCopy}
223
+ copyText={addressText}
224
+ minWidth="w-44"
225
+ />
226
+ <InfoSection
227
+ title="Binary"
228
+ value={(mappingFile != null ? getLastItem(mappingFile) : null) ?? 'Not available'}
229
+ onCopy={onCopy}
230
+ copyText={mappingFile ?? 'Not available'}
231
+ minWidth="w-44"
232
+ />
233
+ <InfoSection
234
+ title="Build ID"
235
+ value={truncateString(getLastItem(mappingBuildID) ?? 'Not available', 28)}
236
+ onCopy={onCopy}
237
+ copyText={mappingBuildID ?? 'Not available'}
238
+ />
239
+ </div>
240
+ <div>
241
+ <div className="flex h-5 gap-1">{labels}</div>
242
+ </div>
243
+ <span className="mx-2 block text-xs text-gray-500">
244
+ {isCopied ? 'Copied!' : 'Hold shift and click on a value to copy.'}
245
+ </span>
246
+ </div>
247
+ </div>
248
+ );
249
+ };
@@ -0,0 +1,76 @@
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 {Table} from 'apache-arrow';
15
+
16
+ import {divide, valueFormatter} from '@parca/utilities';
17
+
18
+ import {
19
+ FIELD_CUMULATIVE,
20
+ FIELD_DIFF,
21
+ FIELD_LOCATION_ADDRESS,
22
+ } from '../../ProfileIcicleGraph/IcicleGraphArrow';
23
+ import {getTextForCumulative, nodeLabel} from '../../ProfileIcicleGraph/IcicleGraphArrow/utils';
24
+
25
+ interface Props {
26
+ table: Table<any>;
27
+ unit: string;
28
+ total: bigint;
29
+ totalUnfiltered: bigint;
30
+ row: number | null;
31
+ level: number;
32
+ }
33
+
34
+ interface GraphTooltipData {
35
+ name: string;
36
+ locationAddress: bigint;
37
+ cumulativeText: string;
38
+ diffText: string;
39
+ diff: bigint;
40
+ row: number;
41
+ }
42
+
43
+ export const useGraphTooltip = ({
44
+ table,
45
+ unit,
46
+ total,
47
+ totalUnfiltered,
48
+ row,
49
+ level,
50
+ }: Props): GraphTooltipData | null => {
51
+ if (row === null) {
52
+ return null;
53
+ }
54
+
55
+ const locationAddress: bigint = table.getChild(FIELD_LOCATION_ADDRESS)?.get(row) ?? 0n;
56
+ const cumulative: bigint = BigInt(table.getChild(FIELD_CUMULATIVE)?.get(row)) ?? 0n;
57
+ const diff: bigint = BigInt(table.getChild(FIELD_DIFF)?.get(row)) ?? 0n;
58
+
59
+ const prevValue = cumulative - diff;
60
+ const diffRatio = diff !== 0n ? divide(diff, prevValue) : 0;
61
+ const diffSign = diff > 0 ? '+' : '';
62
+ const diffValueText = diffSign + valueFormatter(diff, unit, 1);
63
+ const diffPercentageText = diffSign + (diffRatio * 100).toFixed(2) + '%';
64
+ const diffText = `${diffValueText} (${diffPercentageText})`;
65
+
66
+ const name = nodeLabel(table, row, level, false);
67
+
68
+ return {
69
+ name,
70
+ locationAddress,
71
+ cumulativeText: getTextForCumulative(cumulative, totalUnfiltered, total, unit),
72
+ diffText,
73
+ diff,
74
+ row,
75
+ };
76
+ };