All Downloads are FREE. Search and download functionalities are using the official Maven repository.

webapp.src.components.LivePlan.jsx Maven / Gradle / Ivy

There is a newer version: 465
Show newest version
/*
 * 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 - 2024 Weber Informatics LLC | Privacy Policy