Skip to content

Commit 1d819fc

Browse files
committed
Refactors profiler module
1. Moves calculations on a backend side. 2. Fixes Flame chart rendering. 3. Add top functions with sorting 4. Fixes styles on dark and light theme
1 parent 7ad3127 commit 1d819fc

File tree

15 files changed

+268
-122
lines changed

15 files changed

+268
-122
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
"d3-graphviz": "^5.0.2",
8585
"d3-selection": "^3.0.0",
8686
"downloadjs": "^1.4.7",
87-
"flame-chart-js": "^2.3.1",
87+
"flame-chart-js": "^3.0",
8888
"highlight.js": "^11.7.0",
8989
"html-to-image": "^1.11.4",
9090
"lodash.debounce": "^4.0.8",

pages/profiler/[id].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,6 @@ onMounted(getEvent);
8686
8787
.profiler-event__body {
8888
@include layout-body;
89+
@apply h-full;
8990
}
9091
</style>

src/entities/profiler/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
export interface ProfilerCost {
22
[key: string]: number,
3+
34
"ct": number,
45
"wt": number,
56
"cpu": number,
67
"mu": number,
78
"pmu": number
89
}
10+
911
export interface ProfilerEdge {
12+
id: string,
13+
parent: string | null,
1014
caller: string | null,
1115
callee: string,
1216
cost: ProfilerCost
@@ -20,7 +24,8 @@ export interface Profiler {
2024
},
2125
app_name: string,
2226
hostname: string,
27+
profile_uuid: string,
2328
date: number,
2429
peaks: ProfilerCost,
25-
edges: ProfilerEdges
30+
// edges: ProfilerEdges
2631
}

src/features/lib/cytoscape/prepare-data.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type { TEdge, TNode } from "./types";
55

66
const { formatDuration, formatFileSize } = useFormats();
77

