Skip to content

Instantly share code, notes, and snippets.

@chutchinson
Last active December 28, 2015 09:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chutchinson/7480046 to your computer and use it in GitHub Desktop.
Save chutchinson/7480046 to your computer and use it in GitHub Desktop.
PerformanceMonitor implementation in Java. Allows sampling of performance characteristics over time. It's possible to define a performance item with a limited number of samples, and a defined sampling rate (only collect sample into buffer every n milliseconds, regardless of number of calls to record method). Performance items can be tagged for f…
import com.google.common.base.Preconditions;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* Performance monitor that can track multiple performance items at any given
* time. Records values with a timestamp and an optional tag for future data
* processing and analysis. This monitor can persist and restore collected data
* using a CSV format.
*
* @author Chris Hutchinson
*/
public class PerformanceMonitor {
/**
* Performance record consisting of a nanosecond-resolution timestamp, float
* value, and an optional tag.
*/
public static class Record {
public final long timestamp;
public final float value;
public final String tag;
protected Record(long timestamp, float value, String tag) {
this.timestamp = timestamp;
this.value = value;
this.tag = (tag != null) ? tag : "";
}
}
/**
* Named performance item that contains an arbitrary number of records and
* also tracks various statistics (e.g. sum, mean, min, max, etc).
*/
public static class Item {
public final String name;
public final String unit;
public final List<Record> records;
public int samples;
public int limit;
public int count;
public long timestamp;
public float sum;
public float minimum;
public float maximum;
public float mean;
public float value;
protected Item(String name, String unit, int samples, int limit) {
this.name = name;
this.unit = unit;
this.records = new ArrayList<Record>();
this.samples = samples;
this.limit = limit;
this.timestamp = System.nanoTime();
}
/**
* Adds a record to this performance item. The item is only added if and
* only if the delta of the record timestamp and the cached timestamp is
* within the configured sampling time range.
*
* @see Item#samples
* @param record
*/
public void add(final Record record) {
if (limit > 0 && this.records.size() >= limit) {
this.records.remove(0);
}
this.records.add(record);
this.count++;
this.value = record.value;
this.sum += record.value;
this.mean = (this.sum / this.count);
if (this.count <= 1) {
this.minimum = record.value;
this.maximum = record.value;
} else {
this.minimum = Math.min(this.minimum, record.value);
this.maximum = Math.max(this.maximum, record.value);
}
this.timestamp = record.timestamp;
}
/**
* Removes all contained records and resets all computed statistics.
*/
public void reset() {
this.records.clear();
this.timestamp = 0;
this.count = 0;
this.sum = 0;
this.minimum = 0;
this.maximum = 0;
this.mean = 0;
}
}
private final Map<String, Item> items;
public PerformanceMonitor() {
this.items = new HashMap<String, Item>();
}
/**
* Begins performance monitoring operations for an item with the specified
* name, no sampling rate limit, and no record limit.
*
* @see PerformanceMonitor#begin(String, int)
* @param name
*/
public void begin(String name, String unit) {
this.begin(name, unit, 0, 0);
}
/**
* Begins performance monitoring operations for an item with the specified
* name and sampling rate. If an item with the specified name is already
* being monitored then this method will have no effect.
*
* @param name item name
* @param samples sampling rate (in ms)
* @param limit maximum number of records
*/
public void begin(String name, String unit, int samples, int limit) {
Preconditions.checkNotNull(name);
Preconditions.checkNotNull(unit);
Preconditions.checkArgument(samples >= 0, "samples must be >= 0");
Preconditions.checkArgument(limit >= 0, "limit must be >= 0");
Preconditions.checkArgument(!name.contains(","), "name must not contain commas");
if (!this.items.containsKey(name)) {
this.items.put(name, new Item(name, unit, samples, limit));
}
}
/**
* Stops performance monitoring for the specified item name and removes all
* collected data and statistics.
*
* @param name item name
*/
public void end(String name) {
this.items.remove(name);
}
/**
* Retrieves the performance item with the specified name. If the item does
* not exist this method will return null.
*
* @param name item name
* @return performance item if exists, otherwise null
*/
public final Item item(String name) {
return this.items.get(name);
}
/**
* Marks the specified performance item's timestamp with the current
* high-resolution tick count.
*
* @param name item name
*/
public final void mark(String name) {
final Item item = this.item(name);
if (item != null) {
item.timestamp = System.nanoTime();
}
}
public final Iterable<Item> items() {
return this.items.values();
}
public void record(String name, float value) {
this.record(name, value, null);
}
public void record(String name, float value, String tag) {
this.record(name, System.nanoTime(), value, tag);
}
public void record(String name, long timestamp, float value, String tag) {
if (this.items.containsKey(name)) {
this.record(this.items.get(name), timestamp, value, tag);
}
}
private void record(Item item, long timestamp, float value, String tag) {
if (timestamp - item.timestamp >= item.samples) {
item.add(new Record(timestamp, value, tag));
}
}
public long recordElapsed(String name) {
return this.recordElapsed(name, TimeUnit.MILLISECONDS);
}
public long recordElapsed(String name, TimeUnit unit) {
final Item item = this.item(name);
long elapsed = 0;
if (item != null) {
elapsed = this.elapsed(item.timestamp, unit);
this.record(name, elapsed);
}
return elapsed;
}
/**
* Removes all performance items from this monitor.
*/
public void clear() {
this.items.clear();
}
/**
* Removes all collected records and statistics for all items in this
* performance monitor, but does not remove the performance item from the
* monitor.
*/
public void reset() {
for (final Item item : this.items.values()) {
item.reset();
}
}
/**
* Loads a comma-separated values (CSV) file created by a performance
* monitor into this performance monitor. The data file must include a
* header line.
*
* @see PerformanceMonitor#save
* @param file file to load
* @throws IOException if an I/O error occurs
*/
public void load(File file) throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(
new FileInputStream(file)));
boolean reading = (reader.readLine() != null);
while (reading) {
final String line = reader.readLine();
if (line != null) {
final String[] data = line.split(",");
if (data.length == 5) {
this.begin(data[0], data[1]);
this.record(data[0], Long.parseLong(data[2]),
Float.parseFloat(data[3]), data[4]);
}
} else {
reading = false;
}
}
} finally {
if (reader != null) {
reader.close();
}
}
}
/**
* Saves all performance items currently stored in this performance monitor
* to the specified file in a comma-separated values file format with the
* following specifications:
* <ul>
* <li>records terminated by \r\n</li>
* <li>records delimited by ,</li>
* <li>record length of 4 columns</li>
* <li>header</li>
* </ul>
*
* @param path
* @param append
* @throws IOException
*/
public void save(File path, boolean append) throws IOException {
if (!path.isDirectory()) {
throw new IllegalArgumentException("path must be a directory!");
}
final String nl = "\r\n";
for (final Item item : this.items.values()) {
File file = new File(path, String.format("%s_%s.csv", item.name, item.unit));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file, append)));
try {
writer.write("timestamp, value, tag");
writer.write(nl);
if (item.count > 0) {
for (final Record record : item.records) {
writer.write(record.timestamp + "," + record.value + "," + record.tag);
writer.write(nl);
}
}
} finally {
writer.close();
}
}
}
private long elapsed(long time, TimeUnit unit) {
long delta = System.nanoTime() - time;
switch (unit) {
case NANOSECONDS:
return (delta);
case MICROSECONDS:
return (delta / 1000);
case MILLISECONDS:
return (delta / 1000000);
case SECONDS:
return (delta / 1000000 / 1000);
case MINUTES:
return (delta / 1000000 / 1000 / 60);
case HOURS:
return (delta / 1000000 / 1000 / 60 / 60);
case DAYS:
return (delta / 1000000 / 1000 / 60 / 60 / 24);
}
return 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment