Skip to content

Commit ca855d2

Browse files
authored
feat: graphviz visualization support (#1759)
1 parent f581aee commit ca855d2

File tree

11 files changed

+751
-21
lines changed

11 files changed

+751
-21
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ module.exports = {
2020
transformIgnorePatterns: [
2121
// force us to not transpile these dependencies
2222
// https://stackoverflow.com/a/69150188
23-
'node_modules/(?!(true-myth|d3|d3-array|internmap|d3-scale|react-notifications-component))',
23+
'node_modules/(?!(true-myth|d3|d3-array|internmap|d3-scale|react-notifications-component|graphviz-react))',
2424
],
2525
globals: {
2626
'ts-jest': {

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@
161161
"webpack-plugin-hash-output": "^3.2.1"
162162
},
163163
"dependencies": {
164+
"graphviz-react": "^1.2.5",
164165
"@babel/plugin-transform-runtime": "^7.16.4",
165166
"@babel/preset-env": "^7.10.4",
166167
"@babel/preset-react": "^7.12.10",
@@ -277,6 +278,8 @@
277278
"resolutions": {
278279
"react": "16.14.0",
279280
"react-dom": "16.14.0",
280-
"jquery": "3.6.0"
281+
"jquery": "3.6.0",
282+
"d3-graphviz": "5.0.2",
283+
"d3-selection": "3.0.0"
281284
}
282285
}

packages/pyroscope-flamegraph/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"peerDependencies": {
2828
"react": ">=16.14.0",
2929
"react-dom": ">=16.14.0",
30+
"graphviz-react": "^1.2.5",
3031
"true-myth": "^5.1.2"
3132
},
3233
"devDependencies": {
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export type ViewTypes = 'flamegraph' | 'both' | 'table' | 'sandwich';
1+
export type ViewTypes =
2+
| 'flamegraph'
3+
| 'both'
4+
| 'table'
5+
| 'sandwich'
6+
| 'graphviz';

packages/pyroscope-flamegraph/src/FlameGraph/FlameGraphRenderer.tsx

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
/* eslint-disable react/jsx-props-no-spreading */
55
/* eslint-disable react/destructuring-assignment */
66
/* eslint-disable no-nested-ternary */
7+
/* eslint-disable global-require */
78

89
import React, { Dispatch, SetStateAction, ReactNode, Component } from 'react';
910
import clsx from 'clsx';
1011
import { Maybe } from 'true-myth';
1112
import { createFF, Flamebearer, Profile } from '@pyroscope/models/src';
1213
import NoData from '@pyroscope/webapp/javascript/ui/NoData';
14+
1315
import Graph from './FlameGraphComponent';
1416
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1517
// @ts-ignore: let's move this to typescript some time in the future
@@ -19,13 +21,31 @@ import {
1921
calleesProfile,
2022
callersProfile,
2123
} from '../convert/sandwichViewProfiles';
24+
import toGraphviz from '../convert/toGraphviz';
2225
import { DefaultPalette } from './FlameGraphComponent/colorPalette';
2326
import styles from './FlamegraphRenderer.module.scss';
2427
import PyroscopeLogo from '../logo-v3-small.svg';
2528
import decode from './decode';
2629
import { FitModes } from '../fitMode/fitMode';
2730
import { ViewTypes } from './FlameGraphComponent/viewTypes';
2831

32+
interface IGraphvizProps {
33+
dot: string;
34+
options?: object;
35+
className?: string;
36+
}
37+
38+
// this is to make sure that graphviz-react is not used in node.js
39+
let Graphviz = (obj: IGraphvizProps) => {
40+
if (obj) {
41+
return null;
42+
}
43+
return null;
44+
};
45+
if (typeof process === 'undefined') {
46+
Graphviz = require('graphviz-react').Graphviz;
47+
}
48+
2949
// Still support old flamebearer format
3050
// But prefer the new 'profile' one
3151
function mountFlamebearer(p: { profile?: Profile; flamebearer?: Flamebearer }) {
@@ -511,13 +531,83 @@ class FlameGraphRenderer extends Component<
511531
);
512532
})();
513533

534+
// export type Flamebearer = {
535+
// /**
536+
// * List of names
537+
// */
538+
// names: string[];
539+
// /**
540+
// * List of level
541+
// *
542+
// * This is NOT the same as in the flamebearer
543+
// * that we receive from the server.
544+
// * As in there are some transformations required
545+
// * (see deltaDiffWrapper)
546+
// */
547+
// levels: number[][];
548+
// numTicks: number;
549+
// maxSelf: number;
550+
551+
// /**
552+
// * Sample Rate, used in text information
553+
// */
554+
// sampleRate: number;
555+
// units: Units;
556+
557+
// spyName: SpyName;
558+
// // format: 'double' | 'single';
559+
// // leftTicks?: number;
560+
// // rightTicks?: number;
561+
// } & addTicks;
562+
563+
const graphvizPane = (() => {
564+
// TODO(@petethepig): I don't understand what's going on with types here
565+
// need to fix at some point
566+
const flamebearer = this.state.flamebearer as ShamefulAny;
567+
// flamebearer
568+
const dot =
569+
flamebearer.metadata?.format && flamebearer.flamebearer?.levels
570+
? toGraphviz(flamebearer)
571+
: null;
572+
573+
// Graphviz doesn't update position and scale value on rerender
574+
// so image sometimes moves out of the screen
575+
// to fix it we remounting graphViz component by updating key
576+
const key = `graphviz-pane-${
577+
flamebearer?.appName || String(new Date().valueOf())
578+
}`;
579+
580+
return (
581+
<div className={styles.graphVizPane} key={key}>
582+
{dot ? (
583+
<Graphviz
584+
// options https://github.com/magjac/d3-graphviz#supported-options
585+
options={{
586+
zoom: true,
587+
width: '150%',
588+
height: '100%',
589+
scale: 1,
590+
// 'true' by default, but causes warning
591+
// https://github.com/magjac/d3-graphviz/blob/master/README.md#defining-the-hpcc-jswasm-script-tag
592+
useWorker: false,
593+
}}
594+
dot={dot}
595+
/>
596+
) : (
597+
<div>NO DATA</div>
598+
)}
599+
</div>
600+
);
601+
})();
602+
514603
const dataUnavailable =
515604
!this.state.flamebearer || this.state.flamebearer.names.length <= 1;
516605
const panes = decidePanesOrder(
517606
this.state.view,
518607
flameGraphPane,
519608
tablePane,
520-
sandwichPane
609+
sandwichPane,
610+
graphvizPane
521611
);
522612

523613
return (
@@ -577,7 +667,8 @@ function decidePanesOrder(
577667
view: FlamegraphRendererState['view'],
578668
flamegraphPane: JSX.Element | null,
579669
tablePane: JSX.Element,
580-
sandwichPane: JSX.Element
670+
sandwichPane: JSX.Element,
671+
graphvizPane: JSX.Element
581672
) {
582673
switch (view) {
583674
case 'table': {
@@ -593,6 +684,10 @@ function decidePanesOrder(
593684
case 'both': {
594685
return [tablePane, flamegraphPane];
595686
}
687+
688+
case 'graphviz': {
689+
return [graphvizPane];
690+
}
596691
default: {
597692
throw new Error(`Invalid view '${view}'`);
598693
}

packages/pyroscope-flamegraph/src/FlameGraph/FlamegraphRenderer.module.scss

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,59 @@
9191
text-align: right;
9292
}
9393
}
94+
95+
.graphVizPane {
96+
display: flex;
97+
flex: 1;
98+
99+
// hacky better than !important
100+
&#{&} {
101+
margin-right: 0;
102+
border: 1px solid var(--ps-ui-border);
103+
}
104+
105+
div[id^='graphviz'] {
106+
width: 100%;
107+
overflow: hidden;
108+
109+
:global {
110+
.graph > polygon {
111+
// graphviz overlay
112+
fill: none;
113+
}
114+
115+
.node {
116+
polygon {
117+
// node box
118+
// stroke: var(--ps-fl-toolbar-btn-bg);
119+
}
120+
121+
text[text-anchor='middle'] {
122+
// node caption
123+
// fill: var(--ps-toolbar-icon-color);
124+
}
125+
}
126+
127+
.edge {
128+
text[text-anchor='middle'] {
129+
// edge caption
130+
fill: var(--ps-toolbar-icon-color);
131+
// fill: red;
132+
}
133+
134+
a {
135+
path {
136+
// arrow body
137+
// stroke: var(--ps-fl-toolbar-btn-bg);
138+
}
139+
140+
polygon {
141+
// arrow head
142+
// stroke: var(--ps-fl-toolbar-btn-bg);
143+
// fill: var(--ps-fl-toolbar-btn-bg);
144+
}
145+
}
146+
}
147+
}
148+
}
149+
}

packages/pyroscope-flamegraph/src/Toolbar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010
import classNames from 'classnames/bind';
1111
import { faUndo } from '@fortawesome/free-solid-svg-icons/faUndo';
1212
import { faCompressAlt } from '@fortawesome/free-solid-svg-icons/faCompressAlt';
13+
import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons/faProjectDiagram';
1314
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons/faEllipsisV';
1415
import { Maybe } from 'true-myth';
1516
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -182,6 +183,7 @@ const Toolbar = memo(
182183
),
183184
width: TOOLBAR_SQUARE_WIDTH + DIVIDER_WIDTH,
184185
};
186+
185187
const viewSectionItem = enableChangingDisplay
186188
? {
187189
el: (
@@ -192,7 +194,7 @@ const Toolbar = memo(
192194
/>
193195
),
194196
// sandwich view is hidden in diff view
195-
width: TOOLBAR_SQUARE_WIDTH * (flamegraphType === 'single' ? 4 : 3), // 1px is to display divider
197+
width: TOOLBAR_SQUARE_WIDTH * (flamegraphType === 'single' ? 5 : 3), // 1px is to display divider
196198
}
197199
: null;
198200
const exportDataItem = isValidElement(ExportData)
@@ -403,6 +405,11 @@ const getViewOptions = (
403405
Icon: FlamegraphIcon,
404406
},
405407
{ label: 'Sandwich', value: 'sandwich', Icon: SandwichIcon },
408+
{
409+
label: 'GraphViz',
410+
value: 'graphviz',
411+
Icon: () => <FontAwesomeIcon icon={faProjectDiagram} />,
412+
},
406413
]
407414
: [
408415
{ label: 'Table', value: 'table', Icon: TableIcon },

packages/pyroscope-flamegraph/src/__snapshots__/Toolbar.spec.tsx.snap

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ exports[`ProfileHeader should render toolbar correctly 1`] = `
169169
</div>
170170
<div
171171
class="item"
172-
style="width: 176px;"
172+
style="width: 220px;"
173173
>
174174
<div
175175
class="viewType"
@@ -264,6 +264,29 @@ exports[`ProfileHeader should render toolbar correctly 1`] = `
264264
</g>
265265
</svg>
266266
</button>
267+
<button
268+
aria-label="GraphViz"
269+
class="button default toggleViewButton noIcon"
270+
data-mui-internal-clone-element="true"
271+
data-testid="graphviz"
272+
type="button"
273+
>
274+
<svg
275+
aria-hidden="true"
276+
class="svg-inline--fa fa-project-diagram fa-w-20 "
277+
data-icon="project-diagram"
278+
data-prefix="fas"
279+
focusable="false"
280+
role="img"
281+
viewBox="0 0 640 512"
282+
xmlns="http://www.w3.org/2000/svg"
283+
>
284+
<path
285+
d="M384 320H256c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V352c0-17.67-14.33-32-32-32zM192 32c0-17.67-14.33-32-32-32H32C14.33 0 0 14.33 0 32v128c0 17.67 14.33 32 32 32h95.72l73.16 128.04C211.98 300.98 232.4 288 256 288h.28L192 175.51V128h224V64H192V32zM608 0H480c-17.67 0-32 14.33-32 32v128c0 17.67 14.33 32 32 32h128c17.67 0 32-14.33 32-32V32c0-17.67-14.33-32-32-32z"
286+
fill="currentColor"
287+
/>
288+
</svg>
289+
</button>
267290
</div>
268291
</div>
269292
</div>

0 commit comments

Comments
 (0)