From 82cc7cafb7247bca1945a0372df661465eeaff2e Mon Sep 17 00:00:00 2001 From: Morten Haraldsen Date: Tue, 10 Dec 2024 11:16:32 +0100 Subject: [PATCH] Make the Total column use the same resolution (#10) * Make the Total column use the same resolution, to make it easier to read. * Avoid outputting color codes when none is active. --------- Co-authored-by: Morten Haraldsen --- pom.xml | 2 +- src/main/java/com/ethlo/ascii/Table.java | 25 +++- src/main/java/com/ethlo/ascii/TableCell.java | 4 + src/main/java/com/ethlo/ascii/TableTheme.java | 5 + src/main/java/com/ethlo/time/Chronograph.java | 2 +- .../java/com/ethlo/time/ChronographData.java | 6 +- src/main/java/com/ethlo/time/ReportUtil.java | 133 +++++++++--------- .../com/ethlo/time/TableOutputformatter.java | 12 +- .../ethlo/time/TaskPerformanceStatistics.java | 4 + .../util/IndexedCollectionStatistics.java | 5 + .../ethlo/time/TableOutputformatterTest.java | 61 ++++++++ .../java/com/ethlo/util/DurationUtilTest.java | 4 +- .../com/ethlo/util/ListPerformanceTest.java | 4 +- 13 files changed, 181 insertions(+), 86 deletions(-) create mode 100644 src/test/java/com/ethlo/time/TableOutputformatterTest.java diff --git a/pom.xml b/pom.xml index d8002e8..5c7e245 100755 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ chronograph Chronograph Easy-to-use stopwatch for task performance statistics - 3.0.0 + 3.1.0-SNAPSHOT Apache License, Version 2.0 diff --git a/src/main/java/com/ethlo/ascii/Table.java b/src/main/java/com/ethlo/ascii/Table.java index d0d76f0..ac57950 100644 --- a/src/main/java/com/ethlo/ascii/Table.java +++ b/src/main/java/com/ethlo/ascii/Table.java @@ -73,7 +73,7 @@ private Map getHasColumnContent(final List rows) { for (int i = 0; i < row.getCells().size(); i++) { - result.compute(i, (key, value) -> row.getCells().get(key).getValue().length() > 0); + result.compute(i, (key, value) -> !row.getCells().get(key).getValue().isEmpty()); } } return result; @@ -89,15 +89,25 @@ private int calculateTotalWidth(final Map maxLengths) public String render(String title) { - final String titleRow = title != null ? (theme.getCellBackground().value() + theme.getStringColor().value() + StringUtil.adjustPadRight(theme.getPadding() + title, tableWidth) + AnsiColor.RESET.value()) : ""; - return NEWLINE + titleRow + NEWLINE + theme.getCellBackground().value() + toString(rows); + final String content = StringUtil.adjustPadRight(theme.getPadding() + title, tableWidth); + String colored; + if (theme.hasColors()) + { + colored = theme.getCellBackground().value() + theme.getStringColor().value() + content + AnsiColor.RESET.value(); + } + else + { + colored = content; + } + final String titleRow = title != null ? colored : ""; + return titleRow + NEWLINE + theme.getCellBackground().value() + toString(rows); } private String toString(final List rows) { final StringBuilder sb = new StringBuilder(); - final boolean hasVerticalSeparator = theme.getVerticalSeparator().length() != 0; + final boolean hasVerticalSeparator = !theme.getVerticalSeparator().isEmpty(); for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) { @@ -115,7 +125,12 @@ private String toString(final List rows) sb.append(StringUtil.repeat(verticalSep(), width)); } sb.append(theme.getVerticalSpacerColor().value()).append(theme.getCellBackground().value()); - sb.append(getCellEnd(rowIndex)).append(AnsiColor.RESET.value()).append(NEWLINE); + sb.append(getCellEnd(rowIndex)); + if (theme.hasColors()) + { + sb.append(AnsiColor.RESET.value()); + } + sb.append(NEWLINE); } } else diff --git a/src/main/java/com/ethlo/ascii/TableCell.java b/src/main/java/com/ethlo/ascii/TableCell.java index 7c399a3..63d9338 100644 --- a/src/main/java/com/ethlo/ascii/TableCell.java +++ b/src/main/java/com/ethlo/ascii/TableCell.java @@ -42,6 +42,10 @@ public TableCell(final String value, final boolean left, final boolean isNumeric public static String color(final String value, AnsiColor color, AnsiBackgroundColor backgroundColor) { + if (color == AnsiColor.NONE && backgroundColor == AnsiBackgroundColor.NONE) + { + return value; + } return color.value() + backgroundColor.value() + value + AnsiColor.RESET.value(); } diff --git a/src/main/java/com/ethlo/ascii/TableTheme.java b/src/main/java/com/ethlo/ascii/TableTheme.java index 17de799..0d51983 100644 --- a/src/main/java/com/ethlo/ascii/TableTheme.java +++ b/src/main/java/com/ethlo/ascii/TableTheme.java @@ -245,6 +245,11 @@ public String getName() return name; } + public boolean hasColors() + { + return cellBackground != AnsiBackgroundColor.NONE || stringColor != AnsiColor.NONE || numericColor != AnsiColor.NONE; + } + public static final class Builder { public String topCross = "+"; diff --git a/src/main/java/com/ethlo/time/Chronograph.java b/src/main/java/com/ethlo/time/Chronograph.java index 22843ff..38c2c15 100644 --- a/src/main/java/com/ethlo/time/Chronograph.java +++ b/src/main/java/com/ethlo/time/Chronograph.java @@ -782,6 +782,6 @@ public ChronographData getTaskData() .stream() .map(task -> new TaskPerformanceStatistics(task.getName(), task.getSampleSize(), task.getDurationStatistics())) .toList(); - return new ChronographData(name, stats, getTotalTime()); + return new ChronographData(name, stats); } } diff --git a/src/main/java/com/ethlo/time/ChronographData.java b/src/main/java/com/ethlo/time/ChronographData.java index c886b6e..4e0d710 100644 --- a/src/main/java/com/ethlo/time/ChronographData.java +++ b/src/main/java/com/ethlo/time/ChronographData.java @@ -34,11 +34,11 @@ public class ChronographData private final List taskStatistics; private final Duration totalTime; - public ChronographData(final String name, final List taskStatistics, final Duration totalTime) + public ChronographData(final String name, final List taskStatistics) { this.name = name; this.taskStatistics = taskStatistics; - this.totalTime = totalTime; + this.totalTime = Duration.ofNanos(taskStatistics.stream().mapToLong(t -> t.performanceStatistics().getElapsedTotal().toNanos()).sum()); } public static ChronographData combine(final String name, final List toCombine) @@ -96,6 +96,6 @@ public ChronographData merge(String chronographName, ChronographData chronograph } return t; })); - return new ChronographData(chronographName, new ArrayList<>(joined.values()), Duration.ofNanos(joined.values().stream().mapToLong(t -> t.performanceStatistics().getElapsedTotal().toNanos()).sum())); + return new ChronographData(chronographName, new ArrayList<>(joined.values())); } } diff --git a/src/main/java/com/ethlo/time/ReportUtil.java b/src/main/java/com/ethlo/time/ReportUtil.java index 091c7ae..d5a2ba2 100644 --- a/src/main/java/com/ethlo/time/ReportUtil.java +++ b/src/main/java/com/ethlo/time/ReportUtil.java @@ -20,86 +20,54 @@ * #L% */ +import java.math.BigDecimal; import java.math.RoundingMode; import java.text.NumberFormat; import java.time.Duration; +import java.time.temporal.ChronoUnit; public class ReportUtil { - public static final int SECONDS_PER_HOUR = 3_600; - public static final int SECONDS_PER_MINUTE = 60; - public static final int NANOS_PER_MILLI = 1_000_000; private static final int NANOS_PER_MICRO = 1_000; + private static final long NANOS_PER_MILLI = 1_000_000; + private static final long NANOS_PER_SECOND = 1_000_000_000; + public static String humanReadable(Duration duration) { - final long seconds = duration.getSeconds(); - final int hours = (int) seconds / SECONDS_PER_HOUR; - int remainder = (int) seconds - hours * SECONDS_PER_HOUR; - final int mins = remainder / SECONDS_PER_MINUTE; - remainder = remainder - mins * SECONDS_PER_MINUTE; - final int secs = remainder; - - final long nanos = duration.getNano(); - final int millis = (int) nanos / NANOS_PER_MILLI; - remainder = (int) nanos - millis * NANOS_PER_MILLI; - final int micros = remainder / NANOS_PER_MICRO; - remainder = remainder - micros * NANOS_PER_MICRO; - final int nano = remainder; - - final NumberFormat nf = NumberFormat.getNumberInstance(); - nf.setMinimumIntegerDigits(2); - - final NumberFormat df = NumberFormat.getNumberInstance(); - df.setMinimumFractionDigits(2); - df.setMaximumFractionDigits(2); - df.setRoundingMode(RoundingMode.HALF_UP); - - final StringBuilder sb = new StringBuilder(); - if (hours > 0) - { - sb.append(nf.format(hours)).append(":"); - } - if (hours > 0 || mins > 0) - { - sb.append(nf.format(mins)).append(":"); - } + return humanReadable(duration, getSummaryResolution(duration)); + } - final boolean hasMinuteOrMore = hours > 0 || mins > 0; - final boolean hasSecondOrMore = hasMinuteOrMore || secs > 0; - if (hasSecondOrMore && !hasMinuteOrMore) - { - final NumberFormat dfSec = NumberFormat.getNumberInstance(); - dfSec.setMinimumFractionDigits(0); - dfSec.setMaximumFractionDigits(0); - dfSec.setMinimumIntegerDigits(3); - dfSec.setMaximumIntegerDigits(3); - sb.append(seconds).append('.').append(dfSec.format(nanos / (double) NANOS_PER_MILLI)).append(" s"); - } - else if (hasSecondOrMore) - { - sb.append(nf.format(secs)).append(".").append(millis); - } - else + public static String humanReadable(Duration duration, ChronoUnit unit) + { + switch (unit) { - // Sub-second - if (millis > 0) - { - sb.append(df.format(nanos / (double) NANOS_PER_MILLI)).append(" ms "); - } - - if (millis == 0 && micros > 0) - { - sb.append(df.format(nanos / (double) NANOS_PER_MICRO)).append(" us "); - } + case NANOS: + return duration.toNanos() + " ns"; + case MICROS: + return BigDecimal.valueOf(duration.toNanos()).divide(BigDecimal.valueOf(NANOS_PER_MICRO), 2, RoundingMode.HALF_UP) + " us"; + case MILLIS: + return BigDecimal.valueOf(duration.toNanos()).divide(BigDecimal.valueOf(NANOS_PER_MILLI), 2, RoundingMode.HALF_UP) + " ms"; + case SECONDS: + return BigDecimal.valueOf(duration.toNanos()).divide(BigDecimal.valueOf(NANOS_PER_SECOND), 3, RoundingMode.HALF_UP) + " s"; + case MINUTES: + case HOURS: + default: + // Use dynamic HH:mm:ss format + long seconds = duration.getSeconds(); + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + long remainingSeconds = seconds % 60; - if (millis == 0 && micros == 0 && nano > 0) - { - sb.append(nano).append(" ns "); - } + if (hours > 0) + { + return String.format("%d:%02d:%02d", hours, minutes, remainingSeconds); + } + else + { + return String.format("%02d:%02d", minutes, remainingSeconds); + } } - - return sb.toString().trim(); } public static String formatInteger(final long value) @@ -108,4 +76,37 @@ public static String formatInteger(final long value) nf.setGroupingUsed(true); return nf.format(value); } + + public static ChronoUnit getSummaryResolution(final Duration totalExecutionTime) + { + if (totalExecutionTime == null || totalExecutionTime.isNegative() || totalExecutionTime.isZero()) + { + return ChronoUnit.NANOS; + } + + final long nanos = totalExecutionTime.toNanos(); + + // Define thresholds for units + if (nanos < 1_000) + { // Less than 1 microsecond + return ChronoUnit.NANOS; + } + else if (nanos < 1_000_000) + { // Less than 1 millisecond + return ChronoUnit.MICROS; + } + else if (nanos < 1_000_000_000) + { // Less than 1 second + return ChronoUnit.MILLIS; + } + else if (nanos < 60L * 1_000_000_000) + { // Less than 1 minute + return ChronoUnit.SECONDS; + } + else if (nanos < 3600L * 1_000_000_000) + { // Less than 1 hour + return ChronoUnit.MINUTES; + } + return ChronoUnit.HOURS; + } } diff --git a/src/main/java/com/ethlo/time/TableOutputformatter.java b/src/main/java/com/ethlo/time/TableOutputformatter.java index 356709c..2e3d3c8 100644 --- a/src/main/java/com/ethlo/time/TableOutputformatter.java +++ b/src/main/java/com/ethlo/time/TableOutputformatter.java @@ -9,9 +9,9 @@ * 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. @@ -23,6 +23,7 @@ import java.math.RoundingMode; import java.text.NumberFormat; import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; @@ -73,7 +74,7 @@ private static TableRow getTableRow(final OutputConfig outputConfig, Duration to final PerformanceStatistics performanceStatistics = taskStats.performanceStatistics(); - outputTotal(outputConfig, row, performanceStatistics); + outputTotal(outputConfig, row, performanceStatistics, totalTime); addInvocations(outputConfig, taskStats, nf, row, invocations); @@ -105,11 +106,12 @@ private static void outputPercentiles(final OutputConfig outputConfig, final Tab } } - private static void outputTotal(final OutputConfig outputConfig, final TableRow row, final PerformanceStatistics performanceStatistics) + private static void outputTotal(final OutputConfig outputConfig, final TableRow row, final PerformanceStatistics performanceStatistics, final Duration totalTime) { if (outputConfig.total()) { - final String str = ReportUtil.humanReadable(performanceStatistics.getElapsedTotal()); + final ChronoUnit summaryResolution = ReportUtil.getSummaryResolution(totalTime); + final String str = ReportUtil.humanReadable(performanceStatistics.getElapsedTotal(), summaryResolution); row.append(new TableCell(str, false, true)); } } diff --git a/src/main/java/com/ethlo/time/TaskPerformanceStatistics.java b/src/main/java/com/ethlo/time/TaskPerformanceStatistics.java index d432fe7..f2bf8a5 100644 --- a/src/main/java/com/ethlo/time/TaskPerformanceStatistics.java +++ b/src/main/java/com/ethlo/time/TaskPerformanceStatistics.java @@ -24,4 +24,8 @@ public record TaskPerformanceStatistics(String name, long sampleSize, PerformanceStatistics performanceStatistics) { + public TaskPerformanceStatistics(String name, PerformanceStatistics performanceStatistics) + { + this(name, performanceStatistics.getTotalInvocations(), performanceStatistics); + } } diff --git a/src/main/java/com/ethlo/util/IndexedCollectionStatistics.java b/src/main/java/com/ethlo/util/IndexedCollectionStatistics.java index 1be5479..16b9b83 100644 --- a/src/main/java/com/ethlo/util/IndexedCollectionStatistics.java +++ b/src/main/java/com/ethlo/util/IndexedCollectionStatistics.java @@ -141,4 +141,9 @@ public Long getStandardDeviation() } return (long) Math.sqrt(standardDeviation / count); } + + public long getSum() + { + return sum; + } } diff --git a/src/test/java/com/ethlo/time/TableOutputformatterTest.java b/src/test/java/com/ethlo/time/TableOutputformatterTest.java new file mode 100644 index 0000000..a411c14 --- /dev/null +++ b/src/test/java/com/ethlo/time/TableOutputformatterTest.java @@ -0,0 +1,61 @@ +package com.ethlo.time; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.ethlo.time.statistics.PerformanceStatistics; +import com.ethlo.util.IndexedCollectionStatistics; +import com.ethlo.util.LongList; + +class TableOutputformatterTest +{ + + @Test + void format() + { + final List taskData = List.of( + new TaskPerformanceStatistics("Quick", 20, new PerformanceStatistics(new IndexedCollectionStatistics(new LongList().addAll(List.of(454L, 566L, 499L, 504L, 604L)))))); + final ChronographData chronographData = new ChronographData("My test", taskData); + final String result = new TableOutputformatter().format(chronographData); + //assertThat(result).isEqualTo("mehh"); + } + + @Test + void formatWithMultipleTasks() + { + // Prepare test data + var stats1 = new IndexedCollectionStatistics(new LongList().addAll(List.of(454L, 566L, 499L, 504L, 604L))); + assertThat(stats1.getSum()).isEqualTo(2627L); + var stats2 = new IndexedCollectionStatistics(new LongList().addAll(List.of(1000_000L, 1200_000L, 1100_000L, 1150_000L))); + assertThat(stats2.getSum()).isEqualTo(4_450_000L); + var stats3 = new IndexedCollectionStatistics(new LongList().addAll(List.of(1200_100_000L, 1400_100_000L, 1800_100_000L))); + assertThat(stats3.getSum()).isEqualTo(4_400_300_000L); + + var task1 = new TaskPerformanceStatistics("Quick", new PerformanceStatistics(stats1)); + var task2 = new TaskPerformanceStatistics("Moderate", new PerformanceStatistics(stats2)); + var task3 = new TaskPerformanceStatistics("Slow", new PerformanceStatistics(stats3)); + + var chronographData = new ChronographData("My test", List.of(task1, task2, task3)); + assertThat(chronographData.getTotalTime().toNanos()).isEqualTo(4_404_752_627L); + + // Format the data + var result = new TableOutputformatter().format(chronographData); + + // Assert the result + assertThat(result).isEqualToIgnoringWhitespace(""" + My test + +----------+---------+-------+--------+---------+-----------+---------+---------+---------+ + | Task | Total | Count | % | Median | Std dev | Mean | Min | Max | + +----------+---------+-------+--------+---------+-----------+---------+---------+---------+ + | Quick | 0.000 s | 5 | 0.0% | 504 ns | 53 ns | 525 ns | 454 ns | 604 ns | + | Moderate | 0.004 s | 4 | 0.1% | 1.13 ms | 73.95 us | 1.11 ms | 1.00 ms | 1.20 ms | + | Slow | 4.400 s | 3 | 99.9% | 1.400 s | 249.44 ms | 1.467 s | 1.200 s | 1.800 s | + +----------+---------+-------+--------+---------+-----------+---------+---------+---------+ + | Sum | 4.405 s | 12 | 100.0% | | | | | | + +----------+---------+-------+--------+---------+-----------+---------+---------+---------+"""); + } + +} \ No newline at end of file diff --git a/src/test/java/com/ethlo/util/DurationUtilTest.java b/src/test/java/com/ethlo/util/DurationUtilTest.java index e836777..b6580dc 100644 --- a/src/test/java/com/ethlo/util/DurationUtilTest.java +++ b/src/test/java/com/ethlo/util/DurationUtilTest.java @@ -33,13 +33,13 @@ public class DurationUtilTest @Test public void humanReadableFormatMoreThanHour() { - assertThat(ReportUtil.humanReadable(Duration.ofSeconds(4712).withNanos(123456789))).isEqualTo("01:18:32.123"); + assertThat(ReportUtil.humanReadable(Duration.ofSeconds(4712).withNanos(123456789))).isEqualTo("1:18:32"); } @Test public void humanReadableFormatLessThanHour() { - assertThat(ReportUtil.humanReadable(Duration.ofSeconds(2000).withNanos(123456789))).isEqualTo("33:20.123"); + assertThat(ReportUtil.humanReadable(Duration.ofSeconds(2000).withNanos(123456789))).isEqualTo("33:20"); } @Test diff --git a/src/test/java/com/ethlo/util/ListPerformanceTest.java b/src/test/java/com/ethlo/util/ListPerformanceTest.java index 1fd31cc..19aad0c 100644 --- a/src/test/java/com/ethlo/util/ListPerformanceTest.java +++ b/src/test/java/com/ethlo/util/ListPerformanceTest.java @@ -29,8 +29,6 @@ import java.util.LinkedList; import java.util.List; -import com.ethlo.ascii.AnsiBackgroundColor; - import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -136,10 +134,10 @@ private Chronograph performAddBenchmark(final int runs, final int size) void performanceTestMediumSort() { final Chronograph c = performSortBenchmark(10, 500_000); + output(c, TableTheme.DEFAULT); output(c, TableTheme.RED_HERRING); output(c, TableTheme.SINGLE); output(c, TableTheme.DOUBLE); - output(c, TableTheme.DEFAULT.begin().cellBackground(AnsiBackgroundColor.BRIGHT_CYAN).build()); output(c, TableTheme.ROUNDED); output(c, TableTheme.MINIMAL); output(c, TableTheme.COMPACT);