Add the front end for pprof profiling (#541)

This commit is contained in:
Jingyi Qu
2026-04-13 10:38:26 +08:00
committed by GitHub
parent ed4841450c
commit 54ba66db92
21 changed files with 1321 additions and 0 deletions

View 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
}
}
}
`,
};

View File

@@ -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 = "";

View 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}}`;

View File

@@ -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",

View File

@@ -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",

View File

@@ -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: "下载",

View File

@@ -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
View 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
View 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[];
};

View File

@@ -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> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
}

View 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>

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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 },

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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"];