@parca/profile 0.13.11-alpha.1 → 0.14.1
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
CHANGED
|
@@ -3,21 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
## [0.
|
|
6
|
+
## [0.14.1](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.14.1) (2022-07-25)
|
|
7
7
|
|
|
8
8
|
**Note:** Version bump only for package @parca/profile
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## [0.13.11-alpha.0](https://github.com/parca-dev/parca/compare/ui-v0.13.10...ui-v0.13.11-alpha.0) (2022-06-24)
|
|
10
|
+
# [0.14.0](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.14.0) (2022-07-25)
|
|
15
11
|
|
|
16
12
|
**Note:** Version bump only for package @parca/profile
|
|
17
13
|
|
|
14
|
+
## [0.13.15](https://github.com/parca-dev/parca/compare/ui-v0.13.14...ui-v0.13.15) (2022-07-12)
|
|
18
15
|
|
|
16
|
+
**Note:** Version bump only for package @parca/profile
|
|
19
17
|
|
|
18
|
+
## [0.13.13](https://github.com/parca-dev/parca/compare/ui-v0.13.12...ui-v0.13.13) (2022-06-29)
|
|
20
19
|
|
|
20
|
+
**Note:** Version bump only for package @parca/profile
|
|
21
21
|
|
|
22
22
|
## [0.13.10](https://github.com/parca-dev/parca/compare/ui-v0.13.9...ui-v0.13.10) (2022-06-22)
|
|
23
23
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parca/profile",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Profile viewing libraries",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@
|
|
7
|
-
"@parca/
|
|
8
|
-
"@parca/
|
|
9
|
-
"
|
|
6
|
+
"@iconify/react": "^3.2.2",
|
|
7
|
+
"@parca/client": "^0.14.1",
|
|
8
|
+
"@parca/dynamicsize": "^0.14.1",
|
|
9
|
+
"@parca/parser": "^0.14.1",
|
|
10
|
+
"d3-scale": "^4.0.2",
|
|
11
|
+
"react-copy-to-clipboard": "^5.1.0"
|
|
10
12
|
},
|
|
11
13
|
"main": "src/index.tsx",
|
|
12
14
|
"scripts": {
|
|
@@ -19,5 +21,5 @@
|
|
|
19
21
|
"access": "public",
|
|
20
22
|
"registry": "https://registry.npmjs.org/"
|
|
21
23
|
},
|
|
22
|
-
"gitHead": "
|
|
24
|
+
"gitHead": "0521a654350bbd2f758b7e2695bac1001ca11a63"
|
|
23
25
|
}
|
package/src/ProfileView.tsx
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import React, {useEffect, useState} from 'react';
|
|
2
2
|
import {parseParams} from '@parca/functions';
|
|
3
3
|
import {QueryServiceClient, QueryRequest_ReportType} from '@parca/client';
|
|
4
|
-
import {Button, Card, useGrpcMetadata, useParcaTheme} from '@parca/components';
|
|
4
|
+
import {Button, Card, SearchNodes, useGrpcMetadata, useParcaTheme} from '@parca/components';
|
|
5
5
|
|
|
6
|
+
import ProfileShareButton from './components/ProfileShareButton';
|
|
6
7
|
import ProfileIcicleGraph from './ProfileIcicleGraph';
|
|
7
8
|
import {ProfileSource} from './ProfileSource';
|
|
8
9
|
import {useQuery} from './useQuery';
|
|
9
10
|
import TopTable from './TopTable';
|
|
11
|
+
import {downloadPprof} from './utils';
|
|
10
12
|
|
|
11
13
|
import './ProfileView.styles.css';
|
|
12
14
|
|
|
@@ -46,6 +48,11 @@ export const ProfileView = ({
|
|
|
46
48
|
const metadata = useGrpcMetadata();
|
|
47
49
|
const {loader} = useParcaTheme();
|
|
48
50
|
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Reset the current path when the profile source changes
|
|
53
|
+
setCurPath([]);
|
|
54
|
+
}, [profileSource]);
|
|
55
|
+
|
|
49
56
|
useEffect(() => {
|
|
50
57
|
let showLoaderTimeout;
|
|
51
58
|
if (isLoading && !isLoaderVisible) {
|
|
@@ -67,31 +74,18 @@ export const ProfileView = ({
|
|
|
67
74
|
return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
const downloadPProf = (e: React.MouseEvent<HTMLElement>) => {
|
|
77
|
+
const downloadPProf = async (e: React.MouseEvent<HTMLElement>) => {
|
|
71
78
|
e.preventDefault();
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
console.log('Expected pprof report, got:', response.report.oneofKind);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const blob = new Blob([response.report.pprof], {type: 'application/octet-stream'});
|
|
86
|
-
|
|
87
|
-
const link = document.createElement('a');
|
|
88
|
-
link.href = window.URL.createObjectURL(blob);
|
|
89
|
-
link.download = 'profile.pb.gz';
|
|
90
|
-
link.click();
|
|
91
|
-
})
|
|
92
|
-
.catch(error => {
|
|
93
|
-
console.error('Error while querying', error);
|
|
94
|
-
});
|
|
80
|
+
try {
|
|
81
|
+
const blob = await downloadPprof(profileSource.QueryRequest(), queryClient, metadata);
|
|
82
|
+
const link = document.createElement('a');
|
|
83
|
+
link.href = window.URL.createObjectURL(blob);
|
|
84
|
+
link.download = 'profile.pb.gz';
|
|
85
|
+
link.click();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Error while querying', error);
|
|
88
|
+
}
|
|
95
89
|
};
|
|
96
90
|
|
|
97
91
|
const resetIcicleGraph = () => setCurPath([]);
|
|
@@ -119,13 +113,18 @@ export const ProfileView = ({
|
|
|
119
113
|
<Card.Body>
|
|
120
114
|
<div className="flex py-3 w-full">
|
|
121
115
|
<div className="w-2/5 flex space-x-4">
|
|
122
|
-
<div>
|
|
116
|
+
<div className="flex space-x-1">
|
|
117
|
+
<ProfileShareButton
|
|
118
|
+
queryRequest={profileSource.QueryRequest()}
|
|
119
|
+
queryClient={queryClient}
|
|
120
|
+
/>
|
|
121
|
+
|
|
123
122
|
<Button color="neutral" onClick={downloadPProf}>
|
|
124
123
|
Download pprof
|
|
125
124
|
</Button>
|
|
126
125
|
</div>
|
|
127
126
|
|
|
128
|
-
|
|
127
|
+
<SearchNodes />
|
|
129
128
|
</div>
|
|
130
129
|
|
|
131
130
|
<div className="flex ml-auto">
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {useState} from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import {Icon} from '@iconify/react';
|
|
4
|
+
import {Button} from '@parca/components';
|
|
5
|
+
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
value: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
|
|
14
|
+
const ResultBox = ({value, className = ''}: Props) => {
|
|
15
|
+
const [isCopied, setIsCopied] = useState<boolean>(false);
|
|
16
|
+
|
|
17
|
+
const onCopy = () => {
|
|
18
|
+
setIsCopied(true);
|
|
19
|
+
(window.document?.activeElement as HTMLElement)?.blur();
|
|
20
|
+
if (timeoutHandle != null) {
|
|
21
|
+
clearTimeout(timeoutHandle);
|
|
22
|
+
}
|
|
23
|
+
timeoutHandle = setTimeout(() => setIsCopied(false), 3000);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cx('flex flex-row w-full', {[className]: className?.length > 0})}>
|
|
28
|
+
<span className="flex justify-center items-center border border-r-0 w-16 rounded-l">
|
|
29
|
+
<Icon icon="ant-design:link-outlined" />
|
|
30
|
+
</span>
|
|
31
|
+
<input
|
|
32
|
+
type="text"
|
|
33
|
+
className="border text-sm bg-inherit w-full px-1 py-2 flex-grow"
|
|
34
|
+
value={value}
|
|
35
|
+
readOnly
|
|
36
|
+
/>
|
|
37
|
+
<CopyToClipboard text={value} onCopy={onCopy}>
|
|
38
|
+
<Button
|
|
39
|
+
variant="link"
|
|
40
|
+
className="border border-l-0 w-fit whitespace-nowrap p-4 items-center !text-indigo-600 dark:!text-indigo-400 rounded-none rounded-r"
|
|
41
|
+
>
|
|
42
|
+
{isCopied ? 'Copied!' : 'Copy Link'}
|
|
43
|
+
</Button>
|
|
44
|
+
</CopyToClipboard>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default ResultBox;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {useState} from 'react';
|
|
2
|
+
import {Button, Modal} from '@parca/components';
|
|
3
|
+
import {Icon} from '@iconify/react';
|
|
4
|
+
import {QueryRequest, QueryServiceClient} from '@parca/client';
|
|
5
|
+
import ResultBox from './ResultBox';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
queryRequest: QueryRequest;
|
|
9
|
+
queryClient: QueryServiceClient;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ProfileShareModalProps {
|
|
13
|
+
queryRequest: QueryRequest;
|
|
14
|
+
queryClient: QueryServiceClient;
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
closeModal: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ProfileShareModal = ({
|
|
20
|
+
isOpen,
|
|
21
|
+
closeModal,
|
|
22
|
+
queryRequest,
|
|
23
|
+
queryClient,
|
|
24
|
+
}: ProfileShareModalProps) => {
|
|
25
|
+
const [isShared, setIsShared] = useState(false);
|
|
26
|
+
const [loading, setLoading] = useState<boolean>(false);
|
|
27
|
+
const [error, setError] = useState<string>('');
|
|
28
|
+
const [description, setDescription] = useState<string>('');
|
|
29
|
+
const [sharedLink, setSharedLink] = useState<string>('');
|
|
30
|
+
const isFormDataValid = () => true;
|
|
31
|
+
|
|
32
|
+
const handleSubmit: () => void = async () => {
|
|
33
|
+
try {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
const {response} = await queryClient.shareProfile({queryRequest, description});
|
|
36
|
+
setSharedLink(response.link);
|
|
37
|
+
setLoading(false);
|
|
38
|
+
setIsShared(true);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(err);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
setError(err.toString());
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const onClose = () => {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
setError('');
|
|
49
|
+
setDescription('');
|
|
50
|
+
setIsShared(false);
|
|
51
|
+
closeModal();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Modal isOpen={isOpen} closeModal={onClose} title="Share Profile" className="w-[420px]">
|
|
56
|
+
<form className="py-2">
|
|
57
|
+
<p className="text-sm text-gray-500 dark:text-gray-300">
|
|
58
|
+
Note: Shared profiles can be accessed by anyone with the link, even from people outside
|
|
59
|
+
your organisation.
|
|
60
|
+
</p>
|
|
61
|
+
{!isShared || error?.length > 0 ? (
|
|
62
|
+
<>
|
|
63
|
+
<p className="text-sm text-gray-500 dark:text-gray-300 mt-3 mb-2">
|
|
64
|
+
Enter a description (optional)
|
|
65
|
+
</p>
|
|
66
|
+
<textarea
|
|
67
|
+
className="border w-full text-gray-500 dark:text-gray-300 bg-inherit text-sm px-2 py-2"
|
|
68
|
+
value={description}
|
|
69
|
+
onChange={e => setDescription(e.target.value)}
|
|
70
|
+
></textarea>
|
|
71
|
+
<Button
|
|
72
|
+
className="w-fit mt-4"
|
|
73
|
+
onClick={e => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
handleSubmit();
|
|
76
|
+
}}
|
|
77
|
+
disabled={loading || !isFormDataValid()}
|
|
78
|
+
type="submit"
|
|
79
|
+
>
|
|
80
|
+
{loading ? 'Sharing' : 'Share'}
|
|
81
|
+
</Button>
|
|
82
|
+
{error !== '' ? <p>Something went wrong please try again</p> : null}
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<ResultBox value={sharedLink} className="mt-4" />
|
|
87
|
+
<div className="flex justify-center mt-8">
|
|
88
|
+
<Button variant="neutral" className="w-fit" onClick={onClose}>
|
|
89
|
+
Close
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
</>
|
|
93
|
+
)}
|
|
94
|
+
</form>
|
|
95
|
+
</Modal>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const ProfileShareButton = ({queryRequest, queryClient}: Props) => {
|
|
100
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<>
|
|
104
|
+
<Button color="neutral" className="w-fit" onClick={() => setIsOpen(true)}>
|
|
105
|
+
<Icon icon="ei:share-apple" width={20} />
|
|
106
|
+
</Button>
|
|
107
|
+
<ProfileShareModal
|
|
108
|
+
isOpen={isOpen}
|
|
109
|
+
closeModal={() => setIsOpen(false)}
|
|
110
|
+
queryRequest={queryRequest}
|
|
111
|
+
queryClient={queryClient}
|
|
112
|
+
/>
|
|
113
|
+
</>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default ProfileShareButton;
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
import {QueryRequest, QueryRequest_ReportType, QueryServiceClient} from '@parca/client';
|
|
2
|
+
import {RpcMetadata} from '@protobuf-ts/runtime-rpc';
|
|
3
|
+
|
|
1
4
|
export const hexifyAddress = (address?: string): string => {
|
|
2
5
|
if (address == null) {
|
|
3
6
|
return '';
|
|
4
7
|
}
|
|
5
8
|
return `0x${parseInt(address, 10).toString(16)}`;
|
|
6
9
|
};
|
|
10
|
+
|
|
11
|
+
export const downloadPprof = async (
|
|
12
|
+
request: QueryRequest,
|
|
13
|
+
queryClient: QueryServiceClient,
|
|
14
|
+
metadata: RpcMetadata
|
|
15
|
+
) => {
|
|
16
|
+
const req = {
|
|
17
|
+
...request,
|
|
18
|
+
reportType: QueryRequest_ReportType.PPROF,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const {response} = await queryClient.query(req, {meta: metadata});
|
|
22
|
+
if (response.report.oneofKind !== 'pprof') {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Expected pprof report, got: ${
|
|
25
|
+
response.report.oneofKind !== undefined ? response.report.oneofKind : 'undefined'
|
|
26
|
+
}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const blob = new Blob([response.report.pprof], {type: 'application/octet-stream'});
|
|
30
|
+
return blob;
|
|
31
|
+
};
|