This is an automated email from the ASF dual-hosted git repository. qiuxiafan pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-booster-ui.git
The following commit(s) were added to refs/heads/main by this push: new 3c8b316b feat: introduce flame graph to the trace profiling (#407) 3c8b316b is described below commit 3c8b316b76720c8949449af02116d05923706348 Author: Starry <zhouzi...@apache.org> AuthorDate: Mon Aug 5 20:48:42 2024 +0800 feat: introduce flame graph to the trace profiling (#407) --- src/types/ebpf.d.ts | 15 ++ src/utils/flameGraph.ts | 24 +++ .../related/ebpf/components/EBPFStack.vue | 9 +- src/views/dashboard/related/profile/Content.vue | 182 ++++++++++++++++++++- .../related/profile/components/SpanTree.vue | 32 +++- .../dashboard/related/profile/components/data.ts | 6 +- 6 files changed, 247 insertions(+), 21 deletions(-) diff --git a/src/types/ebpf.d.ts b/src/types/ebpf.d.ts index 36ed2082..53badf56 100644 --- a/src/types/ebpf.d.ts +++ b/src/types/ebpf.d.ts @@ -77,6 +77,21 @@ export type StackElement = { rateOfRoot?: 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 = { id: string; parentId: string; diff --git a/src/utils/flameGraph.ts b/src/utils/flameGraph.ts new file mode 100644 index 00000000..98e4d192 --- /dev/null +++ b/src/utils/flameGraph.ts @@ -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; +} diff --git a/src/views/dashboard/related/ebpf/components/EBPFStack.vue b/src/views/dashboard/related/ebpf/components/EBPFStack.vue index 988f2193..190d732d 100644 --- a/src/views/dashboard/related/ebpf/components/EBPFStack.vue +++ b/src/views/dashboard/related/ebpf/components/EBPFStack.vue @@ -28,6 +28,7 @@ limitations under the License. --> import type { StackElement } from "@/types/ebpf"; import { AggregateTypes } from "./data"; import "d3-flame-graph/dist/d3-flamegraph.css"; + import { treeForeach } from "@/utils/flameGraph"; /*global Nullable, defineProps*/ const props = defineProps({ @@ -180,14 +181,6 @@ limitations under the License. --> 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( () => ebpfStore.analyzeTrees, () => { diff --git a/src/views/dashboard/related/profile/Content.vue b/src/views/dashboard/related/profile/Content.vue index e6f3be83..2adcfc94 100644 --- a/src/views/dashboard/related/profile/Content.vue +++ b/src/views/dashboard/related/profile/Content.vue @@ -19,9 +19,11 @@ limitations under the License. --> <SegmentList /> </div> <div class="item"> - <SpanTree @loading="loadTrees" /> + <SpanTree @loading="loadTrees" @displayMode="setDisplayMode" /> <div class="thread-stack"> + <div id="graph-stack" ref="graph" v-show="displayMode == 'flame'" /> <StackTable + v-show="displayMode == 'tree'" v-if="profileStore.analyzeTrees.length" :data="profileStore.analyzeTrees" :highlightTop="profileStore.highlightTop" @@ -34,19 +36,175 @@ limitations under the License. --> </div> </template> <script lang="ts" setup> - import { ref } from "vue"; + /*global Nullable*/ + import { ref, watch } from "vue"; import TaskList from "./components/TaskList.vue"; import SegmentList from "./components/SegmentList.vue"; import SpanTree from "./components/SpanTree.vue"; import StackTable from "./components/Stack/Index.vue"; 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 displayMode = ref<string>("tree"); const profileStore = useProfileStore(); function loadTrees(l: boolean) { 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("<", "<").replace(">", ">"); + 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> <style lang="scss" scoped> .content { @@ -78,4 +236,22 @@ limitations under the License. --> overflow: hidden; 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> diff --git a/src/views/dashboard/related/profile/components/SpanTree.vue b/src/views/dashboard/related/profile/components/SpanTree.vue index 217473e3..e69f86a6 100644 --- a/src/views/dashboard/related/profile/components/SpanTree.vue +++ b/src/views/dashboard/related/profile/components/SpanTree.vue @@ -19,12 +19,20 @@ limitations under the License. --> <el-input class="input mr-10 ml-5" readonly :value="profileStore.currentSegment.traceId" size="small" /> <Selector size="small" - :value="mode" - :options="ProfileMode" - placeholder="Select a mode" + :value="dataMode" + :options="ProfileDataMode" + placeholder="Please select a profile data mode" @change="spanModeChange" 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()"> {{ t("analyze") }} </el-button> @@ -49,13 +57,14 @@ limitations under the License. --> import type { Span } from "@/types/trace"; import type { Option } from "@/types/app"; import { ElMessage } from "element-plus"; - import { ProfileMode } from "./data"; + import { ProfileDataMode, ProfileDisplayMode } from "./data"; /* global defineEmits*/ - const emits = defineEmits(["loading"]); + const emits = defineEmits(["loading", "displayMode"]); const { t } = useI18n(); const profileStore = useProfileStore(); - const mode = ref<string>("include"); + const dataMode = ref<string>("include"); + const displayMode = ref<string>("tree"); const message = ref<string>(""); const timeRange = ref<Array<{ start: number; end: number }>>([]); @@ -64,10 +73,15 @@ limitations under the License. --> } function spanModeChange(item: Option[]) { - mode.value = item[0].value; + dataMode.value = item[0].value; updateTimeRange(); } + function selectDisplayMode(item: Option[]) { + displayMode.value = item[0].value; + emits("displayMode", displayMode.value); + } + async function analyzeProfile() { if (!profileStore.currentSpan.profiled) { ElMessage.info("It's a un-profiled span"); @@ -92,7 +106,7 @@ limitations under the License. --> } function updateTimeRange() { - if (mode.value === "include") { + if (dataMode.value === "include") { timeRange.value = [ { start: profileStore.currentSpan.startTime, @@ -158,7 +172,7 @@ limitations under the License. --> .profile-trace-detail-wrapper { padding: 5px 0; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); + border-bottom: 1px solid rgb(0 0 0 / 10%); width: 100%; } diff --git a/src/views/dashboard/related/profile/components/data.ts b/src/views/dashboard/related/profile/components/data.ts index a382e5a9..16bd5e69 100644 --- a/src/views/dashboard/related/profile/components/data.ts +++ b/src/views/dashboard/related/profile/components/data.ts @@ -14,10 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export const ProfileMode: any[] = [ +export const ProfileDataMode: any[] = [ { label: "Include Children", value: "include" }, { label: "Exclude Children", value: "exclude" }, ]; +export const ProfileDisplayMode: any[] = [ + { label: "Tree Graph", value: "tree" }, + { label: "Flame Graph", value: "flame" }, +]; export const NewTaskField = { service: { key: "", label: "None" }, monitorTime: { key: "0", label: "monitor now" },