From 8c4bbf5b90745ddbc1995bb0b450827182a9776e Mon Sep 17 00:00:00 2001 From: Peter Kosztolanyi Date: Mon, 15 Sep 2025 17:26:24 +0200 Subject: [PATCH] Add query stage pipeline operator task statistics to Preview Web UI --- .../webapp-preview/src/api/webapp/api.ts | 6 + .../src/components/flow/StageOperatorNode.tsx | 413 ++++++++++++++---- .../src/components/flow/flowUtils.ts | 22 +- .../webapp-preview/src/utils/utils.ts | 10 +- 4 files changed, 364 insertions(+), 87 deletions(-) diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts index 75ca28d3d945..c94fca1560b8 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts @@ -282,6 +282,11 @@ export interface QueryStageStats { operatorSummaries: QueryStageOperatorSummary[] } +export interface QueryPipeline { + pipelineId: number + operatorSummaries: QueryStageOperatorSummary[] +} + export interface QueryTask { lastHeartbeat: string needsPlan: boolean @@ -303,6 +308,7 @@ export interface QueryTask { totalCpuTime: string totalScheduledTime: string userMemoryReservation: string + pipelines: QueryPipeline[] firstStartTime: string lastStartTime: string lastEndTime: string diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/flow/StageOperatorNode.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/flow/StageOperatorNode.tsx index e14e28fa4980..f7c23d740206 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/flow/StageOperatorNode.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/flow/StageOperatorNode.tsx @@ -11,14 +11,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Box, Card, CardContent, Grid2 as Grid, Typography } from '@mui/material' + +import { useState } from 'react' +import { + Box, + Card, + CardContent, + CardActionArea, + Dialog, + DialogTitle, + DialogContent, + Grid2 as Grid, + Typography, + DialogActions, + Button, + TableContainer, + Table, + TableBody, + TableRow, + TableCell, + Divider, +} from '@mui/material' +import { BarChart } from '@mui/x-charts/BarChart' import { Handle, Position } from '@xyflow/react' import { STAGE_OPERATOR_NODE_WIDTH } from './layout' -import { QueryStageOperatorSummary } from '../../api/webapp/api.ts' +import { QueryStageOperatorSummary, QueryTask } from '../../api/webapp/api.ts' import { formatCount, formatDataSize, formatDuration, + getTaskNumber, parseAndFormatDataSize, parseDataSize, parseDuration, @@ -29,6 +51,7 @@ export interface IStageOperatorNodeProps { data: { label: string stats: QueryStageOperatorSummary + tasks: QueryTask[] layoutDirection: LayoutDirectionType } } @@ -45,7 +68,8 @@ export interface IStageOperatorNodeProps { * - Connected via edges showing data flow between operators in the pipeline */ export const StageOperatorNode = (props: IStageOperatorNodeProps) => { - const { label, layoutDirection, stats } = props.data + const { label, layoutDirection, stats, tasks } = props.data + const [showModal, setShowModal] = useState(false) const getTotalWallTime = (stats: QueryStageOperatorSummary) => { return ( @@ -64,12 +88,231 @@ export const StageOperatorNode = (props: IStageOperatorNodeProps) => { ) } + const getOperatorTasks = ( + tasks: QueryTask[], + operatorSummary: QueryStageOperatorSummary + ): QueryStageOperatorSummary[] => { + return tasks + .slice() + .sort((taskA, taskB) => getTaskNumber(taskA.taskStatus.taskId) - getTaskNumber(taskB.taskStatus.taskId)) + .flatMap( + (task) => + task.stats.pipelines + .filter((pipeline) => pipeline.pipelineId === operatorSummary.pipelineId) + .flatMap((pipeline) => + pipeline.operatorSummaries.filter( + (operator) => operator.operatorId === operatorSummary.operatorId + ) + ) ?? [] + ) + } + + const operatorTasks = getOperatorTasks(tasks, stats) const totalWallTime = getTotalWallTime(stats) const totalCpuTime = getTotalCpuTime(stats) const rowInputRate = totalWallTime === 0 ? 0 : stats.inputPositions / (totalWallTime / 1000.0) const byteInputRate = totalWallTime === 0 ? 0 : (parseDataSize(stats.inputDataSize) || 0) / (totalWallTime / 1000.0) + const rowOutputRate = totalWallTime === 0 ? 0 : stats.outputPositions / totalWallTime + const byteOutputRate = + totalWallTime === 0 ? 0 : (parseDataSize(stats.outputDataSize) || 0) / (totalWallTime / 1000.0) + + const handleClick = () => { + setShowModal(true) + } + + const handleModalClose = () => { + setShowModal(false) + } + + const StatisticRow = ({ + label, + operatorTasks, + supplier, + valueFormatter, + }: { + label: string + supplier: (stats: QueryStageOperatorSummary) => number | null + operatorTasks: QueryStageOperatorSummary[] + valueFormatter: typeof formatCount | typeof formatDataSize | typeof formatDuration + }) => { + const xAxisData: string[] = operatorTasks.map((_, index) => `Task ${index}`) + const seriesData = operatorTasks.map((operatorTask) => supplier(operatorTask)) + + return ( + <> + + + {label} + + + + + + + ) + } + + const OperatorDetailDialog = () => ( + + + + Pipeline {stats.pipelineId} + + + {stats.operatorType} + + + + + + + + + + Input + + {formatCount(stats.inputPositions) + + ' rows (' + + parseAndFormatDataSize(stats.inputDataSize) + + ')'} + + + + Input rate + + {formatCount(rowInputRate) + + ' rows/s (' + + formatDataSize(byteInputRate) + + '/s)'} + + + + Output + + {formatCount(stats.outputPositions) + + ' rows (' + + parseAndFormatDataSize(stats.outputDataSize) + + ')'} + + + + Output rate + + {formatCount(rowOutputRate) + + ' rows/s (' + + formatDataSize(byteOutputRate) + + '/s)'} + + + +
+
+
+ + + + + + CPU time + {formatDuration(totalCpuTime)} + + + Wall time + {formatDuration(totalWallTime)} + + + Blocked + {formatDuration(parseDuration(stats.blockedWall) || 0)} + + + Drivers + {stats.totalDrivers} + + + Tasks + {operatorTasks.length} + + +
+
+
+ + + Statistics + + + + + + Tasks + + + + + + stats.inputPositions} + valueFormatter={formatCount} + /> + parseDataSize(stats.inputDataSize)} + valueFormatter={formatDataSize} + /> + stats.outputPositions} + valueFormatter={formatCount} + /> + parseDataSize(stats.outputDataSize)} + valueFormatter={formatDataSize} + /> +
+
+ + + +
+ ) + return ( { width: STAGE_OPERATOR_NODE_WIDTH, }} > - - - - - {label} - + + + + + + {label} + + + + + {formatCount(rowInputRate) + ' rows/s (' + formatDataSize(byteInputRate) + '/s)'} + + - - - {formatCount(rowInputRate) + ' rows/s (' + formatDataSize(byteInputRate) + '/s)'} - - - - - - - Output - - - - - {formatCount(stats.outputPositions) + - ' rows (' + - parseAndFormatDataSize(stats.outputDataSize) + - ')'} - - - - - Drivers - - - - - {stats.totalDrivers} - - - - - CPU time - + + + + Output + + + + + {formatCount(stats.outputPositions) + + ' rows (' + + parseAndFormatDataSize(stats.outputDataSize) + + ')'} + + + + + Drivers + + + + + {stats.totalDrivers} + + + + + CPU time + + + + + {formatDuration(totalCpuTime)} + + + + + Wall time + + + + + {formatDuration(totalWallTime)} + + + + + Blocked + + + + + {formatDuration(parseDuration(stats.blockedWall) || 0)} + + + + + Input + + + + + {formatCount(stats.inputPositions) + + ' rows (' + + parseAndFormatDataSize(stats.inputDataSize) + + ')'} + + - - - {formatDuration(totalCpuTime)} - - - - - Wall time - - - - - {formatDuration(totalWallTime)} - - - - - Blocked - - - - - {formatDuration(parseDuration(stats.blockedWall) || 0)} - - - - - Input - - - - - {formatCount(stats.inputPositions) + - ' rows (' + - parseAndFormatDataSize(stats.inputDataSize) + - ')'} - - - - + + + + ({ @@ -131,6 +132,7 @@ export const createStageOperatorNode = ( index, label: operatorSummary.operatorType, stats: operatorSummary, + tasks, layoutDirection, }, parentId: pipelineId, @@ -390,6 +392,7 @@ const countOperatorChainDepth = (stageOperatorSummary: QueryStageOperatorSummary const generateStageOperatorNodes = ( pipelineId: string, stageOperatorSummary: QueryStageOperatorSummary, + tasks: QueryTask[], childIndex: number, layoutDirection: LayoutDirectionType ): Node[] => { @@ -398,6 +401,7 @@ const generateStageOperatorNodes = ( pipelineId, stageOperatorSummary.operatorId.toString(), stageOperatorSummary, + tasks, childIndex, layoutDirection ), @@ -405,7 +409,13 @@ const generateStageOperatorNodes = ( if (stageOperatorSummary.child) { nodes.push( - ...generateStageOperatorNodes(pipelineId, stageOperatorSummary.child, childIndex + 1, layoutDirection) + ...generateStageOperatorNodes( + pipelineId, + stageOperatorSummary.child, + tasks, + childIndex + 1, + layoutDirection + ) ) } @@ -441,7 +451,13 @@ export const getStagePerformanceFlowElements = ( key, layoutDirection ) - const childNodes: Node[] = generateStageOperatorNodes(pipelineId, stageOperatorSummary, 0, layoutDirection) + const childNodes: Node[] = generateStageOperatorNodes( + pipelineId, + stageOperatorSummary, + stage.tasks, + 0, + layoutDirection + ) return [pipelineStageNode, ...childNodes] }) diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/utils/utils.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/utils/utils.ts index 4e3c29aff1e1..b171252029dd 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/utils/utils.ts +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/utils/utils.ts @@ -113,6 +113,10 @@ export function getTaskIdSuffix(taskId: string): string { return taskId.slice(taskId.indexOf('.') + 1, taskId.length) } +export function getTaskNumber(taskId: string): number { + return Number.parseInt(getTaskIdSuffix(getTaskIdSuffix(taskId))) +} + export function getHostname(url: string): string { let hostname = new URL(url).hostname if (hostname.charAt(0) === '[' && hostname.charAt(hostname.length - 1) === ']') { @@ -145,7 +149,11 @@ export function precisionRound(n: number | null): string { return Math.round(n).toString() } -export function formatDuration(duration: number): string { +export function formatDuration(duration: number | null): string { + if (duration == null) { + return '' + } + let unit = 'ms' if (duration > 1000) { duration /= 1000