Skip to content

Commit

Permalink
Make the Total column use the same resolution (#10)
Browse files Browse the repository at this point in the history
* 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 <dev@ethlo.com>
  • Loading branch information
ethlo and Morten Haraldsen authored Dec 10, 2024
1 parent b741b0b commit 82cc7ca
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 86 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<artifactId>chronograph</artifactId>
<name>Chronograph</name>
<description>Easy-to-use stopwatch for task performance statistics</description>
<version>3.0.0</version>
<version>3.1.0-SNAPSHOT</version>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
Expand Down
25 changes: 20 additions & 5 deletions src/main/java/com/ethlo/ascii/Table.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private Map<Integer, Boolean> getHasColumnContent(final List<TableRow> 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;
Expand All @@ -89,15 +89,25 @@ private int calculateTotalWidth(final Map<Integer, Integer> 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<TableRow> 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++)
{
Expand All @@ -115,7 +125,12 @@ private String toString(final List<TableRow> 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
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/ethlo/ascii/TableCell.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/ethlo/ascii/TableTheme.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "+";
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/ethlo/time/Chronograph.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
6 changes: 3 additions & 3 deletions src/main/java/com/ethlo/time/ChronographData.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public class ChronographData
private final List<TaskPerformanceStatistics> taskStatistics;
private final Duration totalTime;

public ChronographData(final String name, final List<TaskPerformanceStatistics> taskStatistics, final Duration totalTime)
public ChronographData(final String name, final List<TaskPerformanceStatistics> 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<Chronograph> toCombine)
Expand Down Expand Up @@ -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()));
}
}
133 changes: 67 additions & 66 deletions src/main/java/com/ethlo/time/ReportUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}
12 changes: 7 additions & 5 deletions src/main/java/com/ethlo/time/TableOutputformatter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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));
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/ethlo/time/TaskPerformanceStatistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@

public record TaskPerformanceStatistics(String name, long sampleSize, PerformanceStatistics performanceStatistics)
{
public TaskPerformanceStatistics(String name, PerformanceStatistics performanceStatistics)
{
this(name, performanceStatistics.getTotalInvocations(), performanceStatistics);
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/ethlo/util/IndexedCollectionStatistics.java
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,9 @@ public Long getStandardDeviation()
}
return (long) Math.sqrt(standardDeviation / count);
}

public long getSum()
{
return sum;
}
}
61 changes: 61 additions & 0 deletions src/test/java/com/ethlo/time/TableOutputformatterTest.java
Original file line number Diff line number Diff line change
@@ -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<TaskPerformanceStatistics> 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% | | | | | |
+----------+---------+-------+--------+---------+-----------+---------+---------+---------+""");
}

}
Loading

0 comments on commit 82cc7ca

Please sign in to comment.