8-
98
const getColorForCallCount = (callCount: number) => {
109
if (callCount <= 1) {
1110
return '#fff'; // Sky Blue for 1 call
@@ -91,7 +90,7 @@ const invertHexColor = (hexInput: string) => {
9190
return (yiq >= 128) ? '#000' : '#fff';
9291
}
9392
const formatValue = (value: number, metric: string): string | number => {
94-
const metricFormatMap: Record<string, (v: number) => string|number> = {
93+
const metricFormatMap: Record<string, (v: number) => string | number> = {
9594
p_mu: (a: number) => `${a}%`,
9695
p_pmu: (a: number) => `${a}%`,
9796
p_cpu: (a: number) => `${a}%`,
@@ -118,28 +117,22 @@ export const prepareData: (
118117
nodes: TNode[],
119118
edges: TEdge[]
120119
}) =
121-
(edges: ProfilerEdges, metric , threshold = 1, percent = 10) => Object.values(edges)
120+
(edges: ProfilerEdges, metric, threshold = 1, percent = 10) => Object.values(edges)
122121
.reduce((arr, edge: ProfilerEdge, index) => {
123-
let nodeColor = '#fff';
124-
let nodeTextColor = '#000';
125-
let edgeColor = '#fff';
122+
let nodeColor: string = '#fff';
123+
let nodeTextColor: string = '#000';
124+
let edgeColor: string = '#fff';
126125
let edgeLabel: string = edge.cost.ct > 1 ? `${edge.cost.ct}x` : '';
127126

128-
if (metric === GraphTypes.CALLS) {
129-
const metricKey = `ct`;
130-
const isImportantNode: boolean = edge.cost[metricKey] >= percent;
131-
if (!isImportantNode) {
132-
return arr
133-
}
127+
const metricKey: string = metric === GraphTypes.CALLS ? `ct` : `p_${metric}`;
128+
const isImportantNode: boolean = edge.cost[metricKey] >= percent;
129+
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
130+
return arr
131+
}
134132

133+
if (metric === GraphTypes.CALLS) {
135134
nodeColor = getColorForCallCount(edge.cost[metricKey]);
136135
} else {
137-
const metricKey = `p_${metric}`;
138-
const isImportantNode: boolean = edge.cost[metricKey] >= percent;
139-
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
140-
return arr
141-
}
142-
143136
nodeColor = isImportantNode ? getColorForPercentCount(edge.cost[metricKey]) : '#fff';
144137
nodeTextColor = isImportantNode ? invertHexColor(nodeColor) : '#000';
145138

@@ -149,31 +142,23 @@ export const prepareData: (
149142
edgeLabel = `${formatValue(edge.cost[metricKey], metricKey)}${postfix}`;
150143
}
151144

152-
const metricKey = `p_${metric}`;
153-
154-
const isImportantNode = edge.cost.p_pmu > 10;
155-
156-
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
157-
return arr
158-
}
159-
160145
arr.nodes.push({
161146
data: {
162-
id: edge.callee,
147+
id: edge.id,
163148
name: edge.callee as string,
164149
cost: edge.cost,
165150
color: nodeColor,
166151
textColor: nodeTextColor
167152
}
168153
})
169154

170-
const hasNodeSource = arr.nodes.find(node => node.data.id === edge.caller);
155+
const hasNodeSource = arr.nodes.find(node => node.data.id === edge.parent);
171156

172157
if (index > 0 && hasNodeSource) {
173158
arr.edges.push({
174159
data: {
175-
source: edge.caller || '',
176-
target: edge.callee,
160+
source: edge.parent || '',
161+
target: edge.id,
177162
color: edgeColor,
178163
label: edgeLabel,
179164
weight: edge.cost.ct,

src/features/lib/cytoscape/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ProfilerCost } from "~/src/entities/profiler/types";
33
export type TNode = {
44
data: {
55
id: string,
6+
parent: string | null,
67
name: string,
78
cost?: ProfilerCost,
89
color?: string,

src/features/lib/cytoscape/use-cytoscape.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { initialize } from "./inicialize";
2+
// TODO: no need anymore
23
import { prepareData as buildData } from "./prepare-data";
34

45
export const useCytoscape = () => ({
Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,93 @@
11
import type { FlameChartNode } from "flame-chart-js/dist/types";
2-
import type { ProfilerCost, ProfilerEdge, ProfilerEdges } from "~/src/entities/profiler/types";
2+
import type { ProfilerCost, ProfilerEdges } from "~/src/entities/profiler/types";
33
import { GraphTypes } from "~/src/shared/types";
44

55
type FlameChartData = FlameChartNode & {
66
cost: ProfilerCost
77
}
88
export const build = (edges: ProfilerEdges, field: GraphTypes): FlameChartData => {
9-
let walked = [] as ProfilerEdge['callee'][]
9+
return buildWaterfall(edges)[0]
10+
}
1011

11-
const datum: Record<string, FlameChartData> = {}
12+
// TODO: add types
13+
function buildWaterfall(events) {
14+
const waterfall = [];
15+
const eventCache = {};
1216

13-
Object.values(edges).forEach((edge) => {
14-
const parent = edge.caller
15-
const target = edge.callee
17+
// First pass to create each event and find its parent.
18+
for (const key of Object.keys(events)) {
19+
const event = events[key];
20+
const duration = event.cost.wt || 0;
21+
const eventData = {
22+
name: event.callee,
23+
cost: event.cost,
24+
start: 0, // Temporarily zero, will adjust based on the parent later
25+
duration: duration,
26+
type: 'task',
27+
children: [],
28+
color: getColorForPercentCount(event.cost.p_wt),
29+
};
1630

17-
const duration = (edge.cost[String(field)] || 0) > 0 ? edge.cost[String(field)] / 1_000 : 0
18-
const start = 0
31+
eventCache[event.id] = eventData;
1932

20-
if (target && !datum[target]) {
21-
datum[target] = {
22-
name: target,
23-
start,
24-
duration,
25-
cost: edge.cost,
26-
children: []
33+
if (event.parent) {
34+
// If there's a parent, add to its children list.
35+
const parentEventData = eventCache[event.parent];
36+
if (parentEventData) {
37+
parentEventData.children.push(eventData);
2738
}
39+
} else {
40+
// No parent implies it is a top-level event.
41+
waterfall.push(eventData);
2842
}
43+
}
2944

30-
if (parent && !datum[parent]) {
31-
datum[parent] = {
32-
name: parent,
33-
start,
34-
duration,
35-
cost: edge.cost,
36-
children: []
37-
}
38-
}
45+
// Second pass to adjust start times based on the order in the children array.
46+
function adjustStartTimes(eventList, startTime) {
47+
for (let i = 0; i < eventList.length; i++) {
48+
const event = eventList[i];
49+
event.start = startTime;
50+
startTime += event.duration; // Next event starts after the current event ends.
3951

40-
// NOTE: walked skips several targettions (recursion detected), should be fixed
41-
if (!parent || walked.includes(target)) {
42-
// console.log(node, target)
43-
return
52+
// Recursively adjust times for children.
53+
adjustStartTimes(event.children, event.start);
4454
}
55+
}
4556

46-
if (datum[parent] && datum[parent].children) {
47-
const parentChildren = datum[parent].children || []
57+
// Start the adjustment from the root events.
58+
adjustStartTimes(waterfall, 0);
4859

49-
const lastChild = parentChildren ? parentChildren[parentChildren.length - 1]: null
50-
datum[target].start = lastChild ? lastChild.start + lastChild.duration : datum[target].start
51-
} else {
52-
datum[target].start += datum[parent].start
53-
}
54-
55-
datum[parent].children?.push(datum[target])
56-
walked.push(target)
57-
})
60+
return waterfall;
61+
}
5862

59-
walked = []
63+
const getColorForPercentCount = (percent: number): string => {
64+
if (percent <= 10) {
65+
return '#B3E5FC'; // Light Blue
66+
}
67+
if (percent <= 20) {
68+
return '#81D4FA'; // Light Sky Blue
69+
}
70+
if (percent <= 30) {
71+
return '#4FC3F7'; // Vivid Light Blue
72+
}
73+
if (percent <= 40) {
74+
return '#29B6F6'; // Bright Light Blue
75+
}
76+
if (percent <= 50) {
77+
return '#FFCDD2'; // Pink (Light Red)
78+
}
79+
if (percent <= 60) {
80+
return '#FFB2B2'; // Lighter Red
81+
}
82+
if (percent <= 70) {
83+
return '#FF9E9E'; // Soft Red
84+
}
85+
if (percent <= 80) {
86+
return '#FF8989'; // Soft Coral
87+
}
88+
if (percent <= 90) {
89+
return '#FF7474'; // Soft Tomato
90+
}
6091

61-
return datum['main()']
62-
}
92+
return '#FF5F5F'; // Light Coral
93+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
// TODO: no need anymore
12
export * from './use-flame-chart';

src/screens/profiler/ui/call-graph/call-graph.vue

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<script lang="ts" setup>
22
import { ref, computed, onMounted } from "vue";
3-
import { RenderGraph, useRenderGraph } from "~/src/widgets/ui";
3+
import { RenderGraph } from "~/src/widgets/ui";
44
import type { Profiler } from "~/src/entities/profiler/types";
55
import { GraphTypes } from "~/src/shared/types";
66
import { IconSvg } from "~/src/shared/ui";
77
import { CallStatBoard } from "../call-stat-board";
8-
9-
const { prepare } = useRenderGraph();
8+
import { REST_API_URL } from "~/src/shared/lib/io";
109
1110
type Props = {
1211
payload: Profiler;
@@ -22,8 +21,9 @@ const isReadyGraph = ref(false);
2221
2322
const container = ref<HTMLElement>();
2423
25-
const graphElements = computed(() =>
26-
prepare(props.payload.edges, metric.value, threshold.value, percent.value)
24+
const graphElements = computed(async () =>
25+
// TODO: move to api service
26+
await fetch(`${REST_API_URL}/api/profiler/${props.payload.profile_uuid}/call-graph?threshold=${threshold.value}&percentage=${percent.value}&metric=${metric.value}`).then((response) => response.json())
2727
);
2828
2929
const percentLabel = computed(() =>
@@ -72,13 +72,13 @@ const setMinPercent = (value: number) => {
7272
:height="graphHeight"
7373
>
7474
<template #default="{ data: { name, cost } }">
75-
<CallStatBoard :edge="{ callee: name, caller: '', cost }" />
75+
<CallStatBoard :edge="{ callee: name, caller: '', cost }"/>
7676
</template>
7777
</RenderGraph>
7878

7979
<div class="call-graph__toolbar">
8080
<button title="Full screen" @click="isFullscreen = !isFullscreen">
81-
<IconSvg name="fullscreen" class="call-graph__toolbar-icon" />
81+
<IconSvg name="fullscreen" class="call-graph__toolbar-icon"/>
8282
</button>
8383
<button
8484
class="call-graph__toolbar-action"
@@ -89,6 +89,15 @@ const setMinPercent = (value: number) => {
8989
>
9090
CPU
9191
</button>
92+
<button
93+
class="call-graph__toolbar-action"
94+
:class="{
95+
'call-graph__toolbar-action--active': metric === GraphTypes.WALL_TIME,
96+
}"
97+
@click="setMetric(GraphTypes.WALL_TIME)"
98+
>
99+
Wall time
100+
</button>
92101
<button
93102
class="call-graph__toolbar-action"
94103
:class="{
@@ -108,15 +117,19 @@ const setMinPercent = (value: number) => {
108117
>
109118
Memory usage
110119
</button>
120+
121+
<!--
122+
// TODO: Add support on backend
111123
<button
112-
class="call-graph__toolbar-action"
113-
:class="{
114-
'call-graph__toolbar-action--active': metric === GraphTypes.CALLS,
115-
}"
116-
@click="setMetric(GraphTypes.CALLS)"
117-
>
118-
Calls
124+
class="call-graph__toolbar-action"
125+
:class="{
126+
'call-graph__toolbar-action--active': metric === GraphTypes.CALLS,
127+
}"
128+
@click="setMetric(GraphTypes.CALLS)"
129+
>
130+
Calls
119131
</button>
132+
-->
120133
</div>
121134

122135
<div class="call-graph__toolbar call-graph__toolbar--right">
@@ -158,7 +171,7 @@ const setMinPercent = (value: number) => {
158171
@import "src/assets/mixins";
159172
160173
.call-graph {
161-
@apply relative flex rounded border border-gray-900 min-h-min min-w-min h-full;
174+
@apply relative flex rounded min-h-min min-w-min h-full bg-white -mt-3 pt-3 dark:bg-gray-800;
162175
}
163176
164177
.call-graph__graph {

0 commit comments

Comments
 (0)