DebugUtil.java

// 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 org.apache.doris.common.util;

import org.apache.doris.common.Pair;
import org.apache.doris.proto.Types;
import org.apache.doris.thrift.TPlanNodeRuntimeStatsItem;
import org.apache.doris.thrift.TUniqueId;

import com.google.common.base.Strings;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DecimalFormat;
import java.util.List;
import java.util.UUID;

public class DebugUtil {
    public static final DecimalFormat DECIMAL_FORMAT_SCALE_3 = new DecimalFormat("0.000");

    public static int THOUSAND = 1000;
    public static int MILLION = 1000 * THOUSAND;
    public static int BILLION = 1000 * MILLION;

    public static int SECOND = 1000; // ms
    public static int MINUTE = 60 * SECOND;
    public static int HOUR = 60 * MINUTE;

    public static long KILOBYTE = 1024;
    public static long MEGABYTE = 1024 * KILOBYTE;
    public static long GIGABYTE = 1024 * MEGABYTE;
    public static long TERABYTE = 1024 * GIGABYTE;

    public static Pair<Double, String> getUint(long value) {
        Double doubleValue = Double.valueOf(value);
        String unit = "";
        if (value >= BILLION) {
            unit = "B";
            doubleValue /= BILLION;
        } else if (value >= MILLION) {
            unit = "M";
            doubleValue /= MILLION;
        } else if (value >= THOUSAND) {
            unit = "K";
            doubleValue /= THOUSAND;
        }
        Pair<Double, String> returnValue  = Pair.of(doubleValue, unit);
        return returnValue;
    }

    // Print the value (timestamp in ms) to builder
    // ATTN: for hour and minute granularity, we ignore ms precision.
    public static void printTimeMs(long value, StringBuilder builder) {
        long newValue = value;
        if (newValue == 0) {
            builder.append("0");
        } else {
            boolean hour = false;
            boolean minute = false;
            if (newValue >= HOUR) {
                builder.append(newValue / HOUR).append("hour");
                newValue %= HOUR;
                hour = true;
            }
            if (newValue >= MINUTE) {
                builder.append(newValue / MINUTE).append("min");
                newValue %= MINUTE;
                minute = true;
            }
            if (!hour && newValue >= SECOND) {
                builder.append(newValue / SECOND).append("sec");
                newValue %= SECOND;
            }
            if (!hour && !minute) {
                builder.append(newValue).append("ms");
            }
        }
    }

    public static String getPrettyStringMs(long timestampMs) {
        StringBuilder builder = new StringBuilder();
        printTimeMs(timestampMs, builder);
        return builder.toString();
    }

    public static Pair<Double, String> getByteUint(long value) {
        Double doubleValue = Double.valueOf(value);
        String unit = "";
        if (value == 0) {
            // nothing
            unit = "";
        } else if (value > TERABYTE) {
            unit = "TB";
            doubleValue /= TERABYTE;
        } else if (value > GIGABYTE) {
            unit = "GB";
            doubleValue /= GIGABYTE;
        } else if (value > MEGABYTE) {
            unit = "MB";
            doubleValue /= MEGABYTE;
        } else if (value > KILOBYTE)  {
            unit = "KB";
            doubleValue /= KILOBYTE;
        } else {
            unit = "B";
        }
        Pair<Double, String> returnValue  = Pair.of(doubleValue, unit);
        return returnValue;
    }

    public static String printByteWithUnit(long value) {
        Pair<Double, String> quotaUnitPair = getByteUint(value);
        String readableQuota = DebugUtil.DECIMAL_FORMAT_SCALE_3.format(quotaUnitPair.first) + " "
                + quotaUnitPair.second;
        return readableQuota;
    }

    public static String printId(final TUniqueId id) {
        if (id == null) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        builder.append(Long.toHexString(id.hi)).append("-").append(Long.toHexString(id.lo));
        return builder.toString();
    }

    // id is a String generated by DebugUtil.printId(TUniqueId)
    public static TUniqueId parseTUniqueIdFromString(String id) {
        if (Strings.isNullOrEmpty(id)) {
            throw new NumberFormatException("invalid query id");
        }

        String[] parts = id.split("-");
        if (parts.length != 2) {
            throw new NumberFormatException("invalid query id");
        }

        TUniqueId uniqueId = new TUniqueId();
        try {
            uniqueId.setHi(Long.parseUnsignedLong(parts[0], 16));
            uniqueId.setLo(Long.parseUnsignedLong(parts[1], 16));
        } catch (NumberFormatException e) {
            throw new NumberFormatException("invalid query id:" + e.getMessage());
        }

        return uniqueId;
    }

