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

com.hazelcast.org.apache.calcite.rel.externalize.RelDotWriter Maven / Gradle / Ivy

There is a newer version: 5.5.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you 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.
 */
package com.hazelcast.org.apache.calcite.rel.externalize;

import com.hazelcast.org.apache.calcite.plan.hep.HepRelVertex;
import com.hazelcast.org.apache.calcite.plan.volcano.RelSubset;
import com.hazelcast.org.apache.calcite.rel.RelNode;
import com.hazelcast.org.apache.calcite.rel.RelWriter;
import com.hazelcast.org.apache.calcite.rel.metadata.RelMetadataQuery;
import com.hazelcast.org.apache.calcite.sql.SqlExplainLevel;
import com.hazelcast.org.apache.calcite.util.Pair;
import com.hazelcast.org.apache.calcite.util.Util;

import com.hazelcast.com.google.common.collect.HashMultimap;
import com.hazelcast.com.google.common.collect.Multimap;

import com.hazelcast.org.checkerframework.checker.nullness.qual.Nullable;
import org.immutables.value.Value;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

/**
 * Utility to dump a rel node plan in dot format.
 */
@Value.Enclosing
public class RelDotWriter extends RelWriterImpl {

  //~ Instance fields --------------------------------------------------------

  /**
   * Adjacent list of the plan graph.
   */
  private final Map> outArcTable = new LinkedHashMap<>();

  private Map nodeLabels = new HashMap<>();

  private Multimap nodeStyles = HashMultimap.create();

  private final WriteOption option;

  //~ Constructors -----------------------------------------------------------

  public RelDotWriter(
      PrintWriter pw, SqlExplainLevel detailLevel,
      boolean withIdPrefix) {
    this(pw, detailLevel, withIdPrefix, WriteOption.DEFAULT);
  }

  public RelDotWriter(
      PrintWriter pw, SqlExplainLevel detailLevel,
      boolean withIdPrefix, WriteOption option) {
    super(pw, detailLevel, withIdPrefix);
    this.option = option;
  }

  //~ Methods ----------------------------------------------------------------

  @Override protected void explain_(RelNode rel,
      List> values) {
    // get inputs
    List inputs = getInputs(rel);
    outArcTable.put(rel, inputs);

    // generate node label
    String label = getRelNodeLabel(rel, values);
    nodeLabels.put(rel, label);

    if (highlightNode(rel)) {
      nodeStyles.put(rel, "bold");
    }

    explainInputs(inputs);
  }

  protected String getRelNodeLabel(
      RelNode rel,
      List> values) {
    List labels = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
    if (withIdPrefix) {
      sb.append(rel.getId()).append(":");
    }
    sb.append(rel.getRelTypeName());
    labels.add(sb.toString());
    sb.setLength(0);

    if (detailLevel != SqlExplainLevel.NO_ATTRIBUTES) {
      for (Pair value : values) {
        if (value.right instanceof RelNode) {
          continue;
        }
        sb.append(value.left)
            .append(" = ")
            .append(value.right);
        labels.add(sb.toString());
        sb.setLength(0);
      }
    }

    switch (detailLevel) {
    case ALL_ATTRIBUTES:
      sb.append("rowcount = ")
          .append(mq.getRowCount(rel))
          .append(" cumulative cost = ")
          .append(mq.getCumulativeCost(rel))
          .append(" ");
      break;
    default:
      break;
    }
    switch (detailLevel) {
    case NON_COST_ATTRIBUTES:
    case ALL_ATTRIBUTES:
      if (!withIdPrefix) {
        // If we didn't print the rel id at the start of the line, print
        // it at the end.
        sb.append("id = ").append(rel.getId());
      }
      break;
    default:
      break;
    }
    labels.add(sb.toString().trim());
    sb.setLength(0);

    // format labels separately and then concat them
    int leftSpace = option.maxNodeLabelLength();
    List newlabels = new ArrayList<>();
    for (int i = 0; i < labels.size(); i++) {
      if (option.maxNodeLabelLength() != -1 && leftSpace <= 0) {
        if (i < labels.size() - 1) {
          // this is not the last label, but we have to stop here
          newlabels.add("...");
        }
        break;
      }
      String formatted = formatNodeLabel(labels.get(i), option.maxNodeLabelLength());
      newlabels.add(formatted);
      leftSpace -= formatted.length();
    }

    return "\"" + String.join("\\n", newlabels) + "\"";
  }

