mirror of
https://github.com/apache/skywalking-booster-ui.git
synced 2026-04-24 15:25:21 +00:00
Add the front end for pprof profiling (#541)
This commit is contained in:
79
src/graphql/fragments/pprof.ts
Normal file
79
src/graphql/fragments/pprof.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 GetPprofTaskList = {
|
||||
variable: "$request: PprofTaskListRequest!",
|
||||
query: `
|
||||
pprofTaskList: queryPprofTaskList(request: $request) {
|
||||
errorReason
|
||||
tasks {
|
||||
id
|
||||
serviceId
|
||||
serviceInstanceIds
|
||||
createTime
|
||||
events
|
||||
duration
|
||||
dumpPeriod
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const GetPprofTaskProcess = {
|
||||
variable: "$taskId: String!",
|
||||
query: `
|
||||
taskProgress: queryPprofTaskProgress(taskId: $taskId) {
|
||||
logs {
|
||||
id
|
||||
instanceId
|
||||
instanceName
|
||||
operationType
|
||||
operationTime
|
||||
}
|
||||
errorInstanceIds
|
||||
successInstanceIds
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const CreatePprofTask = {
|
||||
variable: "$pprofTaskCreationRequest: PprofTaskCreationRequest!",
|
||||
query: `
|
||||
task: createPprofTask(pprofTaskCreationRequest: $pprofTaskCreationRequest) {
|
||||
id
|
||||
errorReason
|
||||
code
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const GetPprofAnalyze = {
|
||||
variable: "$request: PprofAnalyzationRequest!",
|
||||
query: `
|
||||
analysisResult: queryPprofAnalyze(request: $request) {
|
||||
tree {
|
||||
elements {
|
||||
id
|
||||
parentId
|
||||
symbol: codeSignature
|
||||
dumpCount: total
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import * as event from "./query/event";
|
||||
import * as ebpf from "./query/ebpf";
|
||||
import * as demandLog from "./query/demand-log";
|
||||
import * as asyncProfile from "./query/async-profile";
|
||||
import * as pprof from "./query/pprof";
|
||||
|
||||
const query: { [key: string]: string } = {
|
||||
...app,
|
||||
@@ -41,6 +42,7 @@ const query: { [key: string]: string } = {
|
||||
...ebpf,
|
||||
...demandLog,
|
||||
...asyncProfile,
|
||||
...pprof,
|
||||
};
|
||||
class Graphql {
|
||||
queryData = "";
|
||||
|
||||
26
src/graphql/query/pprof.ts
Normal file
26
src/graphql/query/pprof.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { GetPprofTaskList, GetPprofTaskProcess, CreatePprofTask, GetPprofAnalyze } from "../fragments/pprof";
|
||||
|
||||
export const getPprofTaskList = `query getPprofTaskList(${GetPprofTaskList.variable}) {${GetPprofTaskList.query}}`;
|
||||
|
||||
export const getPprofTaskProcess = `query getPprofTaskProcess(${GetPprofTaskProcess.variable}) {${GetPprofTaskProcess.query}}`;
|
||||
|
||||
export const savePprofTask = `mutation createPprofTask(${CreatePprofTask.variable}) {${CreatePprofTask.query}}`;
|
||||
|
||||
export const getPprofAnalyze = `query getPprofAnalyze(${GetPprofAnalyze.variable}) {${GetPprofAnalyze.query}}`;
|
||||
@@ -395,6 +395,7 @@ const msg = {
|
||||
errorInstances: "Error Instances",
|
||||
successInstances: "Success Instances",
|
||||
profilingEvents: "Async Profiling Events",
|
||||
pprofEvent: "PProf Event",
|
||||
execArgs: "Exec Args",
|
||||
instances: "Instances",
|
||||
snapshot: "Snapshot",
|
||||
@@ -407,7 +408,16 @@ const msg = {
|
||||
maxDuration: "Max Duration",
|
||||
minutes: "Minutes",
|
||||
invalidProfilingDurationRange: "Please enter a valid duration between 1 and 900 seconds",
|
||||
invalidPprofDuration: "Please enter a valid duration in minutes",
|
||||
invalidPprofDumpPeriod: "Please enter a valid dump period",
|
||||
pprofDumpPeriod: "Dump Period",
|
||||
pprofDurationHint: "Duration is required for CPU, BLOCK and MUTEX tasks.",
|
||||
pprofDumpPeriodBlockHint:
|
||||
"For BLOCK tasks, dump period is required and represents the blocked nanoseconds sampling rate. 1 samples every event.",
|
||||
pprofDumpPeriodMutexHint:
|
||||
"For MUTEX tasks, dump period is required and represents the contention occurrences sampling rate. 1 samples every event.",
|
||||
taskCreatedSuccessfully: "Task created successfully",
|
||||
taskCreationFailed: "Task creation failed",
|
||||
runQuery: "Run Query",
|
||||
spansTable: "Spans Table",
|
||||
download: "Download",
|
||||
|
||||
@@ -394,6 +394,7 @@ const msg = {
|
||||
errorInstances: "Error Instances",
|
||||
successInstances: "Success Instances",
|
||||
profilingEvents: "Async Profiling Events",
|
||||
pprofEvent: "Evento PProf",
|
||||
execArgs: "Exec Args",
|
||||
instances: "Instances",
|
||||
snapshot: "Snapshot",
|
||||
@@ -407,7 +408,16 @@ const msg = {
|
||||
maxDuration: "Duración Máxima",
|
||||
minutes: "Minutos",
|
||||
invalidProfilingDurationRange: "Por favor ingrese una duración válida entre 1 y 900 segundos",
|
||||
invalidPprofDuration: "Por favor ingrese una duración válida en minutos",
|
||||
invalidPprofDumpPeriod: "Por favor ingrese un período de volcado válido",
|
||||
pprofDumpPeriod: "Período de Volcado",
|
||||
pprofDurationHint: "La duración es obligatoria para tareas CPU, BLOCK y MUTEX.",
|
||||
pprofDumpPeriodBlockHint:
|
||||
"Para tareas BLOCK, el período de volcado es obligatorio y representa la tasa de muestreo en nanosegundos bloqueados. 1 muestrea todos los eventos.",
|
||||
pprofDumpPeriodMutexHint:
|
||||
"Para tareas MUTEX, el período de volcado es obligatorio y representa la tasa de muestreo por ocurrencias de contención. 1 muestrea todos los eventos.",
|
||||
taskCreatedSuccessfully: "Tarea creada exitosamente",
|
||||
taskCreationFailed: "Error al crear la tarea",
|
||||
runQuery: "Ejecutar Consulta",
|
||||
spansTable: "Tabla de Lapso",
|
||||
download: "Descargar",
|
||||
|
||||
@@ -393,6 +393,7 @@ const msg = {
|
||||
errorInstances: "错误的实例",
|
||||
successInstances: "成功的实例",
|
||||
profilingEvents: "异步分析事件",
|
||||
pprofEvent: "PProf 事件",
|
||||
execArgs: "String任务扩展",
|
||||
instances: "实例",
|
||||
snapshot: "快照",
|
||||
@@ -405,7 +406,14 @@ const msg = {
|
||||
maxDuration: "最大时长",
|
||||
minutes: "分钟",
|
||||
invalidProfilingDurationRange: "请输入1到900秒之间的有效时长",
|
||||
invalidPprofDuration: "请输入有效的分钟数",
|
||||
invalidPprofDumpPeriod: "请输入有效的采样率",
|
||||
pprofDumpPeriod: "采样率",
|
||||
pprofDurationHint: "CPU、BLOCK 和 MUTEX 任务必须设置采样时长。",
|
||||
pprofDumpPeriodBlockHint: "BLOCK 任务必须设置采样率,单位是纳秒。设置为 1 表示采集所有阻塞事件。",
|
||||
pprofDumpPeriodMutexHint: "MUTEX 任务必须设置采样率,单位是竞争次数。设置为 1 表示采集所有互斥竞争事件。",
|
||||
taskCreatedSuccessfully: "任务创建成功",
|
||||
taskCreationFailed: "任务创建失败",
|
||||
runQuery: "运行查询",
|
||||
spansTable: "Spans表格",
|
||||
download: "下载",
|
||||
|
||||
@@ -51,6 +51,7 @@ export const ControlsTypes = [
|
||||
WidgetType.Ebpf,
|
||||
WidgetType.NetworkProfiling,
|
||||
WidgetType.AsyncProfiling,
|
||||
WidgetType.Pprof,
|
||||
WidgetType.ThirdPartyApp,
|
||||
WidgetType.ContinuousProfiling,
|
||||
WidgetType.TaskTimeline,
|
||||
|
||||
138
src/store/modules/pprof.ts
Normal file
138
src/store/modules/pprof.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { defineStore } from "pinia";
|
||||
import type { PprofTask, PprofTaskCreationRequest, PprofStackElement, PprofTaskProgress } from "@/types/pprof";
|
||||
import { store } from "@/store";
|
||||
import graphql from "@/graphql";
|
||||
import { useAppStoreWithOut } from "@/store/modules/app";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import type { Instance } from "@/types/selector";
|
||||
|
||||
interface PprofState {
|
||||
taskList: Array<PprofTask>;
|
||||
selectedTask: Nullable<PprofTask>;
|
||||
taskProgress: Nullable<PprofTaskProgress>;
|
||||
instances: Instance[];
|
||||
analyzeTrees: PprofStackElement[];
|
||||
loadingTree: boolean;
|
||||
loadingTasks: boolean;
|
||||
}
|
||||
|
||||
export const pprofStore = defineStore({
|
||||
id: "pprof",
|
||||
state: (): PprofState => ({
|
||||
taskList: [],
|
||||
selectedTask: null,
|
||||
taskProgress: null,
|
||||
instances: [],
|
||||
analyzeTrees: [],
|
||||
loadingTree: false,
|
||||
loadingTasks: false,
|
||||
}),
|
||||
actions: {
|
||||
setSelectedTask(task: Nullable<PprofTask>) {
|
||||
this.selectedTask = task || {};
|
||||
},
|
||||
setAnalyzeTrees(tree: PprofStackElement[]) {
|
||||
this.analyzeTrees = tree;
|
||||
},
|
||||
async getTaskList() {
|
||||
const selectorStore = useSelectorStore();
|
||||
if (!selectorStore.currentService?.id) {
|
||||
return;
|
||||
}
|
||||
this.loadingTasks = true;
|
||||
const response = await graphql.query("getPprofTaskList").params({
|
||||
request: {
|
||||
serviceId: selectorStore.currentService?.id,
|
||||
limit: 10000,
|
||||
},
|
||||
});
|
||||
this.loadingTasks = false;
|
||||
if (response.errors) {
|
||||
return response;
|
||||
}
|
||||
this.taskList = response.data.pprofTaskList.tasks || [];
|
||||
this.selectedTask = this.taskList[0] || {};
|
||||
this.setAnalyzeTrees([]);
|
||||
this.setSelectedTask(this.selectedTask);
|
||||
if (!this.taskList.length) {
|
||||
return response;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
async getTaskLogs(param: { taskId: string }) {
|
||||
const response = await graphql.query("getPprofTaskProcess").params(param);
|
||||
if (response.errors) {
|
||||
return response;
|
||||
}
|
||||
this.taskProgress = response.data.taskProgress;
|
||||
return response;
|
||||
},
|
||||
async getServiceInstances(param: { serviceId: string; isRelation?: boolean }) {
|
||||
if (!param.serviceId) {
|
||||
return null;
|
||||
}
|
||||
const response = await graphql.query("queryInstances").params({
|
||||
serviceId: param.serviceId,
|
||||
duration: useAppStoreWithOut().durationTime,
|
||||
});
|
||||
if (!response.errors) {
|
||||
this.instances = (response.data.pods || []).map((d: Instance) => {
|
||||
d.value = d.id || "";
|
||||
return d;
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
async createTask(param: PprofTaskCreationRequest) {
|
||||
if (!param.serviceId) {
|
||||
return;
|
||||
}
|
||||
const response = await graphql.query("savePprofTask").params({ pprofTaskCreationRequest: param });
|
||||
if (response.errors) {
|
||||
return response;
|
||||
}
|
||||
this.getTaskList();
|
||||
return response;
|
||||
},
|
||||
async getPprofAnalyze(params: { taskId: string; instanceIds: Array<string> }) {
|
||||
if (!params.instanceIds.length) {
|
||||
this.analyzeTrees = [];
|
||||
return new Promise((resolve) => resolve({}));
|
||||
}
|
||||
this.loadingTree = true;
|
||||
const response = await graphql.query("getPprofAnalyze").params({ request: params });
|
||||
this.loadingTree = false;
|
||||
if (response.errors) {
|
||||
this.analyzeTrees = [];
|
||||
return response;
|
||||
}
|
||||
const { analysisResult } = response.data;
|
||||
if (!analysisResult) {
|
||||
this.analyzeTrees = [];
|
||||
return response;
|
||||
}
|
||||
this.analyzeTrees = [analysisResult.tree];
|
||||
return response;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function usePprofStore() {
|
||||
return pprofStore(store);
|
||||
}
|
||||
72
src/types/pprof.ts
Normal file
72
src/types/pprof.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 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 type PprofTask = {
|
||||
id: string;
|
||||
serviceId: string;
|
||||
serviceInstanceIds: string[];
|
||||
createTime: number;
|
||||
events: string;
|
||||
duration?: number;
|
||||
dumpPeriod?: number;
|
||||
logs?: PprofTaskLog[];
|
||||
errorInstanceIds?: string[];
|
||||
successInstanceIds?: string[];
|
||||
};
|
||||
|
||||
export type PprofTaskCreationRequest = {
|
||||
serviceId: string;
|
||||
serviceInstanceIds: string[];
|
||||
events: string;
|
||||
duration?: number;
|
||||
dumpPeriod?: number;
|
||||
};
|
||||
|
||||
export type PprofStackElement = {
|
||||
id: string;
|
||||
parentId: string;
|
||||
symbol: string;
|
||||
dumpCount: number;
|
||||
self: number;
|
||||
elements?: PprofStackElement[];
|
||||
};
|
||||
|
||||
export type PprofTaskProgress = {
|
||||
errorInstanceIds: string[];
|
||||
successInstanceIds: string[];
|
||||
logs: PprofTaskLog[];
|
||||
};
|
||||
|
||||
type PprofTaskLog = {
|
||||
id: string;
|
||||
instanceId: string;
|
||||
instanceName: string;
|
||||
operationType: string;
|
||||
operationTime: number;
|
||||
};
|
||||
|
||||
export type PprofFlameGraphNode = {
|
||||
id: string;
|
||||
originId: string;
|
||||
name: string;
|
||||
parentId: string;
|
||||
symbol: string;
|
||||
dumpCount: number;
|
||||
self: number;
|
||||
value: number;
|
||||
children?: PprofFlameGraphNode[];
|
||||
};
|
||||
@@ -22,3 +22,14 @@ export function treeForeach(tree: any, func: (node: any) => void) {
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
export function escapeHtml(str: string): string {
|
||||
const htmlEscapes: Record<string, string> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
||||
}
|
||||
|
||||
86
src/views/dashboard/controls/Pprof.vue
Normal file
86
src/views/dashboard/controls/Pprof.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<!-- 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="operation cp">
|
||||
<Icon iconName="ellipsis_v" size="middle" />
|
||||
</span>
|
||||
</template>
|
||||
<div class="tools" @click="removeWidget">
|
||||
<span>{{ t("delete") }}</span>
|
||||
</div>
|
||||
</el-popover>
|
||||
<Header />
|
||||
<Content :config="props.data" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useDashboardStore } from "@/store/modules/dashboard";
|
||||
import Content from "../related/pprof/Content.vue";
|
||||
import Header from "../related/pprof/Header.vue";
|
||||
import type { LayoutConfig } from "@/types/dashboard";
|
||||
|
||||
/*global defineProps*/
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object as PropType<LayoutConfig>,
|
||||
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: $font-size-smaller;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.operation {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 10px;
|
||||
font-size: $font-size-smaller;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.tools {
|
||||
padding: 5px 0;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: $active-color;
|
||||
background-color: $popper-hover-bg-color;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,7 @@ import Event from "./Event.vue";
|
||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||
import ContinuousProfiling from "./ContinuousProfiling.vue";
|
||||
import AsyncProfiling from "./AsyncProfiling.vue";
|
||||
import Pprof from "./Pprof.vue";
|
||||
import TimeRange from "./TimeRange.vue";
|
||||
import ThirdPartyApp from "./ThirdPartyApp.vue";
|
||||
import TaskTimeline from "./TaskTimeline.vue";
|
||||
@@ -45,6 +46,7 @@ export default {
|
||||
NetworkProfiling,
|
||||
ContinuousProfiling,
|
||||
AsyncProfiling,
|
||||
Pprof,
|
||||
TimeRange,
|
||||
ThirdPartyApp,
|
||||
TaskTimeline,
|
||||
|
||||
@@ -26,6 +26,7 @@ import Event from "./Event.vue";
|
||||
import NetworkProfiling from "./NetworkProfiling.vue";
|
||||
import ContinuousProfiling from "./ContinuousProfiling.vue";
|
||||
import AsyncProfiling from "./AsyncProfiling.vue";
|
||||
import Pprof from "./Pprof.vue";
|
||||
import TimeRange from "./TimeRange.vue";
|
||||
import ThirdPartyApp from "./ThirdPartyApp.vue";
|
||||
import TaskTimeline from "./TaskTimeline.vue";
|
||||
@@ -45,5 +46,6 @@ export default {
|
||||
ThirdPartyApp,
|
||||
ContinuousProfiling,
|
||||
AsyncProfiling,
|
||||
Pprof,
|
||||
TaskTimeline,
|
||||
};
|
||||
|
||||
@@ -151,6 +151,7 @@ export enum WidgetType {
|
||||
NetworkProfiling = "NetworkProfiling",
|
||||
ContinuousProfiling = "ContinuousProfiling",
|
||||
AsyncProfiling = "AsyncProfiling",
|
||||
Pprof = "Pprof",
|
||||
ThirdPartyApp = "ThirdPartyApp",
|
||||
TaskTimeline = "TaskTimeline",
|
||||
}
|
||||
@@ -173,6 +174,7 @@ export const ServiceTools = [
|
||||
{ name: "insert_chart", content: "Add eBPF Profiling", id: WidgetType.Ebpf },
|
||||
{ name: "continuous_profiling", content: "Add Continuous Profiling", id: WidgetType.ContinuousProfiling },
|
||||
{ name: "async_profiling", content: "Add Async Profiling", id: WidgetType.AsyncProfiling },
|
||||
{ name: "chart", content: "Add PProf Profiling", id: WidgetType.Pprof },
|
||||
{ name: "assignment", content: "Add Log", id: WidgetType.Log },
|
||||
{ name: "demand", content: "Add On Demand Log", id: WidgetType.DemandLog },
|
||||
{ name: "event", content: "Add Event", id: WidgetType.Event },
|
||||
|
||||
68
src/views/dashboard/related/pprof/Content.vue
Normal file
68
src/views/dashboard/related/pprof/Content.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<!-- 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="mb-20 mt-10">
|
||||
<Filter />
|
||||
</div>
|
||||
<div class="stack" v-loading="pprofStore.loadingTree">
|
||||
<PprofStack />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { usePprofStore } from "@/store/modules/pprof";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import TaskList from "./components/TaskList.vue";
|
||||
import Filter from "./components/Filter.vue";
|
||||
import PprofStack from "./components/PprofStack.vue";
|
||||
|
||||
const pprofStore: ReturnType<typeof usePprofStore> = usePprofStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await pprofStore.getServiceInstances({ serviceId: selectorStore.currentService?.id || "" });
|
||||
if (resp?.errors) {
|
||||
ElMessage.error(resp.errors);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
height: calc(100% - 30px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vis-graph {
|
||||
height: 100%;
|
||||
flex-grow: 2;
|
||||
min-width: 700px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: calc(100% - 330px);
|
||||
}
|
||||
|
||||
.stack {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
height: calc(100% - 100px);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
46
src/views/dashboard/related/pprof/Header.vue
Normal file
46
src/views/dashboard/related/pprof/Header.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- 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">PProf Profiling</div>
|
||||
<el-button class="mr-20" size="small" type="primary" @click="() => (newTask = true)">
|
||||
{{ 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 } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import NewTask from "./components/NewTask.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const newTask = ref<boolean>(false);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
padding: 10px;
|
||||
font-size: $font-size-smaller;
|
||||
border-bottom: 1px solid $border-color;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
}
|
||||
</style>
|
||||
80
src/views/dashboard/related/pprof/components/Filter.vue
Normal file
80
src/views/dashboard/related/pprof/components/Filter.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- 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">
|
||||
<Selector
|
||||
class="filter-selector"
|
||||
:multiple="true"
|
||||
:value="serviceInstanceIds"
|
||||
size="small"
|
||||
:options="instances"
|
||||
placeholder="Select instances"
|
||||
@change="changeInstances"
|
||||
/>
|
||||
<el-button type="primary" size="small" @click="analyzeProfiling">
|
||||
{{ t("analyze") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { usePprofStore } from "@/store/modules/pprof";
|
||||
import type { Instance } from "@/types/selector";
|
||||
import type { Option } from "@/types/app";
|
||||
|
||||
const { t } = useI18n();
|
||||
const pprofStore = usePprofStore();
|
||||
const serviceInstanceIds = ref<string[]>([]);
|
||||
const instances = computed(() =>
|
||||
pprofStore.instances.filter((d: Instance) =>
|
||||
(pprofStore.selectedTask?.successInstanceIds || []).includes(d.id || ""),
|
||||
),
|
||||
);
|
||||
|
||||
function changeInstances(options: Option[]) {
|
||||
serviceInstanceIds.value = options.map((d: Option) => d.value);
|
||||
pprofStore.setAnalyzeTrees([]);
|
||||
}
|
||||
|
||||
async function analyzeProfiling() {
|
||||
const instanceIds = (pprofStore.instances || [])
|
||||
.filter((d: Instance) => (serviceInstanceIds.value || []).includes(d.value))
|
||||
.map((d: Instance) => d.id || "") as string[];
|
||||
const res = await pprofStore.getPprofAnalyze({
|
||||
instanceIds,
|
||||
taskId: pprofStore.selectedTask?.id || "",
|
||||
});
|
||||
if ((res as { errors?: string }).errors) {
|
||||
ElMessage.error((res as { errors: string }).errors);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => pprofStore.selectedTask?.successInstanceIds,
|
||||
(value) => {
|
||||
serviceInstanceIds.value = value || [];
|
||||
pprofStore.setAnalyzeTrees([]);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style>
|
||||
.filter-selector {
|
||||
width: 400px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
185
src/views/dashboard/related/pprof/components/NewTask.vue
Normal file
185
src/views/dashboard/related/pprof/components/NewTask.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<!-- 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="pprof-task">
|
||||
<div>
|
||||
<div class="label">{{ t("instance") }}</div>
|
||||
<Selector
|
||||
class="profile-input"
|
||||
:multiple="true"
|
||||
:value="serviceInstanceIds"
|
||||
size="small"
|
||||
:options="pprofStore.instances"
|
||||
placeholder="Select instances"
|
||||
@change="changeInstances"
|
||||
:filterable="true"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ t("pprofEvent") }}</div>
|
||||
<Radio class="mb-5" :value="eventType" :options="PprofEvents" @change="changeEventType" />
|
||||
</div>
|
||||
<div v-if="requiresDuration">
|
||||
<div class="label">{{ t("duration") }}</div>
|
||||
<Radio class="mb-5" :value="duration" :options="DurationOptions" @change="changeDuration" />
|
||||
<div v-if="duration === DurationOptions[5].value" class="custom-duration">
|
||||
<div class="label">{{ t("customDuration") }} ({{ t("minutes") }})</div>
|
||||
<el-input
|
||||
size="small"
|
||||
class="profile-input"
|
||||
v-model="customDurationMinutes"
|
||||
type="number"
|
||||
:min="1"
|
||||
placeholder="Enter duration in minutes"
|
||||
/>
|
||||
</div>
|
||||
<div class="hint">{{ t("pprofDurationHint") }}</div>
|
||||
</div>
|
||||
<div v-if="requiresDumpPeriod">
|
||||
<div class="label">{{ t("pprofDumpPeriod") }}</div>
|
||||
<el-input
|
||||
size="small"
|
||||
class="profile-input"
|
||||
v-model="dumpPeriod"
|
||||
type="number"
|
||||
:min="1"
|
||||
placeholder="Enter dump period"
|
||||
/>
|
||||
<div class="hint">
|
||||
{{ eventType === "BLOCK" ? t("pprofDumpPeriodBlockHint") : t("pprofDumpPeriodMutexHint") }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="createTask" type="primary" class="create-task-btn" :loading="loading">
|
||||
{{ t("createTask") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { usePprofStore } from "@/store/modules/pprof";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { DurationOptions, PprofEvents, DurationRequiredEvents, DumpPeriodRequiredEvents } from "./data";
|
||||
import type { PprofTaskCreationRequest } from "@/types/pprof";
|
||||
|
||||
/* global defineEmits */
|
||||
const emits = defineEmits(["close"]);
|
||||
const pprofStore = usePprofStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const { t } = useI18n();
|
||||
const serviceInstanceIds = ref<string[]>([]);
|
||||
const eventType = ref<string>(PprofEvents[0].value);
|
||||
const duration = ref<string>(DurationOptions[1].value);
|
||||
const customDurationMinutes = ref<number>(5);
|
||||
const dumpPeriod = ref<number | string>("");
|
||||
const loading = ref<boolean>(false);
|
||||
const requiresDuration = computed(() => DurationRequiredEvents.includes(eventType.value));
|
||||
const requiresDumpPeriod = computed(() => DumpPeriodRequiredEvents.includes(eventType.value));
|
||||
|
||||
function changeDuration(val: string) {
|
||||
duration.value = val;
|
||||
}
|
||||
|
||||
function changeEventType(val: string) {
|
||||
eventType.value = val;
|
||||
}
|
||||
|
||||
function changeInstances(options: { id: string }[]) {
|
||||
serviceInstanceIds.value = options.map((d: { id: string }) => d.id);
|
||||
}
|
||||
|
||||
async function createTask() {
|
||||
const params: PprofTaskCreationRequest = {
|
||||
serviceId: selectorStore.currentService?.id || "",
|
||||
serviceInstanceIds: serviceInstanceIds.value,
|
||||
events: eventType.value,
|
||||
};
|
||||
|
||||
if (requiresDuration.value) {
|
||||
const finalDuration =
|
||||
duration.value === DurationOptions[5].value ? Number(customDurationMinutes.value) : Number(duration.value);
|
||||
if (!finalDuration || finalDuration < 1) {
|
||||
ElMessage.error(t("invalidPprofDuration"));
|
||||
return;
|
||||
}
|
||||
params.duration = finalDuration;
|
||||
}
|
||||
|
||||
if (requiresDumpPeriod.value) {
|
||||
const finalDumpPeriod = Number(dumpPeriod.value);
|
||||
if (!finalDumpPeriod || finalDumpPeriod < 1) {
|
||||
ElMessage.error(t("invalidPprofDumpPeriod"));
|
||||
return;
|
||||
}
|
||||
params.dumpPeriod = finalDumpPeriod;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const res = await pprofStore.createTask(params);
|
||||
loading.value = false;
|
||||
if (!res) {
|
||||
ElMessage.error(t("taskCreationFailed"));
|
||||
return;
|
||||
}
|
||||
if (res.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
return;
|
||||
}
|
||||
const result = res.data?.task;
|
||||
if (!result) {
|
||||
ElMessage.error(t("taskCreationFailed"));
|
||||
return;
|
||||
}
|
||||
if (result.errorReason) {
|
||||
ElMessage.error(result.errorReason);
|
||||
return;
|
||||
}
|
||||
emits("close");
|
||||
ElMessage.success(t("taskCreatedSuccessfully"));
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.pprof-task {
|
||||
margin: 0 auto;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: 10px;
|
||||
font-size: $font-size-normal;
|
||||
}
|
||||
|
||||
.profile-input {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.create-task-btn {
|
||||
width: 600px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.custom-duration {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: $font-size-smaller;
|
||||
color: var(--text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
170
src/views/dashboard/related/pprof/components/PprofStack.vue
Normal file
170
src/views/dashboard/related/pprof/components/PprofStack.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<!-- 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 ref="graph"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
/* global Nullable */
|
||||
import { ref, watch } from "vue";
|
||||
import * as d3 from "d3";
|
||||
import d3tip from "d3-tip";
|
||||
import { flamegraph } from "d3-flame-graph";
|
||||
import { usePprofStore } from "@/store/modules/pprof";
|
||||
import type { PprofStackElement, PprofFlameGraphNode } from "@/types/pprof";
|
||||
import "d3-flame-graph/dist/d3-flamegraph.css";
|
||||
import { treeForeach, escapeHtml } from "@/utils/flameGraph";
|
||||
|
||||
const pprofStore = usePprofStore();
|
||||
const stackTree = ref<Nullable<PprofFlameGraphNode>>(null);
|
||||
const selectStack = ref<Nullable<PprofFlameGraphNode>>(null);
|
||||
const graph = ref<Nullable<HTMLDivElement>>(null);
|
||||
const flameChart = ref<any>(null);
|
||||
const min = ref<number>(1);
|
||||
const max = ref<number>(1);
|
||||
|
||||
function drawGraph() {
|
||||
if (flameChart.value) {
|
||||
flameChart.value.destroy();
|
||||
}
|
||||
if (!pprofStore.analyzeTrees.length || !graph.value) {
|
||||
stackTree.value = null;
|
||||
return;
|
||||
}
|
||||
const root: PprofFlameGraphNode = {
|
||||
parentId: "0",
|
||||
originId: "1",
|
||||
name: "Virtual Root",
|
||||
children: [],
|
||||
value: 0,
|
||||
id: "1",
|
||||
symbol: "Virtual Root",
|
||||
dumpCount: 0,
|
||||
self: 0,
|
||||
};
|
||||
countRange();
|
||||
const elements = processTree((pprofStore.analyzeTrees[0].elements || []) as PprofStackElement[]);
|
||||
if (!elements) {
|
||||
stackTree.value = null;
|
||||
return;
|
||||
}
|
||||
stackTree.value = elements;
|
||||
const treeRoot = { ...root, ...elements };
|
||||
const width = 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: PprofFlameGraphNode }) => {
|
||||
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: PprofFlameGraphNode } & { parent: { data: PprofFlameGraphNode } }) => {
|
||||
const name = escapeHtml(d.data.name);
|
||||
const rateOfParent =
|
||||
(d.parent &&
|
||||
`<div class="mb-5">Percentage Of Selected: ${
|
||||
(
|
||||
(d.data.dumpCount / ((selectStack.value && selectStack.value.dumpCount) || treeRoot.dumpCount)) *
|
||||
100
|
||||
).toFixed(3) + "%"
|
||||
}</div>`) ||
|
||||
"";
|
||||
const rateOfRoot = `<div class="mb-5">Percentage Of Root: ${
|
||||
((d.data.dumpCount / treeRoot.dumpCount) * 100).toFixed(3) + "%"
|
||||
}</div>`;
|
||||
return `<div class="mb-5 name">Symbol: ${name}</div>
|
||||
<div class="mb-5">Total: ${d.data.dumpCount}</div>
|
||||
<div class="mb-5">Self: ${d.data.self}</div>
|
||||
${rateOfParent}${rateOfRoot}`;
|
||||
})
|
||||
.style("max-width", "400px");
|
||||
flameChart.value.tooltip(tip);
|
||||
d3.select(graph.value).datum(treeRoot).call(flameChart.value);
|
||||
}
|
||||
|
||||
function countRange() {
|
||||
const list = (pprofStore.analyzeTrees[0]?.elements || []).map((ele: PprofStackElement) => ele.dumpCount);
|
||||
max.value = Math.max(...(list.length ? list : [1]));
|
||||
min.value = Math.min(...(list.length ? list : [1]));
|
||||
}
|
||||
|
||||
function processTree(arr: PprofStackElement[]): Nullable<PprofFlameGraphNode> {
|
||||
const obj: Record<string, PprofFlameGraphNode> = {};
|
||||
const childrenByParentId: Record<string, PprofFlameGraphNode[]> = {};
|
||||
let res = null as Nullable<PprofFlameGraphNode>;
|
||||
const copyArr = arr.map((item) => {
|
||||
const node: PprofFlameGraphNode = {
|
||||
parentId: String(Number(item.parentId) + 1),
|
||||
originId: String(Number(item.id) + 1),
|
||||
id: item.id,
|
||||
name: item.symbol,
|
||||
symbol: item.symbol,
|
||||
dumpCount: item.dumpCount,
|
||||
self: item.self,
|
||||
value: 0,
|
||||
};
|
||||
obj[node.originId] = node;
|
||||
// Group nodes by their parentId
|
||||
if (childrenByParentId[node.parentId]) {
|
||||
childrenByParentId[node.parentId].push(node);
|
||||
} else {
|
||||
childrenByParentId[node.parentId] = [node];
|
||||
}
|
||||
return node;
|
||||
});
|
||||
const scale = d3.scaleLinear().domain([min.value, max.value]).range([1, 200]);
|
||||
// Link children to parents in O(n) using the adjacency map
|
||||
for (const item of copyArr) {
|
||||
item.value = Number(scale(item.dumpCount).toFixed(4));
|
||||
if (item.parentId === "0") {
|
||||
res = item as PprofFlameGraphNode;
|
||||
}
|
||||
const children = childrenByParentId[item.originId];
|
||||
if (children) {
|
||||
item.children = children;
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
return null;
|
||||
}
|
||||
treeForeach([res], (node: PprofFlameGraphNode) => {
|
||||
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(
|
||||
() => pprofStore.analyzeTrees,
|
||||
() => {
|
||||
drawGraph();
|
||||
},
|
||||
);
|
||||
</script>
|
||||
284
src/views/dashboard/related/pprof/components/TaskList.vue
Normal file
284
src/views/dashboard/related/pprof/components/TaskList.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<!-- 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" v-loading="pprofStore.loadingTasks">
|
||||
<div class="profile-t-tool flex-h">{{ t("taskList") }}</div>
|
||||
<div class="profile-t-wrapper">
|
||||
<div class="no-data" v-show="!pprofStore.taskList.length">
|
||||
{{ t("noData") }}
|
||||
</div>
|
||||
<table class="profile-t">
|
||||
<tr
|
||||
class="profile-tr cp"
|
||||
v-for="(i, index) in pprofStore.taskList"
|
||||
@click="changeTask(i)"
|
||||
:key="index"
|
||||
:class="{
|
||||
selected: pprofStore.selectedTask?.id === i.id,
|
||||
}"
|
||||
>
|
||||
<td class="profile-td">
|
||||
<div class="ell">
|
||||
<span>{{ i.id }}</span>
|
||||
<a class="profile-btn r" @click="() => (showDetail = true)">
|
||||
<Icon iconName="view" size="middle" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="grey ell sm task-info">
|
||||
<span class="mr-10 sm">
|
||||
{{ dateFormat(i.createTime) }}
|
||||
</span>
|
||||
<span class="task-type">{{ i.events }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="showDetail" :destroy-on-close="true" fullscreen @closed="showDetail = false">
|
||||
<div class="profile-detail flex-v" v-if="pprofStore.selectedTask?.id">
|
||||
<div>
|
||||
<h5 class="mb-10">{{ t("task") }}.</h5>
|
||||
<div class="mb-10 clear item">
|
||||
<span class="g-sm-4 grey">ID:</span>
|
||||
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.id }}</span>
|
||||
</div>
|
||||
<div class="mb-10 clear item">
|
||||
<span class="g-sm-4 grey">{{ t("service") }}:</span>
|
||||
<span class="g-sm-8 wba">{{ service }}</span>
|
||||
</div>
|
||||
<div class="mb-10 clear item">
|
||||
<span class="g-sm-4 grey">{{ t("events") }}:</span>
|
||||
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.events }}</span>
|
||||
</div>
|
||||
<div class="mb-10 clear item" v-if="pprofStore.selectedTask.duration !== undefined">
|
||||
<span class="g-sm-4 grey">{{ t("duration") }}:</span>
|
||||
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.duration }}{{ t("minutes") }}</span>
|
||||
</div>
|
||||
<div class="mb-10 clear item" v-if="pprofStore.selectedTask.dumpPeriod !== undefined">
|
||||
<span class="g-sm-4 grey">{{ t("pprofDumpPeriod") }}:</span>
|
||||
<span class="g-sm-8 wba">{{ pprofStore.selectedTask.dumpPeriod }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-5 mt-10" v-show="pprofStore.selectedTask?.logs?.length"> {{ t("logs") }}. </h5>
|
||||
<div v-for="(i, index) in Object.keys(instanceLogs)" :key="index">
|
||||
<div class="sm">
|
||||
<span class="mr-10 grey">{{ t("instance") }}:</span>
|
||||
<span>{{ i }}</span>
|
||||
</div>
|
||||
<div v-for="(d, logIndex) in instanceLogs[i]" :key="`${d.instanceId}-${logIndex}`">
|
||||
<span class="mr-10 grey">{{ t("operationType") }}:</span>
|
||||
<span class="mr-20">{{ d.operationType }}</span>
|
||||
<span class="mr-10 grey">{{ t("time") }}:</span>
|
||||
<span>{{ dateFormat(d.operationTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-10 mt-10" v-show="errorInstances.length"> {{ t("errorInstances") }}</h5>
|
||||
<div v-for="(instance, index) in errorInstances" :key="instance.value || index">
|
||||
<div class="mb-10 sm">
|
||||
<span class="mr-10 grey">{{ t("instance") }}:</span>
|
||||
<span>{{ instance.label }}</span>
|
||||
</div>
|
||||
<div v-for="(d, attrIndex) in instance.attributes" :key="d.value + attrIndex">
|
||||
<span class="mr-10 grey">{{ d.name }}:</span>
|
||||
<span class="mr-20">{{ d.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-10 mt-10" v-show="successInstances.length"> {{ t("successInstances") }}</h5>
|
||||
<div v-for="(instance, index) in successInstances" :key="instance.value || index">
|
||||
<div class="mb-10 sm">
|
||||
<span class="mr-10 grey">{{ t("instance") }}:</span>
|
||||
<span>{{ instance.label }}</span>
|
||||
</div>
|
||||
<div v-for="(d, attrIndex) in instance.attributes" :key="d.value + attrIndex">
|
||||
<span class="mr-10 grey">{{ d.name }}:</span>
|
||||
<span class="mr-20">{{ d.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSelectorStore } from "@/store/modules/selectors";
|
||||
import { usePprofStore } from "@/store/modules/pprof";
|
||||
import type { TaskLog } from "@/types/profile";
|
||||
import type { PprofTask } from "@/types/pprof";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { dateFormat } from "@/utils/dateFormat";
|
||||
import type { Instance, Service } from "@/types/selector";
|
||||
|
||||
/*global Nullable*/
|
||||
const { t } = useI18n();
|
||||
const pprofStore = usePprofStore();
|
||||
const selectorStore = useSelectorStore();
|
||||
const showDetail = ref<boolean>(false);
|
||||
const service = ref<string>("");
|
||||
const instanceLogs = ref<Record<string, TaskLog[]>>({});
|
||||
const errorInstances = ref<Instance[]>([]);
|
||||
const successInstances = ref<Instance[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
fetchTasks();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => pprofStore.instances,
|
||||
() => {
|
||||
syncTaskDetails(pprofStore.selectedTask);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
async function fetchTasks() {
|
||||
const res = await pprofStore.getTaskList();
|
||||
if (res?.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
return;
|
||||
}
|
||||
const errorReason = res?.data?.pprofTaskList?.errorReason;
|
||||
if (errorReason) {
|
||||
ElMessage.error(errorReason);
|
||||
return;
|
||||
}
|
||||
if (pprofStore.selectedTask?.id) {
|
||||
await changeTask(pprofStore.selectedTask as PprofTask);
|
||||
}
|
||||
}
|
||||
|
||||
function syncTaskDetails(item?: Nullable<PprofTask>) {
|
||||
if (!item?.id) {
|
||||
instanceLogs.value = {};
|
||||
errorInstances.value = [];
|
||||
successInstances.value = [];
|
||||
return;
|
||||
}
|
||||
errorInstances.value = pprofStore.instances.filter((d: Instance) =>
|
||||
d.id ? (item.errorInstanceIds || []).includes(d.id) : false,
|
||||
);
|
||||
successInstances.value = pprofStore.instances.filter((d: Instance) =>
|
||||
d.id ? (item.successInstanceIds || []).includes(d.id) : false,
|
||||
);
|
||||
instanceLogs.value = {};
|
||||
for (const d of item.logs || []) {
|
||||
if (instanceLogs.value[d.instanceName]) {
|
||||
instanceLogs.value[d.instanceName].push(d);
|
||||
} else {
|
||||
instanceLogs.value[d.instanceName] = [d];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTask(item: PprofTask) {
|
||||
if (item.id !== pprofStore.selectedTask?.id) {
|
||||
pprofStore.setAnalyzeTrees([]);
|
||||
pprofStore.setSelectedTask(item);
|
||||
}
|
||||
service.value = (selectorStore.services.find((s: Service) => s.id === item.serviceId) || {}).label || "";
|
||||
const res = await pprofStore.getTaskLogs({ taskId: item.id });
|
||||
if (res?.errors) {
|
||||
ElMessage.error(res.errors);
|
||||
return;
|
||||
}
|
||||
const selectedTask = {
|
||||
...item,
|
||||
...pprofStore.taskProgress,
|
||||
};
|
||||
pprofStore.setSelectedTask(selectedTask);
|
||||
syncTaskDetails(selectedTask);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.profile-task-list {
|
||||
width: 300px;
|
||||
height: calc(100% - 20px);
|
||||
overflow: auto;
|
||||
border-right: 1px solid var(--sw-trace-list-border);
|
||||
}
|
||||
|
||||
.item span {
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.profile-td {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid var(--sw-trace-list-border);
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: var(--sw-list-selected);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.profile-tr {
|
||||
&:hover {
|
||||
background-color: var(--sw-list-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.profile-t-tool {
|
||||
padding: 5px 10px;
|
||||
font-weight: bold;
|
||||
border-right: 1px solid var(--sw-trace-list-border);
|
||||
border-bottom: 1px solid var(--sw-trace-list-border);
|
||||
background-color: var(--sw-table-header);
|
||||
}
|
||||
|
||||
.profile-btn {
|
||||
color: $font-color;
|
||||
padding: 1px 3px;
|
||||
border-radius: 2px;
|
||||
font-size: $font-size-smaller;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.task-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-type {
|
||||
color: var(--sw-profile-task-type);
|
||||
background-color: var(--sw-list-selected);
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: $font-size-smaller;
|
||||
}
|
||||
</style>
|
||||
39
src/views/dashboard/related/pprof/components/data.ts
Normal file
39
src/views/dashboard/related/pprof/components/data.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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 DurationOptions = [
|
||||
{ value: "1", label: "1 min" },
|
||||
{ value: "5", label: "5 min" },
|
||||
{ value: "10", label: "10 min" },
|
||||
{ value: "15", label: "15 min" },
|
||||
{ value: "30", label: "30 min" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
];
|
||||
|
||||
export const PprofEvents = [
|
||||
{ label: "CPU", value: "CPU" },
|
||||
{ label: "HEAP", value: "HEAP" },
|
||||
{ label: "BLOCK", value: "BLOCK" },
|
||||
{ label: "MUTEX", value: "MUTEX" },
|
||||
{ label: "GOROUTINE", value: "GOROUTINE" },
|
||||
{ label: "THREADCREATE", value: "THREADCREATE" },
|
||||
{ label: "ALLOCS", value: "ALLOCS" },
|
||||
];
|
||||
|
||||
export const DurationRequiredEvents = ["CPU", "BLOCK", "MUTEX"];
|
||||
|
||||
export const DumpPeriodRequiredEvents = ["BLOCK", "MUTEX"];
|
||||
Reference in New Issue
Block a user