feat: Implement the eBPF profile widget on dashboard (#72)

This commit is contained in:
Fine0830
2022-04-24 20:24:23 +08:00
committed by GitHub
parent 393885324b
commit 8a07b1d804
38 changed files with 1894 additions and 113 deletions

View File

@@ -44,7 +44,7 @@ limitations under the License. -->
:style="{ fontSize: '13px', width: '100%' }"
v-loading="loading"
ref="multipleTableRef"
:default-sort="{ prop: 'name' }"
:default-sort="{ prop: 'name', order: 'ascending' }"
@selection-change="handleSelectionChange"
height="637px"
size="small"

View File

@@ -0,0 +1,95 @@
<!-- 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. -->
<template>
<div class="profile-wrapper flex-v">
<el-popover
placement="bottom"
trigger="click"
:width="100"
v-if="dashboardStore.editMode"
>
<template #reference>
<span class="delete cp">
<Icon iconName="ellipsis_v" size="middle" class="operation" />
</span>
</template>
<div class="tools" @click="removeWidget">
<span>{{ t("delete") }}</span>
</div>
</el-popover>
<Header />
<Content />
</div>
</template>
<script lang="ts" setup>
import type { PropType } from "vue";
import { useI18n } from "vue-i18n";
import { useDashboardStore } from "@/store/modules/dashboard";
import Header from "../related/ebpf/Header.vue";
import Content from "../related/ebpf/Content.vue";
/*global defineProps */
const props = defineProps({
data: {
type: Object as PropType<any>,
default: () => ({ graph: {} }),
},
activeIndex: { type: String, default: "" },
});
const { t } = useI18n();
const dashboardStore = useDashboardStore();
function removeWidget() {
dashboardStore.removeControls(props.data);
}
</script>
<style lang="scss" scoped>
.profile-wrapper {
width: 100%;
height: 100%;
font-size: 12px;
position: relative;
}
.delete {
position: absolute;
top: 5px;
right: 3px;
}
.header {
padding: 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
}
.tools {
padding: 5px 0;
color: #999;
cursor: pointer;
position: relative;
text-align: center;
&:hover {
color: #409eff;
background-color: #eee;
}
}
.trace {
width: 100%;
overflow: auto;
}
</style>

View File

@@ -97,7 +97,7 @@ limitations under the License. -->
:key="item.i"
@click="clickTabGrid($event, item)"
:class="{ active: activeTabWidget === item.i }"
drag-ignore-from="svg.d3-trace-tree, .dragger, .micro-topo-chart"
:drag-ignore-from="dragIgnoreFrom"
>
<component
:is="item.type"
@@ -122,6 +122,8 @@ import Trace from "./Trace.vue";
import Profile from "./Profile.vue";
import Log from "./Log.vue";
import Text from "./Text.vue";
import Ebpf from "./Ebpf.vue";
import { dragIgnoreFrom } from "../data";
const props = {
data: {
@@ -132,7 +134,7 @@ const props = {
};
export default defineComponent({
name: "Tab",
components: { Topology, Widget, Trace, Profile, Log, Text },
components: { Topology, Widget, Trace, Profile, Log, Text, Ebpf },
props,
setup(props) {
const { t } = useI18n();
@@ -248,6 +250,7 @@ export default defineComponent({
canEditTabName,
showTools,
t,
dragIgnoreFrom,
};
},
});

View File

@@ -21,5 +21,6 @@ import Trace from "./Trace.vue";
import Profile from "./Profile.vue";
import Log from "./Log.vue";
import Text from "./Text.vue";
import Ebpf from "./Ebpf.vue";
export default { Tab, Widget, Trace, Topology, Profile, Log, Text };
export default { Tab, Widget, Trace, Topology, Profile, Log, Text, Ebpf };

View File

@@ -14,6 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const dragIgnoreFrom =
"svg.d3-trace-tree, .dragger, .micro-topo-chart, .schedules";
export const PodsChartTypes = ["EndpointList", "InstanceList"];
@@ -181,7 +183,8 @@ export const ServiceTools = [
{ name: "library_books", content: "Text", id: "addText" },
{ name: "device_hub", content: "Topology", id: "addTopology" },
{ name: "merge", content: "Trace", id: "addTrace" },
{ name: "timeline", content: "Profile", id: "addProfile" },
{ name: "timeline", content: "Trace Profiling", id: "addProfile" },
{ name: "insert_chart", content: "eBPF Profiling", id: "addEbpf" },
{ name: "assignment", content: "Log", id: "addLog" },
];
export const InstanceTools = [

View File

@@ -31,7 +31,7 @@ limitations under the License. -->
:key="item.i"
@click="clickGrid(item)"
:class="{ active: dashboardStore.activedGridItem === item.i }"
drag-ignore-from="svg.d3-trace-tree, .dragger, .micro-topo-chart"
:drag-ignore-from="dragIgnoreFrom"
>
<component :is="item.type" :data="item" />
</grid-item>
@@ -45,6 +45,7 @@ import { useDashboardStore } from "@/store/modules/dashboard";
import { useSelectorStore } from "@/store/modules/selectors";
import { LayoutConfig } from "@/types/dashboard";
import controls from "../controls/index";
import { dragIgnoreFrom } from "../data";
export default defineComponent({
name: "Layout",
@@ -72,6 +73,7 @@ export default defineComponent({
dashboardStore,
clickGrid,
t,
dragIgnoreFrom,
};
},
});

View File

@@ -258,7 +258,7 @@ async function setSourceSelector() {
let currentPod;
if (states.currentPod) {
currentPod = selectorStore.pods.find(
(d: { id: string }) => d.label === states.currentPod
(d: { label: string }) => d.label === states.currentPod
);
} else {
currentPod = selectorStore.pods.find((d: { id: string }) => d.id === pod);
@@ -283,10 +283,10 @@ async function setDestSelector() {
return;
}
const destPod = params.destPodId || selectorStore.destPods[0].id;
let currentDestPod = "";
let currentDestPod = { label: "" };
if (states.currentDestPod) {
currentDestPod = selectorStore.pods.find(
(d: { id: string }) => d.label === states.currentDestPod
(d: { label: string }) => d.label === states.currentDestPod
);
} else {
currentDestPod = selectorStore.destPods.find(
@@ -317,19 +317,23 @@ async function getServices() {
let s;
if (states.currentService) {
s = (selectorStore.services || []).find(
(d) => d.label === states.currentService
(d: { label: string }) => d.label === states.currentService
);
} else {
s = (selectorStore.services || []).find((d, index) => index === 0);
s = (selectorStore.services || []).find(
(d: unknown, index: number) => index === 0
);
}
selectorStore.setCurrentService(s || null);
let d;
if (states.currentService) {
d = (selectorStore.services || []).find(
(d) => d.label === states.currentDestService
(d: { label: string }) => d.label === states.currentDestService
);
} else {
d = (selectorStore.services || []).find((d, index) => index === 1);
d = (selectorStore.services || []).find(
(d: unknown, index: number) => index === 1
);
}
selectorStore.setCurrentDestService(d || null);
if (!selectorStore.currentService) {
@@ -431,6 +435,9 @@ function setTabControls(id: string) {
case "addProfile":
dashboardStore.addTabControls("Profile");
break;
case "addEbpf":
dashboardStore.addTabControls("Ebpf");
break;
case "addTopology":
dashboardStore.addTabControls("Topology");
break;
@@ -457,6 +464,9 @@ function setControls(id: string) {
case "addProfile":
dashboardStore.addControl("Profile");
break;
case "addEbpf":
dashboardStore.addControl("Ebpf");
break;
case "addLog":
dashboardStore.addControl("Log");
break;
@@ -484,9 +494,13 @@ async function fetchPods(
if (setPod) {
let p;
if (states.currentPod) {
p = selectorStore.pods.find((d) => d.label === states.currentPod);
p = selectorStore.pods.find(
(d: { label: unknown }) => d.label === states.currentPod
);
} else {
p = selectorStore.pods.find((d, index) => index === 0);
p = selectorStore.pods.find(
(d: unknown, index: number) => index === 0
);
}
selectorStore.setCurrentPod(p || null);
states.currentPod = selectorStore.currentPod.label;
@@ -497,9 +511,13 @@ async function fetchPods(
if (setPod) {
let p;
if (states.currentPod) {
p = selectorStore.pods.find((d) => d.label === states.currentPod);
p = selectorStore.pods.find(
(d: { label: string }) => d.label === states.currentPod
);
} else {
p = selectorStore.pods.find((d, index) => index === 0);
p = selectorStore.pods.find(
(d: { label: string }, index: number) => index === 0
);
}
selectorStore.setCurrentPod(p || null);
states.currentPod = selectorStore.currentPod.label;
@@ -514,9 +532,13 @@ async function fetchPods(
if (setPod) {
let p;
if (states.currentDestPod) {
p = selectorStore.pods.find((d) => d.label === states.currentDestPod);
p = selectorStore.pods.find(
(d: { label: string }) => d.label === states.currentDestPod
);
} else {
p = selectorStore.pods.find((d, index) => index === 0);
p = selectorStore.pods.find(
(d: { label: string }, index: number) => index === 0
);
}
selectorStore.setCurrentDestPod(p || null);
states.currentDestPod = selectorStore.currentDestPod.label;
@@ -530,9 +552,13 @@ async function fetchPods(
if (setPod) {
let p;
if (states.currentDestPod) {
p = selectorStore.pods.find((d) => d.label === states.currentDestPod);
p = selectorStore.pods.find(
(d: { label: string }) => d.label === states.currentDestPod
);
} else {
p = selectorStore.pods.find((d, index) => index === 0);
p = selectorStore.pods.find(
(d: { label: string }, index: number) => index === 0
);
}
selectorStore.setCurrentDestPod(p || null);
states.currentDestPod = selectorStore.currentDestPod.label;

View File

@@ -0,0 +1,50 @@
<!-- 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. -->
<template>
<div class="flex-h content">
<TaskList />
<div class="vis-graph ml-5">
<div class="item">
<EBPFSchedules />
</div>
<div class="item">
<EBPFStack />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import TaskList from "./components/TaskList.vue";
import EBPFSchedules from "./components/EBPFSchedules.vue";
import EBPFStack from "./components/EBPFStack.vue";
</script>
<style lang="scss" scoped>
.content {
height: calc(100% - 30px);
width: 100%;
}
.vis-graph {
height: 100%;
width: calc(100% - 300px);
}
.item {
width: 100%;
overflow: auto;
height: calc(50% - 10px);
padding-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,104 @@
<!-- 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. -->
<template>
<div class="flex-h header">
<div class="title">eBPF Profiling</div>
<el-button type="primary" size="small" @click="createTask">
{{ t("newTask") }}
</el-button>
</div>
<el-dialog
v-model="newTask"
:destroy-on-close="true"
fullscreen
@closed="newTask = false"
>
<NewTask @close="newTask = false" />
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useSelectorStore } from "@/store/modules/selectors";
import { ElMessage } from "element-plus";
import NewTask from "./components/NewTask.vue";
import { useDashboardStore } from "@/store/modules/dashboard";
import { useAppStoreWithOut } from "@/store/modules/app";
import { EntityType } from "../../data";
const ebpfStore = useEbpfStore();
const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const { t } = useI18n();
const newTask = ref<boolean>(false);
searchTasks();
async function searchTasks() {
const serviceId =
(selectorStore.currentService && selectorStore.currentService.id) || "";
const res = await ebpfStore.getTaskList(serviceId);
if (res.errors) {
ElMessage.error(res.errors);
}
}
async function createTask() {
if (!selectorStore.currentService) {
return;
}
newTask.value = true;
ebpfStore.getCreateTaskData(selectorStore.currentService.id);
}
watch(
() => selectorStore.currentService,
() => {
searchTasks();
}
);
watch(
() => appStore.durationTime,
() => {
if (dashboardStore.entity === EntityType[1].value) {
searchTasks();
}
}
);
</script>
<style lang="scss" scoped>
.header {
padding: 5px 20px 5px 10px;
font-size: 12px;
border-bottom: 1px solid #dcdfe6;
justify-content: space-between;
}
.name {
width: 270px;
}
.new-btn {
float: right;
}
.title {
font-weight: bold;
line-height: 24px;
}
</style>

View File

@@ -0,0 +1,244 @@
<!-- 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. -->
<template>
<div class="filters">
<Selector
:value="selectedLabels"
:options="labels"
size="small"
placeholder="Please select labels"
@change="changeLabels"
class="inputs mr-10"
:multiple="true"
/>
<el-popover placement="bottom" :width="680" trigger="click">
<template #reference>
<el-button type="primary" size="small">
{{ t("processSelect") }}
</el-button>
</template>
<el-input
v-model="searchText"
placeholder="Please input name"
class="input-with-search"
size="small"
@change="searchProcesses"
>
<template #append>
<el-button size="small">
<Icon size="sm" iconName="search" />
</el-button>
</template>
</el-input>
<el-table
:data="currentProcesses"
ref="multipleTableRef"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column
v-for="(h, index) of TableHeader"
:property="h.property"
:label="h.label"
:key="index"
width="150"
/>
<el-table-column width="300" label="Attributes">
<template #default="scope">
{{ scope.row.attributes.map((d: {name: string, value: string}) => `${d.name}=${d.value}`).join("; ") }}
</template>
</el-table-column>
</el-table>
<el-pagination
class="pagination"
background
small
layout="prev, pager, next"
:page-size="pageSize"
:total="processes.length"
@current-change="changePage"
@prev-click="changePage"
@next-click="changePage"
/>
</el-popover>
<el-button type="primary" size="small" @click="analyzeEBPF">
{{ t("analyze") }}
</el-button>
</div>
<div ref="timeline" class="schedules"></div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { Option } from "@/types/app";
import { TableHeader } from "./data";
import { useEbpfStore } from "@/store/modules/ebpf";
import { EBPFProfilingSchedule, Process } from "@/types/ebpf";
import { DataSet, Timeline } from "vis-timeline/standalone";
import "vis-timeline/styles/vis-timeline-graph2d.css";
import { ElMessage, ElTable } from "element-plus";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
const pageSize = 5;
/*global Nullable */
const multipleTableRef = ref<InstanceType<typeof ElTable>>();
const selectedProcesses = ref<string[]>([]);
const timeline = ref<Nullable<HTMLDivElement>>(null);
const visGraph = ref<Nullable<any>>(null);
const labels = ref<Option[]>([{ label: "All", value: "0" }]);
const processes = ref<Process[]>([]);
const currentProcesses = ref<Process[]>([]);
const selectedLabels = ref<string[]>(["0"]);
const searchText = ref<string>("");
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
new Date(dayjs(date).format(pattern));
function changeLabels(opt: any[]) {
const arr = opt.map((d) => d.value);
selectedLabels.value = arr;
}
const handleSelectionChange = (arr: Process[]) => {
selectedProcesses.value = arr.map((d: Process) => d.id);
};
async function analyzeEBPF() {
let arr: string[] = selectedLabels.value;
if (selectedLabels.value.includes("0")) {
arr = labels.value.map((d: Option) => d.value);
}
const ranges: { start: number; end: number }[] = [];
const scheduleIdList = ebpfStore.eBPFSchedules.flatMap(
(d: EBPFProfilingSchedule) => {
const l = d.process.labels.find((d: string) => arr.includes(d));
const i = selectedProcesses.value.includes(d.process.id);
if (l || i) {
ranges.push({
start: d.startTime,
end: d.endTime,
});
return d.scheduleId;
}
}
);
let timeRanges: { start: number; end: number }[] = [];
for (const r of ranges) {
if (timeRanges.length) {
for (const t of timeRanges) {
if (r.start > t.start && r.start < t.end) {
if (r.end > t.end) {
t.end = r.end;
}
}
}
} else {
timeRanges.push(r);
}
}
const res = await ebpfStore.getEBPFAnalyze({
scheduleIdList,
timeRanges,
});
if (res.data.errors) {
ElMessage.error(res.data.errors);
return;
}
}
function visTimeline() {
if (visGraph.value) {
visGraph.value.destroy();
}
labels.value = [{ label: "All", value: "0" }];
selectedLabels.value = ["0"];
processes.value = [];
const schedules = ebpfStore.eBPFSchedules.map(
(d: EBPFProfilingSchedule, index: number) => {
for (const l of d.process.labels) {
labels.value.push({ label: l, value: l });
}
processes.value.push(d.process);
return {
id: index + 1,
content: d.process.name,
start: dateFormat(d.startTime),
end: dateFormat(d.endTime),
};
}
);
searchProcesses();
if (!timeline.value) {
return;
}
const h = timeline.value.getBoundingClientRect().height;
const items: any = new DataSet(schedules);
const options = {
height: h,
width: "100%",
locale: "en",
};
visGraph.value = new Timeline(timeline.value, items, options);
}
function changePage(pageIndex: number) {
searchProcesses(pageIndex);
}
function searchProcesses(pageIndex?: any) {
const arr = processes.value.filter(
(d: { name: string; instanceName: string }) =>
d.name.includes(searchText.value) ||
d.instanceName.includes(searchText.value)
);
currentProcesses.value = arr.splice(
(pageIndex - 1 || 0) * pageSize,
pageSize * (pageIndex || 1)
);
}
watch(
() => ebpfStore.eBPFSchedules,
() => {
visTimeline();
}
);
</script>
<style lang="scss" scoped>
.filters {
margin: 5px 0;
}
.schedules {
width: calc(100% - 5px);
margin: 0 5px 5px 0;
height: calc(100% - 60px);
min-height: 150px;
}
.inputs {
width: 300px;
}
.input-with-search {
width: 650px;
margin-bottom: 5px;
}
.pagination {
margin-top: 10px;
}
</style>

View File

@@ -0,0 +1,148 @@
<!-- 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. -->
<template>
<div id="graph-stack" ref="graph">
<span class="tip" v-show="ebpfStore.tip">{{ ebpfStore.tip }}</span>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import * as d3 from "d3";
import d3tip from "d3-tip";
import { flamegraph } from "d3-flame-graph";
import { useEbpfStore } from "@/store/modules/ebpf";
import { StackElement } from "@/types/ebpf";
import "d3-flame-graph/dist/d3-flamegraph.css";
/*global Nullable*/
const ebpfStore = useEbpfStore();
const stackTree = ref<Nullable<StackElement>>(null);
const graph = ref<Nullable<HTMLDivElement>>(null);
const flameChart = ref<any>(null);
function drawGraph() {
if (flameChart.value) {
flameChart.value.destroy();
}
if (!ebpfStore.analyzeTrees.length) {
return (stackTree.value = null);
}
stackTree.value = processTree(ebpfStore.analyzeTrees);
const w = (graph.value && graph.value.getBoundingClientRect().width) || 10;
flameChart.value = flamegraph()
.width(w - 15)
.cellHeight(18)
.transitionDuration(750)
.minFrameSize(5)
.transitionEase(d3.easeCubic as any)
.sort(true)
.title("")
.selfValue(false)
.setColorMapper((d, originalColor) =>
d.highlight ? "#6aff8f" : originalColor
);
const tip = (d3tip as any)()
.attr("class", "d3-tip")
.direction("w")
.html(
(d: { data: StackElement }) =>
`<div class="mb-5">Symbol: ${d.data.name}</div><div class="mb-5">Dump Count: ${d.data.dumpCount}</div>`
);
flameChart.value.tooltip(tip);
d3.select("#graph-stack").datum(stackTree.value).call(flameChart.value);
}
function processTree(arr: StackElement[]) {
const copyArr = JSON.parse(JSON.stringify(arr));
const obj: any = {};
let res = null;
let min = 1;
let max = 1;
for (const item of copyArr) {
item.originId = item.id;
item.name = item.symbol;
delete item.id;
obj[item.originId] = item;
if (item.dumpCount > max) {
max = item.dumpCount;
}
if (item.dumpCount < min) {
min = item.dumpCount;
}
}
const scale = d3.scaleLinear().domain([min, max]).range([1, 200]);
for (const item of copyArr) {
if (item.parentId === "0") {
const val = Number(scale(item.dumpCount).toFixed(4));
res = item;
res.value = val;
}
for (const key in obj) {
if (item.originId === obj[key].parentId) {
const val = Number(scale(obj[key].dumpCount).toFixed(4));
obj[key].value = val;
if (item.children) {
item.children.push(obj[key]);
} else {
item.children = [obj[key]];
}
}
}
}
treeForeach([res], (node: StackElement) => {
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;
}
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,
() => {
drawGraph();
}
);
</script>
<style>
#graph-stack {
width: 100%;
height: 100%;
cursor: pointer;
}
.tip {
display: inline-block;
width: 100%;
text-align: center;
color: red;
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,165 @@
<!-- 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. -->
<template>
<div class="ebpf-task" v-if="eBPFStore.couldProfiling">
<div>
<div class="label">{{ t("labels") }}</div>
<Selector
class="profile-input"
size="small"
:value="labels"
:options="eBPFStore.labels"
placeholder="Select labels"
:multiple="true"
@change="changeLabel"
/>
</div>
<div>
<div class="label">{{ t("targetType") }}</div>
<Selector
class="profile-input"
size="small"
:value="type"
:options="TargetTypes"
placeholder="Select a type"
:isRemote="true"
@change="changeType"
/>
</div>
<div>
<div class="label">{{ t("monitorTime") }}</div>
<div>
<Radio
:value="monitorTime"
:options="InitTaskField.monitorTimeEn"
@change="changeMonitorTime"
/>
<span class="date">
<TimePicker
:value="time"
position="bottom"
format="YYYY-MM-DD HH:mm:ss"
@input="changeTimeRange"
/>
</span>
</div>
</div>
<div>
<div class="label">{{ t("monitorDuration") }}</div>
<el-input
class="profile-input"
v-model="monitorDuration"
size="small"
placeholder="none"
type="number"
:min="1"
:max="60"
/>
Min
</div>
<div>
<el-button @click="createTask" type="primary" class="create-task-btn">
{{ t("createTask") }}
</el-button>
</div>
</div>
<div v-else>{{ t("ebpfTip") }}</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useEbpfStore } from "@/store/modules/ebpf";
import { useSelectorStore } from "@/store/modules/selectors";
import { useAppStoreWithOut } from "@/store/modules/app";
import { ElMessage } from "element-plus";
import { InitTaskField, TargetTypes } from "./data";
/* global defineEmits */
const emits = defineEmits(["close"]);
const eBPFStore = useEbpfStore();
const selectorStore = useSelectorStore();
const appStore = useAppStoreWithOut();
const { t } = useI18n();
const labels = ref<string[]>([]);
const type = ref<string>(TargetTypes[0].value);
const monitorTime = ref<string>(InitTaskField.monitorTimeEn[0].value);
const monitorDuration = ref<number>(10);
const time = ref<Date>(appStore.durationRow.start);
function changeMonitorTime(opt: string) {
monitorTime.value = opt;
}
function changeLabel(opt: any[]) {
labels.value = opt.map((d) => d.value);
}
function changeType(opt: any[]) {
type.value = opt[0].value;
}
async function createTask() {
if (!labels.value.length) {
ElMessage.warning("no labels");
return;
}
const date = monitorTime.value === "0" ? new Date() : time.value;
const params = {
serviceId: selectorStore.currentService.id,
processLabels: labels.value,
startTime: date.getTime(),
duration: monitorDuration.value * 60,
targetType: "ON_CPU",
};
const res = await eBPFStore.createTask(params);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
if (!res.data.createTaskData.status) {
ElMessage.error(res.data.createTaskData.errorReason);
return;
}
ElMessage.success("Task created successfully");
emits("close");
}
function changeTimeRange(val: Date) {
time.value = val;
}
</script>
<style lang="scss" scoped>
.ebpf-task {
margin: 0 auto;
width: 400px;
}
.date {
font-size: 12px;
}
.label {
margin-top: 10px;
font-size: 14px;
}
.profile-input {
width: 300px;
}
.create-task-btn {
width: 300px;
margin-top: 50px;
}
</style>

View File

@@ -0,0 +1,195 @@
<!-- 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. -->
<template>
<div class="profile-task-list flex-v">
<div class="profile-task-wrapper flex-v">
<div class="profile-t-tool flex-h">{{ t("taskList") }}</div>
<div class="profile-t-wrapper">
<div class="no-data" v-show="!ebpfStore.taskList.length">
{{ t("noData") }}
</div>
<table class="profile-t">
<tr
class="profile-tr cp"
v-for="(i, index) in ebpfStore.taskList"
@click="changeTask(i)"
:key="index"
>
<td
class="profile-td"
:class="{
selected: selectedTask.taskId === i.taskId,
}"
>
<div class="ell">
<span>{{ i.processLabels.join(" ") }}</span>
<a class="profile-btn r" @click="viewDetail = true">
<Icon iconName="view" size="middle" />
</a>
</div>
<div class="grey ell sm">
<span class="mr-10 sm">{{ dateFormat(i.taskStartTime) }}</span>
<span class="mr-10 sm">
{{
dateFormat(i.taskStartTime + i.fixedTriggerDuration * 1000)
}}
</span>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
<el-dialog
v-model="viewDetail"
:destroy-on-close="true"
fullscreen
@closed="viewDetail = false"
>
<div class="profile-detail flex-v">
<div>
<h5 class="mb-10">{{ t("task") }}.</h5>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("taskId") }}:</span>
<span class="g-sm-8 wba">
{{ selectedTask.taskId }}
</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("service") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.serviceName }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("labels") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.processLabels }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("monitorTime") }}:</span>
<span class="g-sm-8 wba">
{{ dateFormat(selectedTask.taskStartTime) }}
</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("monitorDuration") }}:</span>
<span class="g-sm-8 wba">
{{ selectedTask.fixedTriggerDuration / 60 }} min
</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("triggerType") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.triggerType }}</span>
</div>
<div class="mb-10 clear item">
<span class="g-sm-4 grey">{{ t("targetType") }}:</span>
<span class="g-sm-8 wba">{{ selectedTask.targetType }}</span>
</div>
</div>
</div>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import { useEbpfStore } from "@/store/modules/ebpf";
import { EBPFTaskList } from "@/types/ebpf";
import { ElMessage } from "element-plus";
const { t } = useI18n();
const ebpfStore = useEbpfStore();
const dateFormat = (date: number, pattern = "YYYY-MM-DD HH:mm:ss") =>
dayjs(date).format(pattern);
const selectedTask = ref<EBPFTaskList | Record<string, never>>({});
const viewDetail = ref<boolean>(false);
async function changeTask(item: EBPFTaskList) {
selectedTask.value = item;
const res = await ebpfStore.getEBPFSchedules({
taskId: item.taskId,
});
if (res.errors) {
ElMessage.error(res.errors);
}
}
watch(
() => ebpfStore.taskList,
() => {
selectedTask.value = ebpfStore.taskList[0] || {};
}
);
</script>
<style lang="scss" scoped>
.profile-task-list {
width: 300px;
height: calc(100% - 10px);
overflow: auto;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.item span {
height: 21px;
}
.profile-td {
padding: 5px 10px;
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
&.selected {
background-color: #ededed;
}
}
.no-data {
text-align: center;
margin-top: 10px;
}
.profile-t-wrapper {
overflow: auto;
flex-grow: 1;
}
.profile-t {
width: 100%;
border-spacing: 0;
table-layout: fixed;
flex-grow: 1;
position: relative;
border: none;
}
.profile-tr {
&:hover {
background-color: rgba(0, 0, 0, 0.04);
}
}
.profile-t-tool {
padding: 5px 10px;
font-weight: bold;
border-right: 1px solid rgba(0, 0, 0, 0.07);
border-bottom: 1px solid rgba(0, 0, 0, 0.07);
background: #f3f4f9;
}
.profile-btn {
color: #3d444f;
padding: 1px 3px;
border-radius: 2px;
font-size: 12px;
float: right;
}
</style>

View File

@@ -0,0 +1,51 @@
/**
* 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 const ProfileMode: any[] = [
{ label: "Include Children", value: "include" },
{ label: "Exclude Children", value: "exclude" },
];
export const NewTaskField = {
service: { key: "", label: "None" },
monitorTime: { key: "0", label: "monitor now" },
monitorDuration: { key: 5, label: "5 min" },
minThreshold: 0,
dumpPeriod: { key: 10, label: "10ms" },
endpointName: "",
maxSamplingCount: { key: 5, label: "5" },
};
export const TargetTypes = [{ label: "ON_CPU", value: "ON_CPU" }];
export const InitTaskField = {
monitorTimeEn: [
{ value: "0", label: "monitor now" },
{ value: "1", label: "set start time" },
],
monitorTimeCn: [
{ value: "0", label: "此刻" },
{ value: "1", label: "设置时间" },
],
monitorDuration: [
{ value: "5", label: "5 min" },
{ value: "10", label: "10 min" },
{ value: "15", label: "15 min" },
],
};
export const TableHeader = [
{ property: "name", label: "Name" },
{ property: "instanceName", label: "Instance Name" },
];

View File

@@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License. -->
<template>
<div class="flex-h header">
<!-- <div class="mr-10" v-if="dashboardStore.entity==='All'">
<span class="grey mr-5">{{ t("service") }}:</span>
<Selector
size="small"
:value="service.value"
:options="profileStore.services"
placeholder="Select a service"
@change="changeService"
/>
</div> -->
<div class="mr-10">
<span class="grey mr-5">{{ t("endpointName") }}:</span>
<el-input v-model="endpointName" class="name" size="small" />
<Selector
class="name"
size="small"
:value="endpointName"
:options="profileStore.endpoints"
placeholder="Select a endpoint"
:isRemote="true"
@change="changeEndpoint"
@query="searchEndpoints"
/>
</div>
<el-button
class="search-btn"
@@ -65,27 +64,30 @@ const appStore = useAppStoreWithOut();
const selectorStore = useSelectorStore();
const dashboardStore = useDashboardStore();
const { t } = useI18n();
// const service = ref<any>({});
const endpointName = ref<string>("");
const newTask = ref<boolean>(false);
searchTasks();
// getServices();
searchEndpoints("");
// async function getServices() {
// const res = await profileStore.getServices(dashboardStore.layerId);
async function searchEndpoints(keyword: string) {
if (!selectorStore.currentService) {
return;
}
const service = selectorStore.currentService.id;
const res = await profileStore.getEndpoints(service, keyword);
// if (res.errors) {
// ElMessage.error(res.errors);
// return;
// }
// service.value = profileStore.services[0];
// searchTasks();
// }
if (res.errors) {
ElMessage.error(res.errors);
return;
}
endpointName.value = profileStore.endpoints[0].value;
}
function changeEndpoint(opt: any[]) {
endpointName.value = opt[0].value;
}
// function changeService(opt: any[]) {
// service.value = opt[0];
// }
async function searchTasks() {
profileStore.setConditions({
serviceId:

View File

@@ -17,7 +17,16 @@ limitations under the License. -->
<div class="profile-task">
<div>
<div class="label">{{ t("endpointName") }}</div>
<el-input v-model="endpointName" class="profile-input" size="small" />
<Selector
class="profile-input"
size="small"
:value="endpointName"
:options="profileStore.endpoints"
placeholder="Select a endpoint"
:isRemote="true"
@change="changeEndpoint"
@query="searchEndpoints"
/>
</div>
<div>
<div class="label">{{ t("monitorTime") }}</div>
@@ -105,6 +114,20 @@ const minThreshold = ref<number>(0);
const dumpPeriod = ref<string>(InitTaskField.dumpPeriod[0].value);
const maxSamplingCount = ref<string>(InitTaskField.maxSamplingCount[0].value);
async function searchEndpoints(keyword: string) {
if (!selectorStore.currentService) {
return;
}
const service = selectorStore.currentService.id;
const res = await profileStore.getEndpoints(service, keyword);
if (res.errors) {
ElMessage.error(res.errors);
return;
}
endpointName.value = profileStore.taskEndpoints[0].value;
}
function changeMonitorTime(opt: string) {
monitorTime.value = opt;
}
@@ -121,10 +144,13 @@ function changeMaxSamplingCount(opt: any[]) {
maxSamplingCount.value = opt[0].value;
}
function changeEndpoint(opt: any[]) {
endpointName.value = opt[0].value;
}
async function createTask() {
emits("close");
const date =
monitorTime.value === "0" ? appStore.durationRow.start : time.value;
const date = monitorTime.value === "0" ? new Date() : time.value;
const params = {
serviceId: selectorStore.currentService.id,
endpointName: endpointName.value,
@@ -153,7 +179,7 @@ function changeTimeRange(val: Date) {
<style lang="scss" scoped>
.profile-task {
margin: 0 auto;
width: 350px;
width: 400px;
}
.date {

View File

@@ -36,7 +36,7 @@ limitations under the License. -->
<div class="ell">
<span>{{ i.endpointName }}</span>
<a class="profile-btn r" @click="viewTask($event, i)">
<Icon iconName="library_books" size="middle" />
<Icon iconName="view" size="middle" />
</a>
</div>
<div class="grey ell sm">

View File

@@ -88,7 +88,7 @@ limitations under the License. -->
</div>
<div>
<el-button
class="grey"
class="grey small"
:class="{ ghost: displayMode !== 'List' }"
@click="displayMode = 'List'"
>
@@ -96,7 +96,7 @@ limitations under the License. -->
{{ t("list") }}
</el-button>
<el-button
class="grey"
class="grey small"
:class="{ ghost: displayMode !== 'Tree' }"
@click="displayMode = 'Tree'"
>
@@ -104,7 +104,7 @@ limitations under the License. -->
{{ t("tree") }}
</el-button>
<el-button
class="grey"
class="grey small"
:class="{ ghost: displayMode !== 'Table' }"
@click="displayMode = 'Table'"
>
@@ -112,7 +112,7 @@ limitations under the License. -->
{{ t("table") }}
</el-button>
<el-button
class="grey"
class="grey small"
:class="{ ghost: displayMode !== 'Statistics' }"
@click="displayMode = 'Statistics'"
>