Skip to content

Commit

Permalink
Tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
ethlo committed Aug 15, 2019
1 parent daf2e81 commit 830d8a0
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 96 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ethlo.time</groupId>
<artifactId>stopwatch</artifactId>
<artifactId>chronograph</artifactId>
<version>0.1-SNAPSHOT</version>
<licenses>
<license>
Expand Down
124 changes: 49 additions & 75 deletions src/main/java/com/ethlo/time/Chronograph.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 @@ -20,27 +20,26 @@
* #L%
*/

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
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;

public class Chronograph
{
private final Map<String, TaskInfo> taskTimings;
private final ConcurrentLinkedQueue<String> order = new ConcurrentLinkedQueue<>();
private final Map<String, TaskInfo> taskInfos;

private Chronograph()
{
taskTimings = new ConcurrentHashMap<>(16);
taskInfos = new ConcurrentHashMap<>(16);
}

public static Chronograph create()
Expand All @@ -54,19 +53,40 @@ public void start(String task)
{
throw new IllegalArgumentException("task cannot be null");
}
final TaskInfo taskTiming = taskTimings.computeIfAbsent(task, TaskInfo::new);
taskTiming.started();

final TaskInfo taskTiming = taskInfos.computeIfAbsent(task, taskName->{
order.add(taskName);
return new TaskInfo(taskName);
});
taskTiming.start();
}

public void stop()
{
final long ts = System.nanoTime();
taskInfos.values().forEach(task->task.stopped(ts, true));
}

public boolean isAnyRunning()
{
return taskInfos.values().stream().anyMatch(TaskInfo::isRunning);
}

public void stop(String task)
{
final TaskInfo taskTiming = taskTimings.computeIfAbsent(task, TaskInfo::new);
taskTiming.stopped();
final long ts = System.nanoTime();
final TaskInfo taskInfo = taskInfos.get(task);
if (taskInfo == null)
{
throw new IllegalStateException("No started task with name " + task);
}
taskInfo.stopped(ts, false);
}

public void resetAll()
public synchronized void resetAll()
{
taskTimings.clear();
taskInfos.clear();
order.clear();
}

public Duration getElapsedTime(final String task)
Expand All @@ -76,82 +96,36 @@ public Duration getElapsedTime(final String task)

public TaskInfo getTaskInfo(final String task)
{
return Optional.ofNullable(taskTimings.get(task)).orElseThrow(() -> new IllegalStateException("Unknown task " + task));
return Optional.ofNullable(taskInfos.get(task)).orElseThrow(() -> new IllegalStateException("Unknown task " + task));
}

public Set<String> getTaskNames()
public Collection<String> getTaskNames()
{
return Collections.unmodifiableSet(taskTimings.keySet());
return Collections.unmodifiableCollection(order);
}

public List<TaskInfo> getTaskInfo()
{
return Collections.unmodifiableList(new ArrayList<>(taskTimings.values()));
return Collections.unmodifiableList(new ArrayList<>(taskInfos.values()));
}

/**
* Generate a string with a table describing all tasks performed.
* <p>For custom reporting, call {@link #getTaskInfo()} and use the task info
* directly.
*/
public String prettyPrint()
public boolean isRunning(String task)
{
final StringBuilder sb = new StringBuilder();
sb.append("\n-------------------------------------------------------------------------------\n");
sb.append("| Task | Average | Total | Invocations | % | \n");
sb.append("-------------------------------------------------------------------------------\n");

final NumberFormat pf = NumberFormat.getPercentInstance();
pf.setMinimumIntegerDigits(3);
pf.setGroupingUsed(false);

final NumberFormat nf = NumberFormat.getNumberInstance();
nf.setRoundingMode(RoundingMode.HALF_UP);
//;MinimumIntegerDigits(9);
nf.setGroupingUsed(true);

for (TaskInfo task : taskTimings.values())
{
final String totalTaskTimeStr = humanReadableFormat(Duration.ofNanos(task.getTotalTaskTime()));
final String avgTaskTimeStr = humanReadableFormat(task.getAverageTaskTime());
final String invocationsStr = nf.format(task.getInvocationCount());

sb.append("| ");
sb.append(adjust(task.getName(), 15)).append(" | ");
sb.append(adjust(totalTaskTimeStr, 16)).append(" | ");
sb.append(adjust(avgTaskTimeStr, 16)).append(" | ");
sb.append(adjust(invocationsStr, 12)).append(" | ");
final double pct = task.getTotalTaskTime() * 100 / getTotalTime().toNanos();
sb.append(adjust(Integer.toString((int) pct), 3)).append("% |");
sb.append("\n");
}
return sb.toString();
final TaskInfo taskInfo = taskInfos.get(task);
return taskInfo != null && taskInfo.isRunning();
}

