// Copyright 2012 Georg-August-Universität Göttingen, Germany
//
// 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.
package de.ugoe.cs.util;
import java.io.PrintStream;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
*
* This is a simple implementation for a stop watch that can be used for performance measures.
* A stop watch can be used to measure several splits. Each split is started and stopped using the
* same id provided to the {@link #start(String)} and {@link #stop(String)} methods. The measured
* durations can be retrieved afterwards using {@link #getDuration(String)}.
* {@link #dumpStatistics(PrintStream)} is a convenience method useful to effectively dump all
* information for the different splits.
*
*
* @author Patrick Harms
*/
public class StopWatch implements Serializable{
/**
*
*/
private static final long serialVersionUID = -4216393284789336830L;
/**
* the splits hold internally
*/
private Map mSplits = new HashMap();
/**
* starts a split with the given id. If the split with the id is already started, an
* {@link IllegalStateException} is thrown.
*
* @param id the id of the split to be started
*
* @throws IllegalStateException if the split is already started
*/
public void start(String id) throws IllegalStateException {
Split split = mSplits.get(id);
if (split == null) {
split = new Split(id);
mSplits.put(id, split);
}
split.start();
}
/**
* stops a split with the given id. If the split with the id is already stopped, an
* {@link IllegalStateException} is thrown. If no split with the given id exists, an
* {@link IllegalArgumentException} is thrown.
*
* @param id the id of the split to be stopped
*
* @throws IllegalStateException if the split is not running
* @throws IllegalArgumentException if the split with the given id does not exist
*/
public void stop(String id) throws IllegalStateException, IllegalArgumentException {
Split split = mSplits.get(id);
if (split == null) {
throw new IllegalArgumentException("split with id " + id + " does not exist");
}
split.stop();
}
/**
* returns the duration of a split with the given id. If the split with the id is currently
* running, it is stopped. If no split with the given id exists, an
* {@link IllegalArgumentException} is thrown.
*
* @param id the id of the split for which the duration shall be returned
*
* @return the duration measured for the split
*
* @throws IllegalArgumentException if the split with the given id does not exist
*/
public long getDuration(String id) throws IllegalArgumentException {
Split split = mSplits.get(id);
if (split == null) {
throw new IllegalArgumentException("split with id " + id + " does not exist");
}
if (split.isRunning()) {
split.stop();
}
return split.getDuration();
}
/**
* resets the stop watch and clears all splits
*/
public void reset() {
mSplits.clear();
}
/**
* dumps the statistics about the splits. Splits still running are stopped. The method checks
* if the longest split also covers the other splits. If so, it considers this split as a
* kind of overall duration and dumps the proportion of all other splits to this split in
* percentage.
*
* @param out the stream to dump the statistics to
*/
public void dumpStatistics(PrintStream out) {
if (mSplits.size() <= 0) {
throw new IllegalStateException("no splits registered that could be dumped");
}
Map durations = new HashMap();
// get durations
for (String id : mSplits.keySet()) {
durations.put(id, getDuration(id));
}
// sort by duration
List sortedIds = new LinkedList();
int maxIdLength = 0;
for (Map.Entry entry : durations.entrySet()) {
boolean added = false;
for (int i = 0; i < sortedIds.size(); i++) {
if (durations.get(sortedIds.get(i)) >= entry.getValue()) {
sortedIds.add(i, entry.getKey());
added = true;
break;
}
}
if (!added) {
sortedIds.add(entry.getKey());
}
maxIdLength = Math.max(maxIdLength, entry.getKey().length());
}
// get longest duration and check whether it spans all other entries
String id = sortedIds.get(sortedIds.size() - 1);
Split longestWatch = mSplits.get(id);
boolean longestDurationCoversOthers = true;
for (Map.Entry watch : mSplits.entrySet()) {
if ((watch.getValue().getFirstStart() < longestWatch.getFirstStart()) ||
(watch.getValue().getLastStop() > longestWatch.getLastStop()))
{
longestDurationCoversOthers = false;
break;
}
}
// no finally start the dumping
out.println();
out.println("Watch Statistics");
out.println("================");
for (String sortedId : sortedIds) {
out.print(sortedId);
for (int i = sortedId.length(); i <= maxIdLength; i++) {
out.print(' ');
}
out.print(": ");
out.print(durations.get(sortedId));
out.print(" ms");
out.print(" (");
out.print(mSplits.get(sortedId).getNoOfStarts());
out.print(" starts");
//out.print(", ");
//out.print(1000 * durations.get(sortedId) / mSplits.get(sortedId).getNoOfStarts());
//out.print(" ms per 1000 starts");
if (longestDurationCoversOthers) {
out.print(", ");
out.print(DecimalFormat.getPercentInstance().format
((double) durations.get(sortedId) / longestWatch.getDuration()));
out.print(" of overall duration");
}
out.println(')');
}
out.println();
}
/**
* internally used to store splits
*/
private static class Split implements Serializable {
/**
*
*/
private static final long serialVersionUID = 7767677492954506604L;
/**
* the id of the split
*/
private String id;
/**
* the system time of the first start of the split
*/
private long firstStart = -1;
/**
* the system time of the last start of the split
*/
private long lastStart = -1;
/**
* the system time of the last stop of the split
*/
private long lastStop = -1;
/**
* the duration so far for the split (excluding the time since the last start)
*/
private long duration = 0;
/**
* the number of starts or the splits
*/
private long noOfStarts = 0;
/**
* initializes the split with its id
*/
private Split(String id) {
this.id = id;
}
/**
* starts the split if it is not already started
*/
private void start() throws IllegalStateException {
if (lastStart > -1) {
throw new IllegalStateException("split with id " + id + " already running");
}
lastStart = System.currentTimeMillis();
if (firstStart < 0) {
firstStart = lastStart;
}
noOfStarts++;
}
/**
* checks if the split is currently running
*/
private boolean isRunning() {
return (lastStart > -1);
}
/**
* stops the split if it is not already stopped
*/
private void stop() throws IllegalStateException {
if (lastStart < 0) {
throw new IllegalStateException("split with id " + id + " not running");
}
lastStop = System.currentTimeMillis();
duration += lastStop - lastStart;
lastStart = -1;
}
/**
* returns the fist start of the split
*/
private long getFirstStart() {
return firstStart;
}
/**
* returns the last stop of the split
*/
private long getLastStop() {
return lastStop;
}
/**
* returns the duration of the split measured so far excluding the time since the last
* start if the split is currently started
*/
private long getDuration() {
return duration;
}
/**
* returns the number of starts for the split
*/
private long getNoOfStarts() {
return noOfStarts;
}
}
}