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

io.trino.sql.planner.planprinter.TextRenderer 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.
 */
package io.trino.sql.planner.planprinter;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import io.airlift.units.DataSize;
import io.trino.cost.PlanNodeStatsAndCostSummary;
import io.trino.plugin.base.metrics.TDigestHistogram;
import io.trino.spi.metrics.Metric;
import io.trino.spi.metrics.Metrics;

import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.Iterables.getOnlyElement;
import static java.lang.Double.NEGATIVE_INFINITY;
import static java.lang.Double.POSITIVE_INFINITY;
import static java.lang.Double.isFinite;
import static java.lang.Double.isNaN;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

public class TextRenderer
        implements Renderer
{
    private final boolean verbose;
    private final int level;

    public TextRenderer(boolean verbose, int level)
    {
        this.verbose = verbose;
        this.level = level;
    }

    @Override
    public String render(PlanRepresentation plan)
    {
        StringBuilder output = new StringBuilder();
        NodeRepresentation root = plan.getRoot();
        boolean hasChildren = hasChildren(root, plan);
        return writeTextOutput(output, plan, Indent.newInstance(level, hasChildren), root);
    }

    private String writeTextOutput(StringBuilder output, PlanRepresentation plan, Indent indent, NodeRepresentation node)
    {
        output.append(indent.nodeIndent())
                .append(node.getName())
                .append(node.getDescriptor().entrySet().stream()
                        .filter(entry -> !(entry.getValue().isEmpty() || entry.getValue().equals("[]")))
                        .map(entry -> entry.getKey() + " = " + entry.getValue())
                        .collect(joining(", ", "[", "]")))
                .append("\n");

        String columns = node.getOutputs().stream()
                .map(s -> s.getSymbol() + ":" + s.getType())
                .collect(joining(", "));

        output.append(indentMultilineString("Layout: [" + columns + "]\n", indent.detailIndent()));

        String reorderJoinStatsAndCost = printReorderJoinStatsAndCost(node);
        if (!reorderJoinStatsAndCost.isEmpty()) {
            output.append(indentMultilineString(reorderJoinStatsAndCost, indent.detailIndent()));
        }

        List estimates = node.getEstimates();
        if (!estimates.isEmpty()) {
            output.append(indentMultilineString(printEstimates(estimates), indent.detailIndent()));
        }

        String stats = printStats(plan, node);
        if (!stats.isEmpty()) {
            output.append(indentMultilineString(stats, indent.detailIndent()));
        }

        if (!node.getDetails().isEmpty()) {
            String details = indentMultilineString(Joiner.on("\n").join(node.getDetails()), indent.detailIndent());
            output.append(details);
            if (!details.endsWith("\n")) {
                output.append('\n');
            }
        }

        List children = node.getChildren().stream()
                .map(plan::getNode)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .collect(toList());

        for (Iterator iterator = children.iterator(); iterator.hasNext(); ) {
            NodeRepresentation child = iterator.next();
            writeTextOutput(output, plan, indent.forChild(!iterator.hasNext(), hasChildren(child, plan)), child);
        }

        return output.toString();
    }

    private String printStats(PlanRepresentation plan, NodeRepresentation node)
    {
        StringBuilder output = new StringBuilder();
        if (node.getStats().isEmpty() || !(plan.getTotalCpuTime().isPresent() && plan.getTotalScheduledTime().isPresent() && plan.getTotalBlockedTime().isPresent())) {
            return "";
        }

        PlanNodeStats nodeStats = node.getStats().get();

        double scheduledTimeFraction = 100.0d * nodeStats.getPlanNodeScheduledTime().toMillis() / plan.getTotalScheduledTime().get().toMillis();
        double cpuTimeFraction = 100.0d * nodeStats.getPlanNodeCpuTime().toMillis() / plan.getTotalCpuTime().get().toMillis();
        double blockedTimeFraction = 100.0d * nodeStats.getPlanNodeBlockedTime().toMillis() / plan.getTotalBlockedTime().get().toMillis();

        output.append(format("CPU: %s (%s%%), Scheduled: %s (%s%%), Blocked: %s (%s%%)",
                nodeStats.getPlanNodeCpuTime().convertToMostSuccinctTimeUnit(),
                formatDouble(cpuTimeFraction),
                nodeStats.getPlanNodeScheduledTime().convertToMostSuccinctTimeUnit(),
                formatDouble(scheduledTimeFraction),
                nodeStats.getPlanNodeBlockedTime().convertToMostSuccinctTimeUnit(),
                formatDouble(blockedTimeFraction)));

        output.append(format(", Output: %s (%s)", formatPositions(nodeStats.getPlanNodeOutputPositions()), nodeStats.getPlanNodeOutputDataSize().toString()));
        if (nodeStats.getPlanNodeSpilledDataSize().toBytes() > 0) {
            output.append(format(", Spilled: %s", nodeStats.getPlanNodeSpilledDataSize()));
        }
        output.append("\n");

        printMetrics(output, "connector metrics:", BasicOperatorStats::getConnectorMetrics, nodeStats);
        printMetrics(output, "metrics:", BasicOperatorStats::getMetrics, nodeStats);
        printDistributions(output, nodeStats);

        if (nodeStats instanceof WindowPlanNodeStats) {
            printWindowOperatorStats(output, ((WindowPlanNodeStats) nodeStats).getWindowOperatorStats());
        }

        return output.toString();
    }

    private void printMetrics(StringBuilder output, String label, Function metricsGetter, PlanNodeStats stats)
    {
        if (!verbose) {
            return;
        }

        Map translatedOperatorTypes = translateOperatorTypes(stats.getOperatorTypes());
        for (String operator : translatedOperatorTypes.keySet()) {
            String translatedOperatorType = translatedOperatorTypes.get(operator);
            Map> metrics = metricsGetter.apply(stats.getOperatorStats().get(operator)).getMetrics();

            // filter out empty distributions
            metrics = metrics.entrySet().stream()
                    .filter(entry -> {
                        if (!(entry.getValue() instanceof TDigestHistogram histogram)) {
                            return true;
                        }

                        return histogram.getMin() != 0. || histogram.getMax() != 0.;
                    })
                    .collect(toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));

            if (metrics.isEmpty()) {
                continue;
            }

            output.append(translatedOperatorType + label).append("\n");
            Map> sortedMap = new TreeMap<>(metrics);
            sortedMap.forEach((name, metric) -> output.append(format("  '%s' = %s\n", name, metric)));
        }
    }

    private void printDistributions(StringBuilder output, PlanNodeStats stats)
    {
        Map inputAverages = stats.getOperatorInputPositionsAverages();
        Map inputStdDevs = stats.getOperatorInputPositionsStdDevs();
        Map translatedOperatorTypes = translateOperatorTypes(stats.getOperatorTypes());

        for (String operator : translatedOperatorTypes.keySet()) {
            String translatedOperatorType = translatedOperatorTypes.get(operator);
            double inputAverage = inputAverages.get(operator);

            output.append(translatedOperatorType);
            output.append(format(
                    Locale.US,
                    "Input avg.: %s rows, Input std.dev.: %s%%\n",
                    formatDouble(inputAverage),
                    formatDouble(100.0d * inputStdDevs.get(operator) / inputAverage)));
        }
    }

    private void printWindowOperatorStats(StringBuilder output, WindowOperatorStats stats)
    {
        if (!verbose) {
            // these stats are too detailed for non-verbose mode
            return;
        }

        output.append(format("Active Drivers: [ %d / %d ]\n", stats.getActiveDrivers(), stats.getTotalDrivers()));
        output.append(format("Index size: std.dev.: %s bytes, %s rows\n", formatDouble(stats.getIndexSizeStdDev()), formatDouble(stats.getIndexPositionsStdDev())));
        output.append(format("Index count per driver: std.dev.: %s\n", formatDouble(stats.getIndexCountPerDriverStdDev())));
        output.append(format("Rows per driver: std.dev.: %s\n", formatDouble(stats.getRowsPerDriverStdDev())));
        output.append(format("Size of partition: std.dev.: %s\n", formatDouble(stats.getPartitionRowsStdDev())));
    }

    private static Map translateOperatorTypes(Set operators)
    {
        if (operators.size() == 1) {
            // don't display operator (plan node) name again
            return ImmutableMap.of(getOnlyElement(operators), "");
        }

        if (operators.contains("LookupJoinOperator") && operators.contains("HashBuilderOperator")) {
            // join plan node
            return ImmutableMap.of(
                    "LookupJoinOperator", "Left (probe) ",
                    "HashBuilderOperator", "Right (build) ");
        }

        return ImmutableMap.of();
    }

    private String printReorderJoinStatsAndCost(NodeRepresentation node)
    {
        if (verbose && node.getReorderJoinStatsAndCost().isPresent()) {
            return format("Reorder joins cost : %s\n", formatPlanNodeStatsAndCostSummary(node.getReorderJoinStatsAndCost().get()));
        }
        return "";
    }

    private String printEstimates(List estimates)
    {
        return estimates.stream()
                .map(this::formatPlanNodeStatsAndCostSummary)
                .collect(joining("/", "Estimates: ", "\n"));
    }

    private String formatPlanNodeStatsAndCostSummary(PlanNodeStatsAndCostSummary stats)
    {
        requireNonNull(stats, "stats is null");
        return format("{rows: %s (%s), cpu: %s, memory: %s, network: %s}",
                formatAsLong(stats.getOutputRowCount()),
                formatAsDataSize(stats.getOutputSizeInBytes()),
                formatAsCpuCost(stats.getCpuCost()),
                formatAsDataSize(stats.getMemoryCost()),
                formatAsDataSize(stats.getNetworkCost()));
    }

    private static boolean hasChildren(NodeRepresentation node, PlanRepresentation plan)
    {
        return node.getChildren().stream()
                .map(plan::getNode)
                .anyMatch(Optional::isPresent);
    }

    private static String formatAsLong(double value)
    {
        if (isFinite(value)) {
            return format(Locale.US, "%d", Math.round(value));
        }

        return "?";
    }

    private static String formatAsCpuCost(double value)
    {
        return formatAsDataSize(value).replaceAll("B$", "");
    }

    private static String formatAsDataSize(double value)
    {
        if (isNaN(value)) {
            return "?";
        }
        if (value == POSITIVE_INFINITY) {
            return "+\u221E";
        }
        if (value == NEGATIVE_INFINITY) {
            return "-\u221E";
        }

        return DataSize.succinctBytes(Math.round(value)).toString();
    }

    static String formatDouble(double value)
    {
        if (isFinite(value)) {
            return format(Locale.US, "%.2f", value);
        }

        return "?";
    }

    static String formatPositions(long positions)
    {
        String noun = (positions == 1) ? "row" : "rows";
        return positions + " " + noun;
    }

    static String indentString(int indent)
    {
        return "    ".repeat(indent);
    }

    private static String indentMultilineString(String string, String indent)
    {
        return string.replaceAll("(?m)^", indent);
    }

    private static class Indent
    {
        private static final String VERTICAL_LINE = "\u2502";
        private static final String LAST_NODE = "\u2514\u2500";
        private static final String INTERMEDIATE_NODE = "\u251c\u2500";

        private final String firstLinePrefix;
        private final String nextLinesPrefix;
        private final boolean hasChildren;

        public static Indent newInstance(int level, boolean hasChildren)
        {
            String indent = indentString(level);
            return new Indent(indent, indent, hasChildren);
        }

        private Indent(String firstLinePrefix, String nextLinesPrefix, boolean hasChildren)
        {
            this.firstLinePrefix = firstLinePrefix;
            this.nextLinesPrefix = nextLinesPrefix;
            this.hasChildren = hasChildren;
        }

        public Indent forChild(boolean last, boolean hasChildren)
        {
            String first;
            String next;

            if (last) {
                first = pad(LAST_NODE, 3);
                next = pad("", 3);
            }
            else {
                first = pad(INTERMEDIATE_NODE, 3);
                next = pad(VERTICAL_LINE, 3);
            }

            return new Indent(nextLinesPrefix + first, nextLinesPrefix + next, hasChildren);
        }

        public String nodeIndent()
        {
            return firstLinePrefix;
        }

        public String detailIndent()
        {
            String indent = "";

            if (hasChildren) {
                indent = VERTICAL_LINE;
            }

            return nextLinesPrefix + pad(indent, 4);
        }

        private static String pad(String text, int length)
        {
            checkArgument(text.length() <= length, "text is longer that length");

            return text + " ".repeat(length - text.length());
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy