Skip to content

Commit 77af5ad

Browse files
kosztiwendigo
authored andcommitted
Add query stage performance flow to Preview Web UI
1 parent 6974d81 commit 77af5ad

File tree

9 files changed

+921
-22
lines changed

9 files changed

+921
-22
lines changed

core/trino-web-ui/src/main/resources/webapp-preview/src/api/webapp/api.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,42 @@ export interface QueryRoutine {
217217
authorization: string
218218
}
219219

220+
export interface QueryStageNodeInfo {
221+
'@type': string
222+
id: string
223+
source: QueryStageNodeInfo
224+
sources: QueryStageNodeInfo[]
225+
filteringSource: QueryStageNodeInfo
226+
probeSource: QueryStageNodeInfo
227+
indexSource: QueryStageNodeInfo
228+
left: QueryStageNodeInfo
229+
right: QueryStageNodeInfo
230+
}
231+
220232
export interface QueryStagePlan {
221233
id: string
222234
jsonRepresentation: string
223-
root: {
224-
id: string
225-
}
235+
root: QueryStageNodeInfo
236+
}
237+
238+
export interface QueryStageOperatorSummary {
239+
pipelineId: number
240+
planNodeId: string
241+
operatorId: number
242+
operatorType: string
243+
child: QueryStageOperatorSummary
244+
outputPositions: number
245+
outputDataSize: string
246+
totalDrivers: number
247+
addInputCpu: string
248+
getOutputCpu: string
249+
finishCpu: string
250+
addInputWall: string
251+
getOutputWall: string
252+
finishWall: string
253+
blockedWall: string
254+
inputDataSize: string
255+
inputPositions: number
226256
}
227257

