diff --git a/presto-ui/src/components/QueryDetail.jsx b/presto-ui/src/components/QueryDetail.jsx index 36687ba82286b..b9067692edb12 100644 --- a/presto-ui/src/components/QueryDetail.jsx +++ b/presto-ui/src/components/QueryDetail.jsx @@ -12,7 +12,7 @@ * limitations under the License. */ -import React from "react"; +import {useState, useEffect, useRef} from "react"; import DataTable, {createTheme} from 'react-data-table-component'; import { @@ -349,29 +349,22 @@ const HISTOGRAM_PROPERTIES = { disableHiddenCheck: true, }; -class RuntimeStatsList extends React.Component { - constructor(props) { - super(props); - this.state = { - expanded: false - }; - } +const RuntimeStatsList = ({ stats }) => { + const [expanded, setExpanded] = useState(false); - getExpandedIcon() { - return this.state.expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; - } + const getExpandedIcon = () => { + return expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; + }; - getExpandedStyle() { - return this.state.expanded ? {} : {display: "none"}; - } + const getExpandedStyle = () => { + return expanded ? {} : {display: "none"}; + }; - toggleExpanded() { - this.setState({ - expanded: !this.state.expanded, - }) - } + const toggleExpanded = () => { + setExpanded(!expanded); + }; - renderMetricValue(unit, value) { + const renderMetricValue = (unit, value) => { if (unit === "NANO") { return formatDuration(parseDuration(value + "ns")); } @@ -379,69 +372,52 @@ class RuntimeStatsList extends React.Component { return formatDataSize(value); } return formatCount(value); // NONE - } - - render() { - return ( - - - - - - - - - - - { - Object - .values(this.props.stats) - .sort((m1, m2) => (m1.name.localeCompare(m2.name))) - .map((metric) => - - - - - - - - ) - } - -
Metric NameSumCountMinMax - - - -
{metric.name}{this.renderMetricValue(metric.unit, metric.sum)}{formatCount(metric.count)}{this.renderMetricValue(metric.unit, metric.min)}{this.renderMetricValue(metric.unit, metric.max)}
- ); - } -} - -class StageSummary extends React.Component { - constructor(props) { - super(props); - this.state = { - expanded: false, - lastRender: null, - taskFilter: TASK_FILTER.ALL - }; - } + }; - getExpandedIcon() { - return this.state.expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"; - } + return ( + + + + + + + + + + + { + Object + .values(stats) + .sort((m1, m2) => (m1.name.localeCompare(m2.name))) + .map((metric) => + + + + + + + + ) + } + +
Metric NameSumCountMinMax + + + +
{metric.name}{renderMetricValue(metric.unit, metric.sum)}{formatCount(metric.count)}{renderMetricValue(metric.unit, metric.min)}{renderMetricValue(metric.unit, metric.max)}
+ ); +}; - getExpandedStyle() { - return this.state.expanded ? {} : {display: "none"}; - } +const StageSummary = ({ stage }) => { + const [expanded, setExpanded] = useState(false); + const lastRenderRef = useRef(null); + const [taskFilter, setTaskFilter] = useState(TASK_FILTER.ALL); - toggleExpanded() { - this.setState({ - expanded: !this.state.expanded, - }) - } + const getExpandedIcon = () => (expanded ? "bi bi-chevron-up" : "bi bi-chevron-down"); + const getExpandedStyle = () => (expanded ? {} : { display: "none" }); + const toggleExpanded = () => setExpanded(!expanded); - static renderHistogram(histogramId, inputData, numberFormatter) { + const renderHistogram = (histogramId, inputData, numberFormatter) => { const numBuckets = Math.min(HISTOGRAM_WIDTH, Math.sqrt(inputData.length)); const dataMin = Math.min.apply(null, inputData); const dataMax = Math.max.apply(null, inputData); @@ -450,12 +426,10 @@ class StageSummary extends React.Component { let histogramData = []; if (bucketSize === 0) { histogramData = [inputData.length]; - } - else { + } else { for (let i = 0; i < numBuckets + 1; i++) { histogramData.push(0); } - for (let i in inputData) { const dataPoint = inputData[i]; const bucket = Math.floor((dataPoint - dataMin) / bucketSize); @@ -463,405 +437,379 @@ class StageSummary extends React.Component { } } - const tooltipValueLookups = {'offset': {}}; + const tooltipValueLookups = { offset: {} }; for (let i = 0; i < histogramData.length; i++) { - tooltipValueLookups['offset'][i] = numberFormatter(dataMin + (i * bucketSize)) + "-" + numberFormatter(dataMin + ((i + 1) * bucketSize)); + tooltipValueLookups["offset"][i] = + numberFormatter(dataMin + i * bucketSize) + + "-" + + numberFormatter(dataMin + (i + 1) * bucketSize); } - const stageHistogramProperties = $.extend({}, HISTOGRAM_PROPERTIES, {barWidth: (HISTOGRAM_WIDTH / histogramData.length), tooltipValueLookups: tooltipValueLookups}); + const stageHistogramProperties = $.extend({}, HISTOGRAM_PROPERTIES, { + barWidth: HISTOGRAM_WIDTH / histogramData.length, + tooltipValueLookups: tooltipValueLookups, + }); $(histogramId).sparkline(histogramData, stageHistogramProperties); - } + }; - componentDidUpdate() { - const stage = this.props.stage; - const numTasks = stage.latestAttemptExecutionInfo.tasks.length; + const renderBarChart = (barChartId, inputData, chartProperties) => { + $(barChartId).sparkline( + inputData, + $.extend({}, chartProperties, { numberFormatter: formatDuration }) + ); + }; - // sort the x-axis - stage.latestAttemptExecutionInfo.tasks.sort((taskA, taskB) => getTaskNumber(taskA.taskId) - getTaskNumber(taskB.taskId)); + useEffect(() => { + if (!stage || !stage.latestAttemptExecutionInfo) { + return; + } + + const numTasks = stage.latestAttemptExecutionInfo.tasks.length; + // sort the x-axis without mutating props + const sortedTasks = [...stage.latestAttemptExecutionInfo.tasks].sort( + (taskA, taskB) => getTaskNumber(taskA.taskId) - getTaskNumber(taskB.taskId) + ); - const scheduledTimes = stage.latestAttemptExecutionInfo.tasks.map(task => parseDuration(task.stats.totalScheduledTimeInNanos + "ns")); - const cpuTimes = stage.latestAttemptExecutionInfo.tasks.map(task => parseDuration(task.stats.totalCpuTimeInNanos + "ns")); + const scheduledTimes = sortedTasks.map((task) => + parseDuration(task.stats.totalScheduledTimeInNanos + "ns") + ); + const cpuTimes = sortedTasks.map((task) => + parseDuration(task.stats.totalCpuTimeInNanos + "ns") + ); - // prevent multiple calls to componentDidUpdate (resulting from calls to setState or otherwise) within the refresh interval from re-rendering sparklines/charts - if (this.state.lastRender === null || (Date.now() - this.state.lastRender) >= 1000) { + // prevent multiple re-renders within the refresh interval from re-rendering sparklines/charts + if (lastRenderRef.current === null || Date.now() - lastRenderRef.current >= 1000) { const renderTimestamp = Date.now(); const stageId = getStageNumber(stage.stageId); - StageSummary.renderHistogram('#scheduled-time-histogram-' + stageId, scheduledTimes, formatDuration); - StageSummary.renderHistogram('#cpu-time-histogram-' + stageId, cpuTimes, formatDuration); + renderHistogram("#scheduled-time-histogram-" + stageId, scheduledTimes, formatDuration); + renderHistogram("#cpu-time-histogram-" + stageId, cpuTimes, formatDuration); - if (this.state.expanded) { + if (expanded) { // this needs to be a string otherwise it will also be passed to numberFormatter - const tooltipValueLookups = {'offset': {}}; + const tooltipValueLookups = { offset: {} }; for (let i = 0; i < numTasks; i++) { - tooltipValueLookups['offset'][i] = getStageNumber(stage.stageId) + "." + i; + tooltipValueLookups["offset"][i] = stageId + "." + i; } - const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, {barWidth: BAR_CHART_WIDTH / numTasks, tooltipValueLookups: tooltipValueLookups}); + const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, { + barWidth: BAR_CHART_WIDTH / numTasks, + tooltipValueLookups: tooltipValueLookups, + }); - $('#scheduled-time-bar-chart-' + stageId).sparkline(scheduledTimes, $.extend({}, stageBarChartProperties, {numberFormatter: formatDuration})); - $('#cpu-time-bar-chart-' + stageId).sparkline(cpuTimes, $.extend({}, stageBarChartProperties, {numberFormatter: formatDuration})); + renderBarChart("#scheduled-time-bar-chart-" + stageId, scheduledTimes, stageBarChartProperties); + renderBarChart("#cpu-time-bar-chart-" + stageId, cpuTimes, stageBarChartProperties); } - this.setState({ - lastRender: renderTimestamp - }); + lastRenderRef.current = renderTimestamp; } - } - - renderStageExecutionAttemptsTasks(attempts) { - return attempts.map(attempt => { - return this.renderTaskList(attempt.tasks) - }); - } + }, [stage, expanded]); - renderTaskList(tasks) { - tasks = this.state.expanded ? tasks : []; - tasks = tasks.filter(task => this.state.taskFilter.predicate(task.taskStatus.state), this); + const renderTaskList = (tasks) => { + const visibleTasks = expanded ? tasks : []; + const filteredTasks = visibleTasks.filter((task) => taskFilter.predicate(task.taskStatus.state)); return ( - + - + ); - } - - renderTaskFilterListItem(taskFilter) { - return ( -
  • {taskFilter.text}
  • - ); - } + }; - handleTaskFilterClick(filter, event) { - this.setState({ - taskFilter: filter + const renderStageExecutionAttemptsTasks = (attempts) => { + return attempts.map((attempt) => { + return renderTaskList(attempt.tasks); }); + }; + + const handleTaskFilterClick = (filter, event) => { + setTaskFilter(filter); event.preventDefault(); - } + }; + + const renderTaskFilterListItem = (candidateFilter) => ( +
  • + handleTaskFilterClick(candidateFilter, e)} + > + {candidateFilter.text} + +
  • + ); - renderTaskFilter() { - return (
    + const renderTaskFilter = () => ( +

    Tasks

    - - - + + +
    -
    - -
      - {this.renderTaskFilterListItem(TASK_FILTER.ALL)} - {this.renderTaskFilterListItem(TASK_FILTER.PLANNED)} - {this.renderTaskFilterListItem(TASK_FILTER.RUNNING)} - {this.renderTaskFilterListItem(TASK_FILTER.FINISHED)} - {this.renderTaskFilterListItem(TASK_FILTER.FAILED)} -
    -
    -
    +
    + +
      + {renderTaskFilterListItem(TASK_FILTER.ALL)} + {renderTaskFilterListItem(TASK_FILTER.PLANNED)} + {renderTaskFilterListItem(TASK_FILTER.RUNNING)} + {renderTaskFilterListItem(TASK_FILTER.FINISHED)} + {renderTaskFilterListItem(TASK_FILTER.FAILED)} +
    +
    +
    -
    ); +
    + ); + if (stage === undefined || !stage.hasOwnProperty("plan")) { + return ( + + Information about this stage is unavailable. + + ); } - render() { - const stage = this.props.stage; - if (stage === undefined || !stage.hasOwnProperty('plan')) { - return ( - - Information about this stage is unavailable. - ); - } - - const totalBufferedBytes = stage.latestAttemptExecutionInfo.tasks - .map(task => task.outputBuffers.totalBufferedBytes) - .reduce((a, b) => a + b, 0); + const totalBufferedBytes = stage.latestAttemptExecutionInfo.tasks + .map((task) => task.outputBuffers.totalBufferedBytes) + .reduce((a, b) => a + b, 0); - const stageId = getStageNumber(stage.stageId); + const stageId = getStageNumber(stage.stageId); - return ( - - -
    {stageId}
    - - - - + return ( + + + - ); - } -} + {renderStageExecutionAttemptsTasks([stage.latestAttemptExecutionInfo])} + {renderStageExecutionAttemptsTasks(stage.previousAttemptsExecutionInfos)} + +
    +
    {stageId}
    +
    + + - + - + - - + + - {this.renderStageExecutionAttemptsTasks([stage.latestAttemptExecutionInfo])} - - {this.renderStageExecutionAttemptsTasks(stage.previousAttemptsExecutionInfos)} - -
    - - - + + + - - - - - - - - - - - - + + + + + + + + + + + +
    - Time - -
    Time +
    - Scheduled - - {stage.latestAttemptExecutionInfo.stats.totalScheduledTime} -
    - Blocked - - {stage.latestAttemptExecutionInfo.stats.totalBlockedTime} -
    - CPU - - {stage.latestAttemptExecutionInfo.stats.totalCpuTime} -
    Scheduled{stage.latestAttemptExecutionInfo.stats.totalScheduledTime}
    Blocked{stage.latestAttemptExecutionInfo.stats.totalBlockedTime}
    CPU{stage.latestAttemptExecutionInfo.stats.totalCpuTime}
    - - - + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + +
    - Memory - -
    Memory +
    - Cumulative - - {formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeUserMemory / 1000)} -
    - Cumulative Total - - {formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeTotalMemory / 1000)} -
    - Current - - {stage.latestAttemptExecutionInfo.stats.userMemoryReservation} -
    - Buffers - - {formatDataSize(totalBufferedBytes)} -
    - Peak - - {stage.latestAttemptExecutionInfo.stats.peakUserMemoryReservation} -
    Cumulative{formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeUserMemory / 1000)}
    Cumulative Total{formatDataSize(stage.latestAttemptExecutionInfo.stats.cumulativeTotalMemory / 1000)}
    Current{stage.latestAttemptExecutionInfo.stats.userMemoryReservation}
    Buffers{formatDataSize(totalBufferedBytes)}
    Peak{stage.latestAttemptExecutionInfo.stats.peakUserMemoryReservation}
    - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    - Tasks - -
    Tasks +
    - Pending - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.taskStatus.state === "PLANNED").length} -
    - Running - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.taskStatus.state === "RUNNING").length} -
    - Blocked - - {stage.latestAttemptExecutionInfo.tasks.filter(task => task.stats.fullyBlocked).length} -
    - Total - - {stage.latestAttemptExecutionInfo.tasks.length} -
    Pending{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.taskStatus.state === "PLANNED").length}
    Running{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.taskStatus.state === "RUNNING").length}
    Blocked{stage.latestAttemptExecutionInfo.tasks.filter((task) => task.stats.fullyBlocked).length}
    Total{stage.latestAttemptExecutionInfo.tasks.length}
    - - - + + + - - - + + +
    - Scheduled Time Skew -
    Scheduled Time Skew
    -
    -
    + +
    + +
    - - - + + + - - - + + +
    - CPU Time Skew -
    CPU Time Skew
    -
    -
    + +
    + +
    - - + +
    - - - - + + + +
    - Task Scheduled Time - -
    -
    Task Scheduled Time + +
    + +
    - - - - + + + +
    - Task CPU Time - -
    -
    Task CPU Time + +
    + +
    - {this.renderTaskFilter()} -
    {renderTaskFilter()}
    -
    + + + ); +}; -class StageList extends React.Component { - getStages(stage) { +const StageList = ({ outputStage }) => { + const getStages = (stage) => { if (stage === undefined || !stage.hasOwnProperty('subStages')) { return [] } - return [].concat.apply(stage, stage.subStages.map(this.getStages, this)); - } - - render() { - const stages = this.getStages(this.props.outputStage); - - if (stages === undefined || stages.length === 0) { - return ( -
    -
    - No stage information available. -
    -
    - ); - } + return [].concat.apply(stage, stage.subStages.map(getStages)); + }; - const renderedStages = stages.map(stage => ); + const stages = getStages(outputStage); + if (stages === undefined || stages.length === 0) { return (
    - - - {renderedStages} - -
    + No stage information available.
    ); } -} + + const renderedStages = stages.map(stage => ); + + return ( +
    +
    + + + {renderedStages} + +
    +
    +
    + ); +}; const SMALL_SPARKLINE_PROPERTIES = { width: '100%', @@ -896,49 +844,74 @@ const TASK_FILTER = { }, }; -export class QueryDetail extends React.Component { +const QueryDetail = () => { + const [state, setState] = useState({ + query: null, + lastSnapshotStages: null, - constructor(props) { - super(props); - this.state = { - query: null, - lastSnapshotStages: null, + lastScheduledTime: 0, + lastCpuTime: 0, + lastRowInput: 0, + lastByteInput: 0, - lastScheduledTime: 0, - lastCpuTime: 0, - lastRowInput: 0, - lastByteInput: 0, + scheduledTimeRate: [], + cpuTimeRate: [], + rowInputRate: [], + byteInputRate: [], - scheduledTimeRate: [], - cpuTimeRate: [], - rowInputRate: [], - byteInputRate: [], + reservedMemory: [], - reservedMemory: [], + initialized: false, + ended: false, - initialized: false, - ended: false, + lastRefresh: null, + lastRender: null, - lastRefresh: null, - lastRender: null, + stageRefresh: true, + }); - stageRefresh: true, - }; + const dataSet = useRef({ + lastSnapshotStages: null, + + lastScheduledTime: 0, + lastCpuTime: 0, + lastRowInput: 0, + lastByteInput: 0, + + scheduledTimeRate: [], + cpuTimeRate: [], + rowInputRate: [], + byteInputRate: [], + + reservedMemory: [], + + ended: false, + + lastRefresh: null, + + stageRefresh: true, + }); - this.refreshLoop = this.refreshLoop.bind(this); + const countSharedStackFrames = (stack, parentStack) => { + let n = 0; + const minStackLength = Math.min(stack.length, parentStack.length); + while (n < minStackLength && stack[stack.length - 1 - n] === parentStack[parentStack.length - 1 - n]) { + n++; + } + return n; } - static formatStackTrace(info) { - return QueryDetail.formatStackTraceHelper(info, [], "", ""); + const failureInfoToString = (t) => { + return (t.message !== null) ? (t.type + ": " + t.message) : t.type; } - static formatStackTraceHelper(info, parentStack, prefix, linePrefix) { - let s = linePrefix + prefix + QueryDetail.failureInfoToString(info) + "\n"; + const formatStackTraceHelper = (info, parentStack, prefix, linePrefix) => { + let s = linePrefix + prefix + failureInfoToString(info) + "\n"; if (info.stack) { let sharedStackFrames = 0; if (parentStack !== null) { - sharedStackFrames = QueryDetail.countSharedStackFrames(info.stack, parentStack); + sharedStackFrames = countSharedStackFrames(info.stack, parentStack); } for (let i = 0; i < info.stack.length - sharedStackFrames; i++) { @@ -951,40 +924,22 @@ export class QueryDetail extends React.Component { if (info.suppressed) { for (let i = 0; i < info.suppressed.length; i++) { - s += QueryDetail.formatStackTraceHelper(info.suppressed[i], info.stack, "Suppressed: ", linePrefix + "\t"); + s += formatStackTraceHelper(info.suppressed[i], info.stack, "Suppressed: ", linePrefix + "\t"); } } if (info.cause) { - s += QueryDetail.formatStackTraceHelper(info.cause, info.stack, "Caused by: ", linePrefix); + s += formatStackTraceHelper(info.cause, info.stack, "Caused by: ", linePrefix); } return s; } - static countSharedStackFrames(stack, parentStack) { - let n = 0; - const minStackLength = Math.min(stack.length, parentStack.length); - while (n < minStackLength && stack[stack.length - 1 - n] === parentStack[parentStack.length - 1 - n]) { - n++; - } - return n; - } - - static failureInfoToString(t) { - return (t.message !== null) ? (t.type + ": " + t.message) : t.type; + const formatStackTrace = (info) => { + return formatStackTraceHelper(info, [], "", ""); } - resetTimer() { - clearTimeout(this.timeoutId); - // stop refreshing when query finishes or fails - if (this.state.query === null || !this.state.ended) { - // task.info-update-interval is set to 3 seconds by default - this.timeoutId = setTimeout(this.refreshLoop, 3000); - } - } - - static getQueryURL(id) { + const getQueryURL = (id) => { if (!id || typeof id !== 'string' || id.length === 0) { return "/v1/query/undefined"; } @@ -992,43 +947,66 @@ export class QueryDetail extends React.Component { return sanitizedId.length > 0 ? `/v1/query/${encodeURIComponent(sanitizedId)}` : "/v1/query/undefined"; } + const timerIdRef = useRef(null); + const endedRef = useRef(false); + useEffect(() => { endedRef.current = state.ended; }, [state.ended]); - refreshLoop() { - clearTimeout(this.timeoutId); // to stop multiple series of refreshLoop from going on simultaneously - const queryId = getFirstParameter(window.location.search); + const refreshLoop = () => { + // to stop multiple series of refreshLoop from going on simultaneously + clearTimeout(timerIdRef.current); + if (!endedRef.current) { + timerIdRef.current = setTimeout(fetchData, 3000); + } + }; - $.get(QueryDetail.getQueryURL(queryId), function (query) { - let lastSnapshotStages = this.state.lastSnapshotStage; - if (this.state.stageRefresh) { - lastSnapshotStages = query.outputStage; - } + const fetchData = () => { + const queryId = getFirstParameter(window.location.search); - let lastRefresh = this.state.lastRefresh; - const lastScheduledTime = this.state.lastScheduledTime; - const lastCpuTime = this.state.lastCpuTime; - const lastRowInput = this.state.lastRowInput; - const lastByteInput = this.state.lastByteInput; - const alreadyEnded = this.state.ended; + fetch(getQueryURL(queryId)) + .then(response => response.json()) + .then(query => { + const { + lastSnapshotStages: currentSnapshotStages, + stageRefresh, + lastRefresh: currentLastRefresh, + lastScheduledTime, + lastCpuTime, + lastRowInput, + lastByteInput, + ended: alreadyEnded + } = dataSet.current; + + let lastSnapshotStages = stageRefresh ? query.outputStage : currentSnapshotStages; + let lastRefresh = currentLastRefresh; const nowMillis = Date.now(); - this.setState({ - query: query, - lastSnapshotStage: lastSnapshotStages, - + dataSet.current = { + ...dataSet.current, + lastSnapshotStages: lastSnapshotStages, lastScheduledTime: parseDuration(query.queryStats.totalScheduledTime), lastCpuTime: parseDuration(query.queryStats.totalCpuTime), lastRowInput: query.queryStats.processedInputPositions, lastByteInput: parseDataSize(query.queryStats.processedInputDataSize), + ended: query.finalQueryInfo, + lastRefresh: nowMillis, + }; + setState(prev => ({ + ...prev, + query: query, + lastSnapshotStages: lastSnapshotStages, + lastScheduledTime: parseDuration(query.queryStats.totalScheduledTime), + lastCpuTime: parseDuration(query.queryStats.totalCpuTime), + lastRowInput: query.queryStats.processedInputPositions, + lastByteInput: parseDataSize(query.queryStats.processedInputDataSize), initialized: true, ended: query.finalQueryInfo, - lastRefresh: nowMillis, - }); + })); // i.e. don't show sparklines if we've already decided not to update or if we don't have one previous measurement if (alreadyEnded || (lastRefresh === null && query.state === "RUNNING")) { - this.resetTimer(); + refreshLoop(); return; } @@ -1042,65 +1020,51 @@ export class QueryDetail extends React.Component { const currentCpuTimeRate = (parseDuration(query.queryStats.totalCpuTime) - lastCpuTime) / (elapsedSecsSinceLastRefresh * 1000); const currentRowInputRate = (query.queryStats.processedInputPositions - lastRowInput) / elapsedSecsSinceLastRefresh; const currentByteInputRate = (parseDataSize(query.queryStats.processedInputDataSize) - lastByteInput) / elapsedSecsSinceLastRefresh; - this.setState({ - scheduledTimeRate: addToHistory(currentScheduledTimeRate, this.state.scheduledTimeRate), - cpuTimeRate: addToHistory(currentCpuTimeRate, this.state.cpuTimeRate), - rowInputRate: addToHistory(currentRowInputRate, this.state.rowInputRate), - byteInputRate: addToHistory(currentByteInputRate, this.state.byteInputRate), - reservedMemory: addToHistory(parseDataSize(query.queryStats.userMemoryReservation), this.state.reservedMemory), - }); + dataSet.current = { + ...dataSet.current, + scheduledTimeRate: addToHistory(currentScheduledTimeRate, dataSet.current.scheduledTimeRate), + cpuTimeRate: addToHistory(currentCpuTimeRate, dataSet.current.cpuTimeRate), + rowInputRate: addToHistory(currentRowInputRate, dataSet.current.rowInputRate), + byteInputRate: addToHistory(currentByteInputRate, dataSet.current.byteInputRate), + reservedMemory: addToHistory(parseDataSize(query.queryStats.userMemoryReservation), dataSet.current.reservedMemory), + }; + setState(prev => ({ + ...prev, + scheduledTimeRate: dataSet.current.scheduledTimeRate, + cpuTimeRate: dataSet.current.cpuTimeRate, + rowInputRate: dataSet.current.rowInputRate, + byteInputRate: dataSet.current.byteInputRate, + reservedMemory: dataSet.current.reservedMemory, + })); } - this.resetTimer(); - }.bind(this)) - .fail(() => { - this.setState({ - initialized: true, - }); - this.resetTimer(); - }); - } - - handleStageRefreshClick() { - if (this.state.stageRefresh) { - this.setState({ - stageRefresh: false, - lastSnapshotStages: this.state.query.outputStage, - }); - } - else { - this.setState({ - stageRefresh: true, - }); - } + refreshLoop(); + }) + .catch(() => { + setState(prev => ({ + ...prev, + initialized: true, + })); + refreshLoop(); + }); } - renderStageRefreshButton() { - if (this.state.stageRefresh) { - return - } - else { - return + const updateCharts = () => { + if (state.query === null) { + return; } - } - - componentDidMount() { - this.refreshLoop(); - } - - componentDidUpdate() { - // prevent multiple calls to componentDidUpdate (resulting from calls to setState or otherwise) within the refresh interval from re-rendering sparklines/charts - if (this.state.lastRender === null || (Date.now() - this.state.lastRender) >= 1000) { + // prevent multiple calls to useEffect (resulting from calls to setState or otherwise) within the refresh interval from re-rendering sparklines/charts + if (state.lastRender === null || (Date.now() - state.lastRender) >= 1000) { const renderTimestamp = Date.now(); - $('#scheduled-time-rate-sparkline').sparkline(this.state.scheduledTimeRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, { + $('#scheduled-time-rate-sparkline').sparkline(dataSet.current.scheduledTimeRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, { chartRangeMin: 0, numberFormatter: precisionRound })); - $('#cpu-time-rate-sparkline').sparkline(this.state.cpuTimeRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {chartRangeMin: 0, numberFormatter: precisionRound})); - $('#row-input-rate-sparkline').sparkline(this.state.rowInputRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatCount})); - $('#byte-input-rate-sparkline').sparkline(this.state.byteInputRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatDataSize})); - $('#reserved-memory-sparkline').sparkline(this.state.reservedMemory, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatDataSize})); + $('#cpu-time-rate-sparkline').sparkline(dataSet.current.cpuTimeRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {chartRangeMin: 0, numberFormatter: precisionRound})); + $('#row-input-rate-sparkline').sparkline(dataSet.current.rowInputRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatCount})); + $('#byte-input-rate-sparkline').sparkline(dataSet.current.byteInputRate, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatDataSize})); + $('#reserved-memory-sparkline').sparkline(dataSet.current.reservedMemory, $.extend({}, SMALL_SPARKLINE_PROPERTIES, {numberFormatter: formatDataSize})); - if (this.state.lastRender === null) { + if (state.lastRender === null) { $('#query').each((i, block) => { hljs.highlightBlock(block); }); @@ -1110,17 +1074,61 @@ export class QueryDetail extends React.Component { }); } - this.setState({ + setState(prev => ({ + ...prev, lastRender: renderTimestamp, - }); + })); } $('[data-bs-toggle="tooltip"]')?.tooltip?.(); new Clipboard('.copy-button'); } - renderStages() { - if (this.state.lastSnapshotStage === null) { + useEffect(() => { + fetchData(); + + return () => { + clearTimeout(timerIdRef.current); + }; + }, []) + + useEffect(() => { + updateCharts(); + }) + + + const handleStageRefreshClick = () => { + if (state.stageRefresh) { + setState(prev => ({ + ...prev, + stageRefresh: false, + lastSnapshotStages: prev.query ? prev.query.outputStage : prev.lastSnapshotStages, + })); + dataSet.current.stageRefresh = false; + if (state.query && state.query.outputStage) { + dataSet.current.lastSnapshotStages = state.query.outputStage; + } + } + else { + setState(prev => ({ + ...prev, + stageRefresh: true, + })); + dataSet.current.stageRefresh = true; + } + } + + const renderStageRefreshButton = () => { + if (state.stageRefresh) { + return + } + else { + return + } + } + + const renderStages = () => { + if (state.lastSnapshotStages === null) { return; } @@ -1135,7 +1143,7 @@ export class QueryDetail extends React.Component { - {this.renderStageRefreshButton()} + {renderStageRefreshButton()} @@ -1144,15 +1152,15 @@ export class QueryDetail extends React.Component {
    - +
    ); } - renderPreparedQuery() { - const query = this.state.query; + const renderPreparedQuery = () => { + const {query} = state; if (!query.hasOwnProperty('preparedQuery') || query.preparedQuery === null) { return; } @@ -1174,8 +1182,8 @@ export class QueryDetail extends React.Component { ); } - renderSessionProperties() { - const query = this.state.query; + const renderSessionProperties = () => { + const {query} = state; const properties = []; for (let property in query.session.systemProperties) { @@ -1201,8 +1209,8 @@ export class QueryDetail extends React.Component { return properties; } - renderResourceEstimates() { - const query = this.state.query; + const renderResourceEstimates = () => { + const {query} = state; const estimates = query.session.resourceEstimates; const renderedEstimates = []; @@ -1223,8 +1231,8 @@ export class QueryDetail extends React.Component { return renderedEstimates; } - renderWarningInfo() { - const query = this.state.query; + const renderWarningInfo = () => { + const {query} = state; if (query.warnings.length > 0) { return (
    @@ -1252,8 +1260,8 @@ export class QueryDetail extends React.Component { } } - renderRuntimeStats() { - const query = this.state.query; + const renderRuntimeStats = () => { + const {query} = state; if (query.queryStats.runtimeStats === undefined) { return null; } @@ -1271,8 +1279,8 @@ export class QueryDetail extends React.Component { ); } - renderFailureInfo() { - const query = this.state.query; + const renderFailureInfo = () => { + const {query} = state; if (query.failureInfo) { return (
    @@ -1294,7 +1302,7 @@ export class QueryDetail extends React.Component { Error Code - {query.errorCode.name + " (" + this.state.query.errorCode.code + ")"} + {query.errorCode.name + " (" + query.errorCode.code + ")"} @@ -1307,7 +1315,7 @@ export class QueryDetail extends React.Component {
    -                                            {QueryDetail.formatStackTrace(query.failureInfo)}
    +                                            {formatStackTrace(query.failureInfo)}
                                             
    @@ -1322,480 +1330,480 @@ export class QueryDetail extends React.Component { } } - render() { - const query = this.state.query; - if (query === null || this.state.initialized === false) { - let label = (
    Loading...
    ); - if (this.state.initialized) { - label = "Query not found"; - } - return ( -
    -

    {label}

    -
    - ); - } + const {query} = state; + if (query === null || state.initialized === false) { + let label = (
    Loading...
    ); + if (state.initialized) { + label = "Query not found"; + } return ( -
    - -
    -
    -

    Session

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - User - - {query.session.user} -    - - -
    - Principal - - {query.session.principal} -
    - Source - - {query.session.source} -
    - Catalog - - {query.session.catalog} -
    - Schema - - {query.session.schema} -
    - Client Address - - {query.session.remoteUserAddress} -
    - Client Tags - - {query.session.clientTags.join(", ")} -
    - Session Properties - - {this.renderSessionProperties()} -
    - Resource Estimates - - {this.renderResourceEstimates()} -
    -
    -
    -

    Execution

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Resource Group - - {query.resourceGroupId ? query.resourceGroupId.join(".") : "n/a"} -
    - Submission Time - - {formatShortDateTime(new Date(query.queryStats.createTime))} -
    - Completion Time - - {new Date(query.queryStats.endTime).getTime() !== 0 ? formatShortDateTime(new Date(query.queryStats.endTime)) : ""} -
    - Elapsed Time - - {query.queryStats.elapsedTime} -
    - Prerequisites Wait Time - - {query.queryStats.waitingForPrerequisitesTime} -
    - Queued Time - - {query.queryStats.queuedTime} -
    - Planning Time - - {query.queryStats.totalPlanningTime} -
    - Execution Time - - {query.queryStats.executionTime} -
    - Coordinator - - {getHostname(query.self)} -
    -
    +
    +

    {label}

    +
    + ); + } + + return ( +
    + +
    +
    +

    Session

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + User + + {query.session.user} +    + + +
    + Principal + + {query.session.principal} +
    + Source + + {query.session.source} +
    + Catalog + + {query.session.catalog} +
    + Schema + + {query.session.schema} +
    + Client Address + + {query.session.remoteUserAddress} +
    + Client Tags + + {query.session.clientTags.join(", ")} +
    + Session Properties + + {renderSessionProperties()} +
    + Resource Estimates + + {renderResourceEstimates()} +
    -
    -
    -
    -
    -

    Resource Utilization Summary

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    +

    Execution

    +
    +
    - CPU Time - - {query.queryStats.totalCpuTime} -
    - Scheduled Time - - {query.queryStats.totalScheduledTime} -
    - Blocked Time - - {query.queryStats.totalBlockedTime} -
    - Input Rows - - {formatCount(query.queryStats.processedInputPositions)} -
    - Input Data - - {query.queryStats.processedInputDataSize} -
    - Raw Input Rows - - {formatCount(query.queryStats.rawInputPositions)} -
    - Raw Input Data - - {query.queryStats.rawInputDataSize} -
    - - Shuffled Rows - - - {formatCount(query.queryStats.shuffledPositions)} -
    - - Shuffled Data - - - {query.queryStats.shuffledDataSize} -
    - Peak User Memory - - {query.queryStats.peakUserMemoryReservation} -
    - Peak Total Memory - - {query.queryStats.peakTotalMemoryReservation} -
    - Memory Pool - - {query.memoryPool} -
    - Cumulative User Memory - - {formatDataSize(query.queryStats.cumulativeUserMemory / 1000.0)} -
    - Cumulative Total - - {formatDataSize(query.queryStats.cumulativeTotalMemory / 1000.0)} -
    - Output Rows - - {formatCount(query.queryStats.outputPositions)} -
    - Output Data - - {query.queryStats.outputDataSize} -
    - Written Output Rows - - {formatCount(query.queryStats.writtenOutputPositions)} -
    - Written Output Logical Data Size - - {query.queryStats.writtenOutputLogicalDataSize} -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Resource Group + + {query.resourceGroupId ? query.resourceGroupId.join(".") : "n/a"} +
    + Submission Time + + {formatShortDateTime(new Date(query.queryStats.createTime))} +
    + Completion Time + + {new Date(query.queryStats.endTime).getTime() !== 0 ? formatShortDateTime(new Date(query.queryStats.endTime)) : ""} +
    + Elapsed Time + + {query.queryStats.elapsedTime} +
    + Prerequisites Wait Time + + {query.queryStats.waitingForPrerequisitesTime} +
    + Queued Time + + {query.queryStats.queuedTime} +
    + Planning Time + + {query.queryStats.totalPlanningTime} +
    + Execution Time + + {query.queryStats.executionTime} +
    + Coordinator + + {getHostname(query.self)} +
    +
    +
    +
    +
    +
    +
    +

    Resource Utilization Summary

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {parseDataSize(query.queryStats.spilledDataSize) > 0 && - - {parseDataSize(query.queryStats.spilledDataSize) > 0 && - - - - - } - -
    + CPU Time + + {query.queryStats.totalCpuTime} +
    + Scheduled Time + + {query.queryStats.totalScheduledTime} +
    + Blocked Time + + {query.queryStats.totalBlockedTime} +
    + Input Rows + + {formatCount(query.queryStats.processedInputPositions)} +
    + Input Data + + {query.queryStats.processedInputDataSize} +
    + Raw Input Rows + + {formatCount(query.queryStats.rawInputPositions)} +
    + Raw Input Data + + {query.queryStats.rawInputDataSize} +
    + + Shuffled Rows + + + {formatCount(query.queryStats.shuffledPositions)} +
    + + Shuffled Data + + + {query.queryStats.shuffledDataSize} +
    + Peak User Memory + + {query.queryStats.peakUserMemoryReservation} +
    + Peak Total Memory + + {query.queryStats.peakTotalMemoryReservation} +
    + Memory Pool + + {query.memoryPool} +
    + Cumulative User Memory + + {formatDataSize(query.queryStats.cumulativeUserMemory / 1000.0)} +
    + Cumulative Total + + {formatDataSize(query.queryStats.cumulativeTotalMemory / 1000.0)} +
    + Output Rows + + {formatCount(query.queryStats.outputPositions)} +
    + Output Data + + {query.queryStats.outputDataSize} +
    + Written Output Rows + + {formatCount(query.queryStats.writtenOutputPositions)} +
    + Written Output Logical Data Size + + {query.queryStats.writtenOutputLogicalDataSize} +
    + Written Output Physical Data Size + + {query.queryStats.writtenOutputPhysicalDataSize} +
    - Written Output Physical Data Size + Spilled Data - {query.queryStats.writtenOutputPhysicalDataSize} -
    - Spilled Data - - {query.queryStats.spilledDataSize} -
    -
    -
    -

    Timeline

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Parallelism - -
    -
    Loading ...
    -
    + {query.queryStats.spilledDataSize}
    - {formatCount(this.state.cpuTimeRate[this.state.cpuTimeRate.length - 1])} -
    - Scheduled Time/s - -
    -
    Loading ...
    -
    -
    - {formatCount(this.state.scheduledTimeRate[this.state.scheduledTimeRate.length - 1])} -
    - Input Rows/s - -
    -
    Loading ...
    -
    -
    - {formatCount(this.state.rowInputRate[this.state.rowInputRate.length - 1])} -
    - Input Bytes/s - -
    -
    Loading ...
    -
    -
    - {formatDataSize(this.state.byteInputRate[this.state.byteInputRate.length - 1])} -
    - Memory Utilization - -
    -
    Loading ...
    -
    -
    - {formatDataSize(this.state.reservedMemory[this.state.reservedMemory.length - 1])} -
    -
    + } + + +
    +
    +

    Timeline

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Parallelism + +
    +
    Loading ...
    +
    +
    + {formatCount(state.cpuTimeRate[state.cpuTimeRate.length - 1])} +
    + Scheduled Time/s + +
    +
    Loading ...
    +
    +
    + {formatCount(state.scheduledTimeRate[state.scheduledTimeRate.length - 1])} +
    + Input Rows/s + +
    +
    Loading ...
    +
    +
    + {formatCount(state.rowInputRate[state.rowInputRate.length - 1])} +
    + Input Bytes/s + +
    +
    Loading ...
    +
    +
    + {formatDataSize(state.byteInputRate[state.byteInputRate.length - 1])} +
    + Memory Utilization + +
    +
    Loading ...
    +
    +
    + {formatDataSize(state.reservedMemory[state.reservedMemory.length - 1])} +
    - {this.renderRuntimeStats()} - {this.renderWarningInfo()} - {this.renderFailureInfo()} -
    -
    -

    - Query - - -

    -
    -                            
    -                                {query.query}
    -                            
    -                        
    -
    - {this.renderPreparedQuery()} +
    + {renderRuntimeStats()} + {renderWarningInfo()} + {renderFailureInfo()} +
    +
    +

    + Query + + +

    +
    +                        
    +                            {query.query}
    +                        
    +                    
    - {this.renderStages()} + {renderPreparedQuery()}
    - ); - } -} + {renderStages()} +
    + ); +}; + export default QueryDetail; diff --git a/presto-ui/src/components/QueryHeader.jsx b/presto-ui/src/components/QueryHeader.jsx index 9a374016a05a4..3ad708d04604f 100644 --- a/presto-ui/src/components/QueryHeader.jsx +++ b/presto-ui/src/components/QueryHeader.jsx @@ -17,13 +17,8 @@ import { clsx } from 'clsx'; import {getHumanReadableState, getProgressBarPercentage, getProgressBarTitle, getQueryStateColor, isQueryEnded} from "../utils"; -export class QueryHeader extends React.Component { - constructor(props) { - super(props); - } - - renderProgressBar() { - const query = this.props.query; +export const QueryHeader = ({ query }) => { + const renderProgressBar = () => { const queryStateColor = getQueryStateColor( query.state, query.queryStats && query.queryStats.fullyBlocked, @@ -82,55 +77,49 @@ export class QueryHeader extends React.Component { ); - } - - isActive(path) { - if (window.location.pathname.includes(path)) { - return true; - } + }; - return false; - } + const isActive = (path) => { + return window.location.pathname.includes(path); + }; - render() { - const query = this.props.query; - const queryId = this.props.query.queryId; - const tabs = [ - {path: 'query.html', label: 'Overview'}, - {path: 'plan.html', label: 'Live Plan'}, - {path: 'stage.html', label: 'Stage Performance'}, - {path: 'timeline.html', label: 'Splits'}, - ]; - return ( -
    -
    -
    -

    - {query.queryId} - - -

    -
    -
    - -
    + const {queryId} = query; + const tabs = [ + {path: 'query.html', label: 'Overview'}, + {path: 'plan.html', label: 'Live Plan'}, + {path: 'stage.html', label: 'Stage Performance'}, + {path: 'timeline.html', label: 'Splits'}, + ]; + + return ( +
    +
    +
    +

    + {query.queryId} + + +

    -
    -
    -
    - {this.renderProgressBar()} -
    +
    +
    - ); - } -} +
    +
    +
    + {renderProgressBar()} +
    +
    +
    + ); +}; diff --git a/presto-ui/src/components/QueryList.jsx b/presto-ui/src/components/QueryList.jsx index 52ae8e2b9d8d0..ac34afdf1f605 100644 --- a/presto-ui/src/components/QueryList.jsx +++ b/presto-ui/src/components/QueryList.jsx @@ -12,7 +12,7 @@ * limitations under the License. */ -import React from "react"; +import {useState, useEffect, useRef, useCallback} from "react"; import { formatCount, @@ -29,7 +29,7 @@ import { truncateString } from "../utils"; -function getHumanReadableStateFromInfo(query) { +const getHumanReadableStateFromInfo = (query) => { const progress = query.progress; return getHumanReadableState( query.queryState, @@ -40,10 +40,10 @@ function getHumanReadableStateFromInfo(query) { query.errorCode ? query.errorCode.type : null, query.errorCode ? query.errorCode.name : null ); -} +}; -function ResourceGroupLinks({groupId, length=35}) { +const ResourceGroupLinks = ({groupId, length=35}) => { if (!groupId?.length) return ('n/a'); let previousLen = 0; @@ -68,16 +68,15 @@ function ResourceGroupLinks({groupId, length=35}) { return ( <>{links} ); -} +}; -export class QueryListItem extends React.Component { - static stripQueryTextWhitespace(queryText, isTruncated) { - const lines = queryText.split("\n"); - let minLeadingWhitespace = -1; - for (let i = 0; i < lines.length; i++) { - if (minLeadingWhitespace === 0) { - break; - } +const stripQueryTextWhitespace = (queryText, isTruncated) => { + const lines = queryText.split("\n"); + let minLeadingWhitespace = -1; + for (let i = 0; i < lines.length; i++) { + if (minLeadingWhitespace === 0) { + break; + } if (lines[i].trim().length === 0) { continue; @@ -117,30 +116,28 @@ export class QueryListItem extends React.Component { } } - return isTruncated ? formattedQueryText + "..." : truncateString(formattedQueryText, maxQueryLength); - } + return isTruncated ? formattedQueryText + "..." : truncateString(formattedQueryText, maxQueryLength); +}; - renderWarning() { - const query = this.props.query; +export const QueryListItem = ({ query }) => { + const renderWarning = () => { if (query.warningCodes && query.warningCodes.length) { return ( ); } - } + }; - render() { - const query = this.props.query; - const queryStateColor = getQueryStateColor( - query.queryState, - query.progress && query.progress.blocked, - query.errorCode ? query.errorCode.type : null, - query.errorCode ? query.errorCode.name : null - ); - const progressPercentage = getProgressBarPercentage(query.progress.progressPercentage, query.queryState); - const progressBarStyle = {width: progressPercentage + "%", backgroundColor: queryStateColor}; - const humanReadableState = getHumanReadableStateFromInfo(query); - const progressBarTitle = getProgressBarTitle(query.progress.progressPercentage, query.queryState, humanReadableState); + const queryStateColor = getQueryStateColor( + query.queryState, + query.progress && query.progress.blocked, + query.errorCode ? query.errorCode.type : null, + query.errorCode ? query.errorCode.name : null + ); + const progressPercentage = getProgressBarPercentage(query.progress.progressPercentage, query.queryState); + const progressBarStyle = {width: progressPercentage + "%", backgroundColor: queryStateColor}; + const humanReadableState = getHumanReadableStateFromInfo(query); + const progressBarTitle = getProgressBarTitle(query.progress.progressPercentage, query.queryState, humanReadableState); const driverDetails = (
    @@ -229,103 +226,99 @@ export class QueryListItem extends React.Component { ); } - return ( -
    -
    -
    -
    -
    - {query.queryId} - {this.renderWarning()} -
    -
    - {formatShortTime(new Date(Date.parse(query.createTime)))} -
    + return ( +
    +
    +
    +
    +
    + {query.queryId} + {renderWarning()}
    -
    -
    - -    - {user} - -
    +
    + {formatShortTime(new Date(Date.parse(query.createTime)))}
    -
    -
    - -    - {truncateString(query.source, 35)} - -
    +
    +
    +
    + +    + {user} +
    -
    -
    - -    - - - +
    +
    +
    + +    + {truncateString(query.source, 35)} + +
    +
    +
    +
    + +    + + -
    +
    +
    - { query.progress.completedSplits ? - <> -
    - {newDriverDetails} -
    -
    - {splitDetails} -
    - : + { query.progress.completedSplits ? + <>
    - {driverDetails} + {newDriverDetails}
    - } -
    - {timingDetails} -
    +
    + {splitDetails} +
    + :
    - {memoryDetails} + {driverDetails}
    + } +
    + {timingDetails}
    -
    -
    -
    -
    -
    - {progressBarTitle} -
    +
    + {memoryDetails} +
    +
    +
    +
    +
    +
    +
    + {progressBarTitle}
    -
    -
    -
    {QueryListItem.stripQueryTextWhitespace(query.query, query.queryTruncated)}
    -
    +
    +
    +
    +
    {stripQueryTextWhitespace(query.query, query.queryTruncated)}
    - ); - } -} +
    + ); +}; + +const DisplayedQueriesList = ({ queries }) => { + const queryNodes = queries.map((query) => ( + + )); + return ( +
    + {queryNodes} +
    + ); +}; -class DisplayedQueriesList extends React.Component { - render() { - const queryNodes = this.props.queries.map(function (query) { - return ( - - ); - }.bind(this)); - return ( -
    - {queryNodes} -
    - ); - } -} const FILTER_TYPE = { RUNNING: function (query) { @@ -356,61 +349,68 @@ const SORT_ORDER = { DESCENDING: function (value) {return -value} }; -export class QueryList extends React.Component { - constructor(props) { - super(props); - this.state = { - allQueries: [], - displayedQueries: [], - reorderInterval: 5000, - currentSortType: SORT_TYPE.CREATED, - currentSortOrder: SORT_ORDER.DESCENDING, - stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED], - errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL], - searchString: '', - maxQueries: 100, - lastRefresh: Date.now(), - lastReorder: Date.now(), - initialized: false - }; +export const QueryList = () => { + const [state, setState] = useState({ + allQueries: [], + displayedQueries: [], + reorderInterval: 5000, + currentSortType: SORT_TYPE.CREATED, + currentSortOrder: SORT_ORDER.DESCENDING, + stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED], + errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL], + searchString: '', + maxQueries: 100, + lastRefresh: Date.now(), + lastReorder: Date.now(), + initialized: false, + }); - this.refreshLoop = this.refreshLoop.bind(this); - this.handleSearchStringChange = this.handleSearchStringChange.bind(this); - this.executeSearch = this.executeSearch.bind(this); - this.handleSortClick = this.handleSortClick.bind(this); - } + const timeoutId = useRef(null); + const searchTimeoutId = useRef(null); + const dataSet = useRef({ + allQueries: [], + displayedQueries: [], + reorderInterval: 5000, + currentSortType: SORT_TYPE.CREATED, + currentSortOrder: SORT_ORDER.DESCENDING, + stateFilters: [FILTER_TYPE.RUNNING, FILTER_TYPE.QUEUED], + errorTypeFilters: [ERROR_TYPE.INTERNAL_ERROR, ERROR_TYPE.INSUFFICIENT_RESOURCES, ERROR_TYPE.EXTERNAL], + searchString: '', + maxQueries: 100, + lastReorder: Date.now(), + }); - sortAndLimitQueries(queries, sortType, sortOrder, maxQueries) { - queries.sort(function (queryA, queryB) { + const sortAndLimitQueries = (queries, sortType, sortOrder, maxQueriesValue) => { + queries.sort((queryA, queryB) => { return sortOrder(sortType(queryA) - sortType(queryB)); - }, this); + }); - if (maxQueries !== 0 && queries.length > maxQueries) { - queries.splice(maxQueries, (queries.length - maxQueries)); + if (maxQueriesValue !== 0 && queries.length > maxQueriesValue) { + queries.splice(maxQueriesValue, (queries.length - maxQueriesValue)); } - } + }; - filterQueries(queries, stateFilters, errorTypeFilters, searchString) { - const stateFilteredQueries = queries.filter(function (query) { - for (let i = 0; i < stateFilters.length; i++) { - if (stateFilters[i](query)) { + const filterQueries = (queries, stateFiltersValue, errorTypeFiltersValue, searchStringValue) => { + const stateFilteredQueries = queries.filter((query) => { + for (let i = 0; i < stateFiltersValue.length; i++) { + if (stateFiltersValue[i](query)) { return true; } } - for (let i = 0; i < errorTypeFilters.length; i++) { - if (errorTypeFilters[i](query)) { + for (let i = 0; i < errorTypeFiltersValue.length; i++) { + if (errorTypeFiltersValue[i](query)) { return true; } } return false; }); - if (searchString === '') { + if (searchStringValue === '') { return stateFilteredQueries; } else { - return stateFilteredQueries.filter(function (query) { - const term = searchString.toLowerCase(); + return stateFilteredQueries.filter((query) => { + const term = searchStringValue.toLowerCase(); const humanReadableState = getHumanReadableStateFromInfo(query); if (query.queryId.toLowerCase().indexOf(term) !== -1 || humanReadableState.toLowerCase().indexOf(term) !== -1 || @@ -430,27 +430,17 @@ export class QueryList extends React.Component { return true; } - return query.warningCodes.some(function (warning) { + return query.warningCodes.some((warning) => { if ("warning".indexOf(term) !== -1 || warning.indexOf(term) !== -1) { return true; } }); - - }, this); - } - } - - resetTimer() { - clearTimeout(this.timeoutId); - // stop refreshing when query finishes or fails - if (this.state.query === null || !this.state.ended) { - this.timeoutId = setTimeout(this.refreshLoop, 1000); + }); } - } + }; - refreshLoop() { - clearTimeout(this.timeoutId); // to stop multiple series of refreshLoop from going on simultaneously - clearTimeout(this.searchTimeoutId); + const refreshLoop = useCallback(() => { + clearTimeout(timeoutId.current); $.get('/v1/queryState?includeAllQueries=true&includeAllQueryProgressStats=true&excludeResourceGroupPathInfo=true', function (queryList) { const queryMap = queryList.reduce(function (map, query) { @@ -459,7 +449,7 @@ export class QueryList extends React.Component { }, {}); let updatedQueries = []; - this.state.displayedQueries.forEach(function (oldQuery) { + (dataSet.current.displayedQueries || []).forEach(function (oldQuery) { if (oldQuery.queryId in queryMap) { updatedQueries.push(queryMap[oldQuery.queryId]); queryMap[oldQuery.queryId] = false; @@ -472,108 +462,133 @@ export class QueryList extends React.Component { newQueries.push(queryMap[queryId]); } } - newQueries = this.filterQueries(newQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString); + newQueries = filterQueries(newQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); - const lastRefresh = Date.now(); - let lastReorder = this.state.lastReorder; + const newLastRefresh = Date.now(); + let newLastReorder = dataSet.current.lastReorder; - if (this.state.reorderInterval !== 0 && ((lastRefresh - lastReorder) >= this.state.reorderInterval)) { - updatedQueries = this.filterQueries(updatedQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString); + if (dataSet.current.reorderInterval !== 0 && ((newLastRefresh - newLastReorder) >= dataSet.current.reorderInterval)) { + updatedQueries = filterQueries(updatedQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); updatedQueries = updatedQueries.concat(newQueries); - this.sortAndLimitQueries(updatedQueries, this.state.currentSortType, this.state.currentSortOrder, 0); - lastReorder = Date.now(); + sortAndLimitQueries(updatedQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, 0); + newLastReorder = Date.now(); } else { - this.sortAndLimitQueries(newQueries, this.state.currentSortType, this.state.currentSortOrder, 0); + sortAndLimitQueries(newQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, 0); updatedQueries = updatedQueries.concat(newQueries); } - if (this.state.maxQueries !== 0 && (updatedQueries.length > this.state.maxQueries)) { - updatedQueries.splice(this.state.maxQueries, (updatedQueries.length - this.state.maxQueries)); + if (dataSet.current.maxQueries !== 0 && (updatedQueries.length > dataSet.current.maxQueries)) { + updatedQueries.splice(dataSet.current.maxQueries, (updatedQueries.length - dataSet.current.maxQueries)); } - this.setState({ + dataSet.current.allQueries = queryList; + dataSet.current.displayedQueries = updatedQueries; + dataSet.current.lastReorder = newLastReorder; + + setState(prev => ({ + ...prev, allQueries: queryList, displayedQueries: updatedQueries, - lastRefresh: lastRefresh, - lastReorder: lastReorder, - initialized: true - }); - this.resetTimer(); - }.bind(this)) + lastRefresh: newLastRefresh, + lastReorder: newLastReorder, + initialized: true, + })); + + timeoutId.current = setTimeout(refreshLoop, 1000); + }) .fail(function () { - this.setState({ - initialized: true, - }); - this.resetTimer(); - }.bind(this)); - } + setState(prev => ({ ...prev, initialized: true })); + timeoutId.current = setTimeout(refreshLoop, 1000); + }); + }, []); - componentDidMount() { - this.refreshLoop(); + useEffect(() => { + refreshLoop(); $('[data-bs-toggle="tooltip"]')?.tooltip?.(); - } - handleSearchStringChange(event) { - const newSearchString = event.target.value; - clearTimeout(this.searchTimeoutId); + return () => { + clearTimeout(timeoutId.current); + clearTimeout(searchTimeoutId.current); + }; + }, [refreshLoop]); - this.setState({ - searchString: newSearchString - }); + const executeSearch = useCallback(() => { + clearTimeout(searchTimeoutId.current); - this.searchTimeoutId = setTimeout(this.executeSearch, 200); - } + // Use latest values from dataSet to avoid stale closures during debounce + const newDisplayedQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); + sortAndLimitQueries(newDisplayedQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries); - executeSearch() { - clearTimeout(this.searchTimeoutId); + dataSet.current.displayedQueries = newDisplayedQueries; + setState(prev => ({ ...prev, displayedQueries: newDisplayedQueries })); + }, []); - const newDisplayedQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString); - this.sortAndLimitQueries(newDisplayedQueries, this.state.currentSortType, this.state.currentSortOrder, this.state.maxQueries); + const handleSearchStringChange = (event) => { + const newSearchString = event.target.value; + clearTimeout(searchTimeoutId.current); - this.setState({ - displayedQueries: newDisplayedQueries - }); - } + // Update state and ref immediately for debounce/readers + dataSet.current.searchString = newSearchString; + setState(prev => ({ ...prev, searchString: newSearchString })); + + searchTimeoutId.current = setTimeout(executeSearch, 200); + }; - renderMaxQueriesListItem(maxQueries, maxQueriesText) { + const handleMaxQueriesClick = (event, newMaxQueries) => { + event.preventDefault(); + const filteredQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); + sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, newMaxQueries); + + dataSet.current.maxQueries = newMaxQueries; + dataSet.current.displayedQueries = filteredQueries; + setState(prev => ({ ...prev, maxQueries: newMaxQueries, displayedQueries: filteredQueries })); + }; + + const renderMaxQueriesListItem = (maxQueriesValue, maxQueriesText) => { return ( -
  • {maxQueriesText} +
  • handleMaxQueriesClick(e, maxQueriesValue)}>{maxQueriesText}
  • ); - } + }; - handleMaxQueriesClick(newMaxQueries) { - const filteredQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString); - this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder, newMaxQueries); - - this.setState({ - maxQueries: newMaxQueries, - displayedQueries: filteredQueries - }); - } + const handleReorderClick = (interval) => { + if (dataSet.current.reorderInterval !== interval) { + dataSet.current.reorderInterval = interval; + setState(prev => ({ ...prev, reorderInterval: interval })); + } + }; - renderReorderListItem(interval, intervalText) { + const renderReorderListItem = (interval, intervalText) => { return ( -
  • {intervalText}
  • +
  • handleReorderClick(interval)}>{intervalText}
  • ); - } + }; - handleReorderClick(interval) { - if (this.state.reorderInterval !== interval) { - this.setState({ - reorderInterval: interval, - }); + const handleSortClick = (sortType) => { + const newSortType = sortType; + let newSortOrder = SORT_ORDER.DESCENDING; + + if (state.currentSortType === sortType && state.currentSortOrder === SORT_ORDER.DESCENDING) { + newSortOrder = SORT_ORDER.ASCENDING; } - } - renderSortListItem(sortType, sortText) { - if (this.state.currentSortType === sortType) { - const directionArrow = this.state.currentSortOrder === SORT_ORDER.ASCENDING ? : + const newDisplayedQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); + sortAndLimitQueries(newDisplayedQueries, newSortType, newSortOrder, dataSet.current.maxQueries); + + dataSet.current.displayedQueries = newDisplayedQueries; + dataSet.current.currentSortType = newSortType; + dataSet.current.currentSortOrder = newSortOrder; + setState(prev => ({ ...prev, displayedQueries: newDisplayedQueries, currentSortType: newSortType, currentSortOrder: newSortOrder })); + }; + + const renderSortListItem = (sortType, sortText) => { + if (state.currentSortType === sortType) { + const directionArrow = state.currentSortOrder === SORT_ORDER.ASCENDING ? : ; return (
  • - + handleSortClick(sortType)}> {sortText} {directionArrow}
  • ); @@ -581,188 +596,166 @@ export class QueryList extends React.Component { else { return (
  • - + handleSortClick(sortType)}> {sortText}
  • ); } - } - - handleSortClick(sortType) { - const newSortType = sortType; - let newSortOrder = SORT_ORDER.DESCENDING; + }; - if (this.state.currentSortType === sortType && this.state.currentSortOrder === SORT_ORDER.DESCENDING) { - newSortOrder = SORT_ORDER.ASCENDING; + const handleStateFilterClick = (filter) => { + const newFilters = state.stateFilters.slice(); + if (state.stateFilters.indexOf(filter) > -1) { + newFilters.splice(newFilters.indexOf(filter), 1); + } + else { + newFilters.push(filter); } - const newDisplayedQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, this.state.errorTypeFilters, this.state.searchString); - this.sortAndLimitQueries(newDisplayedQueries, newSortType, newSortOrder, this.state.maxQueries); + const filteredQueries = filterQueries(dataSet.current.allQueries, newFilters, dataSet.current.errorTypeFilters, dataSet.current.searchString); + sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries); - this.setState({ - displayedQueries: newDisplayedQueries, - currentSortType: newSortType, - currentSortOrder: newSortOrder - }); - } + dataSet.current.stateFilters = newFilters; + dataSet.current.displayedQueries = filteredQueries; + setState(prev => ({ ...prev, stateFilters: newFilters, displayedQueries: filteredQueries })); + }; - renderFilterButton(filterType, filterText) { + const renderFilterButton = (filterType, filterText) => { let checkmarkStyle = {color: '#57aac7'}; let classNames = "btn btn-sm btn-info style-check rounded-0"; - if (this.state.stateFilters.indexOf(filterType) > -1) { + if (state.stateFilters.indexOf(filterType) > -1) { classNames += " active"; checkmarkStyle = {color: '#ffffff'}; } return ( - ); - } + }; - handleStateFilterClick(filter) { - const newFilters = this.state.stateFilters.slice(); - if (this.state.stateFilters.indexOf(filter) > -1) { - newFilters.splice(newFilters.indexOf(filter), 1); + const handleErrorTypeFilterClick = (errorType) => { + const newFilters = state.errorTypeFilters.slice(); + if (state.errorTypeFilters.indexOf(errorType) > -1) { + newFilters.splice(newFilters.indexOf(errorType), 1); } else { - newFilters.push(filter); + newFilters.push(errorType); } - const filteredQueries = this.filterQueries(this.state.allQueries, newFilters, this.state.errorTypeFilters, this.state.searchString); - this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder); + const filteredQueries = filterQueries(dataSet.current.allQueries, dataSet.current.stateFilters, newFilters, dataSet.current.searchString); + sortAndLimitQueries(filteredQueries, dataSet.current.currentSortType, dataSet.current.currentSortOrder, dataSet.current.maxQueries); - this.setState({ - stateFilters: newFilters, - displayedQueries: filteredQueries - }); - } + dataSet.current.errorTypeFilters = newFilters; + dataSet.current.displayedQueries = filteredQueries; + setState(prev => ({ ...prev, errorTypeFilters: newFilters, displayedQueries: filteredQueries })); + }; - renderErrorTypeListItem(errorType, errorTypeText) { + const renderErrorTypeListItem = (errorType, errorTypeText) => { let checkmarkStyle = {color: '#ffffff'}; - if (this.state.errorTypeFilters.indexOf(errorType) > -1) { + if (state.errorTypeFilters.indexOf(errorType) > -1) { checkmarkStyle = {color: 'black'}; } return (
  • - + handleErrorTypeFilterClick(errorType)}>  {errorTypeText}
  • ); - } - - handleErrorTypeFilterClick(errorType) { - const newFilters = this.state.errorTypeFilters.slice(); - if (this.state.errorTypeFilters.indexOf(errorType) > -1) { - newFilters.splice(newFilters.indexOf(errorType), 1); - } - else { - newFilters.push(errorType); - } - - const filteredQueries = this.filterQueries(this.state.allQueries, this.state.stateFilters, newFilters, this.state.searchString); - this.sortAndLimitQueries(filteredQueries, this.state.currentSortType, this.state.currentSortOrder); - - this.setState({ - errorTypeFilters: newFilters, - displayedQueries: filteredQueries - }); - } - - render() { - let queryList = ; - if (this.state.displayedQueries === null || this.state.displayedQueries.length === 0) { - let label = (
    Loading...
    ); - if (this.state.initialized) { - if (this.state.allQueries === null || this.state.allQueries.length === 0) { - label = "No queries"; - } - else { - label = "No queries matched filters"; - } + }; + + let queryList = ; + if (state.displayedQueries === null || state.displayedQueries.length === 0) { + let label = (
    Loading...
    ); + if (state.initialized) { + if (state.allQueries === null || state.allQueries.length === 0) { + label = "No queries"; + } + else { + label = "No queries matched filters"; } - queryList = ( -
    -

    {label}

    -
    - ); } + queryList = ( +
    +

    {label}

    +
    + ); + } - return ( -
    -
    -
    -
    - -
    + return ( +
    +
    +
    +
    + +
    -
    - State: - {this.renderFilterButton(FILTER_TYPE.RUNNING, "Running")} - {this.renderFilterButton(FILTER_TYPE.QUEUED, "Queued")} - {this.renderFilterButton(FILTER_TYPE.FINISHED, "Finished")} - -
      - {this.renderErrorTypeListItem(ERROR_TYPE.INTERNAL_ERROR, "Internal Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.EXTERNAL, "External Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.INSUFFICIENT_RESOURCES, "Resources Error")} - {this.renderErrorTypeListItem(ERROR_TYPE.USER_ERROR, "User Error")} -
    +
    + State: + {renderFilterButton(FILTER_TYPE.RUNNING, "Running")} + {renderFilterButton(FILTER_TYPE.QUEUED, "Queued")} + {renderFilterButton(FILTER_TYPE.FINISHED, "Finished")} + +
      + {renderErrorTypeListItem(ERROR_TYPE.INTERNAL_ERROR, "Internal Error")} + {renderErrorTypeListItem(ERROR_TYPE.EXTERNAL, "External Error")} + {renderErrorTypeListItem(ERROR_TYPE.INSUFFICIENT_RESOURCES, "Resources Error")} + {renderErrorTypeListItem(ERROR_TYPE.USER_ERROR, "User Error")} +
    -
    -   -
    - -
      - {this.renderSortListItem(SORT_TYPE.CREATED, "Creation Time")} - {this.renderSortListItem(SORT_TYPE.ELAPSED, "Elapsed Time")} - {this.renderSortListItem(SORT_TYPE.CPU, "CPU Time")} - {this.renderSortListItem(SORT_TYPE.EXECUTION, "Execution Time")} - {this.renderSortListItem(SORT_TYPE.CURRENT_MEMORY, "Current Memory")} - {this.renderSortListItem(SORT_TYPE.CUMULATIVE_MEMORY, "Cumulative User Memory")} -
    -
    -   -
    - -
      - {this.renderReorderListItem(1000, "1s")} - {this.renderReorderListItem(5000, "5s")} - {this.renderReorderListItem(10000, "10s")} - {this.renderReorderListItem(30000, "30s")} -
      - {this.renderReorderListItem(0, "Off")} -
    -
    -   -
    - -
      - {this.renderMaxQueriesListItem(20, "20 queries")} - {this.renderMaxQueriesListItem(50, "50 queries")} - {this.renderMaxQueriesListItem(100, "100 queries")} -
      - {this.renderMaxQueriesListItem(0, "All queries")} -
    -
    +
    +   +
    + +
      + {renderSortListItem(SORT_TYPE.CREATED, "Creation Time")} + {renderSortListItem(SORT_TYPE.ELAPSED, "Elapsed Time")} + {renderSortListItem(SORT_TYPE.CPU, "CPU Time")} + {renderSortListItem(SORT_TYPE.EXECUTION, "Execution Time")} + {renderSortListItem(SORT_TYPE.CURRENT_MEMORY, "Current Memory")} + {renderSortListItem(SORT_TYPE.CUMULATIVE_MEMORY, "Cumulative User Memory")} +
    +
    +   +
    + +
      + {renderReorderListItem(1000, "1s")} + {renderReorderListItem(5000, "5s")} + {renderReorderListItem(10000, "10s")} + {renderReorderListItem(30000, "30s")} +
      + {renderReorderListItem(0, "Off")} +
    +
    +   +
    + +
      + {renderMaxQueriesListItem(20, "20 queries")} + {renderMaxQueriesListItem(50, "50 queries")} + {renderMaxQueriesListItem(100, "100 queries")} +
      + {renderMaxQueriesListItem(0, "All queries")} +
    +
    - {queryList} -
    - ); - } -} + {queryList} +
    + ); +}; -export default QueryList; \ No newline at end of file +export default QueryList; diff --git a/presto-ui/src/components/QueryViewer.jsx b/presto-ui/src/components/QueryViewer.jsx index 0694ce95db812..35da127c4fd79 100644 --- a/presto-ui/src/components/QueryViewer.jsx +++ b/presto-ui/src/components/QueryViewer.jsx @@ -34,7 +34,7 @@ const FileForm = ({ onChange }) => (
    ); -export function QueryViewer() { +export const QueryViewer = () => { const [state, setState] = React.useState({ initialized: false, ended: false, @@ -88,6 +88,6 @@ export function QueryViewer() {
    ); -} +}; export default QueryViewer; \ No newline at end of file diff --git a/presto-ui/src/components/StageDetail.jsx b/presto-ui/src/components/StageDetail.jsx index d47c2f117976c..bec27c97c9915 100644 --- a/presto-ui/src/components/StageDetail.jsx +++ b/presto-ui/src/components/StageDetail.jsx @@ -12,13 +12,13 @@ * limitations under the License. */ -import React from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import ReactDOM from "react-dom"; -import { createRoot } from 'react-dom/client'; +import { createRoot } from "react-dom/client"; import ReactDOMServer from "react-dom/server"; import * as dagreD3 from "dagre-d3-es"; import * as d3 from "d3"; -import { clsx } from 'clsx'; +import { clsx } from "clsx"; import { formatCount, @@ -28,160 +28,153 @@ import { getFirstParameter, getTaskNumber, isQueryEnded, - parseDuration + parseDuration, } from "../utils"; import { initializeGraph, initializeSvg } from "../d3utils"; -import {QueryHeader} from "./QueryHeader"; +import { QueryHeader } from "./QueryHeader"; function getTotalWallTime(operator) { - return parseDuration(operator.addInputWall) + parseDuration(operator.getOutputWall) + parseDuration(operator.finishWall) + parseDuration(operator.blockedWall) + return ( + parseDuration(operator.addInputWall) + + parseDuration(operator.getOutputWall) + + parseDuration(operator.finishWall) + + parseDuration(operator.blockedWall) + ); } -class OperatorSummary extends React.Component { - render() { - const operator = this.props.operator; - - const totalWallTime = parseDuration(operator.addInputWall) + parseDuration(operator.getOutputWall) + parseDuration(operator.finishWall) + parseDuration(operator.blockedWall); +const OperatorSummary = ({ operator }) => { + const totalWallTime = + parseDuration(operator.addInputWall) + + parseDuration(operator.getOutputWall) + + parseDuration(operator.finishWall) + + parseDuration(operator.blockedWall); - const rowInputRate = totalWallTime === 0 ? 0 : (1.0 * operator.inputPositions) / (totalWallTime / 1000.0); - const byteInputRate = totalWallTime === 0 ? 0 : (1.0 * operator.inputDataSizeInBytes) / (totalWallTime / 1000.0); + const rowInputRate = totalWallTime === 0 ? 0 : (1.0 * operator.inputPositions) / (totalWallTime / 1000.0); + const byteInputRate = totalWallTime === 0 ? 0 : (1.0 * operator.inputDataSizeInBytes) / (totalWallTime / 1000.0); - return ( -
    -
    -
    - {operator.operatorType} -
    -
    - {formatCount(rowInputRate) + " rows/s (" + formatDataSize(byteInputRate) + "/s)"} -
    -
    - - + return ( +
    +
    +
    {operator.operatorType}
    +
    {formatCount(rowInputRate) + " rows/s (" + formatDataSize(byteInputRate) + "/s)"}
    +
    +
    + + - - - + + - - + + - - + + + - - -
    Output - Output - - {formatCount(operator.outputPositions) + " rows (" + formatDataSize(operator.outputDataSizeInBytes) + ")"} + {formatCount(operator.outputPositions) + + " rows (" + + formatDataSize(operator.outputDataSizeInBytes) + + ")"}
    - Drivers - - {operator.totalDrivers} - Drivers{operator.totalDrivers}
    - Wall Time - - {formatDuration(totalWallTime)} - Wall Time{formatDuration(totalWallTime)}
    - Blocked - - {formatDuration(parseDuration(operator.blockedWall))} - Blocked{formatDuration(parseDuration(operator.blockedWall))}
    Input - Input - - {formatCount(operator.inputPositions) + " rows (" + formatDataSize(operator.inputDataSizeInBytes) + ")"} + {formatCount(operator.inputPositions) + + " rows (" + + formatDataSize(operator.inputDataSizeInBytes) + + ")"}
    -
    - ); - } -} + + +
    + ); +}; const BAR_CHART_PROPERTIES = { - type: 'bar', - barSpacing: '0', - height: '80px', - barColor: '#747F96', - zeroColor: '#8997B3', + type: "bar", + barSpacing: "0", + height: "80px", + barColor: "#747F96", + zeroColor: "#8997B3", chartRangeMin: 0, - tooltipClassname: 'sparkline-tooltip', - tooltipFormat: 'Task {{offset:offset}} - {{value}}', + tooltipClassname: "sparkline-tooltip", + tooltipFormat: "Task {{offset:offset}} - {{value}}", disableHiddenCheck: true, }; -function OperatorStatistic({ id, name, operators, supplier, renderer }) { - - React.useEffect(() => { +const OperatorStatistic = ({ id, name, operators, supplier, renderer }) => { + useEffect(() => { const statistic = operators.map(supplier); const numTasks = operators.length; - const tooltipValueLookups = { 'offset': {} }; + const tooltipValueLookups = { offset: {} }; for (let i = 0; i < numTasks; i++) { - tooltipValueLookups['offset'][i] = "" + i; + tooltipValueLookups["offset"][i] = "" + i; } - const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, { barWidth: 800 / numTasks, tooltipValueLookups: tooltipValueLookups }); - $('#operator-statics-' + id).sparkline(statistic, $.extend({}, stageBarChartProperties, { numberFormatter: renderer })); - + const stageBarChartProperties = $.extend({}, BAR_CHART_PROPERTIES, { + barWidth: 800 / numTasks, + tooltipValueLookups: tooltipValueLookups, + }); + $("#operator-statics-" + id).sparkline( + statistic, + $.extend({}, stageBarChartProperties, { numberFormatter: renderer }) + ); }, [operators, supplier, renderer]); return (
    -
    - {name} -
    +
    {name}
    ); -} +}; -function OperatorDetail({ index, operator, tasks }) { +const OperatorDetail = ({ index, operator, tasks }) => { const selectedStatistics = [ { name: "Total Wall Time", id: "totalWallTime", supplier: getTotalWallTime, - renderer: formatDuration + renderer: formatDuration, }, { name: "Input Rows", id: "inputPositions", supplier: operator => operator.inputPositions, - renderer: formatCount + renderer: formatCount, }, { name: "Input Data Size", id: "inputDataSizeInBytes", supplier: operator => operator.inputDataSizeInBytes, - renderer: formatDataSize + renderer: formatDataSize, }, { name: "Output Rows", id: "outputPositions", supplier: operator => operator.outputPositions, - renderer: formatCount + renderer: formatCount, }, { name: "Output Data Size", id: "outputDataSizeInBytes", supplier: operator => operator.outputDataSizeInBytes, - renderer: formatDataSize + renderer: formatDataSize, }, ]; const getOperatorTasks = () => { // sort the x-axis - const tasksSorted = tasks.sort(function (taskA, taskB) { + const tasksSorted = tasks.sort(function(taskA, taskB) { return getTaskNumber(taskA.taskId) - getTaskNumber(taskB.taskId); }); @@ -199,7 +192,7 @@ function OperatorDetail({ index, operator, tasks }) { }); return operatorTasks; - } + }; const operatorTasks = getOperatorTasks(); const totalWallTime = getTotalWallTime(operator); @@ -226,158 +219,150 @@ function OperatorDetail({ index, operator, tasks }) {
    - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    - Input - - {formatCount(operator.inputPositions) + " rows (" + formatDataSize(operator.inputDataSizeInBytes) + ")"} -
    - Input Rate - - {formatCount(rowInputRate) + " rows/s (" + formatDataSize(byteInputRate) + "/s)"} -
    - Output - - {formatCount(operator.outputPositions) + " rows (" + formatDataSize(operator.outputDataSizeInBytes) + ")"} -
    - Output Rate - - {formatCount(rowOutputRate) + " rows/s (" + formatDataSize(byteOutputRate) + "/s)"} -
    Input + {formatCount(operator.inputPositions) + + " rows (" + + formatDataSize(operator.inputDataSizeInBytes) + + ")"} +
    Input Rate + {formatCount(rowInputRate) + + " rows/s (" + + formatDataSize(byteInputRate) + + "/s)"} +
    Output + {formatCount(operator.outputPositions) + + " rows (" + + formatDataSize(operator.outputDataSizeInBytes) + + ")"} +
    Output Rate + {formatCount(rowOutputRate) + + " rows/s (" + + formatDataSize(byteOutputRate) + + "/s)"} +
    - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    - Wall Time - - {formatDuration(totalWallTime)} -
    - Blocked - - {formatDuration(parseDuration(operator.blockedWall))} -
    - Drivers - - {operator.totalDrivers} -
    - Tasks - - {operatorTasks.length} -
    Wall Time{formatDuration(totalWallTime)}
    Blocked{formatDuration(parseDuration(operator.blockedWall))}
    Drivers{operator.totalDrivers}
    Tasks{operatorTasks.length}
    - - Statistic - + Statistic
    - - Tasks - + Tasks
    - { - selectedStatistics.map(function (statistic) { - return ( - - ); - }) - } -

    -

    + {selectedStatistics.map(function(statistic) { + return ( + + ); + })} +

    +

    ); -} - -class StageOperatorGraph extends React.Component { - componentDidMount() { - this.updateD3Graph(); - } - - componentDidUpdate() { - this.updateD3Graph(); - } - - handleOperatorClick(event) { - if (event.target.hasOwnProperty("__data__") && event.target.__data__ !== undefined) { - $('#operator-detail-modal').modal("show") - - const pipelineId = (event?.target?.__data__ || "").split('-').length > 0 ? parseInt((event?.target?.__data__ || '').split('-')[1] || '0') : 0; - const operatorId = (event?.target?.__data__ || "").split('-').length > 0 ? parseInt((event?.target?.__data__ || '').split('-')[2] || '0') : 0; - const stage = this.props.stage; +}; - let operatorStageSummary = null; - const operatorSummaries = stage.latestAttemptExecutionInfo.stats.operatorSummaries; - for (let i = 0; i < operatorSummaries.length; i++) { - if (operatorSummaries[i].pipelineId === pipelineId && operatorSummaries[i].operatorId === operatorId) { - operatorStageSummary = operatorSummaries[i]; +const StageOperatorGraph = ({ stage }) => { + const handleOperatorClick = useCallback( + event => { + if (event.target.hasOwnProperty("__data__") && event.target.__data__ !== undefined) { + $("#operator-detail-modal").modal("show"); + + const pipelineId = + (event?.target?.__data__ || "").split("-").length > 0 + ? parseInt((event?.target?.__data__ || "").split("-")[1] || "0") + : 0; + const operatorId = + (event?.target?.__data__ || "").split("-").length > 0 + ? parseInt((event?.target?.__data__ || "").split("-")[2] || "0") + : 0; + + let operatorStageSummary = null; + const operatorSummaries = stage.latestAttemptExecutionInfo.stats.operatorSummaries; + for (let i = 0; i < operatorSummaries.length; i++) { + if ( + operatorSummaries[i].pipelineId === pipelineId && + operatorSummaries[i].operatorId === operatorId + ) { + operatorStageSummary = operatorSummaries[i]; + } } + const container = document.getElementById("operator-detail"); + const root = createRoot(container); + root.render( + + ); } - const container = document.getElementById('operator-detail'); - const root = createRoot(container); - root.render(); - } else { - return; - } - } + }, + [stage] + ); - computeOperatorGraphs() { + const computeOperatorGraphs = useCallback(() => { const pipelineOperators = new Map(); - this.props.stage.latestAttemptExecutionInfo.stats.operatorSummaries.forEach(operator => { + stage.latestAttemptExecutionInfo.stats.operatorSummaries.forEach(operator => { if (!pipelineOperators.has(operator.pipelineId)) { pipelineOperators.set(operator.pipelineId, []); } pipelineOperators.get(operator.pipelineId).push(operator); - }); + }); const result = new Map(); pipelineOperators.forEach((pipelineOperators, pipelineId) => { // sort deep-copied operators in this pipeline from source to sink - const linkedOperators = pipelineOperators.map(a => Object.assign({}, a)).sort((a, b) => a.operatorId - b.operatorId); + const linkedOperators = pipelineOperators + .map(a => Object.assign({}, a)) + .sort((a, b) => a.operatorId - b.operatorId); const sinkOperator = linkedOperators[linkedOperators.length - 1]; const sourceOperator = linkedOperators[0]; @@ -392,38 +377,43 @@ class StageOperatorGraph extends React.Component { }); return result; - } + }, [stage]); - computeD3StageOperatorGraph(graph, operator, sink, pipelineNode) { + const computeD3StageOperatorGraph = useCallback((graph, operator, sink, pipelineNode) => { const operatorNodeId = "operator-" + operator.pipelineId + "-" + operator.operatorId; - - // this is a non-standard use of ReactDOMServer, but it's the cleanest way to unify DagreD3 with React - const html = ReactDOMServer.renderToString(); - graph.setNode(operatorNodeId, {class: "operator-stats", label: html, labelType: "html"}); + const html = ReactDOMServer.renderToString( + + ); + graph.setNode(operatorNodeId, { class: "operator-stats", label: html, labelType: "html" }); if (operator.hasOwnProperty("child")) { - this.computeD3StageOperatorGraph(graph, operator.child, operatorNodeId, pipelineNode); + computeD3StageOperatorGraph(graph, operator.child, operatorNodeId, pipelineNode); } if (sink !== null) { - graph.setEdge(operatorNodeId, sink, {class: "plan-edge", arrowheadClass: "plan-arrowhead"}); + graph.setEdge(operatorNodeId, sink, { class: "plan-edge", arrowheadClass: "plan-arrowhead" }); } graph.setParent(operatorNodeId, pipelineNode); - } + }, []); - updateD3Graph() { - if (!this.props.stage) { + const updateD3Graph = useCallback(() => { + if (!stage) { return; } - const operatorGraphs = this.computeOperatorGraphs(); + const operatorGraphs = computeOperatorGraphs(); const graph = initializeGraph(); operatorGraphs.forEach((operator, pipelineId) => { const pipelineNodeId = "pipeline-" + pipelineId; - graph.setNode(pipelineNodeId, {label: "Pipeline " + pipelineId + " ", clusterLabelPos: 'top', style: 'fill: #2b2b2b', labelStyle: 'fill: #fff'}); - this.computeD3StageOperatorGraph(graph, operator, null, pipelineNodeId) + graph.setNode(pipelineNodeId, { + label: "Pipeline " + pipelineId + " ", + clusterLabelPos: "top", + style: "fill: #2b2b2b", + labelStyle: "fill: #fff", + }); + computeD3StageOperatorGraph(graph, operator, null, pipelineNodeId); }); $("#operator-canvas").html(""); @@ -434,218 +424,243 @@ class StageOperatorGraph extends React.Component { const render = new dagreD3.render(); render(d3.select("#operator-canvas g"), graph); - svg.selectAll("g.operator-stats").on("click", this.handleOperatorClick.bind(this)); + svg.selectAll("g.operator-stats").on("click", handleOperatorClick); svg.attr("height", graph.graph().height); svg.attr("width", graph.graph().width); - } - else { + } else { $(".graph-container").css("display", "none"); } - } + }, [stage, computeOperatorGraphs, computeD3StageOperatorGraph, handleOperatorClick]); - render() { - const stage = this.props.stage; - - if (!stage.hasOwnProperty('plan')) { - return ( -
    -

    Stage does not have a plan

    + if (!stage.hasOwnProperty("plan")) { + return ( +
    +
    +

    Stage {stage.stageId} does not have a plan

    - ); - } +
    + ); + } - const latestAttemptExecutionInfo = stage.latestAttemptExecutionInfo; - if (!latestAttemptExecutionInfo.hasOwnProperty('stats') || !latestAttemptExecutionInfo.stats.hasOwnProperty("operatorSummaries") || latestAttemptExecutionInfo.stats.operatorSummaries.length === 0) { - return ( -
    -
    -

    Operator data not available for {stage.stageId}

    -
    + const {latestAttemptExecutionInfo} = stage; + if ( + !latestAttemptExecutionInfo.hasOwnProperty("stats") || + !latestAttemptExecutionInfo.stats.hasOwnProperty("operatorSummaries") || + latestAttemptExecutionInfo.stats.operatorSummaries.length === 0 + ) { + return ( +
    +
    +

    Operator data not available for {stage.stageId}

    - ); - } - - return null; +
    + ); } -} -export class StageDetail extends React.Component { - constructor(props) { - super(props); - this.state = { - initialized: false, - ended: false, + useEffect(() => { + updateD3Graph(); + }, [updateD3Graph]); - selectedStageId: null, - query: null, + return null; +}; - lastRefresh: null, - lastRender: null - }; +const findStage = (stageId, currentStage) => { + if (stageId === null) { + return null; + } - this.refreshLoop = this.refreshLoop.bind(this); + if (currentStage.stageId === stageId) { + return currentStage; } - resetTimer() { - clearTimeout(this.timeoutId); - // stop refreshing when query finishes or fails - if (this.state.query === null || !this.state.ended) { - this.timeoutId = setTimeout(this.refreshLoop, 1000); + for (let i = 0; i < currentStage.subStages.length; i++) { + const stage = findStage(stageId, currentStage.subStages[i]); + if (stage !== null) { + return stage; } } - static getQueryURL(id) { - if (!id || typeof id !== 'string' || id.length === 0) { - return "/v1/query/undefined"; - } - const sanitizedId = id.replace(/[^a-z0-9_]/gi, ''); - return sanitizedId.length > 0 ? `/v1/query/${encodeURIComponent(sanitizedId)}` : "/v1/query/undefined"; - } - refreshLoop() { - clearTimeout(this.timeoutId); // to stop multiple series of refreshLoop from going on simultaneously - const queryString = getFirstParameter(window.location.search).split('.'); - const rawQueryId = queryString.length > 0 ? queryString[0] : ""; - let selectedStageId = this.state.selectedStageId; - if (selectedStageId === null) { - selectedStageId = 0; - if (queryString.length > 1) { - selectedStageId = parseInt(queryString[1]); - } - } + return null; +}; - - $.get(StageDetail.getQueryURL(rawQueryId), query => { - this.setState({ - initialized: true, - ended: query.finalQueryInfo, +const getAllStageIds = (result, currentStage) => { + result.push(currentStage.plan.id); + currentStage.subStages.forEach(stage => { + getAllStageIds(result, stage); + }); +}; - selectedStageId: selectedStageId, - query: query, - }); - this.resetTimer(); - }) - } +const getInitialStageIdFromQuery = () => { + const queryString = getFirstParameter(window.location.search).split("."); + return queryString.length > 1 ? parseInt(queryString[1]) : 0; +}; - componentDidMount() { - this.refreshLoop(); - } +export const StageDetail = () => { + const [initialized, setInitialized] = useState(false); + const [ended, setEnded] = useState(false); + const [selectedStageId, setSelectedStageId] = useState(getInitialStageIdFromQuery()); + const [query, setQuery] = useState(null); - findStage(stageId, currentStage) { - if (stageId === null) { - return null; - } + const timerId = useRef(null); + const endedRef = useRef(false); + const selectedStageIdRef = useRef(getInitialStageIdFromQuery()); - if (currentStage.stageId === stageId) { - return currentStage; + const getQueryURL = id => { + if (!id || typeof id !== "string" || id.length === 0) { + return "/v1/query/undefined"; } + const sanitizedId = id.replace(/[^a-z0-9_]/gi, ""); + return sanitizedId.length > 0 ? `/v1/query/${encodeURIComponent(sanitizedId)}` : "/v1/query/undefined"; + }; + + // keep refs in sync to avoid stale closures in the polling loop + useEffect(() => { + endedRef.current = ended; + }, [ended]); + useEffect(() => { + selectedStageIdRef.current = selectedStageId; + }, [selectedStageId]); + + const refreshLoop = useCallback(() => { + clearTimeout(timerId.current); + const queryString = getFirstParameter(window.location.search).split("."); + const rawQueryId = queryString.length > 0 ? queryString[0] : ""; - for (let i = 0; i < currentStage.subStages.length; i++) { - const stage = this.findStage(stageId, currentStage.subStages[i]); - if (stage !== null) { - return stage; - } - } + fetch(getQueryURL(rawQueryId)) + .then(response => response.json()) + .then(q => { + setQuery(q); + setInitialized(true); + setEnded(q.finalQueryInfo); - return null; - } + if (!endedRef.current) { + timerId.current = setTimeout(refreshLoop, 1000); + } + }) + .catch(error => { + console.error("Error fetching query:", error); + if (!endedRef.current) { + timerId.current = setTimeout(refreshLoop, 1000); + } + }); + }, []); - getAllStageIds(result, currentStage) { - result.push(currentStage.plan.id); - currentStage.subStages.forEach(stage => { - this.getAllStageIds(result, stage); - }); - } + useEffect(() => { + refreshLoop(); + return () => { + clearTimeout(timerId.current); + }; + }, [refreshLoop]); - render() { - if (!this.state.query) { - let label = (
    Loading...
    ); - if (this.state.initialized) { - label = "Query not found"; - } - return ( -
    -

    {label}

    -
    - ); + if (!query) { + let label =
    Loading...
    ; + if (initialized) { + label = "Query not found"; } + return ( +
    +
    +

    {label}

    +
    +
    + ); + } - if (!this.state.query.outputStage) { - return ( -
    -

    Query does not have an output stage

    + if (!query.outputStage) { + return ( +
    +
    +

    Query does not have an output stage

    - ); - } +
    + ); + } - const query = this.state.query; - const allStages = []; - this.getAllStageIds(allStages, query.outputStage); + const allStages = []; + getAllStageIds(allStages, query.outputStage); - const stage = this.findStage(query.queryId + "." + this.state.selectedStageId, query.outputStage); - if (stage === null) { - return ( -
    -

    Stage not found

    + // Avoid stale/null selection: fall back to the first available stage id + const effectiveSelectedStageId = + selectedStageId === null || selectedStageId === undefined + ? allStages.length > 0 + ? allStages[0] + : 0 + : selectedStageId; + + const stage = findStage(query.queryId + "." + effectiveSelectedStageId, query.outputStage); + if (stage === null) { + return ( +
    +
    +

    Stage not found

    - ); - } +
    + ); + } - let stageOperatorGraph = null; - if (!isQueryEnded(query.state)) { - stageOperatorGraph = ( -
    -
    -

    Operator graph will appear automatically when query completes.

    -
    Loading...
    -
    + let stageOperatorGraph = null; + if (!isQueryEnded(query.state)) { + stageOperatorGraph = ( +
    +
    +

    Operator graph will appear automatically when query completes.

    +
    Loading...
    - ) - } - else { - stageOperatorGraph = ; - } +
    + ); + } else { + stageOperatorGraph = ; + } - return ( -
    - -
    -
    -
    -
    -

    Stage {stage.plan.id}

    -
    -
    -
    -
    - - -
    + return ( +
    + +
    +
    +
    +
    +

    Stage {stage.plan.id}

    +
    +
    +
    +
    + +
    -
    -
    -
    - {stageOperatorGraph} -
    -
    - ); - } -} +
    +
    +
    {stageOperatorGraph}
    +
    +
    + ); +}; export default StageDetail;