
webapp.src.components.LivePlan.jsx Maven / Gradle / Ivy
/*
* 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.
*/
//@flow
import React from "react";
import ReactDOMServer from "react-dom/server";
import * as dagreD3 from "dagre-d3";
import * as d3 from "d3";
import {
formatRows,
getStageStateColor,
initializeGraph,
initializeSvg,
parseAndFormatDataSize,
truncateString
} from "../utils";
import {QueryHeader} from "./QueryHeader";
type StageStatisticsProps = {
stage: any,
}
type StageStatisticsState = {}
type StageNodeInfo = {
stageId: string,
id: string,
root: string,
distribution: any,
stageStats: any,
state: string,
nodes: Map,
}
class StageStatistics extends React.Component {
static getStages(queryInfo): Map {
const stages: Map = new Map();
StageStatistics.flattenStage(queryInfo.outputStage, stages);
return stages;
}
static flattenStage(stageInfo, result) {
stageInfo.subStages.forEach(function (stage) {
StageStatistics.flattenStage(stage, result);
});
const nodes = new Map();
StageStatistics.flattenNode(result, stageInfo.plan.root, JSON.parse(stageInfo.plan.jsonRepresentation), nodes);
result.set(stageInfo.plan.id, {
stageId: stageInfo.stageId,
id: stageInfo.plan.id,
root: stageInfo.plan.root.id,
distribution: stageInfo.plan.distribution,
stageStats: stageInfo.stageStats,
state: stageInfo.state,
nodes: nodes
});
}
static flattenNode(stages, rootNodeInfo, node: any, result: Map) {
result.set(node.id, {
id: node.id,
name: node['name'],
descriptor: node['descriptor'],
details: node['details'],
sources: node.children.map(node => node.id),
});
node.children.forEach(function (child) {
StageStatistics.flattenNode(stages, rootNodeInfo, child, result);
});
}
render() {
const stage = this.props.stage;
const stats = this.props.stage.stageStats;
return (
Stage {stage.id}
{stage.state}
CPU: {stats.totalCpuTime}
Buffered: {parseAndFormatDataSize(stats.bufferedDataSize)}
{stats.fullyBlocked ?
Blocked: {stats.totalBlockedTime} :
Blocked: {stats.totalBlockedTime}
}
Memory: {parseAndFormatDataSize(stats.userMemoryReservation)}
Splits: {"Q:" + stats.queuedDrivers + ", R:" + stats.runningDrivers + ", F:" + stats.completedDrivers}
Input: {parseAndFormatDataSize(stats.rawInputDataSize) + " / " + formatRows(stats.rawInputPositions)}
);
}
}
type PlanNodeProps = {
id: string,
name: string,
descriptor: Map,
details: string[],
sources: string[],
}
type PlanNodeState = {}
class PlanNode extends React.Component {
constructor(props: PlanNodeProps) {
super(props);
}
render() {
// get join distribution type by matching details to a regular expression
var distribution = "";
var matchArray = this.props.details.join("\n").match(/Distribution:\s+(\w+)/);
if (matchArray !== null) {
distribution = " (" + matchArray[1] + ")";
}
var descriptor = Object.entries(this.props.descriptor)
.map(([key, value]) => key + " = " + String(value))
.join(", ");
descriptor = "(" + descriptor + ")";
return (
" + this.props.name + "" + descriptor}>
{this.props.name + distribution}
{truncateString(descriptor, 35)}
);
}
}
type LivePlanProps = {
queryId: string,
isEmbedded: boolean,
}
type LivePlanState = {
initialized: boolean,
ended: boolean,
query: ?any,
graph: any,
svg: any,
render: any,
}
export class LivePlan extends React.Component {
timeoutId: TimeoutID;
constructor(props: LivePlanProps) {
super(props);
this.state = {
initialized: false,
ended: false,
query: null,
graph: initializeGraph(),
svg: null,
render: new dagreD3.render(),
};
}
resetTimer() {
clearTimeout(this.timeoutId);
// stop refreshing when query finishes or fails
if (this.state.query === null || !this.state.ended) {
this.timeoutId = setTimeout(this.refreshLoop.bind(this), 1000);
}
}
refreshLoop() {
clearTimeout(this.timeoutId); // to stop multiple series of refreshLoop from going on simultaneously
fetch('/ui/api/query/' + this.props.queryId)
.then(response => response.json())
.then(query => {
this.setState({
query: query,
initialized: true,
ended: query.finalQueryInfo,
});
this.resetTimer();
})
.catch(() => {
this.setState({
initialized: true,
});
this.resetTimer();
});
}
static handleStageClick(stageCssId: string) {
window.open("stage.html?" + stageCssId, '_blank');
}
componentDidMount() {
this.refreshLoop.bind(this)();
new window.ClipboardJS('.copy-button');
}
updateD3Stage(stage: StageNodeInfo, graph: any, allStages: Map) {
const clusterId = stage.stageId;
const stageRootNodeId = "stage-" + stage.id + "-root";
const color = getStageStateColor(stage);
graph.setNode(clusterId, {style: 'fill: ' + color, labelStyle: 'fill: #fff'});
// 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(stageRootNodeId, {class: "stage-stats", label: html, labelType: "html"});
graph.setParent(stageRootNodeId, clusterId);
graph.setEdge("node-" + stage.root, stageRootNodeId, {style: "visibility: hidden"});
stage.nodes.forEach(node => {
const nodeId = "node-" + node.id;
const nodeHtml = ReactDOMServer.renderToString( );
graph.setNode(nodeId, {label: nodeHtml, style: 'fill: #fff', labelType: "html"});
graph.setParent(nodeId, clusterId);
node.sources.forEach(source => {
graph.setEdge("node-" + source, nodeId, {class: "plan-edge", arrowheadClass: "plan-arrowhead"});
});
var sourceFragmentIds = node.descriptor['sourceFragmentIds'];
if (sourceFragmentIds) {
var remoteSources = sourceFragmentIds.replace('[', '').replace(']', '').split(', ');
if (remoteSources.length > 0) {
graph.setNode(nodeId, {label: '', shape: "circle"});
remoteSources.forEach(sourceId => {
const source = allStages.get(sourceId);
if (source) {
const sourceStats = source.stageStats;
graph.setEdge("stage-" + sourceId + "-root", nodeId, {
class: "plan-edge",
style: "stroke-width: 4px",
arrowheadClass: "plan-arrowhead",
label: parseAndFormatDataSize(sourceStats.outputDataSize) + " / " + formatRows(sourceStats.outputPositions),
labelStyle: "color: #fff; font-weight: bold; font-size: 24px;",
labelType: "html",
});
}
});
}
}
});
}
updateD3Graph() {
if (!this.state.svg) {
this.setState({
svg: initializeSvg("#plan-canvas"),
});
return;
}
if (!this.state.query) {
return;
}
const graph = this.state.graph;
const stages = StageStatistics.getStages(this.state.query);
stages.forEach(stage => {
this.updateD3Stage(stage, graph, stages);
});
const inner = d3.select("#plan-canvas g");
this.state.render(inner, graph);
const svg = this.state.svg;
svg.selectAll("g.cluster").on("click", LivePlan.handleStageClick);
const width = parseInt(window.getComputedStyle(document.getElementById("live-plan"), null).getPropertyValue("width").replace(/px/, "")) - 50;
const height = parseInt(window.getComputedStyle(document.getElementById("live-plan"), null).getPropertyValue("height").replace(/px/, "")) - 50;
const graphHeight = graph.graph().height + 100;
const graphWidth = graph.graph().width + 100;
if (this.state.ended) {
// Zoom doesn't deal well with DOM changes
const initialScale = Math.min(width / graphWidth, height / graphHeight);
const zoom = d3.zoom().scaleExtent([initialScale, 1]).on("zoom", function () {
inner.attr("transform", d3.event.transform);
});
svg.call(zoom);
svg.call(zoom.transform, d3.zoomIdentity.translate((width - graph.graph().width * initialScale) / 2, 20).scale(initialScale));
svg.attr('height', height);
svg.attr('width', width);
}
else {
svg.attr('height', graphHeight);
svg.attr('width', graphWidth);
}
}
componentDidUpdate() {
this.updateD3Graph();
//$FlowFixMe
$('[data-toggle="tooltip"]').tooltip()
}
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}
);
}
let loadingMessage = null;
if (query && !query.outputStage) {
loadingMessage = (
Live plan graph will appear automatically when query starts running.
Loading...
)
}
// TODO: Refactor components to move refreshLoop to parent rather than using this property
const queryHeader = this.props.isEmbedded ? null : ;
return (
{queryHeader}
{loadingMessage}
{this.state.ended ? "Scroll to zoom." : "Zoom disabled while query is running." } Click stage to view additional statistics
);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy