@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/GraphTooltipArrow/Content.d.ts +3 -1
  3. package/dist/GraphTooltipArrow/Content.js +44 -6
  4. package/dist/GraphTooltipArrow/ExpandOnHoverValue.js +1 -1
  5. package/dist/MetricsGraph/useMetricsGraphDimensions.js +9 -0
  6. package/dist/ProfileIcicleGraph/IcicleGraphArrow/index.js +1 -1
  7. package/dist/ProfileView/ProfileViewContext.d.ts +14 -0
  8. package/dist/ProfileView/ProfileViewContext.js +30 -0
  9. package/dist/ProfileView/ViewSelector.js +1 -0
  10. package/dist/ProfileView/index.d.ts +8 -2
  11. package/dist/ProfileView/index.js +19 -10
  12. package/dist/ProfileViewWithData.js +22 -0
  13. package/dist/SourceView/Highlighter.d.ts +16 -0
  14. package/dist/SourceView/Highlighter.js +80 -0
  15. package/dist/SourceView/LineNo.d.ts +6 -0
  16. package/dist/SourceView/LineNo.js +23 -0
  17. package/dist/SourceView/index.d.ts +11 -0
  18. package/dist/SourceView/index.js +37 -0
  19. package/dist/styles.css +1 -1
  20. package/dist/useQuery.d.ts +3 -0
  21. package/dist/useQuery.js +17 -1
  22. package/package.json +10 -8
  23. package/src/GraphTooltipArrow/Content.tsx +88 -9
  24. package/src/GraphTooltipArrow/ExpandOnHoverValue.tsx +1 -1
  25. package/src/MetricsGraph/useMetricsGraphDimensions.ts +9 -0
  26. package/src/ProfileIcicleGraph/IcicleGraphArrow/index.tsx +1 -0
  27. package/src/ProfileView/ProfileViewContext.tsx +52 -0
  28. package/src/ProfileView/ViewSelector.tsx +1 -0
  29. package/src/ProfileView/index.tsx +128 -93
  30. package/src/ProfileViewWithData.tsx +31 -0
  31. package/src/SourceView/Highlighter.tsx +196 -0
  32. package/src/SourceView/LineNo.tsx +31 -0
  33. package/src/SourceView/index.tsx +74 -0
  34. 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 {Callgraph as CallgraphType, Flamegraph, QueryServiceClient, Top} from '@parca/client';
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
- <div className="py-3">
318
- <Card>
319
- <Card.Body>
320
- <div className="flex w-full py-3">
321
- <div className="flex space-x-4 lg:w-1/2">
322
- <div className="flex space-x-1">
323
- {profileSource !== undefined && queryClient !== undefined ? (
324
- <ProfileShareButton
325
- queryRequest={profileSource.QueryRequest()}
326
- queryClient={queryClient}
327
- />
328
- ) : null}
329
-
330
- <Button
331
- color="neutral"
332
- onClick={e => {
333
- e.preventDefault();
334
- onDownloadPProf();
335
- }}
336
- disabled={pprofDownloading}
337
- >
338
- {pprofDownloading != null && pprofDownloading
339
- ? 'Downloading'
340
- : 'Download pprof'}
341
- </Button>
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="ml-auto flex gap-2">
347
- <ViewSelector
348
- defaultValue=""
349
- navigateTo={navigateTo}
350
- position={-1}
351
- placeholderText="Add panel..."
352
- primary
353
- addView={true}
354
- disabled={isMultiPanelView || dashboardItems.length < 1}
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
- </div>
358
-
359
- <div className="w-full" ref={ref}>
360
- {isLoaderVisible ? (
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;