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 <[email protected]>
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" },