Skip to content

Commit

Permalink
Refactors profiler module
Browse files Browse the repository at this point in the history
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
  • Loading branch information
butschster committed Jun 8, 2024
1 parent 7ad3127 commit 1d819fc
Show file tree
Hide file tree
Showing 15 changed files with 268 additions and 122 deletions.
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

0 comments on commit 1d819fc

Please sign in to comment.