Skip to content

Commit

Permalink
Enhance xhprof call graph visualization
Browse files Browse the repository at this point in the history
- Implement color gradient for nodes to represent call frequency, ranging from white (1 call) to dark blue (>1000 calls).
- Introduce color gradients for CPU and memory usage on call graph nodes, indicating the percentage of total usage.
- Add functionality to filter nodes by a specific percentage of CPU and memory usage.
  • Loading branch information
butschster committed Nov 11, 2023
1 parent 2653738 commit bedb61b
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 43 deletions.
2 changes: 1 addition & 1 deletion components/ProfilePageFlamegraph/ProfilePageFlamegraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,6 @@ export default defineComponent({
}
.profiler-page-flamegraph__canvas {
@apply bg-gray-300 w-full h-full;
@apply bg-gray-300 w-full h-full px-5;
}
</style>
51 changes: 40 additions & 11 deletions components/ProfilerPageCallGraph/ProfilerPageCallGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
{{ name }}
</h4>

<StatBoard :cost="cost" />
<StatBoard :cost="cost"/>
</div>
</template>
</RenderGraph>
Expand All @@ -36,6 +36,13 @@
>
CPU
</button>
<button
class="profiler-page-call-graph__toolbar-action"
:class="{ 'font-bold': metric === graphMetrics.MEMORY }"
@click="setMetric(graphMetrics.MEMORY)"
>
Memory usage
</button>
<button
class="profiler-page-call-graph__toolbar-action"
:class="{ 'font-bold': metric === graphMetrics.MEMORY_CHANGE }"
Expand All @@ -45,17 +52,17 @@
</button>
<button
class="profiler-page-call-graph__toolbar-action"
:class="{ 'font-bold': metric === graphMetrics.MEMORY }"
@click="setMetric(graphMetrics.MEMORY)"
:class="{ 'font-bold': metric === graphMetrics.CALLS }"
@click="setMetric(graphMetrics.CALLS)"
>
Memory usage
Calls
</button>
</div>

<div
class="profiler-page-call-graph__toolbar profiler-page-call-graph__toolbar--right"
>
<label class="profiler-page-call-graph__toolbar-input-wr">
<label class="profiler-page-call-graph__toolbar-input-wr" v-if="metric !== graphMetrics.CALLS">
Threshold

<input
Expand All @@ -68,21 +75,36 @@
@input="setThreshold($event.target.value)"
/>
</label>


<label class="profiler-page-call-graph__toolbar-input-wr">
{{ percentLabel }}

<input
class="profiler-page-call-graph__toolbar-input"
type="number"
:value="min_percent"
:min="metric === graphMetrics.CALLS ? 1 : 0"
:max="metric === graphMetrics.CALLS ? 1000 : 100"
:step="metric === graphMetrics.CALLS ? 10 : 5"
@input="setMinPercent($event.target.value)"
/>
</label>
</div>
</div>
</template>

<script lang="ts">
import IconSvg from "~/components/IconSvg/IconSvg.vue";
import { defineComponent, PropType } from "vue";
import { GraphTypes, Profiler } from "~/config/types";
import { calcGraphData } from "~/utils/calc-graph-data";
import {defineComponent, PropType} from "vue";
import {GraphTypes, Profiler} from "~/config/types";
import {calcGraphData} from "~/utils/calc-graph-data";
import RenderGraph from "~/components/RenderGraph/RenderGraph.vue";
import StatBoard from "~/components/StatBoard/StatBoard.vue";
export default defineComponent({
components: { StatBoard, RenderGraph, IconSvg },
components: {StatBoard, RenderGraph, IconSvg},
props: {
event: {
type: Object as PropType<Profiler>,
Expand All @@ -95,15 +117,19 @@ export default defineComponent({
isFullscreen: false,
metric: GraphTypes.CPU as GraphTypes,
threshold: 1,
min_percent: 10,
isReadyGraph: false,
};
},
computed: {
percentLabel() {
return this.metric === GraphTypes.CALLS ? "Min calls" : "Percent";
},
graphElements() {
return calcGraphData(this.event.edges, this.metric, this.threshold);
return calcGraphData(this.event.edges, this.metric, this.threshold, this.min_percent);
},
graphKey() {
return `${this.metric}-${this.threshold}`;
return `${this.metric}-${this.threshold}-${this.min_percent}`;
},
graphMetrics() {
return GraphTypes;
Expand All @@ -125,6 +151,9 @@ export default defineComponent({
setThreshold(threshold: number): void {
this.threshold = threshold;
},
setMinPercent(percent: number): void {
this.min_percent = percent;
},
setReadyGraph(): void {
this.isReadyGraph = true;
},
Expand Down
2 changes: 1 addition & 1 deletion components/RenderGraph/RenderGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const stylesConfig: Stylesheet[] = [
"target-arrow-color": "data(color)",
content: "data(label)",
color: "#fff",
"curve-style": "taxi",
"curve-style": "bezier",
"taxi-direction": "downward",
"edge-distances": "node-position",
"control-point-distance": "5px",
Expand Down
35 changes: 28 additions & 7 deletions components/StatBoard/StatBoard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
<div v-for="item in statItems" :key="item.title" class="stat-board__item">
<h4 class="stat-board__item-name">
{{ item.title }}

<span class="stat-board__item-percent" v-if="item.percent > 0">
[{{ item.percent }}%]
</span>
</h4>

<strong class="stat-board__item-value">
Expand All @@ -13,9 +17,9 @@
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import { ProfilerCost } from "~/config/types";
import { humanFileSize, formatDuration } from "~/utils/formats";
import {defineComponent, PropType} from "vue";
import {ProfilerCost} from "~/config/types";
import {humanFileSize, formatDuration} from "~/utils/formats";
export default defineComponent({
props: {
Expand All @@ -26,26 +30,39 @@ export default defineComponent({
},
computed: {
statItems() {
const undef = '';
let cpu = formatDuration(this.cost.cpu || 0) || undef;
let wt = formatDuration(this.cost.wt || 0) || undef;
let mu = humanFileSize(this.cost.mu || 0) || undef;
let pmu = humanFileSize(this.cost.pmu || 0) || undef;
return [
{
title: "Calls",
value: this.cost.ct || 0,
percent: null,
},
{
title: "CPU time",
value: formatDuration(this.cost.cpu || 0) || "",
value: cpu,
percent: this.cost?.p_cpu,
},
{
title: "Wall time",
value: formatDuration(this.cost.wt || 0) || "",
value: wt,
percent: this.cost?.p_wt,
},
{
title: "Memory usage",
value: humanFileSize(this.cost.mu || 0) || "",
value: mu,
percent: this.cost?.p_mu,
},
{
title: "Change memory",
value: humanFileSize(this.cost.pmu || 0) || "",
value: pmu,
percent: this.cost?.p_pmu,
},
];
},
Expand Down Expand Up @@ -81,4 +98,8 @@ export default defineComponent({
.stat-board__item-value {
@apply text-2xs sm:text-xs md:text-base truncate;
}
.stat-board__item-percent {
@apply text-2xs truncate ml-1;
}
</style>
5 changes: 3 additions & 2 deletions config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ export type TGraphEdge = {
}

export enum GraphTypes {
CPU= 'cpu' ,
CPU = 'cpu',
MEMORY_CHANGE = 'pmu',
MEMORY = 'mu'
MEMORY = 'mu',
CALLS = 'calls'
}
134 changes: 113 additions & 21 deletions utils/calc-graph-data.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,80 @@
import {humanFileSize, formatDuration} from "~/utils/formats";
import { GraphTypes, ProfilerEdge, ProfilerEdges, TGraphEdge, TGraphNode } from "~/config/types";
import {formatDuration, humanFileSize} from "~/utils/formats";
import {GraphTypes, ProfilerEdge, ProfilerEdges, TGraphEdge, TGraphNode} from "~/config/types";

function getColorForCallCount(callCount): string {
if (callCount <= 1) {
return '#fff'; // Sky Blue for 1 call
} else if (callCount <= 10) {
return '#7BC8F6'; // Lighter Sky Blue
} else if (callCount <= 25) {
return '#4DA6FF'; // Light Blue
} else if (callCount <= 50) {
return '#1A8FFF'; // Brighter Blue
} else if (callCount <= 75) {
return '#007FFF'; // Azure Blue
} else if (callCount <= 100) {
return '#0059B3'; // Royal Blue
} else if (callCount <= 250) {
return '#FFD700'; // Golden
} else if (callCount <= 500) {
return '#FFA500'; // Orange
} else if (callCount <= 750) {
return '#FF8C00'; // Dark Orange
} else if (callCount <= 1000) {
return '#FF4500'; // OrangeRed
} else if (callCount <= 2500) {
return '#FF0000'; // Red
}

return '#8B0000'; // Dark Red for 1000 to 1750 calls
}

function getColorForPercentCount(percent): string {
if (percent <= 10) {
return '#FFFFFF'; // White
} else if (percent <= 20) {
return '#f19797'; // Lighter shade towards dark red
} else if (percent <= 30) {
return '#d93939'; // Light shade towards dark red
} else if (percent <= 40) {
return '#ad1e1e'; // Intermediate lighter shade towards dark red
} else if (percent <= 50) {
return '#982525'; // Intermediate shade towards dark red
} else if (percent <= 60) {
return '#862323'; // Intermediate darker shade towards dark red
} else if (percent <= 70) {
return '#671d1d'; // Darker shade towards dark red
} else if (percent <= 80) {
return '#540d0d'; // More towards dark red
} else if (percent <= 90) {
return '#340707'; // Almost dark red
}

return '#2d0606'; // Dark red
}

function invertHexColor(hex): string {
// If the first character is a hash, remove it for processing
hex = hex.replace('#', '');

// Convert hex to RGB
let r = parseInt(hex.substr(0, 2), 16);
let g = parseInt(hex.substr(2, 2), 16);
let b = parseInt(hex.substr(4, 2), 16);

// Calculate the YIQ ratio
let yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;

// Return black for bright colors, white for dark colors
return (yiq >= 128) ? '#000' : '#fff';
}

const formatValue = (value: number, metric: string): string | number => {
const metricFormatMap: Record<string, (v: number) => string|number> = {
p_mu: (a: number) => `${a}%`,
p_pmu: (a: number) => `${a}%`,
p_cpu: (a: number) => `${a}%`,
p_wt: (a: number) => `${a}%`,
const metricFormatMap: Record<string, (v: number) => string | number> = {
p_mu: (a: number): string => `${a}%`,
p_pmu: (a: number): string => `${a}%`,
p_cpu: (a: number): string => `${a}%`,
p_wt: (a: number): string => `${a}%`,
mu: humanFileSize,
d_mu: humanFileSize,
pmu: humanFileSize,
Expand All @@ -23,42 +91,66 @@ const formatValue = (value: number, metric: string): string | number => {
export const calcGraphData: (
edges: ProfilerEdges,
metric: GraphTypes,
threshold: number
threshold: number,
minPercent: number
) => ({
nodes: TGraphNode[],
edges: TGraphEdge[]
}) =
(edges: ProfilerEdges, metric , threshold = 1) => Object.values(edges)
(edges: ProfilerEdges, metric: GraphTypes, threshold: number = 1, minPercent: number = 10) => Object.values(edges)
.reduce((arr, edge: ProfilerEdge, index) => {
const metricKey = `p_${metric}`;
let nodeColor: string = '#fff';
let nodeTextColor: string = '#000';
let edgeColor: string = '#fff';
let edgeLabel: string = edge.cost.ct > 1 ? `${edge.cost.ct}x` : '';

if (metric === GraphTypes.CALLS) {
const metricKey: string = `ct`;
const isImportantNode: boolean = edge.cost[metricKey] >= minPercent;
if (!isImportantNode) {
return arr
}

const isImportantNode = edge.cost.p_pmu > 10;
nodeColor = getColorForCallCount(edge.cost[metricKey]);
} else {
const metricKey: string = `p_${metric}`;
const isImportantNode: boolean = edge.cost[metricKey] >= minPercent;
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
return arr
}

nodeColor = isImportantNode ? getColorForPercentCount(edge.cost[metricKey]) : '#fff';
nodeTextColor = isImportantNode ? invertHexColor(nodeColor) : '#000';

edgeColor = nodeColor;

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


arr.nodes.push({
data: {
id: edge.callee,
name: edge.callee as string,
cost: edge.cost,
color: isImportantNode ? '#e74c3c' : '#fff',
textColor: isImportantNode ? '#fff' : '#000'
color: nodeColor,
textColor: nodeTextColor
}
})

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

if (index > 0 && hasNodeSource) {
const postfix = edge.cost.ct > 1 ? ` - ${edge.cost.ct }x` : '';

arr.edges.push({ data: {
arr.edges.push({
data: {
source: edge.caller || '',
target: edge.callee,
color: edge.cost.p_pmu > 10 ? '#e74c3c' : '#fff',
label: `${formatValue(edge.cost[metricKey], metricKey)}${postfix}`
}})
color: edgeColor,
label: edgeLabel,
weight: edge.cost.ct,
}
})
}

return arr
Expand Down

0 comments on commit bedb61b

Please sign in to comment.