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("<", "&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>
 <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" },

Reply via email to