| /** |
| * 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.heron.scheduler.dryrun; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| |
| import com.google.common.base.Optional; |
| import com.google.common.base.Strings; |
| |
| import org.apache.heron.common.basics.ByteAmount; |
| import org.apache.heron.spi.packing.PackingPlan; |
| import org.apache.heron.spi.packing.Resource; |
| |
| /** |
| * Formatter utilities |
| */ |
| public final class FormatterUtils { |
| |
| public FormatterUtils(boolean rich) { |
| this.rich = rich; |
| } |
| |
| /** |
| * If render in rich format (with color and text style) |
| */ |
| private final boolean rich; |
| |
| /** |
| * Simple and self-contained support of using ANSI escape codes. |
| * |
| * @see <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI escape code</a> |
| * |
| */ |
| private static final String ANSI_RESET = "\u001B[0m"; |
| private static final String ANSI_RED = "\u001B[31m"; |
| private static final String ANSI_GREEN = "\u001B[32m"; |
| private static final String ANSI_BOLD = "\u001B[1m"; |
| |
| /* |
| * Unicode of long strike overlay. A character followed by \u0036 will |
| * be rendered on terminal as itself being struck through |
| * |
| * See: http://unicode.org/charts/PDF/U0300.pdf |
| */ |
| private static final String STRIKETHROUGH = "\u0336"; |
| |
| public enum TextColor { |
| DEFAULT, |
| RED, |
| GREEN |
| } |
| |
| public enum TextStyle { |
| DEFAULT, |
| BOLD, |
| STRIKETHROUGH |
| } |
| |
| public enum ContainerChange { |
| UNAFFECTED, |
| MODIFIED, |
| NEW, |
| REMOVED |
| } |
| |
| |
| /** |
| * Poor man's tabulate implementation |
| * |
| * Each tabulates consists of a list of rows. Each row consists of a list of cells. |
| * |
| */ |
| |
| /** |
| * Cell is the smallest unit in a tabulate. More generally, it is a class |
| * that represents a piece of text with style and color added. |
| */ |
| public static class Cell { |
| // Text in the cell |
| private final String text; |
| |
| // Length of the text. It is used to calculate the proper widht of a column |
| private final int length; |
| private String formatter; |
| private TextColor color; |
| private TextStyle style; |
| |
| |
| public Cell(String text) { |
| this.text = text; |
| this.length = text.length(); |
| this.formatter = "%s"; |
| this.color = TextColor.DEFAULT; |
| this.style = TextStyle.DEFAULT; |
| } |
| |
| public Cell(String text, TextColor color) { |
| this(text); |
| this.color = color; |
| } |
| |
| public Cell(String text, TextStyle style) { |
| this(text); |
| this.style = style; |
| } |
| |
| public Cell(String text, TextColor color, TextStyle style) { |
| this(text); |
| this.color = color; |
| this.style = style; |
| } |
| |
| public void setColor(TextColor color) { |
| this.color = color; |
| } |
| |
| public void setStyle(TextStyle style) { |
| this.style = style; |
| } |
| |
| public void setFormatter(String formatter) { |
| this.formatter = formatter; |
| } |
| |
| public final int getLength() { |
| return this.length; |
| } |
| |
| /** |
| * Convert Cell to String |
| * @param rich if render in rich format |
| * @return |
| */ |
| public String toString(boolean rich) { |
| StringBuilder builder = new StringBuilder(); |
| String formattedText = String.format(formatter, text); |
| if (rich) { |
| switch (style) { |
| case BOLD: |
| builder.append(ANSI_BOLD); |
| builder.append(formattedText); |
| break; |
| /* Adding strike-through effect to a string is different. One needs to append unicode of |
| long strikethrough overlay to each single character in a string of characters. */ |
| case STRIKETHROUGH: |
| for (int i = 0; i < formattedText.length(); i++) { |
| builder.append(formattedText.charAt(i)); |
| builder.append(STRIKETHROUGH); |
| } |
| break; |
| case DEFAULT: |
| builder.append(formattedText); |
| break; |
| default: |
| throw new RuntimeException("Unknown text style: " + style); |
| } |
| switch (color) { |
| case RED: |
| builder.insert(0, ANSI_RED); |
| break; |
| case GREEN: |
| builder.insert(0, ANSI_GREEN); |
| break; |
| case DEFAULT: |
| break; |
| default: |
| throw new RuntimeException("Unknown text color: " + color); |
| } |
| // Only append ANSI reset escape code if text style or text color is added |
| if (style != TextStyle.DEFAULT || color != TextColor.DEFAULT) { |
| builder.append(ANSI_RESET); |
| } |
| } else { |
| builder.append(formattedText); |
| } |
| return builder.toString(); |
| } |
| } |
| |
| /** |
| * Row, which consists a list of cells. |
| * |
| * <pre> |
| * ---------------------- |
| * | xxxx | yyyy | zzzz | <- a list of cells |
| * ---------------------- |
| * ^ |
| * |------- separator: "|" |
| * </pre> |
| */ |
| public static class Row { |
| private List<Cell> row; |
| private static final String SEPARATOR = "|"; |
| |
| public Row(List<String> row) { |
| this.row = new ArrayList<>(); |
| for (String text: row) { |
| this.row.add(new Cell(text)); |
| } |
| } |
| |
| /** |
| * Set color for a list of cells in a row |
| * @param color |
| */ |
| public void setColor(TextColor color) { |
| for (Cell cell: row) { |
| cell.setColor(color); |
| } |
| } |
| |
| /** |
| * Set style for a list of cells in a row |
| * @param style |
| */ |
| public void setStyle(TextStyle style) { |
| for (Cell cell: row) { |
| cell.setStyle(style); |
| } |
| } |
| |
| /** |
| * Set formatter for each cell in the row |
| * @param formatters |
| */ |
| public void setFormatters(List<String> formatters) { |
| for (int i = 0; i < formatters.size(); i++) { |
| row.get(i).setFormatter(formatters.get(i)); |
| } |
| } |
| |
| public int size() { |
| return row.size(); |
| } |
| |
| /** |
| * Get length of the cell at index {@code index} in the row |
| * @param index |
| * @return length of the cell |
| */ |
| public Cell getCell(int index) { |
| return row.get(index); |
| } |
| |
| public String toString(boolean rich) { |
| List<String> renderedCells = new ArrayList<>(); |
| for (Cell c: row) { |
| renderedCells.add(c.toString(rich)); |
| } |
| return String.format("%s %s %s", |
| SEPARATOR, String.join(" " + SEPARATOR + " ", renderedCells), SEPARATOR); |
| } |
| } |
| |
| |
| /** |
| * Table, which consists of a title and a list of rows below the title |
| * |
| * <pre> |
| * ============================ |
| * | title1 | title2 | title3 | <----- title |
| * ============================ |
| * | xxxxxx | yyy | zzzzzz | <-| |
| * ---------------------------- |----- rows |
| * | gggg | uuuuu | ooo | <-| |
| * </pre> |
| */ |
| public static class Table { |
| private Row title; |
| private List<Row> rows; |
| |
| public Table(Row title, List<Row> rows) { |
| this.title = title; |
| this.rows = rows; |
| } |
| |
| private StringBuilder addRow(StringBuilder builder, String row) { |
| builder.append(row); |
| builder.append('\n'); |
| return builder; |
| } |
| |
| /** |
| * Calculate proper width for each column. |
| * |
| * Notice that if a cell contains text "foo" in red and bold style, the internal |
| * representation will be "\u001B[31m\u001B[1mfoo\u001B[0m". However during the |
| * calculation, ANSI escape codes/Unicode overlay should not be counted because they only |
| * serve as visual effect and do not take extra space on terminal. |
| * |
| * @return a list of integers specifying proper length of each column |
| */ |
| private List<Integer> calculateColumnsMax() { |
| List<Integer> width = new ArrayList<>(); |
| for (int i = 0; i < title.size(); i++) { |
| width.add(title.getCell(i).getLength()); |
| } |
| for (Row row: rows) { |
| for (int i = 0; i < row.size(); i++) { |
| width.set(i, Math.max(width.get(i), row.getCell(i).getLength())); |
| } |
| } |
| return width; |
| } |
| /** |
| * Generate formatter for each row based on rows. Width of a column is the |
| * max width of all cells on that column. |
| * |
| * Explanation of the {@code metaCellFormatter}: |
| * |
| * If the column max width is 8, then {@code String.format(metaCellFormatter, 8)} |
| * gives us {@code "%8s"} |
| * |
| * @see <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Formatter.html"> |
| * Formatter</a> |
| * |
| * @return formatter for rows |
| */ |
| private List<String> generateRowFormatter() { |
| List<String> formatters = new ArrayList<>(); |
| List<Integer> columnsMax = calculateColumnsMax(); |
| String metaCellFormatter = "%%%ds"; |
| for (Integer width: columnsMax) { |
| formatters.add(String.format(metaCellFormatter, width)); |
| } |
| return formatters; |
| } |
| |
| /** |
| * Generate length of table frame |
| * |
| * Definition of table frame: |
| * |
| * =============================== <- table frame |
| * | xxxxx | yyyyy | zzzzz | sss | |
| * =============================== |
| * | ..... | ..... | ..... | ... | |
| * |
| * The constant 3 comes from two spaces surrounding the text |
| * and one separator after the text. |
| * |
| * space |
| * | |
| * --------- |
| * | xxxxx | <- one separator |
| * --------- |
| * | |
| * space |
| * |
| * The final constant 1 is the leftmost separator of a row |
| * |
| * @return |
| */ |
| private int calculateFrameLength() { |
| int total = 0; |
| for (Integer width: calculateColumnsMax()) { |
| total += width + 3; |
| } |
| return total + 1; |
| } |
| |
| /** |
| * Format rows and title into a table |
| * @param rich if render table in rich format |
| * @return Formatted table |
| */ |
| public String createTable(boolean rich) { |
| // Generate formatter for each cell in a single row |
| List<String> formatters = generateRowFormatter(); |
| // Set formatter for each row |
| title.setFormatters(formatters); |
| for (Row row: rows) { |
| row.setFormatters(formatters); |
| } |
| // Calculate length for frames |
| int frameLength = calculateFrameLength(); |
| // Start building table |
| StringBuilder builder = new StringBuilder(); |
| // Add upper frame |
| addRow(builder, Strings.repeat("=", frameLength)); |
| // Add title |
| addRow(builder, title.toString(rich)); |
| // Add one single line to separate title and content |
| addRow(builder, Strings.repeat("-", frameLength)); |
| // Add each row |
| for (Row row: rows) { |
| addRow(builder, row.toString(rich)); |
| } |
| // Add lower frame |
| addRow(builder, Strings.repeat("=", frameLength)); |
| return builder.toString(); |
| } |
| } |
| |
| private static final List<String> TITLE_NAMES = Arrays.asList( |
| "component", "task ID", "CPU", "RAM (MB)", "disk (MB)"); |
| |
| |
| /******************************** Auxiliary functions ********************************/ |
| |
| /** |
| * Format new amount associated with change in percentage |
| * For example, with {@code oldAmount = 2} and {@code newAmount = 1} |
| * the result Cell is " -50.00%" (in red color) |
| * @param oldAmount old resource usage |
| * @param newAmount new resource usage |
| * @return formatted chagne in percentage if oldAmount and newAmount differ |
| */ |
| public static Optional<Cell> percentageChange(double oldAmount, double newAmount) { |
| double delta = newAmount - oldAmount; |
| double percentage = delta / oldAmount * 100.0; |
| if (percentage == 0.0) { |
| return Optional.absent(); |
| } else { |
| String sign = ""; |
| if (percentage > 0.0) { |
| sign = "+"; |
| } |
| Cell cell = new Cell(String.format("%s%.2f%%", sign, percentage)); |
| // set color to red if percentage drops, to green if percentage increases |
| if ("".equals(sign)) { |
| cell.setColor(TextColor.RED); |
| } else { |
| cell.setColor(TextColor.GREEN); |
| } |
| return Optional.of(cell); |
| } |
| } |
| |
| public String renderContainerName(Integer containerId) { |
| return new Cell(String.format("Container %d", containerId), |
| FormatterUtils.TextStyle.BOLD).toString(rich); |
| } |
| |
| public String renderContainerChange(ContainerChange change) { |
| Cell c = new Cell(change.toString()); |
| switch (change) { |
| case NEW: |
| c.setColor(TextColor.GREEN); |
| break; |
| case REMOVED: |
| c.setColor(TextColor.RED); |
| break; |
| default: |
| break; |
| } |
| return c.toString(rich); |
| } |
| |
| public Row rowOfInstancePlan(PackingPlan.InstancePlan plan, |
| TextColor color, TextStyle style) { |
| String taskId = String.valueOf(plan.getTaskId()); |
| String cpu = String.valueOf(plan.getResource().getCpu()); |
| String ram = String.valueOf(plan.getResource().getRam().asMegabytes()); |
| String disk = String.valueOf(plan.getResource().getDisk().asMegabytes()); |
| List<String> cells = Arrays.asList( |
| plan.getComponentName(), taskId, cpu, ram, disk); |
| Row row = new Row(cells); |
| row.setStyle(style); |
| row.setColor(color); |
| return row; |
| } |
| |
| public String renderOneContainer(List<Row> rows) { |
| Row title = new Row(TITLE_NAMES); |
| title.setStyle(TextStyle.BOLD); |
| return new Table(title, rows).createTable(rich); |
| } |
| |
| public String renderResourceUsage(Resource resource) { |
| double cpu = resource.getCpu(); |
| ByteAmount ram = resource.getRam(); |
| ByteAmount disk = resource.getDisk(); |
| return String.format("CPU: %s, RAM: %s MB, Disk: %s MB", |
| cpu, ram.asMegabytes(), disk.asMegabytes()); |
| } |
| |
| public String renderResourceUsageChange(Resource oldResource, Resource newResource) { |
| double oldCpu = oldResource.getCpu(); |
| double newCpu = newResource.getCpu(); |
| Optional<Cell> cpuUsageChange = FormatterUtils.percentageChange(oldCpu, newCpu); |
| long oldRam = oldResource.getRam().asMegabytes(); |
| long newRam = newResource.getRam().asMegabytes(); |
| Optional<Cell> ramUsageChange = FormatterUtils.percentageChange(oldRam, newRam); |
| long oldDisk = oldResource.getDisk().asMegabytes(); |
| long newDisk = newResource.getDisk().asMegabytes(); |
| Optional<Cell> diskUsageChange = FormatterUtils.percentageChange(oldDisk, newDisk); |
| String cpuUsage = String.format("CPU: %s", newCpu); |
| if (cpuUsageChange.isPresent()) { |
| cpuUsage += String.format(" (%s)", cpuUsageChange.get().toString(rich)); |
| } |
| String ramUsage = String.format("RAM: %s MB", newRam); |
| if (ramUsageChange.isPresent()) { |
| ramUsage += String.format(" (%s)", ramUsageChange.get().toString(rich)); |
| } |
| String diskUsage = String.format("Disk: %s MB", newDisk); |
| if (diskUsageChange.isPresent()) { |
| diskUsage += String.format(" (%s)", diskUsageChange.get().toString(rich)); |
| } |
| return String.join(", ", cpuUsage, ramUsage, diskUsage); |
| } |
| } |