diff --git a/src/main/java/com/ethlo/time/Chronograph.java b/src/main/java/com/ethlo/time/Chronograph.java index a75f603..47b9ba0 100644 --- a/src/main/java/com/ethlo/time/Chronograph.java +++ b/src/main/java/com/ethlo/time/Chronograph.java @@ -28,7 +28,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @@ -54,7 +53,7 @@ public void start(String task) throw new IllegalArgumentException("task cannot be null"); } - final TaskInfo taskTiming = taskInfos.computeIfAbsent(task, taskName->{ + final TaskInfo taskTiming = taskInfos.computeIfAbsent(task, taskName -> { order.add(taskName); return new TaskInfo(taskName); }); @@ -64,7 +63,7 @@ public void start(String task) public void stop() { final long ts = System.nanoTime(); - taskInfos.values().forEach(task->task.stopped(ts, true)); + taskInfos.values().forEach(task -> task.stopped(ts, true)); } public boolean isAnyRunning() @@ -117,6 +116,7 @@ public boolean isRunning(String task) /** * See {@link Report#prettyPrint(Chronograph)} + * * @return A formatted string with the task details */ public String prettyPrint() diff --git a/src/main/java/com/ethlo/time/DurationUtil.java b/src/main/java/com/ethlo/time/DurationUtil.java new file mode 100644 index 0000000..a48babc --- /dev/null +++ b/src/main/java/com/ethlo/time/DurationUtil.java @@ -0,0 +1,104 @@ +package com.ethlo.time; + +/*- + * #%L + * chronograph + * %% + * Copyright (C) 2019 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ + +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.Duration; + +public class DurationUtil +{ + 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; + + 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(":"); + } + + 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 + { + // Sub-second + if (millis > 0) + { + sb.append(df.format(nanos / (double) NANOS_PER_MILLI)).append("m "); + } + + if (millis == 0 && micros > 0) + { + sb.append(df.format(nanos / (double) NANOS_PER_MICRO)).append("μ "); + } + + if (millis == 0 && micros == 0 && nano > 0) + { + sb.append(nano).append("n "); + } + } + + return sb.toString().trim(); + } +} diff --git a/src/main/java/com/ethlo/time/Report.java b/src/main/java/com/ethlo/time/Report.java index 7e12251..7d7fbf2 100644 --- a/src/main/java/com/ethlo/time/Report.java +++ b/src/main/java/com/ethlo/time/Report.java @@ -35,14 +35,13 @@ public class Report public static String prettyPrint(Chronograph chronograph) { final StringBuilder sb = new StringBuilder(); - sb.append("\n-------------------------------------------------------------------------------\n"); - sb.append("| Task | Average | Total | Invocations | % | \n"); - sb.append("-------------------------------------------------------------------------------\n"); + sb.append("\n--------------------------------------------------------------------------------\n"); + sb.append("| Task | Average | Total | Invocations | % | \n"); + sb.append("--------------------------------------------------------------------------------\n"); final NumberFormat pf = NumberFormat.getPercentInstance(); pf.setMinimumFractionDigits(1); pf.setMaximumFractionDigits(1); - pf.setMinimumIntegerDigits(2); pf.setGroupingUsed(false); final NumberFormat nf = NumberFormat.getNumberInstance(); @@ -53,32 +52,48 @@ public static String prettyPrint(Chronograph chronograph) { final TaskInfo task = chronograph.getTaskInfo(name); - final String totalTaskTimeStr = humanReadableFormat(Duration.ofNanos(task.getTotalTaskTime())); - final String avgTaskTimeStr = humanReadableFormat(task.getAverageTaskTime()); + final String totalTaskTimeStr = DurationUtil.humanReadable(Duration.ofNanos(task.getTotalTaskTime())); + final String avgTaskTimeStr = DurationUtil.humanReadable(task.getAverageTaskTime()); final String invocationsStr = nf.format(task.getInvocationCount()); sb.append("| "); - sb.append(adjustWidth(task.getName(), 15)).append(" | "); - sb.append(adjustWidth(avgTaskTimeStr, 14)).append(" | "); - sb.append(adjustWidth(totalTaskTimeStr, 15)).append(" | "); - sb.append(adjustWidth(invocationsStr, 13)).append(" | "); + sb.append(adjustPadRight(task.getName(), 21)).append(" | "); + sb.append(adjustPadLeft(avgTaskTimeStr, 12)).append(" | "); + sb.append(adjustPadLeft(totalTaskTimeStr, 12)).append(" | "); + sb.append(adjustPadLeft(invocationsStr, 13)).append(" | "); final Duration totalTime = chronograph.getTotalTime(); final double pct = totalTime.isZero() ? 0D : task.getTotalTaskTime() / (double) totalTime.toNanos(); - sb.append(adjustWidth(pf.format(pct), 6)).append(" |"); + sb.append(adjustPadLeft(pf.format(pct), 6)).append(" |"); sb.append("\n"); } + + if (chronograph.getTaskNames().size() > 1) + { + sb.append(totals(chronograph)); + } + return sb.toString(); } - private static String humanReadableFormat(Duration duration) + private static String totals(final Chronograph chronograph) + { + return repeat("-", 80) + "\n" + + "| " + adjustPadRight("Total" + ": " + DurationUtil.humanReadable(chronograph.getTotalTime()), 76) + " |" + "\n" + + repeat("-", 80) + "\n"; + } + + private static String repeat(final String s, final int count) { - return duration.toString() - .substring(2) - .replaceAll("(\\d[HMS])(?!$)", "$1 ") - .toLowerCase(); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) + { + sb.append(s); + } + return sb.toString(); } - private static String adjustWidth(final String s, final int width) + + private static String adjustPadRight(final String s, final int width) { if (s.length() >= width) { @@ -92,4 +107,20 @@ private static String adjustWidth(final String s, final int width) } return new String(result); } + + private static String adjustPadLeft(final String s, final int width) + { + if (s.length() >= width) + { + return s.substring(0, width); + } + + final char[] result = new char[width]; + Arrays.fill(result, ' '); + for (int i = 0; i < s.length(); i++) + { + result[i + (width - s.length())] = s.charAt(i); + } + return new String(result); + } } diff --git a/src/test/java/com/ethlo/time/ChronographTest.java b/src/test/java/com/ethlo/time/ChronographTest.java index ae3ed81..ba8a7ad 100644 --- a/src/test/java/com/ethlo/time/ChronographTest.java +++ b/src/test/java/com/ethlo/time/ChronographTest.java @@ -132,6 +132,49 @@ public void resetAll() assertThat(chronograph.getTaskNames()).isEmpty(); } + @Test + public void testIsRunning() + { + final Chronograph chronograph = Chronograph.create(); + assertThat(chronograph.isRunning(taskName)).isFalse(); + chronograph.start(taskName); + assertThat(chronograph.isRunning(taskName)).isTrue(); + chronograph.stop(taskName); + assertThat(chronograph.isRunning(taskName)).isFalse(); + } + + @Test + public void getTaskInfo() + { + final Chronograph chronograph = Chronograph.create(); + chronograph.start("a"); + chronograph.start("b"); + chronograph.start("c"); + chronograph.stop(); + assertThat(chronograph.getTaskInfo()).hasSize(3); + } + + @Test + public void getTotalTaskTimeForEmpty() + { + final Chronograph chronograph = Chronograph.create(); + assertThat(chronograph.getTotalTime()).isEqualTo(Duration.ZERO); + chronograph.start(taskName); + assertThat(chronograph.getTotalTime()).isEqualTo(Duration.ZERO); + chronograph.stop(); + assertThat(chronograph.getTotalTime()).isNotEqualTo(Duration.ZERO); + } + + @Test + public void testIsAnyRunning() + { + final Chronograph chronograph = Chronograph.create(); + chronograph.start(taskName); + assertThat(chronograph.isAnyRunning()).isTrue(); + chronograph.stop(); + assertThat(chronograph.isAnyRunning()).isFalse(); + } + private static final long SLEEP_PRECISION = TimeUnit.MILLISECONDS.toNanos(2); private static final long SPIN_YIELD_PRECISION = TimeUnit.MILLISECONDS.toNanos(2); diff --git a/src/test/java/com/ethlo/time/DurationUtilTest.java b/src/test/java/com/ethlo/time/DurationUtilTest.java new file mode 100644 index 0000000..cba7dbd --- /dev/null +++ b/src/test/java/com/ethlo/time/DurationUtilTest.java @@ -0,0 +1,78 @@ +package com.ethlo.time; + +/*- + * #%L + * chronograph + * %% + * Copyright (C) 2019 Morten Haraldsen (ethlo) + * %% + * 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. + * #L% + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; + +import org.junit.Test; + +public class DurationUtilTest +{ + @Test + public void humanReadableFormatMoreThanHour() + { + assertThat(DurationUtil.humanReadable(Duration.ofSeconds(4712).withNanos(123456789))).isEqualTo("01:18:32.123"); + } + + @Test + public void humanReadableFormatLessThanHour() + { + assertThat(DurationUtil.humanReadable(Duration.ofSeconds(2000).withNanos(123456789))).isEqualTo("33:20.123"); + } + + @Test + public void humanReadableFormatLessThanMinute() + { + assertThat(DurationUtil.humanReadable(Duration.ofSeconds(8).withNanos(125_956_789))).isEqualTo("8.126s"); + } + + @Test + public void humanReadableFormatLessThanMinute2() + { + assertThat(DurationUtil.humanReadable(Duration.ofSeconds(8).withNanos(1_000_000))).isEqualTo("8.001s"); + } + + @Test + public void humanReadableFormatLessThanMinute3() + { + assertThat(DurationUtil.humanReadable(Duration.ofSeconds(8))).isEqualTo("8.000s"); + } + + @Test + public void humanReadableFormatLessThanSecond() + { + assertThat(DurationUtil.humanReadable(Duration.ofNanos(123_456_789))).isEqualTo("123.46m"); + } + + @Test + public void humanReadableFormatLessThanMillisecond() + { + assertThat(DurationUtil.humanReadable(Duration.ofNanos(456_789))).isEqualTo("456.79μ"); + } + + @Test + public void humanReadableFormatLessThanMicrosecond() + { + assertThat(DurationUtil.humanReadable(Duration.ofNanos(489))).isEqualTo("489n"); + } +}