From e625d56fa4b5477109576e33ede829c1fcd5c2cb Mon Sep 17 00:00:00 2001 From: Peter Kosztolanyi Date: Sun, 19 Jan 2025 17:00:38 +0100 Subject: [PATCH] Add workers pages to the Preview Web UI --- .../main/resources/webapp-preview/src/App.tsx | 2 + .../webapp-preview/src/api/webapp/api.ts | 59 +++ .../src/components/Dashboard.tsx | 2 +- .../src/components/MetricCard.tsx | 27 +- .../src/components/WorkerStatus.tsx | 412 ++++++++++++++++++ .../webapp-preview/src/components/Workers.tsx | 25 -- .../src/components/WorkersList.tsx | 108 +++++ .../resources/webapp-preview/src/constant.ts | 1 + .../resources/webapp-preview/src/router.tsx | 4 +- 9 files changed, 608 insertions(+), 32 deletions(-) create mode 100644 core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkerStatus.tsx delete mode 100644 core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx create mode 100644 core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx index d4854008f850..2a4be98a3d3d 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/App.tsx @@ -32,6 +32,7 @@ import { routers } from './router.tsx' import { useConfigStore, Theme as ThemeStore } from './store' import { darkTheme, lightTheme } from './theme' import trinoLogo from './assets/trino.svg' +import { WorkerStatus } from './components/WorkerStatus.tsx' const App = () => { const config = useConfigStore() @@ -74,6 +75,7 @@ const Screen = () => { {routers.flatMap((router) => { return [] })} + } /> } /> } key={'*'} /> 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 dda2ad0fbb89..48d20f42c2e7 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 @@ -27,6 +27,65 @@ export interface Stats { totalCpuTimeSecs: number } +export interface Worker { + coordinator: boolean + nodeId: string + nodeIp: string + nodeVersion: string + state: string +} + +export interface MemoryAllocationItem { + tag: string + allocation: number +} + +export interface MemoryUsagePool { + freeBytes: number + maxBytes: number + reservedBytes: number + reservedRevocableBytes: number + queryMemoryAllocations: { + [key: string]: MemoryAllocationItem + } + queryMemoryReservations: { + [key: string]: number + } + queryMemoryRevocableReservations: { + [key: string]: number + } +} + +export interface WorkerStatusInfo { + coordinator: boolean + environment: string + externalAddress: string + heapAvailable: number + heapUsed: number + internalAddress: string + memoryInfo: { + availableProcessors: number + pool: MemoryUsagePool + } + nodeId: string + nodeVersion: { + version: string + } + nonHeapUsed: number + processCpuLoad: number + processors: number + systemCpuLoad: number + uptime: string +} + export async function statsApi(): Promise> { return await api.get('/ui/api/stats') } + +export async function workerApi(): Promise> { + return await api.get('/ui/api/worker') +} + +export async function workerStatusApi(nodeId: string): Promise> { + return await api.get(`/ui/api/worker/${nodeId}/status`) +} diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx index 9e6480137f85..701c287b4274 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Dashboard.tsx @@ -157,7 +157,7 @@ export const Dashboard = () => { - + diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx index c0fa342401b5..9f9278038a6a 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/MetricCard.tsx @@ -11,28 +11,47 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Link as RouterLink } from 'react-router-dom' import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' import Typography from '@mui/material/Typography' import { Grid2 as Grid } from '@mui/material' import { SparkLineChart } from '@mui/x-charts/SparkLineChart' +import { styled } from '@mui/material/styles' interface IMetricCardProps { title: string values: number[] numberFormatter?: (n: number | null) => string + link?: string } +const StyledLink = styled(RouterLink)(({ theme }) => ({ + textDecoration: 'none', + color: theme.palette.info.main, + '&:hover': { + textDecoration: 'underline', + }, +})) + export const MetricCard = (props: IMetricCardProps) => { - const { title, values, numberFormatter } = props + const { title, values, numberFormatter, link } = props const lastValue = values[values.length - 1] return ( - - {title} - + {link ? ( + + + {title} + + + ) : ( + + {title} + + )} { + const { showSnackbar } = useSnackbar() + const { nodeId } = useParams() + const theme = useTheme() + const initialFilledHistory = Array(MAX_HISTORY).fill(0) + const [workerStatus, setWorkerStatus] = useState({ + info: null, + processCpuLoad: initialFilledHistory, + systemCpuLoad: initialFilledHistory, + heapPercentUsed: initialFilledHistory, + nonHeapUsed: initialFilledHistory, + lastRefresh: null, + }) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + getWorkerStatus() + const intervalId = setInterval(getWorkerStatus, 1000) + return () => clearInterval(intervalId) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (error) { + showSnackbar(error, 'error') + } + }, [error, showSnackbar]) + + const getWorkerStatus = () => { + setError(null) + if (nodeId) { + workerStatusApi(nodeId).then((apiResponse: ApiResponse) => { + setLoading(false) + if (apiResponse.status === 200 && apiResponse.data) { + const newWorkerStatusInfo: WorkerStatusInfo = apiResponse.data + setWorkerStatus((prevWorkerStatus) => { + return { + info: newWorkerStatusInfo, + processCpuLoad: addToHistory( + newWorkerStatusInfo.processCpuLoad * 100.0, + prevWorkerStatus.processCpuLoad + ), + systemCpuLoad: addToHistory( + newWorkerStatusInfo.systemCpuLoad * 100.0, + prevWorkerStatus.systemCpuLoad + ), + heapPercentUsed: addToHistory( + (newWorkerStatusInfo.heapUsed * 100.0) / newWorkerStatusInfo.heapAvailable, + prevWorkerStatus.heapPercentUsed + ), + nonHeapUsed: addToHistory(newWorkerStatusInfo.nonHeapUsed, prevWorkerStatus.nonHeapUsed), + + lastRefresh: Date.now(), + } + }) + } else { + setError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`) + } + }) + } + } + + const renderPoolChart = (name: string, pool: MemoryUsagePool) => { + if (!pool) { + return <> + } + + const size = pool.maxBytes + const reserved = pool.reservedBytes + const revocable = pool.reservedRevocableBytes + const available = size - reserved - revocable + + return ( + <> + + + {name} Pool + + + {formatDataSize(size)} total + + + + + formatDataSize(value.value), + }, + ]} + height={150} + skipAnimation + /> + + + + ) + } + + const renderPoolQueries = (pool: MemoryUsagePool) => { + if (!pool) { + return <> + } + + const queries: { [key: string]: number[] } = {} + const reservations = pool.queryMemoryReservations + const revocableReservations = pool.queryMemoryRevocableReservations + + for (let query in reservations) { + queries[query] = [reservations[query], 0] + } + + for (let query in revocableReservations) { + if (queries.hasOwnProperty.call(queries, query)) { + queries[query][1] = revocableReservations[query] + } else { + queries[query] = [0, revocableReservations[query]] + } + } + + const size = pool.maxBytes + + if (Object.keys(queries).length === 0) { + return No queries using pool + } + + return ( + + + + + + + Query Id + Reserved % + Reserved + Revocable + + + + {Object.entries(queries) + .sort(([, reservationsA], [, reservationsB]) => reservationsB[0] - reservationsA[0]) + .map(([query, reservations]) => ( + + {query} + + {Math.round((reservations[0] * 100.0) / size)}% + + {formatDataSize(reservations[0])} + {formatDataSize(reservations[1])} + + ))} + +
+
+
+
+ ) + } + + return ( + <> + + Worker Status + + + {loading && } + {error && {Texts.Error.NodeInformationNotLoaded}} + + {!loading && !error && workerStatus.info && ( + <> + + Overview + + + + + + + + + Node ID + {workerStatus.info.nodeId} + + + Heap Memory + {formatDataSize(workerStatus.info.heapAvailable)} + + + Processors + {workerStatus.info.processors} + + +
+
+
+ + + + + + Uptime + {workerStatus.info.uptime} + + + External Address + {workerStatus.info.externalAddress} + + + Internal Address + {workerStatus.info.internalAddress} + + +
+
+
+
+ + + Resource Utilization + + + + + + + + + + Process CPU Utilization + + + + + + + + {formatCount( + workerStatus.processCpuLoad[workerStatus.processCpuLoad.length - 1] + )} + % + + + + + System CPU Utilization + + + + + + + + {formatCount( + workerStatus.systemCpuLoad[workerStatus.systemCpuLoad.length - 1] + )} + % + + + +
+
+
+ + + + + + + Heap Utilization + + + + + + + + {formatCount( + (workerStatus.info.heapUsed * 100) / workerStatus.info.heapAvailable + )} + % + + + + + Non-Heap Memory Used + + + + + + + {formatDataSize(workerStatus.info.nonHeapUsed)} + + +
+
+
+
+ + + Memory + + + + + {renderPoolChart('Memory Usage', workerStatus.info.memoryInfo.pool)} + + {renderPoolQueries(workerStatus.info.memoryInfo.pool)} + + + )} + + ) +} diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx deleted file mode 100644 index 2aa1d884e19a..000000000000 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/Workers.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Licensed 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 { Box, Typography } from '@mui/material' - -export const Workers = () => { - return ( - <> - - Workers - - Placeholder for Workers - - ) -} diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx new file mode 100644 index 000000000000..85d0673c47c0 --- /dev/null +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/components/WorkersList.tsx @@ -0,0 +1,108 @@ +/* + * Licensed 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 { Link as RouterLink } from 'react-router-dom' +import { + Box, + Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material' +import { Worker, workerApi } from '../api/webapp/api.ts' +import { ApiResponse } from '../api/base.ts' +import { useEffect, useState } from 'react' +import { Texts } from '../constant.ts' +import { useSnackbar } from './SnackbarContext.ts' +import { styled } from '@mui/material/styles' + +const StyledLink = styled(RouterLink)(({ theme }) => ({ + textDecoration: 'none', + color: theme.palette.info.main, + '&:hover': { + textDecoration: 'underline', + }, +})) + +export const WorkersList = () => { + const { showSnackbar } = useSnackbar() + const [workersList, setWorkersList] = useState([]) + const [error, setError] = useState(null) + + useEffect(() => { + getWorkersList() + const intervalId = setInterval(getWorkersList, 1000) + return () => clearInterval(intervalId) + }, []) + + useEffect(() => { + if (error) { + showSnackbar(error, 'error') + } + }, [error, showSnackbar]) + + const getWorkersList = () => { + setError(null) + workerApi().then((apiResponse: ApiResponse) => { + if (apiResponse.status === 200 && apiResponse.data) { + setWorkersList(apiResponse.data) + } else { + setError(`${Texts.Error.Communication} ${apiResponse.status}: ${apiResponse.message}`) + } + }) + } + + return ( + <> + + Workers + + + Overview + + + + + + + Node Id + Node IP + Node Version + Coordinator + State + + + + {workersList.map((worker: Worker) => ( + + + {worker.nodeId} + + + {worker.nodeIp} + + {worker.nodeVersion} + {String(worker.coordinator)} + {worker.state} + + ))} + +
+
+ + ) +} diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts b/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts index 675eb1d9546e..5858661cc600 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/constant.ts @@ -42,6 +42,7 @@ export const Texts = { Communication: 'Communication error to Trino.', Forbidden: 'Forbidden', Network: 'The network has wandered off, please try again later!', + NodeInformationNotLoaded: 'Node information could not be loaded', }, Menu: { Header: { diff --git a/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx b/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx index b122c061bf6f..e22ffd21b888 100644 --- a/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx +++ b/core/trino-web-ui/src/main/resources/webapp-preview/src/router.tsx @@ -19,7 +19,7 @@ import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined' import { RouteProps } from 'react-router-dom' import { Dashboard } from './components/Dashboard' import { DemoComponents } from './components/DemoComponents' -import { Workers } from './components/Workers' +import { WorkersList } from './components/WorkersList.tsx' import { QueryHistory } from './components/QueryHistory' import { Texts } from './constant' @@ -48,7 +48,7 @@ export const routers: RouterItems = [ icon: , routeProps: { path: '/workers', - element: , + element: , }, }, {