private String humanReadableFormat(Duration duration) {
return duration.toString()
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.toLowerCase();
}

private String adjust(final String s, final int width)
/**
* See {@link Report#prettyPrint(Chronograph)}
* @return A formatted string with the task details
*/
public String prettyPrint()
{
if (s.length() >= width)
{
return s.substring(0, width);
}

final char[] result = Arrays.copyOf(s.toCharArray(), width);
for (int i = s.length(); i < width; i++)
{
result[i] = ' ';
}
return new String(result);
return Report.prettyPrint(this);
}

private Duration getTotalTime()
public Duration getTotalTime()
{
return Duration.of(taskTimings.values().stream().map(TaskInfo::getTotalTaskTime).reduce(0L, Long::sum), ChronoUnit.NANOS);
return Duration.ofNanos(taskInfos.values().stream().map(TaskInfo::getTotalTaskTime).reduce(0L, Long::sum));
}
}
95 changes: 95 additions & 0 deletions src/main/java/com/ethlo/time/Report.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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;
import java.util.Arrays;

public class Report
{
/**
* Generate a string with a table describing all tasks performed.
* <p>For custom reporting, call {@link Chronograph#getTaskInfo()} and use the task info
* directly.
*/
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");

final NumberFormat pf = NumberFormat.getPercentInstance();
pf.setMinimumFractionDigits(1);
pf.setMaximumFractionDigits(1);
pf.setMinimumIntegerDigits(2);
pf.setGroupingUsed(false);

final NumberFormat nf = NumberFormat.getNumberInstance();
nf.setRoundingMode(RoundingMode.HALF_UP);
nf.setGroupingUsed(true);

for (String name : chronograph.getTaskNames())
{
final TaskInfo task = chronograph.getTaskInfo(name);

final String totalTaskTimeStr = humanReadableFormat(Duration.ofNanos(task.getTotalTaskTime()));
final String avgTaskTimeStr = humanReadableFormat(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(" | ");
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("\n");
}
return sb.toString();
}

private static String humanReadableFormat(Duration duration)
{
return duration.toString()
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.toLowerCase();
}

private static String adjustWidth(final String s, final int width)
{
if (s.length() >= width)
{
return s.substring(0, width);
}

final char[] result = Arrays.copyOf(s.toCharArray(), width);
for (int i = s.length(); i < width; i++)
{
result[i] = ' ';
}
return new String(result);
}
}
48 changes: 30 additions & 18 deletions src/main/java/com/ethlo/time/TaskInfo.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,7 +23,6 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.atomic.AtomicLong;

class TaskInfo
Expand All @@ -34,7 +33,7 @@ class TaskInfo
private final AtomicLong taskStartTimestamp = new AtomicLong();
private volatile boolean running = false;

void started()
void start()
{
if (running)
{
Expand All @@ -46,20 +45,6 @@ void started()
taskStartTimestamp.set(System.nanoTime());
}

void stopped()
{
// The very first operation
final long ts = System.nanoTime();
if (!running)
{
throw new IllegalStateException("Task " + name + " is not started");
}
running = false;

invocationCounts.incrementAndGet();
totalTaskTime.addAndGet(ts - taskStartTimestamp.get());
}

TaskInfo(final String name)
{
this.name = name;
Expand All @@ -82,6 +67,33 @@ public long getTotalTaskTime()

public Duration getAverageTaskTime()
{
final long invocations = getInvocationCount();
if (invocations == 0)
{
return Duration.ZERO;
}

return Duration.ofNanos(BigDecimal.valueOf(getTotalTaskTime()).divide(BigDecimal.valueOf(getInvocationCount()), RoundingMode.HALF_UP).longValue());
}

public boolean isRunning()
{
return running;
}

void stopped(final long ts, boolean ignoreState)
{
if (!running && !ignoreState)
{
throw new IllegalStateException("Task " + name + " is not started");
}

if (running)
{
running = false;

invocationCounts.incrementAndGet();
totalTaskTime.addAndGet(ts - taskStartTimestamp.get());
}
}
}
4 changes: 2 additions & 2 deletions src/test/java/com/ethlo/time/ChronographTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public void startStopSequenceMultipleTasks() throws InterruptedException
final Chronograph chronograph = Chronograph.create();

final String taskName2 = "bar";
for (int i = 1; i <= 1_000_000; i++)
for (int i = 1; i <= 100_000; i++)
{
chronograph.start(taskName);
microsecondTask();
Expand Down Expand Up @@ -112,7 +112,7 @@ public void nullTask()
public void testGranularity()
{
final Chronograph chronograph = Chronograph.create();
for (int i = 0; i < 10_000_000; i++)
for (int i = 0; i < 1_000_000; i++)
{
chronograph.start(taskName);
chronograph.stop(taskName);
Expand Down

0 comments on commit 830d8a0

Please sign in to comment.