@parca/profile 0.7.10

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.
@@ -0,0 +1,327 @@
1
+ import React from 'react';
2
+ import moment from 'moment';
3
+ import {Query} from '@parca/parser';
4
+ import {
5
+ Label,
6
+ QueryRequest,
7
+ ProfileDiffSelection,
8
+ SingleProfile,
9
+ MergeProfile,
10
+ DiffProfile,
11
+ } from '@parca/client';
12
+ import {Timestamp} from 'google-protobuf/google/protobuf/timestamp_pb';
13
+
14
+ export interface ProfileSource {
15
+ QueryRequest: () => QueryRequest;
16
+ DiffSelection: () => ProfileDiffSelection;
17
+ Describe: () => JSX.Element;
18
+ toString: () => string;
19
+ }
20
+
21
+ export interface ProfileSelection {
22
+ ProfileName: () => string;
23
+ HistoryParams: () => {[key: string]: any};
24
+ ProfileSource: () => ProfileSource;
25
+ Type: () => string;
26
+ }
27
+
28
+ export const timeFormat = 'MMM D, [at] h:mm:s a [(UTC)]';
29
+ export const timeFormatShort = 'MMM D, h:mma';
30
+
31
+ export function ParamsString(params: {[key: string]: string}): string {
32
+ return Object.keys(params)
33
+ .map(function (key) {
34
+ return `${key}=${params[key]}`;
35
+ })
36
+ .join('&');
37
+ }
38
+
39
+ export function SuffixParams(params: {[key: string]: any}, suffix: string): {[key: string]: any} {
40
+ return Object.fromEntries(
41
+ Object.entries(params).map(([key, value]) => [`${key}${suffix}`, value])
42
+ );
43
+ }
44
+
45
+ export function ParseLabels(labels: string[]): Label.AsObject[] {
46
+ return labels.map(function (labelString): Label.AsObject {
47
+ const parts = labelString.split('=', 2);
48
+ return {name: parts[0], value: parts[1]};
49
+ });
50
+ }
51
+
52
+ export function ProfileSelectionFromParams(
53
+ expression: string | undefined,
54
+ from: string | undefined,
55
+ to: string | undefined,
56
+ merge: string | undefined,
57
+ labels: string[] | undefined,
58
+ time: string | undefined
59
+ ): ProfileSelection | null {
60
+ if (
61
+ merge !== undefined &&
62
+ merge === 'true' &&
63
+ from !== undefined &&
64
+ to !== undefined &&
65
+ expression !== undefined
66
+ ) {
67
+ return new MergedProfileSelection(parseInt(from), parseInt(to), expression);
68
+ }
69
+ if (labels !== undefined && time !== undefined) {
70
+ return new SingleProfileSelection(ParseLabels(labels), parseInt(time));
71
+ }
72
+ return null;
73
+ }
74
+
75
+ export class SingleProfileSelection implements ProfileSelection {
76
+ labels: Label.AsObject[];
77
+ time: number;
78
+
79
+ constructor(labels: Label.AsObject[], time: number) {
80
+ this.labels = labels;
81
+ this.time = time;
82
+ }
83
+
84
+ ProfileName(): string {
85
+ const label = this.labels.find(e => e.name === '__name__');
86
+ return label !== undefined ? label.value : '';
87
+ }
88
+
89
+ HistoryParams(): {[key: string]: any} {
90
+ return {
91
+ labels: this.labels.map(label => `${label.name}=${label.value}`),
92
+ time: this.time,
93
+ };
94
+ }
95
+
96
+ Type(): string {
97
+ return 'single';
98
+ }
99
+
100
+ ProfileSource(): ProfileSource {
101
+ return new SingleProfileSource(this.labels, this.time);
102
+ }
103
+ }
104
+
105
+ export class MergedProfileSelection implements ProfileSelection {
106
+ from: number;
107
+ to: number;
108
+ query: string;
109
+
110
+ constructor(from: number, to: number, query: string) {
111
+ this.from = from;
112
+ this.to = to;
113
+ this.query = query;
114
+ }
115
+
116
+ ProfileName(): string {
117
+ return Query.parse(this.query).profileName();
118
+ }
119
+
120
+ HistoryParams(): {[key: string]: string} {
121
+ return {
122
+ mode: 'merge',
123
+ from: this.from.toString(),
124
+ to: this.to.toString(),
125
+ query: this.query,
126
+ };
127
+ }
128
+
129
+ Type(): string {
130
+ return 'merge';
131
+ }
132
+
133
+ ProfileSource(): ProfileSource {
134
+ return new MergedProfileSource(this.from, this.to, this.query);
135
+ }
136
+ }
137
+
138
+ export class SingleProfileSource implements ProfileSource {
139
+ labels: Label.AsObject[];
140
+ time: number;
141
+
142
+ constructor(labels: Label.AsObject[], time: number) {
143
+ this.labels = labels;
144
+ this.time = time;
145
+ }
146
+
147
+ query(): string {
148
+ const seriesQuery = this.labels.reduce(function (agg: string, label: Label.AsObject) {
149
+ return agg + `${label.name}="${label.value}",`;
150
+ }, '{');
151
+ return seriesQuery + '}';
152
+ }
153
+
154
+ DiffSelection(): ProfileDiffSelection {
155
+ const sel = new ProfileDiffSelection();
156
+ sel.setMode(ProfileDiffSelection.Mode.MODE_SINGLE_UNSPECIFIED);
157
+
158
+ const singleProfile = new SingleProfile();
159
+ const ts = new Timestamp();
160
+ ts.fromDate(moment(this.time).toDate());
161
+ singleProfile.setTime(ts);
162
+ singleProfile.setQuery(this.query());
163
+ sel.setSingle(singleProfile);
164
+
165
+ return sel;
166
+ }
167
+
168
+ QueryRequest(): QueryRequest {
169
+ const req = new QueryRequest();
170
+ req.setMode(QueryRequest.Mode.MODE_SINGLE_UNSPECIFIED);
171
+ const singleQueryRequest = new SingleProfile();
172
+ const ts = new Timestamp();
173
+ ts.fromDate(moment(this.time).toDate());
174
+ singleQueryRequest.setTime(ts);
175
+ singleQueryRequest.setQuery(this.query());
176
+ req.setSingle(singleQueryRequest);
177
+ return req;
178
+ }
179
+
180
+ profileName(): string {
181
+ const label = this.labels.find(e => e.name === '__name__');
182
+ return label !== undefined ? label.value : '';
183
+ }
184
+
185
+ Describe(): JSX.Element {
186
+ const profileName = this.profileName();
187
+ return (
188
+ <>
189
+ <p>
190
+ {profileName !== '' ? <a>{profileName} profile of </a> : ''}
191
+ {' '}
192
+ {this.labels
193
+ .filter(label => label.name !== '__name__')
194
+ .map(label => (
195
+ <button
196
+ key={label.name}
197
+ type="button"
198
+ className="inline-block rounded-lg text-gray-700 bg-gray-200 dark:bg-gray-700 dark:text-gray-400 px-2 py-1 text-xs font-bold mr-3"
199
+ >
200
+ {`${label.name}="${label.value}"`}
201
+ </button>
202
+ ))}
203
+ </p>
204
+ <p>{moment(this.time).utc().format(timeFormat)}</p>
205
+ </>
206
+ );
207
+ }
208
+
209
+ stringLabels(): string[] {
210
+ return this.labels
211
+ .filter((label: Label.AsObject) => label.name !== '__name__')
212
+ .map((label: Label.AsObject) => `${label.name}=${label.value}`);
213
+ }
214
+
215
+ toString(): string {
216
+ return `single profile of type ${this.profileName()} with labels ${this.stringLabels().join(
217
+ ', '
218
+ )} collected at ${moment(this.time).utc().format(timeFormat)}`;
219
+ }
220
+ }
221
+
222
+ export class ProfileDiffSource implements ProfileSource {
223
+ a: ProfileSource;
224
+ b: ProfileSource;
225
+
226
+ constructor(a: ProfileSource, b: ProfileSource) {
227
+ this.a = a;
228
+ this.b = b;
229
+ }
230
+
231
+ DiffSelection(): ProfileDiffSelection {
232
+ return new ProfileDiffSelection();
233
+ }
234
+
235
+ QueryRequest(): QueryRequest {
236
+ const req = new QueryRequest();
237
+ req.setMode(QueryRequest.Mode.MODE_DIFF);
238
+ const diffQueryRequest = new DiffProfile();
239
+
240
+ diffQueryRequest.setA(this.a.DiffSelection());
241
+ diffQueryRequest.setB(this.b.DiffSelection());
242
+
243
+ req.setDiff(diffQueryRequest);
244
+ return req;
245
+ }
246
+
247
+ Describe(): JSX.Element {
248
+ return (
249
+ <>
250
+ <p>Browse the comparison</p>
251
+ </>
252
+ );
253
+ }
254
+
255
+ toString(): string {
256
+ return `${this.a.toString()} compared with ${this.b.toString()}`;
257
+ }
258
+ }
259
+
260
+ export class MergedProfileSource implements ProfileSource {
261
+ from: number;
262
+ to: number;
263
+ query: string;
264
+
265
+ constructor(from: number, to: number, query: string) {
266
+ this.from = from;
267
+ this.to = to;
268
+ this.query = query;
269
+ }
270
+
271
+ DiffSelection(): ProfileDiffSelection {
272
+ const sel = new ProfileDiffSelection();
273
+ sel.setMode(ProfileDiffSelection.Mode.MODE_MERGE);
274
+
275
+ const mergeProfile = new MergeProfile();
276
+
277
+ const startTs = new Timestamp();
278
+ startTs.fromDate(moment(this.from).toDate());
279
+ mergeProfile.setStart(startTs);
280
+
281
+ const endTs = new Timestamp();
282
+ endTs.fromDate(moment(this.to).toDate());
283
+ mergeProfile.setEnd(endTs);
284
+
285
+ mergeProfile.setQuery(this.query);
286
+
287
+ sel.setMerge(mergeProfile);
288
+
289
+ return sel;
290
+ }
291
+
292
+ QueryRequest(): QueryRequest {
293
+ const req = new QueryRequest();
294
+ req.setMode(QueryRequest.Mode.MODE_MERGE);
295
+
296
+ const mergeQueryRequest = new MergeProfile();
297
+
298
+ const startTs = new Timestamp();
299
+ startTs.fromDate(moment(this.from).toDate());
300
+ mergeQueryRequest.setStart(startTs);
301
+
302
+ const endTs = new Timestamp();
303
+ endTs.fromDate(moment(this.to).toDate());
304
+ mergeQueryRequest.setEnd(endTs);
305
+
306
+ mergeQueryRequest.setQuery(this.query);
307
+
308
+ req.setMerge(mergeQueryRequest);
309
+
310
+ return req;
311
+ }
312
+
313
+ Describe(): JSX.Element {
314
+ return (
315
+ <a>
316
+ Merge of "{this.query}" from {moment(this.from).utc().format(timeFormat)} to{' '}
317
+ {moment(this.to).utc().format(timeFormat)}
318
+ </a>
319
+ );
320
+ }
321
+
322
+ toString(): string {
323
+ return `merged profiles of query "${this.query}" from ${moment(this.from)
324
+ .utc()
325
+ .format(timeFormat)} to ${moment(this.to).utc().format(timeFormat)}`;
326
+ }
327
+ }
@@ -0,0 +1,164 @@
1
+ import React, {useEffect, useState, useRef} from 'react';
2
+ // import ProfileSVG from './ProfileSVG'
3
+ // import ProfileTop from './ProfileTop'
4
+ import {CalcWidth} from '@parca/dynamicsize';
5
+ import ProfileIcicleGraph from './ProfileIcicleGraph';
6
+ import {ProfileSource} from './ProfileSource';
7
+ import {QueryRequest, QueryResponse, QueryServiceClient, ServiceError} from '@parca/client';
8
+ import Card from '../../../app/web/src/components/ui/Card';
9
+ import Button from '@parca/web/src/components/ui/Button';
10
+ import * as parca_query_v1alpha1_query_pb from '@parca/client/src/parca/query/v1alpha1/query_pb';
11
+
12
+ interface ProfileViewProps {
13
+ queryClient: QueryServiceClient;
14
+ profileSource: ProfileSource;
15
+ }
16
+
17
+ export interface IQueryResult {
18
+ response: QueryResponse | null;
19
+ error: ServiceError | null;
20
+ }
21
+
22
+ function arrayEquals(a, b): boolean {
23
+ return (
24
+ Array.isArray(a) &&
25
+ Array.isArray(b) &&
26
+ a.length === b.length &&
27
+ a.every((val, index) => val === b[index])
28
+ );
29
+ }
30
+
31
+ export const useQuery = (
32
+ client: QueryServiceClient,
33
+ profileSource: ProfileSource
34
+ ): IQueryResult => {
35
+ const [result, setResult] = useState<IQueryResult>({
36
+ response: null,
37
+ error: null,
38
+ });
39
+
40
+ useEffect(() => {
41
+ const req = profileSource.QueryRequest();
42
+ req.setReportType(QueryRequest.ReportType.REPORT_TYPE_FLAMEGRAPH_UNSPECIFIED);
43
+
44
+ client.query(req, (error: ServiceError | null, responseMessage: QueryResponse | null) => {
45
+ setResult({
46
+ response: responseMessage,
47
+ error: error,
48
+ });
49
+ });
50
+ }, [client, profileSource]);
51
+
52
+ return result;
53
+ };
54
+
55
+ export const ProfileView = ({queryClient, profileSource}: ProfileViewProps): JSX.Element => {
56
+ const [curPath, setCurPath] = useState<string[]>([]);
57
+ const {response, error} = useQuery(queryClient, profileSource);
58
+
59
+ if (error != null) {
60
+ return <div className="p-10 flex justify-center">An error occurred: {error.message}</div>;
61
+ }
62
+
63
+ if (response == null) {
64
+ return (
65
+ <div
66
+ style={{
67
+ display: 'flex',
68
+ justifyContent: 'center',
69
+ alignItems: 'center',
70
+ height: 'inherit',
71
+ marginTop: 100,
72
+ }}
73
+ >
74
+ <svg
75
+ className="animate-spin -ml-1 mr-3 h-5 w-5"
76
+ xmlns="http://www.w3.org/2000/svg"
77
+ fill="none"
78
+ viewBox="0 0 24 24"
79
+ >
80
+ <circle
81
+ className="opacity-25"
82
+ cx="12"
83
+ cy="12"
84
+ r="10"
85
+ stroke="currentColor"
86
+ strokeWidth="4"
87
+ ></circle>
88
+ <path
89
+ className="opacity-75"
90
+ fill="currentColor"
91
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
92
+ ></path>
93
+ </svg>
94
+ <span>Loading...</span>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ const downloadPProf = (e: React.MouseEvent<HTMLElement>) => {
100
+ e.preventDefault();
101
+
102
+ const req = profileSource.QueryRequest();
103
+ req.setReportType(QueryRequest.ReportType.REPORT_TYPE_PPROF_UNSPECIFIED);
104
+
105
+ queryClient.query(
106
+ req,
107
+ (
108
+ error: ServiceError | null,
109
+ responseMessage: parca_query_v1alpha1_query_pb.QueryResponse | null
110
+ ) => {
111
+ if (responseMessage !== null) {
112
+ const bytes = responseMessage.getPprof();
113
+ const blob = new Blob([bytes], {type: 'application/octet-stream'});
114
+
115
+ const link = document.createElement('a');
116
+ link.href = window.URL.createObjectURL(blob);
117
+ link.download = 'profile.pb.gz';
118
+ link.click();
119
+ }
120
+ }
121
+ );
122
+ };
123
+
124
+ const resetIcicleGraph = (e: React.MouseEvent<HTMLElement>) => {
125
+ setCurPath([]);
126
+ };
127
+
128
+ const setNewCurPath = (path: string[]) => {
129
+ if (!arrayEquals(curPath, path)) {
130
+ setCurPath(path);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <>
136
+ <div className="py-3">
137
+ <Card>
138
+ <Card.Body>
139
+ <div className="flex space-x-4 py-3">
140
+ <div className="w-1/4">
141
+ <Button color="neutral" onClick={resetIcicleGraph} disabled={curPath.length === 0}>
142
+ Reset View
143
+ </Button>
144
+ </div>
145
+
146
+ <div className="w-full" />
147
+ <div className="w-full" />
148
+ <Button color="neutral" onClick={downloadPProf}>
149
+ Download pprof
150
+ </Button>
151
+ </div>
152
+ <CalcWidth throttle={300} delay={2000}>
153
+ <ProfileIcicleGraph
154
+ curPath={curPath}
155
+ setNewCurPath={setNewCurPath}
156
+ graph={response.getFlamegraph()?.toObject()}
157
+ />
158
+ </CalcWidth>
159
+ </Card.Body>
160
+ </Card>
161
+ </div>
162
+ </>
163
+ );
164
+ };
@@ -0,0 +1,11 @@
1
+ import {SuffixParams, ParseLabels} from '../ProfileSource';
2
+
3
+ test('prefixes keys', () => {
4
+ const input = {key: 'value'};
5
+ expect(SuffixParams(input, '_a')).toMatchObject({key_a: 'value'});
6
+ });
7
+
8
+ test('parses labels', () => {
9
+ const input = ['key=value'];
10
+ expect(ParseLabels(input)).toMatchObject([{name: 'key', value: 'value'}]);
11
+ });
package/src/index.tsx ADDED
@@ -0,0 +1,4 @@
1
+ export * from './IcicleGraph';
2
+ export * from './ProfileIcicleGraph';
3
+ export * from './ProfileSource';
4
+ export * from './ProfileView';