diff --git a/src/graphql/fragments/pprof.ts b/src/graphql/fragments/pprof.ts new file mode 100644 index 00000000..e8eb4f19 --- /dev/null +++ b/src/graphql/fragments/pprof.ts @@ -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 + } + } + } + `, +}; diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 9c72887c..dea5af83 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -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 = ""; diff --git a/src/graphql/query/pprof.ts b/src/graphql/query/pprof.ts new file mode 100644 index 00000000..2b01214c --- /dev/null +++ b/src/graphql/query/pprof.ts @@ -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}}`; diff --git a/src/locales/lang/en.ts b/src/locales/lang/en.ts index 1a0c7773..f5006ff7 100644 --- a/src/locales/lang/en.ts +++ b/src/locales/lang/en.ts @@ -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", diff --git a/src/locales/lang/es.ts b/src/locales/lang/es.ts index b16c8f1e..b7ccca1e 100644 --- a/src/locales/lang/es.ts +++ b/src/locales/lang/es.ts @@ -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", diff --git a/src/locales/lang/zh.ts b/src/locales/lang/zh.ts index d0957acf..c7300bb5 100644 --- a/src/locales/lang/zh.ts +++ b/src/locales/lang/zh.ts @@ -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: "下载", diff --git a/src/store/data.ts b/src/store/data.ts index 4d88138b..1fa947de 100644 --- a/src/store/data.ts +++ b/src/store/data.ts @@ -51,6 +51,7 @@ export const ControlsTypes = [ WidgetType.Ebpf, WidgetType.NetworkProfiling, WidgetType.AsyncProfiling, + WidgetType.Pprof, WidgetType.ThirdPartyApp, WidgetType.ContinuousProfiling, WidgetType.TaskTimeline, diff --git a/src/store/modules/pprof.ts b/src/store/modules/pprof.ts new file mode 100644 index 00000000..b93c3dbd --- /dev/null +++ b/src/store/modules/pprof.ts @@ -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; + selectedTask: Nullable; + taskProgress: Nullable; + 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) { + 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 }) { + 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); +} diff --git a/src/types/pprof.ts b/src/types/pprof.ts new file mode 100644 index 00000000..2f8db183 --- /dev/null +++ b/src/types/pprof.ts @@ -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[]; +}; diff --git a/src/utils/flameGraph.ts b/src/utils/flameGraph.ts index 98e4d192..66274112 100644 --- a/src/utils/flameGraph.ts +++ b/src/utils/flameGraph.ts @@ -22,3 +22,14 @@ export function treeForeach(tree: any, func: (node: any) => void) { } return tree; } + +export function escapeHtml(str: string): string { + const htmlEscapes: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]); +} diff --git a/src/views/dashboard/controls/Pprof.vue b/src/views/dashboard/controls/Pprof.vue new file mode 100644 index 00000000..cd01bc18 --- /dev/null +++ b/src/views/dashboard/controls/Pprof.vue @@ -0,0 +1,86 @@ + + + + diff --git a/src/views/dashboard/controls/index.ts b/src/views/dashboard/controls/index.ts index 9cfe1a64..fd4b142b 100644 --- a/src/views/dashboard/controls/index.ts +++ b/src/views/dashboard/controls/index.ts @@ -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, diff --git a/src/views/dashboard/controls/tab.ts b/src/views/dashboard/controls/tab.ts index 2ae84f11..ab82609a 100644 --- a/src/views/dashboard/controls/tab.ts +++ b/src/views/dashboard/controls/tab.ts @@ -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, }; diff --git a/src/views/dashboard/data.ts b/src/views/dashboard/data.ts index 8a6edbd0..eafccd70 100644 --- a/src/views/dashboard/data.ts +++ b/src/views/dashboard/data.ts @@ -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 }, diff --git a/src/views/dashboard/related/pprof/Content.vue b/src/views/dashboard/related/pprof/Content.vue new file mode 100644 index 00000000..f4059432 --- /dev/null +++ b/src/views/dashboard/related/pprof/Content.vue @@ -0,0 +1,68 @@ + + + + diff --git a/src/views/dashboard/related/pprof/Header.vue b/src/views/dashboard/related/pprof/Header.vue new file mode 100644 index 00000000..ae188a7d --- /dev/null +++ b/src/views/dashboard/related/pprof/Header.vue @@ -0,0 +1,46 @@ + + + + diff --git a/src/views/dashboard/related/pprof/components/Filter.vue b/src/views/dashboard/related/pprof/components/Filter.vue new file mode 100644 index 00000000..ec3bfb22 --- /dev/null +++ b/src/views/dashboard/related/pprof/components/Filter.vue @@ -0,0 +1,80 @@ + + + + diff --git a/src/views/dashboard/related/pprof/components/NewTask.vue b/src/views/dashboard/related/pprof/components/NewTask.vue new file mode 100644 index 00000000..3decaa7c --- /dev/null +++ b/src/views/dashboard/related/pprof/components/NewTask.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/src/views/dashboard/related/pprof/components/PprofStack.vue b/src/views/dashboard/related/pprof/components/PprofStack.vue new file mode 100644 index 00000000..abd14c20 --- /dev/null +++ b/src/views/dashboard/related/pprof/components/PprofStack.vue @@ -0,0 +1,170 @@ + + + diff --git a/src/views/dashboard/related/pprof/components/TaskList.vue b/src/views/dashboard/related/pprof/components/TaskList.vue new file mode 100644 index 00000000..8af871fe --- /dev/null +++ b/src/views/dashboard/related/pprof/components/TaskList.vue @@ -0,0 +1,284 @@ + + + + diff --git a/src/views/dashboard/related/pprof/components/data.ts b/src/views/dashboard/related/pprof/components/data.ts new file mode 100644 index 00000000..34786e41 --- /dev/null +++ b/src/views/dashboard/related/pprof/components/data.ts @@ -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"];