Last active
December 28, 2015 09:39
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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