228258
export interface QueryStageStats {
@@ -249,6 +279,7 @@ export interface QueryStageStats {
249279
totalBufferedBytes: number
250280
failedCumulativeUserMemory: number
251281
peakUserMemoryReservation: string
282+
operatorSummaries: QueryStageOperatorSummary[]
252283
}
253284

254285
export interface QueryTask {

core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryDetails.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import { QueryJson } from './QueryJson'
1818
import { QueryReferences } from './QueryReferences'
1919
import { QueryLivePlan } from './QueryLivePlan'
2020
import { QueryOverview } from './QueryOverview'
21+
import { QueryStagePerformance } from './QueryStagePerformance'
2122
import { Texts } from '../constant.ts'
2223

2324
const tabValues = ['overview', 'livePlan', 'stagePerformance', 'splits', 'json', 'references'] as const
2425
type TabValue = (typeof tabValues)[number]
2526
const tabComponentMap: Record<TabValue, ReactNode> = {
2627
overview: <QueryOverview />,
2728
livePlan: <QueryLivePlan />,
28-
stagePerformance: <Alert severity="error">{Texts.Error.NotImplemented}</Alert>,
29+
stagePerformance: <QueryStagePerformance />,
2930
splits: <Alert severity="error">{Texts.Error.NotImplemented}</Alert>,
3031
json: <QueryJson />,
3132
references: <QueryReferences />,
@@ -59,7 +60,7 @@ export const QueryDetails = () => {
5960
<Tabs value={tabValue} onChange={handleTabChange}>
6061
<Tab value="overview" label="Overview" />
6162
<Tab value="livePlan" label="Live plan" />
62-
<Tab value="stagePerformance" label="Stage performance" disabled />
63+
<Tab value="stagePerformance" label="Stage performance" />
6364
<Tab value="splits" label="Splits" disabled />
6465
<Tab value="json" label="JSON" />
6566
<Tab value="references" label="References" />

core/trino-web-ui/src/main/resources/webapp-preview/src/components/QueryLivePlan.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import { ReactFlow, type Edge, type Node, useNodesState, useEdgesState } from '@
1818
import '@xyflow/react/dist/style.css'
1919
import { queryStatusApi, QueryStatusInfo } from '../api/webapp/api.ts'
2020
import { QueryProgressBar } from './QueryProgressBar'
21-
import { nodeTypes, getLayoutedElements } from './flow/layout'
21+
import { nodeTypes, getLayoutedPlanFlowElements } from './flow/layout'
2222
import { HelpMessage } from './flow/HelpMessage'
23-
import { getFlowElements } from './flow/flowUtils'
23+
import { getPlanFlowElements } from './flow/flowUtils'
2424
import { IQueryStatus, LayoutDirectionType } from './flow/types'
2525
import { ApiResponse } from '../api/base.ts'
2626
import { Texts } from '../constant.ts'
@@ -48,8 +48,8 @@ export const QueryLivePlan = () => {
4848

4949
useEffect(() => {
5050
if (queryStatus.info?.stages) {
51-
const flowElements = getFlowElements(queryStatus.info.stages, layoutDirection)
52-
const layoutedElements = getLayoutedElements(flowElements.nodes, flowElements.edges, {
51+
const flowElements = getPlanFlowElements(queryStatus.info.stages, layoutDirection)
52+
const layoutedElements = getLayoutedPlanFlowElements(flowElements.nodes, flowElements.edges, {
5353
direction: layoutDirection,
5454
})
5555

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
import { useParams } from 'react-router-dom'
15+
import { useEffect, useRef, useState } from 'react'
16+
import {
17+
Alert,
18+
Box,
19+
CircularProgress,
20+
FormControl,
21+
Grid2 as Grid,
22+
InputLabel,
23+
MenuItem,
24+
Select,
25+
SelectChangeEvent,
26+
} from '@mui/material'
27+
import { type Edge, type Node, ReactFlow, useEdgesState, useNodesState } from '@xyflow/react'
28+
import { queryStatusApi, QueryStatusInfo, QueryStage } from '../api/webapp/api.ts'
29+
import { ApiResponse } from '../api/base.ts'
30+
import { Texts } from '../constant.ts'
31+
import { QueryProgressBar } from './QueryProgressBar.tsx'
32+
import { HelpMessage } from './flow/HelpMessage'
33+
import { nodeTypes, getLayoutedStagePerformanceElements } from './flow/layout'
34+
import { LayoutDirectionType } from './flow/types'
35+
import { getStagePerformanceFlowElements } from './flow/flowUtils.ts'
36+
37+
interface IQueryStatus {
38+
info: QueryStatusInfo | null
39+
ended: boolean
40+
}
41+
42+
export const QueryStagePerformance = () => {
43+
const { queryId } = useParams()
44+
const initialQueryStatus: IQueryStatus = {
45+
info: null,
46+
ended: false,
47+
}
48+
49+
const [queryStatus, setQueryStatus] = useState<IQueryStatus>(initialQueryStatus)
50+
const [stagePlanIds, setStagePlanIds] = useState<string[]>([])
51+
const [stagePlanId, setStagePlanId] = useState<string>()
52+
const [stage, setStage] = useState<QueryStage>()
53+
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
54+
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
55+
const [layoutDirection, setLayoutDirection] = useState<LayoutDirectionType>('BT')
56+
57+
const [loading, setLoading] = useState<boolean>(true)
58+
const [error, setError] = useState<string | null>(null)
59+
const queryStatusRef = useRef(queryStatus)
60+
const containerRef = useRef<HTMLDivElement>(null)
61+
62+
useEffect(() => {
63+
queryStatusRef.current = queryStatus
64+
}, [queryStatus])
65+
66+
useEffect(() => {
67+
if (queryStatus.info?.stages) {
68+
const allStagePlanIds = queryStatus.info.stages.stages.map((stage) => stage.plan.id)
69+
setStagePlanIds(allStagePlanIds)
70+
setStagePlanId(allStagePlanIds[0])
71+
}
72+
}, [queryStatus])
73+
74+
useEffect(() => {
75+
if (queryStatus.ended && stagePlanId) {
76+
const stage = queryStatus.info?.stages.stages.find((stage) => stage.plan.id === stagePlanId)
77+
setStage(stage)
78+
79+
if (stage) {
80+
const flowElements = getStagePerformanceFlowElements(stage, layoutDirection)
81+
const layoutedElements = getLayoutedStagePerformanceElements(flowElements.nodes, flowElements.edges, {
82+
direction: layoutDirection,
83+
})
84+
85+
setNodes(layoutedElements.nodes)
86+
setEdges(layoutedElements.edges)
87+
}
88+
}
89+
// eslint-disable-next-line react-hooks/exhaustive-deps
90+
}, [queryStatus, stagePlanId, layoutDirection])
91+
92+
useEffect(() => {
93+
const runLoop = () => {
94+
const queryEnded = !!queryStatusRef.current.info?.finalQueryInfo
95+
if (!queryEnded) {
96+
getQueryStatus()
97+
setTimeout(runLoop, 3000)
98+
}
99+
}
100+
101+
if (queryId) {
102+
queryStatusRef.current = initialQueryStatus
103+
}
104+
105+
runLoop()
106+
// eslint-disable-next-line react-hooks/exhaustive-deps
107+
}, [queryId])
108+
109+
const getQueryStatus = () => {
110+
if (queryId) {
111+
queryStatusApi(queryId, false).then((apiResponse: ApiResponse<QueryStatusInfo>) => {
112+
setLoading(false)
113+
if (apiResponse.status === 200 && apiResponse.data) {
114+
setQueryStatus({
115+
info: apiResponse.data,
116+
ended: apiResponse.data.finalQueryInfo,
117+
})
118+
setError(null)
119+
} else {
120+
setError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`)
121+
}
122+
})
123+
}
124+
}
125+
126+
const smallFormControlSx = {
127+
fontSize: '0.8rem',
128+
}
129+
130+
const smallDropdownMenuPropsSx = {
131+
PaperProps: {
132+
sx: {
133+
'& .MuiMenuItem-root': smallFormControlSx,
134+
},
135+
},
136+
}
137+
138+
const handleStageIdChange = (event: SelectChangeEvent) => {
139+
setStagePlanId(event.target.value as string)
140+
}
141+
142+
return (
143+
<>
144+
{loading && <CircularProgress />}
145+
{error && <Alert severity="error">{Texts.Error.QueryNotFound}</Alert>}
146+
147+
{!loading && !error && queryStatus.info && (
148+
<Grid container spacing={0}>
149+
<Grid size={{ xs: 12 }}>
150+
<Box sx={{ pt: 2 }}>
151+
<Box sx={{ width: '100%' }}>
152+
<QueryProgressBar queryInfoBase={queryStatus.info} />
153+
</Box>
154+
155+
{queryStatus.ended ? (
156+
<Grid container spacing={3}>
157+
<Grid size={{ xs: 12, md: 12 }}>
158+
<Box sx={{ pt: 2 }}>
159+
<Box
160+
ref={containerRef}
161+
sx={{ width: '100%', height: '80vh', border: '1px solid #ccc' }}
162+
>
163+
{stage ? (
164+
<ReactFlow
165+
nodes={nodes}
166+
edges={edges}
167+
onNodesChange={onNodesChange}
168+
onEdgesChange={onEdgesChange}
169+
nodeTypes={nodeTypes}
170+
minZoom={0.1}
171+
proOptions={{ hideAttribution: true }}
172+
defaultViewport={{ x: 200, y: 20, zoom: 0.8 }}
173+
>
174+
<HelpMessage
175+
layoutDirection={layoutDirection}
176+
onLayoutDirectionChange={setLayoutDirection}
177+
additionalContent={
178+
<Box sx={{ mt: 2 }}>
179+
<FormControl size="small" sx={{ minWidth: 200 }}>
180+
<InputLabel sx={smallFormControlSx}>
181+
Stage
182+
</InputLabel>
183+
<Select
184+
label="Stage"
185+
sx={smallFormControlSx}
186+
MenuProps={smallDropdownMenuPropsSx}
187+
value={stagePlanId}
188+
onChange={handleStageIdChange}
189+
>
190+
{stagePlanIds.map((stageId) => (
191+
<MenuItem key={stageId} value={stageId}>
192+
{stageId}
193+
</MenuItem>
194+
))}
195+
</Select>
196+
</FormControl>
197+
</Box>
198+
}
199+
/>
200+
</ReactFlow>
201+
) : (
202+
<Alert severity="error">Stage not found.</Alert>
203+
)}
204+
</Box>
205+
</Box>
206+
</Grid>
207+
</Grid>
208+
) : (
209+
<>
210+
<Box sx={{ width: '100%', mt: 1 }}>
211+
<Alert severity="info">
212+
Operator graph will appear automatically when query completes.
213+
</Alert>
214+
</Box>
215+
</>
216+
)}
217+
</Box>
218+
</Grid>
219+
</Grid>
220+
)}
221+
</>
222+
)
223+
}

core/trino-web-ui/src/main/resources/webapp-preview/src/components/flow/HelpMessage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import { LayoutDirectionType } from './types'
1818
interface IHelpMessageProps {
1919
layoutDirection: LayoutDirectionType
2020
onLayoutDirectionChange: (layoutDirection: LayoutDirectionType) => void
21+
additionalContent?: React.ReactNode
2122
}
2223

23-
export const HelpMessage = ({ layoutDirection, onLayoutDirectionChange }: IHelpMessageProps) => {
24+
export const HelpMessage = ({ layoutDirection, onLayoutDirectionChange, additionalContent }: IHelpMessageProps) => {
2425
const handleLayoutChange = (_event: React.MouseEvent<HTMLElement>, newDirection: LayoutDirectionType | null) => {
2526
if (newDirection !== null) {
2627
onLayoutDirectionChange(newDirection)
@@ -64,6 +65,8 @@ export const HelpMessage = ({ layoutDirection, onLayoutDirectionChange }: IHelpM
6465
<Typography variant="caption">Horizontal</Typography>
6566
</ToggleButton>
6667
</ToggleButtonGroup>
68+
69+
{additionalContent}
6770
</Box>
6871
)
6972
}

0 commit comments

Comments
 (0)