  private static List getInputs(RelNode parent) {
    return Util.transform(parent.getInputs(), child -> {
      if (child instanceof HepRelVertex) {
        return ((HepRelVertex) child).getCurrentRel();
      } else if (child instanceof RelSubset) {
        RelSubset subset = (RelSubset) child;
        return subset.getBestOrOriginal();
      } else {
        return child;
      }
    });
  }

  private void explainInputs(List inputs) {
    for (RelNode input : inputs) {
      if (input == null || nodeLabels.containsKey(input)) {
        continue;
      }
      input.explain(this);
    }
  }

  @Override public RelWriter done(RelNode node) {
    int numOfVisitedNodes = nodeLabels.size();
    super.done(node);
    if (numOfVisitedNodes == 0) {
      // When we enter this method call, no node
      // has been visited. So the current node must be the root of the plan.
      // Now we are exiting the method, all nodes in the plan
      // have been visited, so it is time to dump the plan.

      pw.println("digraph {");

      // print nodes with styles
      for (RelNode rel : nodeStyles.keySet()) {
        String style = String.join(",", nodeStyles.get(rel));
        pw.println(nodeLabels.get(rel) + " [style=\"" + style + "\"]");
      }

      // ordinary arcs
      for (Map.Entry> entry : outArcTable.entrySet()) {
        RelNode src = entry.getKey();
        String srcDesc = nodeLabels.get(src);
        for (int i = 0; i < entry.getValue().size(); i++) {
          RelNode dst = entry.getValue().get(i);

          // label is the ordinal of the arc
          // arc direction from child to parent, to reflect the direction of data flow
          pw.println(nodeLabels.get(dst) + " -> " + srcDesc + " [label=\"" + i + "\"]");
        }
      }
      pw.println("}");
      pw.flush();
    }
    return this;
  }

  /**
   * Format the label into multiple lines according to the options.
   * @param label the original label.
   * @param limit the maximal length of the formatted label.
   *              -1 means no limit.
   * @return the formatted label.
   */
  private String formatNodeLabel(String label, int limit) {
    label = label.trim();

    // escape quotes in the label.
    label = label.replace("\"", "\\\"");

    boolean trimmed = false;
    if (limit != -1 && label.length() > limit) {
      label = label.substring(0, limit);
      trimmed = true;
    }

    if (option.maxNodeLabelPerLine() == -1) {
      // no need to split into multiple lines.
      return label + (trimmed ? "..." : "");
    }

    List descParts = new ArrayList<>();
    for (int idx = 0; idx < label.length(); idx += option.maxNodeLabelPerLine()) {
      int endIdx = idx + option.maxNodeLabelPerLine() > label.length() ? label.length()
          : idx + option.maxNodeLabelPerLine();
      descParts.add(label.substring(idx, endIdx));
    }

    return String.join("\\n", descParts) + (trimmed ? "..." : "");
  }

  boolean highlightNode(RelNode node) {
    Predicate predicate = option.nodePredicate();
    return predicate != null && predicate.test(node);
  }

  /**
   * Options for displaying the rel node plan in dot format.
   */
  @Value.Immutable
  public interface WriteOption {

    /** Default configuration. */
    WriteOption DEFAULT = ImmutableRelDotWriter.WriteOption.of();

    /**
     * The max length of node labels.
     * If the label is too long, the visual display would be messy.
     * -1 means no limit to the label length.
     */
    @Value.Default default int maxNodeLabelLength() {
      return 100;
    }

    /**
     * The max length of node label in a line.
     * -1 means no limitation.
     */
    @Value.Default default int maxNodeLabelPerLine() {
      return 20;
    }

    /**
     * Predicate for nodes that need to be highlighted.
     */
    @Nullable Predicate nodePredicate();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy