Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors profiler module #163

Merged
merged 3 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"d3-graphviz": "^5.0.2",
"d3-selection": "^3.0.0",
"downloadjs": "^1.4.7",
"flame-chart-js": "^2.3.1",
"flame-chart-js": "^3.0",
"highlight.js": "^11.7.0",
"html-to-image": "^1.11.4",
"lodash.debounce": "^4.0.8",
Expand Down
1 change: 1 addition & 0 deletions pages/profiler/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,6 @@ onMounted(getEvent);

.profiler-event__body {
@include layout-body;
@apply h-full;
}
</style>
7 changes: 6 additions & 1 deletion src/entities/profiler/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export interface ProfilerCost {
[key: string]: number,

"ct": number,
"wt": number,
"cpu": number,
"mu": number,
"pmu": number
}

export interface ProfilerEdge {
id: string,
parent: string | null,
caller: string | null,
callee: string,
cost: ProfilerCost
Expand All @@ -20,7 +24,8 @@ export interface Profiler {
},
app_name: string,
hostname: string,
profile_uuid: string,
date: number,
peaks: ProfilerCost,
edges: ProfilerEdges
// edges: ProfilerEdges
}
45 changes: 15 additions & 30 deletions src/features/lib/cytoscape/prepare-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { TEdge, TNode } from "./types";

const { formatDuration, formatFileSize } = useFormats();


const getColorForCallCount = (callCount: number) => {
if (callCount <= 1) {
return '#fff'; // Sky Blue for 1 call
Expand Down Expand Up @@ -91,7 +90,7 @@ const invertHexColor = (hexInput: string) => {
return (yiq >= 128) ? '#000' : '#fff';
}
const formatValue = (value: number, metric: string): string | number => {
const metricFormatMap: Record<string, (v: number) => 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}%`,
Expand All @@ -118,28 +117,22 @@ export const prepareData: (
nodes: TNode[],
edges: TEdge[]
}) =
(edges: ProfilerEdges, metric , threshold = 1, percent = 10) => Object.values(edges)
(edges: ProfilerEdges, metric, threshold = 1, percent = 10) => Object.values(edges)
.reduce((arr, edge: ProfilerEdge, index) => {
let nodeColor = '#fff';
let nodeTextColor = '#000';
let edgeColor = '#fff';
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 = `ct`;
const isImportantNode: boolean = edge.cost[metricKey] >= percent;
if (!isImportantNode) {
return arr
}
const metricKey: string = metric === GraphTypes.CALLS ? `ct` : `p_${metric}`;
const isImportantNode: boolean = edge.cost[metricKey] >= percent;
if (!isImportantNode && edge.cost[metricKey] <= threshold) {
return arr
}

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

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

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

const metricKey = `p_${metric}`;

const isImportantNode = edge.cost.p_pmu > 10;

if (!isImportantNode && edge.cost[metricKey] <= threshold) {
return arr
}

arr.nodes.push({
data: {
id: edge.callee,
id: edge.id,
name: edge.callee as string,
cost: edge.cost,
color: nodeColor,
textColor: nodeTextColor
}
})

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

if (index > 0 && hasNodeSource) {
arr.edges.push({
data: {
source: edge.caller || '',
target: edge.callee,
source: edge.parent || '',
target: edge.id,
color: edgeColor,
label: edgeLabel,
weight: edge.cost.ct,
Expand Down
1 change: 1 addition & 0 deletions src/features/lib/cytoscape/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ProfilerCost } from "~/src/entities/profiler/types";
export type TNode = {
data: {
id: string,
parent: string | null,
name: string,
cost?: ProfilerCost,
color?: string,
Expand Down
1 change: 1 addition & 0 deletions src/features/lib/cytoscape/use-cytoscape.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { initialize } from "./inicialize";
// TODO: no need anymore
import { prepareData as buildData } from "./prepare-data";

export const useCytoscape = () => ({
Expand Down
115 changes: 73 additions & 42 deletions src/features/lib/flame-chart/build.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,93 @@
import type { FlameChartNode } from "flame-chart-js/dist/types";
import type { ProfilerCost, ProfilerEdge, ProfilerEdges } from "~/src/entities/profiler/types";
import type { ProfilerCost, ProfilerEdges } from "~/src/entities/profiler/types";
import { GraphTypes } from "~/src/shared/types";

type FlameChartData = FlameChartNode & {
cost: ProfilerCost
}
export const build = (edges: ProfilerEdges, field: GraphTypes): FlameChartData => {

Check warning on line 8 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'field' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 8 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'buildWaterfall' was used before it was defined

Check warning on line 8 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'field' is defined but never used. Allowed unused args must match /^_/u

Check failure on line 8 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'buildWaterfall' was used before it was defined
let walked = [] as ProfilerEdge['callee'][]
return buildWaterfall(edges)[0]
}

const datum: Record<string, FlameChartData> = {}
// TODO: add types
function buildWaterfall(events) {
const waterfall = [];
const eventCache = {};

Check failure on line 16 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations

Check failure on line 16 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations
Object.values(edges).forEach((edge) => {
const parent = edge.caller
const target = edge.callee
// First pass to create each event and find its parent.
for (const key of Object.keys(events)) {
const event = events[key];
const duration = event.cost.wt || 0;
const eventData = {
name: event.callee,
cost: event.cost,
start: 0, // Temporarily zero, will adjust based on the parent later
duration: duration,
type: 'task',

Check failure on line 26 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

'getColorForPercentCount' was used before it was defined

Check failure on line 26 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

'getColorForPercentCount' was used before it was defined
children: [],
color: getColorForPercentCount(event.cost.p_wt),
};

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

if (target && !datum[target]) {
datum[target] = {
name: target,
start,
duration,
cost: edge.cost,
children: []
if (event.parent) {
// If there's a parent, add to its children list.
const parentEventData = eventCache[event.parent];
if (parentEventData) {
parentEventData.children.push(eventData);
}
} else {
// No parent implies it is a top-level event.
waterfall.push(eventData);
}
}

if (parent && !datum[parent]) {
datum[parent] = {
name: parent,
start,
duration,
cost: edge.cost,
children: []
}
}
// Second pass to adjust start times based on the order in the children array.

Check failure on line 45 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unary operator '++' used

Check failure on line 45 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unary operator '++' used
function adjustStartTimes(eventList, startTime) {
for (let i = 0; i < eventList.length; i++) {
const event = eventList[i];

Check failure on line 48 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Assignment to function parameter 'startTime'

Check failure on line 48 in src/features/lib/flame-chart/build.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Assignment to function parameter 'startTime'
event.start = startTime;
startTime += event.duration; // Next event starts after the current event ends.

// NOTE: walked skips several targettions (recursion detected), should be fixed
if (!parent || walked.includes(target)) {
// console.log(node, target)
return
// Recursively adjust times for children.
adjustStartTimes(event.children, event.start);
}
}

if (datum[parent] && datum[parent].children) {
const parentChildren = datum[parent].children || []
// Start the adjustment from the root events.
adjustStartTimes(waterfall, 0);

const lastChild = parentChildren ? parentChildren[parentChildren.length - 1]: null
datum[target].start = lastChild ? lastChild.start + lastChild.duration : datum[target].start
} else {
datum[target].start += datum[parent].start
}

datum[parent].children?.push(datum[target])
walked.push(target)
})
return waterfall;
}

walked = []
const getColorForPercentCount = (percent: number): string => {
if (percent <= 10) {
return '#B3E5FC'; // Light Blue
}
if (percent <= 20) {
return '#81D4FA'; // Light Sky Blue
}
if (percent <= 30) {
return '#4FC3F7'; // Vivid Light Blue
}
if (percent <= 40) {
return '#29B6F6'; // Bright Light Blue
}
if (percent <= 50) {
return '#FFCDD2'; // Pink (Light Red)
}
if (percent <= 60) {
return '#FFB2B2'; // Lighter Red
}
if (percent <= 70) {
return '#FF9E9E'; // Soft Red
}
if (percent <= 80) {
return '#FF8989'; // Soft Coral
}
if (percent <= 90) {
return '#FF7474'; // Soft Tomato
}

return datum['main()']
}
return '#FF5F5F'; // Light Coral
};
1 change: 1 addition & 0 deletions src/features/lib/flame-chart/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
// TODO: no need anymore
export * from './use-flame-chart';
43 changes: 28 additions & 15 deletions src/screens/profiler/ui/call-graph/call-graph.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { RenderGraph, useRenderGraph } from "~/src/widgets/ui";
import { RenderGraph } from "~/src/widgets/ui";
import type { Profiler } from "~/src/entities/profiler/types";
import { GraphTypes } from "~/src/shared/types";
import { IconSvg } from "~/src/shared/ui";
import { CallStatBoard } from "../call-stat-board";

const { prepare } = useRenderGraph();
import { REST_API_URL } from "~/src/shared/lib/io";

type Props = {
payload: Profiler;
Expand All @@ -22,8 +21,9 @@ const isReadyGraph = ref(false);

const container = ref<HTMLElement>();

const graphElements = computed(() =>
prepare(props.payload.edges, metric.value, threshold.value, percent.value)
const graphElements = computed(async () =>
// TODO: move to api service
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())
);

const percentLabel = computed(() =>
Expand Down Expand Up @@ -72,13 +72,13 @@ const setMinPercent = (value: number) => {
:height="graphHeight"
>
<template #default="{ data: { name, cost } }">
<CallStatBoard :edge="{ callee: name, caller: '', cost }" />
<CallStatBoard :edge="{ callee: name, caller: '', cost }"/>
</template>
</RenderGraph>

<div class="call-graph__toolbar">
<button title="Full screen" @click="isFullscreen = !isFullscreen">
<IconSvg name="fullscreen" class="call-graph__toolbar-icon" />
<IconSvg name="fullscreen" class="call-graph__toolbar-icon"/>
</button>
<button
class="call-graph__toolbar-action"
Expand All @@ -89,6 +89,15 @@ const setMinPercent = (value: number) => {
>
CPU
</button>
<button
class="call-graph__toolbar-action"
:class="{
'call-graph__toolbar-action--active': metric === GraphTypes.WALL_TIME,
}"
@click="setMetric(GraphTypes.WALL_TIME)"
>
Wall time
</button>
<button
class="call-graph__toolbar-action"
:class="{
Expand All @@ -108,15 +117,19 @@ const setMinPercent = (value: number) => {
>
Memory usage
</button>

<!--
// TODO: Add support on backend
<button
class="call-graph__toolbar-action"
:class="{
'call-graph__toolbar-action--active': metric === GraphTypes.CALLS,
}"
@click="setMetric(GraphTypes.CALLS)"
>
Calls
class="call-graph__toolbar-action"
:class="{
'call-graph__toolbar-action--active': metric === GraphTypes.CALLS,
}"
@click="setMetric(GraphTypes.CALLS)"
>
Calls
</button>
-->
</div>

<div class="call-graph__toolbar call-graph__toolbar--right">
Expand Down Expand Up @@ -158,7 +171,7 @@ const setMinPercent = (value: number) => {
@import "src/assets/mixins";

.call-graph {
@apply relative flex rounded border border-gray-900 min-h-min min-w-min h-full;
@apply relative flex rounded min-h-min min-w-min h-full bg-white -mt-3 pt-3 dark:bg-gray-800;
}

.call-graph__graph {
Expand Down
Loading
Loading