Skip to content

Commit c6b74bd

Browse files
authored
Merge pull request #64 from buggregator/feature/59
Enhance xhprof call graph visualization
2 parents 136a8e4 + bedb61b commit c6b74bd

File tree

6 files changed

+186
-43
lines changed

6 files changed

+186
-43
lines changed

components/ProfilePageFlamegraph/ProfilePageFlamegraph.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,6 @@ export default defineComponent({
9494
}
9595
9696
.profiler-page-flamegraph__canvas {
97-
@apply bg-gray-300 w-full h-full;
97+
@apply bg-gray-300 w-full h-full px-5;
9898
}
9999
</style>

components/ProfilerPageCallGraph/ProfilerPageCallGraph.vue

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{{ name }}
1818
</h4>
1919

20-
<StatBoard :cost="cost" />
20+
<StatBoard :cost="cost"/>
2121
</div>
2222
</template>
2323
</RenderGraph>
@@ -36,6 +36,13 @@
3636
>
3737
CPU
3838
</button>
39+
<button
40+
class="profiler-page-call-graph__toolbar-action"
41+
:class="{ 'font-bold': metric === graphMetrics.MEMORY }"
42+
@click="setMetric(graphMetrics.MEMORY)"
43+
>
44+
Memory usage
45+
</button>
3946
<button
4047
class="profiler-page-call-graph__toolbar-action"
4148
:class="{ 'font-bold': metric === graphMetrics.MEMORY_CHANGE }"
@@ -45,17 +52,17 @@
4552
</button>
4653
<button
4754
class="profiler-page-call-graph__toolbar-action"
48-
:class="{ 'font-bold': metric === graphMetrics.MEMORY }"
49-
@click="setMetric(graphMetrics.MEMORY)"
55+
:class="{ 'font-bold': metric === graphMetrics.CALLS }"
56+
@click="setMetric(graphMetrics.CALLS)"
5057
>
51-
Memory usage
58+
Calls
5259
</button>
5360
</div>
5461

5562
<div
5663
class="profiler-page-call-graph__toolbar profiler-page-call-graph__toolbar--right"
5764
>
58-
<label class="profiler-page-call-graph__toolbar-input-wr">
65+
<label class="profiler-page-call-graph__toolbar-input-wr" v-if="metric !== graphMetrics.CALLS">
5966
Threshold
6067

6168
<input
@@ -68,21 +75,36 @@
6875
@input="setThreshold($event.target.value)"
6976
/>
7077
</label>
78+
79+
80+
<label class="profiler-page-call-graph__toolbar-input-wr">
81+
{{ percentLabel }}
82+
83+
<input
84+
class="profiler-page-call-graph__toolbar-input"
85+
type="number"
86+
:value="min_percent"
87+
:min="metric === graphMetrics.CALLS ? 1 : 0"
88+
:max="metric === graphMetrics.CALLS ? 1000 : 100"
89+
:step="metric === graphMetrics.CALLS ? 10 : 5"
90+
@input="setMinPercent($event.target.value)"
91+
/>
92+
</label>
7193
</div>
7294
</div>
7395
</template>
7496

7597
<script lang="ts">
7698
import IconSvg from "~/components/IconSvg/IconSvg.vue";
7799
78-
import { defineComponent, PropType } from "vue";
79-
import { GraphTypes, Profiler } from "~/config/types";
80-
import { calcGraphData } from "~/utils/calc-graph-data";
100+
import {defineComponent, PropType} from "vue";
101+
import {GraphTypes, Profiler} from "~/config/types";
102+
import {calcGraphData} from "~/utils/calc-graph-data";
81103
import RenderGraph from "~/components/RenderGraph/RenderGraph.vue";
82104
import StatBoard from "~/components/StatBoard/StatBoard.vue";
83105
84106
export default defineComponent({
85-
components: { StatBoard, RenderGraph, IconSvg },
107+
components: {StatBoard, RenderGraph, IconSvg},
86108
props: {
87109
event: {
88110
type: Object as PropType<Profiler>,
@@ -95,15 +117,19 @@ export default defineComponent({
95117
isFullscreen: false,
96118
metric: GraphTypes.CPU as GraphTypes,
97119
threshold: 1,
120+
min_percent: 10,
98121
isReadyGraph: false,
99122
};
100123
},
101124
computed: {
125+
percentLabel() {
126+
return this.metric === GraphTypes.CALLS ? "Min calls" : "Percent";
127+
},
102128
graphElements() {
103-
return calcGraphData(this.event.edges, this.metric, this.threshold);
129+
return calcGraphData(this.event.edges, this.metric, this.threshold, this.min_percent);
104130
},
105131
graphKey() {
106-
return `${this.metric}-${this.threshold}`;
132+
return `${this.metric}-${this.threshold}-${this.min_percent}`;
107133
},
108134
graphMetrics() {
109135
return GraphTypes;
@@ -125,6 +151,9 @@ export default defineComponent({
125151
setThreshold(threshold: number): void {
126152
this.threshold = threshold;
127153
},
154+
setMinPercent(percent: number): void {
155+
this.min_percent = percent;
156+
},
128157
setReadyGraph(): void {
129158
this.isReadyGraph = true;
130159
},

components/RenderGraph/RenderGraph.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const stylesConfig: Stylesheet[] = [
6161
"target-arrow-color": "data(color)",
6262
content: "data(label)",
6363
color: "#fff",
64-
"curve-style": "taxi",
64+
"curve-style": "bezier",
6565
"taxi-direction": "downward",
6666
"edge-distances": "node-position",
6767
"control-point-distance": "5px",

components/StatBoard/StatBoard.vue

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<div v-for="item in statItems" :key="item.title" class="stat-board__item">
44
<h4 class="stat-board__item-name">
55
{{ item.title }}
6+
7+
<span class="stat-board__item-percent" v-if="item.percent > 0">
8+
[{{ item.percent }}%]
9+
</span>
610
</h4>
711

812
<strong class="stat-board__item-value">
@@ -13,9 +17,9 @@
1317
</template>
1418

1519
<script lang="ts">
16-
import { defineComponent, PropType } from "vue";
17-
import { ProfilerCost } from "~/config/types";
18-
import { humanFileSize, formatDuration } from "~/utils/formats";
20+
import {defineComponent, PropType} from "vue";
21+
import {ProfilerCost} from "~/config/types";
22+
import {humanFileSize, formatDuration} from "~/utils/formats";
1923
2024
export default defineComponent({
2125
props: {
@@ -26,26 +30,39 @@ export default defineComponent({
2630
},
2731
computed: {
2832
statItems() {
33+
34+
const undef = '';
35+
36+
let cpu = formatDuration(this.cost.cpu || 0) || undef;
37+
let wt = formatDuration(this.cost.wt || 0) || undef;
38+
let mu = humanFileSize(this.cost.mu || 0) || undef;
39+
let pmu = humanFileSize(this.cost.pmu || 0) || undef;
40+
2941
return [
3042
{
3143
title: "Calls",
3244
value: this.cost.ct || 0,
45+
percent: null,
3346
},
3447
{
3548
title: "CPU time",
36-
value: formatDuration(this.cost.cpu || 0) || "",
49+
value: cpu,
50+
percent: this.cost?.p_cpu,
3751
},
3852
{
3953
title: "Wall time",
40-
value: formatDuration(this.cost.wt || 0) || "",
54+
value: wt,
55+
percent: this.cost?.p_wt,
4156
},
4257
{
4358
title: "Memory usage",
44-
value: humanFileSize(this.cost.mu || 0) || "",
59+
value: mu,
60+
percent: this.cost?.p_mu,
4561
},
4662
{
4763
title: "Change memory",
48-
value: humanFileSize(this.cost.pmu || 0) || "",
64+
value: pmu,
65+
percent: this.cost?.p_pmu,
4966
},
5067
];
5168
},
@@ -81,4 +98,8 @@ export default defineComponent({
8198
.stat-board__item-value {
8299
@apply text-2xs sm:text-xs md:text-base truncate;
83100
}
101+
102+
.stat-board__item-percent {
103+
@apply text-2xs truncate ml-1;
104+
}
84105
</style>

config/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,8 @@ export type TGraphEdge = {
411411
}
412412

413413
export enum GraphTypes {
414-
CPU= 'cpu' ,
414+
CPU = 'cpu',
415415
MEMORY_CHANGE = 'pmu',
416-
MEMORY = 'mu'
416+
MEMORY = 'mu',
417+
CALLS = 'calls'
417418
}

utils/calc-graph-data.ts

Lines changed: 113 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,80 @@
1-
import {humanFileSize, formatDuration} from "~/utils/formats";
2-
import { GraphTypes, ProfilerEdge, ProfilerEdges, TGraphEdge, TGraphNode } from "~/config/types";
1+
import {formatDuration, humanFileSize} from "~/utils/formats";
2+
import {GraphTypes, ProfilerEdge, ProfilerEdges, TGraphEdge, TGraphNode} from "~/config/types";
3+
4+
function getColorForCallCount(callCount): string {
5+
if (callCount <= 1) {
6+
return '#fff'; // Sky Blue for 1 call
7+
} else if (callCount <= 10) {
8+
return '#7BC8F6'; // Lighter Sky Blue
9+
} else if (callCount <= 25) {
10+
return '#4DA6FF'; // Light Blue
11+
} else if (callCount <= 50) {
12+
return '#1A8FFF'; // Brighter Blue
13+
} else if (callCount <= 75) {
14+
return '#007FFF'; // Azure Blue
15+
} else if (callCount <= 100) {
16+
return '#0059B3'; // Royal Blue
17+
} else if (callCount <= 250) {
18+
return '#FFD700'; // Golden
19+
} else if (callCount <= 500) {
20+
return '#FFA500'; // Orange
21+
} else if (callCount <= 750) {
22+
return '#FF8C00'; // Dark Orange
23+
} else if (callCount <= 1000) {
24+
return '#FF4500'; // OrangeRed
25+
} else if (callCount <= 2500) {
26+
return '#FF0000'; // Red
27+
}
28+
29+
return '#8B0000'; // Dark Red for 1000 to 1750 calls
30+
}
31+
32+
function getColorForPercentCount(percent): string {
33+
if (percent <= 10) {
34+
return '#FFFFFF'; // White
35+
} else if (percent <= 20) {
36+
return '#f19797'; // Lighter shade towards dark red
37+
} else if (percent <= 30) {
38+
return '#d93939'; // Light shade towards dark red
39+
} else if (percent <= 40) {
40+
return '#ad1e1e'; // Intermediate lighter shade towards dark red
41+
} else if (percent <= 50) {
42+
return '#982525'; // Intermediate shade towards dark red
43+
} else if (percent <= 60) {
44+
return '#862323'; // Intermediate darker shade towards dark red
45+
} else if (percent <= 70) {
46+
return '#671d1d'; // Darker shade towards dark red
47+
} else if (percent <= 80) {
48+
return '#540d0d'; // More towards dark red
49+
} else if (percent <= 90) {
50+
return '#340707'; // Almost dark red
51+
}
52+
53+
return '#2d0606'; // Dark red
54+
}
55+
56+
function invertHexColor(hex): string {
57+
// If the first character is a hash, remove it for processing
58+
hex = hex.replace('#', '');
59+
60+
// Convert hex to RGB
61+
let r = parseInt(hex.substr(0, 2), 16);
62+
let g = parseInt(hex.substr(2, 2), 16);
63+
let b = parseInt(hex.substr(4, 2), 16);
64+
65+
// Calculate the YIQ ratio
66+
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
67+
68+
// Return black for bright colors, white for dark colors
69+
return (yiq >= 128) ? '#000' : '#fff';
70+
}
371

472
const formatValue = (value: number, metric: string): string | number => {
5-
const metricFormatMap: Record<string, (v: number) => string|number> = {
6-
p_mu: (a: number) => `${a}%`,
7-
p_pmu: (a: number) => `${a}%`,
8-
p_cpu: (a: number) => `${a}%`,
9-
p_wt: (a: number) => `${a}%`,
73+
const metricFormatMap: Record<string, (v: number) => string | number> = {
74+
p_mu: (a: number): string => `${a}%`,
75+
p_pmu: (a: number): string => `${a}%`,
76+
p_cpu: (a: number): string => `${a}%`,
77+
p_wt: (a: number): string => `${a}%`,
1078
mu: humanFileSize,
1179
d_mu: humanFileSize,
1280
pmu: humanFileSize,
@@ -23,42 +91,66 @@ const formatValue = (value: number, metric: string): string | number => {
2391
export const calcGraphData: (
2492
edges: ProfilerEdges,
2593
metric: GraphTypes,
26-
threshold: number
94+
threshold: number,
95+
minPercent: number
2796
) => ({
2897
nodes: TGraphNode[],
2998
edges: TGraphEdge[]
3099
}) =
31-
(edges: ProfilerEdges, metric , threshold = 1) => Object.values(edges)
100+
(edges: ProfilerEdges, metric: GraphTypes, threshold: number = 1, minPercent: number = 10) => Object.values(edges)
32101
.reduce((arr, edge: ProfilerEdge, index) => {
33-
const metricKey = `p_${metric}`;
102+
let nodeColor: string = '#fff';
103+
let nodeTextColor: string = '#000';
104+
let edgeColor: string = '#fff';
105+
let edgeLabel: string = edge.cost.ct > 1 ? `${edge.cost.ct}x` : '';
106+
107+
if (metric === GraphTypes.CALLS) {
108+
const metricKey: string = `ct`;
109+
const isImportantNode: boolean = edge.cost[metricKey] >= minPercent;
110+
if (!isImportantNode) {
111+
return arr
112+
}
34113

35-
const isImportantNode = edge.cost.p_pmu > 10;
114+
nodeColor = getColorForCallCount(edge.cost[metricKey]);
115+
} else {
116+
const metricKey: string = `p_${metric}`;
117+
const isImportantNode: boolean = edge.cost[metricKey] >= minPercent;
118+
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
119+
return arr
120+
}
121+
122+
nodeColor = isImportantNode ? getColorForPercentCount(edge.cost[metricKey]) : '#fff';
123+
nodeTextColor = isImportantNode ? invertHexColor(nodeColor) : '#000';
124+
125+
edgeColor = nodeColor;
36126

37-
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
38-
return arr
127+
const postfix: string = edge.cost.ct > 1 ? ` [ ${edge.cost.ct}x ]` : '';
128+
edgeLabel = `${formatValue(edge.cost[metricKey], metricKey)}${postfix}`;
39129
}
40130

131+
41132
arr.nodes.push({
42133
data: {
43134
id: edge.callee,
44135
name: edge.callee as string,
45136
cost: edge.cost,
46-
color: isImportantNode ? '#e74c3c' : '#fff',
47-
textColor: isImportantNode ? '#fff' : '#000'
137+
color: nodeColor,
138+
textColor: nodeTextColor
48139
}
49140
})
50141

51142
const hasNodeSource = arr.nodes.find(node => node.data.id === edge.caller);
52143

53144
if (index > 0 && hasNodeSource) {
54-
const postfix = edge.cost.ct > 1 ? ` - ${edge.cost.ct }x` : '';
55-
56-
arr.edges.push({ data: {
145+
arr.edges.push({
146+
data: {
57147
source: edge.caller || '',
58148
target: edge.callee,
59-
color: edge.cost.p_pmu > 10 ? '#e74c3c' : '#fff',
60-
label: `${formatValue(edge.cost[metricKey], metricKey)}${postfix}`
61-
}})
149+
color: edgeColor,
150+
label: edgeLabel,
151+
weight: edge.cost.ct,
152+
}
153+
})
62154
}
63155

64156
return arr

0 commit comments

Comments
 (0)