    public static String printId(final UUID id) {
        TUniqueId tUniqueId = new TUniqueId(id.getMostSignificantBits(), id.getLeastSignificantBits());
        StringBuilder builder = new StringBuilder();
        builder.append(Long.toHexString(tUniqueId.hi)).append("-").append(Long.toHexString(tUniqueId.lo));
        return builder.toString();
    }

    public static String printId(final Types.PUniqueId id) {
        StringBuilder builder = new StringBuilder();
        builder.append(Long.toHexString(id.getHi())).append("-").append(Long.toHexString(id.getLo()));
        return builder.toString();
    }

    public static String getStackTrace(Exception e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }

    public static String prettyPrintChangedSessionVar(List<List<String>> nestedList) {
        if (nestedList == null || nestedList.isEmpty()) {
            return "";
        }

        StringBuilder output = new StringBuilder();

        // Assuming each inner list has exactly 3 columns
        int[] columnWidths = new int[3];

        // Calculate the maximum width of each column
        // First consider the header widths: "VarName", "CurrentValue", "DefaultValue"
        String[] headers = {"VarName", "CurrentValue", "DefaultValue"};
        for (int i = 0; i < headers.length; i++) {
            columnWidths[i] = headers[i].length();  // Initialize with header length
        }

        // Update column widths based on data
        for (List<String> row : nestedList) {
            for (int i = 0; i < row.size(); i++) {
                columnWidths[i] = Math.max(columnWidths[i], row.get(i).length());
            }
        }

        // Build the table header
        for (int i = 0; i < headers.length; i++) {
            output.append(String.format("%-" + columnWidths[i] + "s", headers[i]));
            if (i < headers.length - 1) {
                output.append(" | ");  // Separator between columns
            }
        }
        output.append("\n");  // Newline after the header

        // Add a separator line for better readability (optional)
        for (int i = 0; i < headers.length; i++) {
            output.append(String.format("%-" + columnWidths[i] + "s", Strings.repeat("-", columnWidths[i])));
            if (i < headers.length - 1) {
                output.append("-|-");  // Separator between columns
            }
        }
        output.append("\n");  // Newline after the separator

        // Build the table body with proper alignment based on column widths
        for (List<String> row : nestedList) {
            for (int i = 0; i < row.size(); i++) {
                String element = row.get(i);
                // Pad with spaces if the element is shorter than the column width
                output.append(String.format("%-" + columnWidths[i] + "s", element));
                if (i < row.size() - 1) {
                    output.append(" | ");  // Separator between columns
                }
            }
            output.append("\n");  // Newline after each row
        }

        return output.toString();
    }

    public static String prettyPrintPlanNodeRuntimeStatsItems(
            List<TPlanNodeRuntimeStatsItem> planNodeRuntimeStatsItems) {
        StringBuilder result = new StringBuilder();
        if (planNodeRuntimeStatsItems == null || planNodeRuntimeStatsItems.isEmpty()) {
            result.append("The list is empty or null.\n");
            return result.toString();
        }

        result.append(String.format("%-10s %-10s %-15s %-15s %-15s %-15s %-15s %-15s %-15s %-15s %-10s %-10s\n",
                "NodeID", "InstanceNum", "InputRows", "OutputRows", "CommonFilterRows", "CommonFilterInputRows",
                "RuntimeFilterRows", "RuntimeFilterInputRows", "JoinBuilderRows", "JoinProbeRows",
                "JoinBuilderSkewRatio", "JoinProbeSkewRatio"));

        for (TPlanNodeRuntimeStatsItem item : planNodeRuntimeStatsItems) {
            result.append(String.format("%-10d %-10d %-15d %-15d %-15d %-15d %-15d %-15d %-15d %-15d %-10d %-10d\n",
                    item.getNodeId(),
                    item.getInstanceNum(),
                    item.getInputRows(),
                    item.getOutputRows(),
                    item.getCommonFilterRows(),
                    item.getCommonFilterInputRows(),
                    item.getRuntimeFilterRows(),
                    item.getRuntimeFilterInputRows(),
                    item.getJoinBuilderRows(),
                    item.getJoinProbeRows(),
                    item.getJoinBuilderSkewRatio(),
                    item.getJoinProberSkewRatio()
            ));
        }
        return result.toString();
    }
}