feat: introduce flame graph to the trace profiling (#407)

This commit is contained in:
Starry 2024-08-05 20:48:42 +08:00 committed by GitHub
parent 6b2b6a5dd2
commit 3c8b316b76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 247 additions and 21 deletions

15
src/types/ebpf.d.ts vendored
View File

@ -77,6 +77,21 @@ export type StackElement = {
rateOfRoot?: string; rateOfRoot?: string;
rateOfParent: string; rateOfParent: string;
}; };
export type TraceProfilingElement = {
id: string;
originId: string;
name: string;
parentId: string;
codeSignature: string;
count: number;
stackType: string;
value: number;
children?: TraceProfilingElement[];
rateOfRoot?: string;
rateOfParent: string;
duration: number;
durationChildExcluded: number;
};
export type AnalyzationTrees = { export type AnalyzationTrees = {
id: string; id: string;
parentId: string; parentId: string;

24
src/utils/flameGraph.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export function treeForeach(tree: any, func: (node: any) => void) {
for (const data of tree) {
data.children && treeForeach(data.children, func);
func(data);
}
return tree;
}

View File

@ -28,6 +28,7 @@ limitations under the License. -->
import type { StackElement } from "@/types/ebpf"; import type { StackElement } from "@/types/ebpf";
import { AggregateTypes } from "./data"; import { AggregateTypes } from "./data";
import "d3-flame-graph/dist/d3-flamegraph.css"; import "d3-flame-graph/dist/d3-flamegraph.css";
import { treeForeach } from "@/utils/flameGraph";
/*global Nullable, defineProps*/ /*global Nullable, defineProps*/
const props = defineProps({ const props = defineProps({
@ -180,14 +181,6 @@ limitations under the License. -->
return res; return res;
} }
function treeForeach(tree: StackElement[], func: (node: StackElement) => void) {
for (const data of tree) {
data.children && treeForeach(data.children, func);
func(data);
}
return tree;
}
watch( watch(
() => ebpfStore.analyzeTrees, () => ebpfStore.analyzeTrees,
() => { () => {

View File

@ -19,9 +19,11 @@ limitations under the License. -->
<SegmentList /> <SegmentList />
</div> </div>
<div class="item"> <div class="item">
<SpanTree @loading="loadTrees" /> <SpanTree @loading="loadTrees" @displayMode="setDisplayMode" />
<div class="thread-stack"> <div class="thread-stack">
<div id="graph-stack" ref="graph" v-show="displayMode == 'flame'" />
<StackTable <StackTable
v-show="displayMode == 'tree'"
v-if="profileStore.analyzeTrees.length" v-if="profileStore.analyzeTrees.length"
:data="profileStore.analyzeTrees" :data="profileStore.analyzeTrees"
:highlightTop="profileStore.highlightTop" :highlightTop="profileStore.highlightTop"
@ -34,19 +36,175 @@ limitations under the License. -->
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; /*global Nullable*/
import { ref, watch } from "vue";
import TaskList from "./components/TaskList.vue"; import TaskList from "./components/TaskList.vue";
import SegmentList from "./components/SegmentList.vue"; import SegmentList from "./components/SegmentList.vue";
import SpanTree from "./components/SpanTree.vue"; import SpanTree from "./components/SpanTree.vue";
import StackTable from "./components/Stack/Index.vue"; import StackTable from "./components/Stack/Index.vue";
import { useProfileStore } from "@/store/modules/profile"; import { useProfileStore } from "@/store/modules/profile";
import type { TraceProfilingElement } from "@/types/ebpf";
import { flamegraph } from "d3-flame-graph";
import * as d3 from "d3";
import d3tip from "d3-tip";
import { treeForeach } from "@/utils/flameGraph";
const stackTree = ref<Nullable<TraceProfilingElement>>(null);
const selectStack = ref<Nullable<TraceProfilingElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
const flameChart = ref<any>(null);
const min = ref<number>(1);
const max = ref<number>(1);
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const displayMode = ref<string>("tree");
const profileStore = useProfileStore(); const profileStore = useProfileStore();
function loadTrees(l: boolean) { function loadTrees(l: boolean) {
loading.value = l; loading.value = l;
} }
function setDisplayMode(mode: string) {
displayMode.value = mode;
}
function drawGraph() {
if (flameChart.value) {
flameChart.value.destroy();
}
if (!profileStore.analyzeTrees.length) {
return (stackTree.value = null);
}
const root: TraceProfilingElement = {
parentId: "0",
originId: "1",
name: "Virtual Root",
children: [],
value: 0,
id: "1",
codeSignature: "Virtual Root",
count: 0,
stackType: "",
rateOfRoot: "",
rateOfParent: "",
duration: 0,
durationChildExcluded: 0,
};
countRange();
for (const tree of profileStore.analyzeTrees) {
const ele = processTree(tree.elements);
root.children && root.children.push(ele);
}
const param = (root.children || []).reduce(
(prev: number[], curr: TraceProfilingElement) => {
prev[0] += curr.value;
prev[1] += curr.count;
return prev;
},
[0, 0],
);
root.value = param[0];
root.count = param[1];
stackTree.value = root;
const width = (graph.value && graph.value.getBoundingClientRect().width) || 0;
const w = width < 800 ? 802 : width;
flameChart.value = flamegraph()
.width(w - 15)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(1)
.transitionEase(d3.easeCubic as any)
.sort(true)
.title("")
.selfValue(false)
.inverted(true)
.onClick((d: { data: TraceProfilingElement }) => {
selectStack.value = d.data;
})
.setColorMapper((d, originalColor) => (d.highlight ? "#6aff8f" : originalColor));
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("s")
.html((d: { data: TraceProfilingElement } & { parent: { data: TraceProfilingElement } }) => {
const name = d.data.name.replace("<", "&lt;").replace(">", "&gt;");
const dumpCount = `<div class="mb-5">Dump Count: ${d.data.count}</div>`;
const duration = `<div class="mb-5">Duration: ${d.data.duration} ns</div>`;
const durationChildExcluded = `<div class="mb-5">DurationChildExcluded: ${d.data.durationChildExcluded} ns</div>`;
const rateOfParent =
(d.parent &&
`<div class="mb-5">Percentage Of Selected: ${
((d.data.count / ((selectStack.value && selectStack.value.count) || root.count)) * 100).toFixed(3) + "%"
}</div>`) ||
"";
const rateOfRoot = `<div class="mb-5">Percentage Of Root: ${
((d.data.count / root.count) * 100).toFixed(3) + "%"
}</div>`;
return `<div class="mb-5 name">CodeSignature: ${name}</div>${dumpCount}${duration}${durationChildExcluded}${rateOfParent}${rateOfRoot}`;
})
.style("max-width", "400px");
flameChart.value.tooltip(tip);
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
}
function countRange() {
const list = [];
for (const tree of profileStore.analyzeTrees) {
for (const ele of tree.elements) {
list.push(ele.count);
}
}
max.value = Math.max(...list);
min.value = Math.min(...list);
}
function processTree(arr: TraceProfilingElement[]) {
const copyArr = JSON.parse(JSON.stringify(arr));
const obj: any = {};
let res = null;
for (const item of copyArr) {
item.parentId = String(Number(item.parentId) + 1);
item.originId = String(Number(item.id) + 1);
item.name = item.codeSignature;
delete item.id;
obj[item.originId] = item;
}
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
for (const item of copyArr) {
if (item.parentId === "1") {
const val = Number(scale(item.count).toFixed(4));
res = item;
res.value = val;
}
for (const key in obj) {
if (item.originId === obj[key].parentId) {
const val = Number(scale(obj[key].count).toFixed(4));
obj[key].value = val;
if (item.children) {
item.children.push(obj[key]);
} else {
item.children = [obj[key]];
}
}
}
}
treeForeach([res], (node: TraceProfilingElement) => {
if (node.children) {
let val = 0;
for (const child of node.children) {
val = child.value + val;
}
node.value = node.value < val ? val : node.value;
}
});
return res;
}
watch(
() => profileStore.analyzeTrees,
() => {
drawGraph();
},
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.content { .content {
@ -78,4 +236,22 @@ limitations under the License. -->
overflow: hidden; overflow: hidden;
height: calc(50% - 20px); height: calc(50% - 20px);
} }
#graph-stack {
width: 100%;
height: 100%;
cursor: pointer;
}
.tip {
display: inline-block;
width: 100%;
text-align: center;
color: red;
margin-top: 20px;
}
.name {
word-wrap: break-word;
}
</style> </style>

View File

@ -19,12 +19,20 @@ limitations under the License. -->
<el-input class="input mr-10 ml-5" readonly :value="profileStore.currentSegment.traceId" size="small" /> <el-input class="input mr-10 ml-5" readonly :value="profileStore.currentSegment.traceId" size="small" />
<Selector <Selector
size="small" size="small"
:value="mode" :value="dataMode"
:options="ProfileMode" :options="ProfileDataMode"
placeholder="Select a mode" placeholder="Please select a profile data mode"
@change="spanModeChange" @change="spanModeChange"
class="mr-10" class="mr-10"
/> />
<Selector
size="small"
:value="displayMode"
:options="ProfileDisplayMode"
placeholder="Please select a profile display mode"
@change="selectDisplayMode"
class="mr-10"
/>
<el-button type="primary" size="small" :disabled="!profileStore.currentSpan.profiled" @click="analyzeProfile()"> <el-button type="primary" size="small" :disabled="!profileStore.currentSpan.profiled" @click="analyzeProfile()">
{{ t("analyze") }} {{ t("analyze") }}
</el-button> </el-button>
@ -49,13 +57,14 @@ limitations under the License. -->
import type { Span } from "@/types/trace"; import type { Span } from "@/types/trace";
import type { Option } from "@/types/app"; import type { Option } from "@/types/app";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { ProfileMode } from "./data"; import { ProfileDataMode, ProfileDisplayMode } from "./data";
/* global defineEmits*/ /* global defineEmits*/
const emits = defineEmits(["loading"]); const emits = defineEmits(["loading", "displayMode"]);
const { t } = useI18n(); const { t } = useI18n();
const profileStore = useProfileStore(); const profileStore = useProfileStore();
const mode = ref<string>("include"); const dataMode = ref<string>("include");
const displayMode = ref<string>("tree");
const message = ref<string>(""); const message = ref<string>("");
const timeRange = ref<Array<{ start: number; end: number }>>([]); const timeRange = ref<Array<{ start: number; end: number }>>([]);
@ -64,10 +73,15 @@ limitations under the License. -->
} }
function spanModeChange(item: Option[]) { function spanModeChange(item: Option[]) {
mode.value = item[0].value; dataMode.value = item[0].value;
updateTimeRange(); updateTimeRange();
} }
function selectDisplayMode(item: Option[]) {
displayMode.value = item[0].value;
emits("displayMode", displayMode.value);
}
async function analyzeProfile() { async function analyzeProfile() {
if (!profileStore.currentSpan.profiled) { if (!profileStore.currentSpan.profiled) {
ElMessage.info("It's a un-profiled span"); ElMessage.info("It's a un-profiled span");
@ -92,7 +106,7 @@ limitations under the License. -->
} }
function updateTimeRange() { function updateTimeRange() {
if (mode.value === "include") { if (dataMode.value === "include") {
timeRange.value = [ timeRange.value = [
{ {
start: profileStore.currentSpan.startTime, start: profileStore.currentSpan.startTime,
@ -158,7 +172,7 @@ limitations under the License. -->
.profile-trace-detail-wrapper { .profile-trace-detail-wrapper {
padding: 5px 0; padding: 5px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgb(0 0 0 / 10%);
width: 100%; width: 100%;
} }

View File

@ -14,10 +14,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
export const ProfileMode: any[] = [ export const ProfileDataMode: any[] = [
{ label: "Include Children", value: "include" }, { label: "Include Children", value: "include" },
{ label: "Exclude Children", value: "exclude" }, { label: "Exclude Children", value: "exclude" },
]; ];
export const ProfileDisplayMode: any[] = [
{ label: "Tree Graph", value: "tree" },
{ label: "Flame Graph", value: "flame" },
];
export const NewTaskField = { export const NewTaskField = {
service: { key: "", label: "None" }, service: { key: "", label: "None" },
monitorTime: { key: "0", label: "monitor now" }, monitorTime: { key: "0", label: "monitor now" },