diff --git a/apps/netmonitor/java/build.xml b/apps/netmonitor/java/build.xml
new file mode 100644
index 000000000..2f4023f49
--- /dev/null
+++ b/apps/netmonitor/java/build.xml
@@ -0,0 +1,77 @@
+
+
NetMonitor [configFilename]
+ *
+ *
+ *
+ */
+public class NetMonitor {
+ private static final Log _log = new Log(NetMonitor.class);
+ public static final String CONFIG_LOCATION_DEFAULT = "netmonitor.config";
+ public static final String HARVEST_DELAY_PROP = "harvestDelaySeconds";
+ public static final int HARVEST_DELAY_DEFAULT = 60;
+ public static final String EXPORT_DELAY_PROP = "exportDelaySeconds";
+ public static final int EXPORT_DELAY_DEFAULT = 120;
+ public static final String SUMMARY_DURATION_PROP = "summaryDurationHours";
+ public static final int SUMMARY_DURATION_DEFAULT = 72;
+ public static final String NETDB_DIR_PROP = "netDbDir";
+ public static final String NETDB_DIR_DEFAULT = "netDb";
+ public static final String EXPORT_DIR_PROP = "exportDir";
+ public static final String EXPORT_DIR_DEFAULT = "monitorData";
+ private String _configLocation;
+ private int _harvestDelay;
+ private int _exportDelay;
+ private String _exportDir;
+ private String _netDbDir;
+ private int _summaryDurationHours;
+ private boolean _isRunning;
+ private Map _peerSummaries;
+
+ public NetMonitor() {
+ this(CONFIG_LOCATION_DEFAULT);
+ }
+ public NetMonitor(String configLocation) {
+ _configLocation = configLocation;
+ _peerSummaries = new HashMap(32);
+ loadConfig();
+ }
+
+ /** read and call parse */
+ private void loadConfig() {
+ Properties props = new Properties();
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(_configLocation);
+ props.load(fis);
+ } catch (IOException ioe) {
+ _log.warn("Error loading the net monitor config", ioe);
+ } finally {
+ if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+ }
+ parseConfig(props);
+ }
+
+ /** interpret the config elements and shove 'em in the vars */
+ private void parseConfig(Properties props) {
+ String val = props.getProperty(HARVEST_DELAY_PROP, ""+HARVEST_DELAY_DEFAULT);
+ try {
+ _harvestDelay = Integer.parseInt(val);
+ } catch (NumberFormatException nfe) {
+ _log.warn("Error parsing the harvest delay [" + val + "]", nfe);
+ _harvestDelay = HARVEST_DELAY_DEFAULT;
+ }
+
+ val = props.getProperty(EXPORT_DELAY_PROP, ""+EXPORT_DELAY_DEFAULT);
+ try {
+ _exportDelay = Integer.parseInt(val);
+ } catch (NumberFormatException nfe) {
+ _log.warn("Error parsing the export delay [" + val + "]", nfe);
+ _exportDelay = EXPORT_DELAY_DEFAULT;
+ }
+
+ val = props.getProperty(SUMMARY_DURATION_PROP, ""+SUMMARY_DURATION_DEFAULT);
+ try {
+ _summaryDurationHours = Integer.parseInt(val);
+ } catch (NumberFormatException nfe) {
+ _log.warn("Error parsing the summary duration [" + val + "]", nfe);
+ _summaryDurationHours = SUMMARY_DURATION_DEFAULT;
+ }
+
+ _netDbDir = props.getProperty(NETDB_DIR_PROP, NETDB_DIR_DEFAULT);
+ _exportDir = props.getProperty(EXPORT_DIR_PROP, EXPORT_DIR_DEFAULT);
+ }
+
+ public void startMonitor() {
+ _isRunning = true;
+ I2PThread t = new I2PThread(new NetMonitorRunner(this));
+ t.setName("DataHarvester");
+ t.setPriority(I2PThread.MIN_PRIORITY);
+ t.setDaemon(false);
+ t.start();
+ }
+
+ public void stopMonitor() { _isRunning = false; }
+ public boolean isRunning() { return _isRunning; }
+ /** how many seconds should we wait between harvestings? */
+ public int getHarvestDelay() { return _harvestDelay; }
+ /** how many seconds should we wait between exporting the data? */
+ public int getExportDelay() { return _exportDelay; }
+ /** where should we export the data? */
+ public String getExportDir() { return _exportDir; }
+ public void setExportDir(String dir) { _exportDir = dir; }
+ public int getSummaryDurationHours() { return _summaryDurationHours; }
+ /** where should we read the data from? */
+ public String getNetDbDir() { return _netDbDir; }
+ /**
+ * what peers are we keeping track of?
+ *
+ * @return list of peer names (H(routerIdentity).toBase64())
+ */
+ public List getPeers() {
+ synchronized (_peerSummaries) {
+ return new ArrayList(_peerSummaries.keySet());
+ }
+ }
+
+ /** what data do we have for the peer? */
+ public PeerSummary getSummary(String peer) {
+ synchronized (_peerSummaries) {
+ return (PeerSummary)_peerSummaries.get(peer);
+ }
+ }
+
+ /** keep track of the given stat on the given peer */
+ public void addData(String peer, String stat, String descr, String valDescr[], long sampleDate, double val[]) {
+ synchronized (_peerSummaries) {
+ if (!_peerSummaries.containsKey(peer))
+ _peerSummaries.put(peer, new PeerSummary(peer));
+ PeerSummary summary = (PeerSummary)_peerSummaries.get(peer);
+ summary.addData(stat, descr, valDescr, sampleDate, val);
+ }
+ }
+
+ /** keep track of the given stat on the given peer */
+ public void addData(String peer, String stat, String descr, String valDescr[], long sampleDate, long val[]) {
+ synchronized (_peerSummaries) {
+ if (!_peerSummaries.containsKey(peer))
+ _peerSummaries.put(peer, new PeerSummary(peer));
+ PeerSummary summary = (PeerSummary)_peerSummaries.get(peer);
+ summary.addData(stat, descr, valDescr, sampleDate, val);
+ }
+ }
+
+ /** keep track of the loaded summary, overwriting any existing summary for the specified peer */
+ public void addSummary(PeerSummary summary) {
+ synchronized (_peerSummaries) {
+ Object rv = _peerSummaries.put(summary.getPeer(), summary);
+ if (rv != summary) _log.error("Updating the peer summary changed objects! old = " + rv + " new = " + summary);
+ }
+ }
+
+ public void importData() {
+ _log.debug("Running import");
+ File dataDir = new File(getExportDir());
+ if (!dataDir.exists()) return;
+ File dataFiles[] = dataDir.listFiles(new FilenameFilter() {
+ public boolean accept(File f, String name) {
+ return name.endsWith(".txt");
+ }
+ });
+ if (dataFiles == null) return;
+ for (int i = 0; i < dataFiles.length; i++) {
+ FileInputStream fis = null;
+ boolean delete = false;
+ try {
+ fis = new FileInputStream(dataFiles[i]);
+ PeerSummaryReader.getInstance().read(this, fis);
+ } catch (IOException ioe) {
+ _log.error("Error reading the data file " + dataFiles[i].getAbsolutePath(), ioe);
+ delete = true;
+ } finally {
+ if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+ if (delete) dataFiles[i].delete();
+ }
+ }
+ _log.debug(dataFiles.length + " summaries imported");
+ }
+
+ /** drop all the old summary data */
+ public void coallesceData() {
+ synchronized (_peerSummaries) {
+ for (Iterator iter = _peerSummaries.values().iterator(); iter.hasNext(); ) {
+ PeerSummary summary = (PeerSummary)iter.next();
+ summary.coallesceData(_summaryDurationHours * 60*60*1000);
+ }
+ }
+ }
+
+ public static final void main(String args[]) {
+ if (args.length == 1)
+ new NetMonitor(args[0]).startMonitor();
+ else
+ new NetMonitor(CONFIG_LOCATION_DEFAULT).startMonitor();
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java
new file mode 100644
index 000000000..5995270ad
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/NetMonitorRunner.java
@@ -0,0 +1,145 @@
+package net.i2p.netmonitor;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import java.io.IOException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+
+import net.i2p.util.Clock;
+import net.i2p.util.Log;
+import net.i2p.data.DataFormatException;
+import net.i2p.data.RouterInfo;
+
+/**
+ * Active process that drives the monitoring by periodically rading the
+ * netDb dir, pumping the router data throug the data harvester, updating
+ * the state, and coordinating the export.
+ *
+ */
+class NetMonitorRunner implements Runnable {
+ private static final Log _log = new Log(NetMonitorRunner.class);
+ private NetMonitor _monitor;
+ /**
+ * @param monitor who do we give our data to?
+ */
+ public NetMonitorRunner(NetMonitor monitor) {
+ _monitor = monitor;
+ }
+
+ public void run() {
+ runImport();
+ long now = Clock.getInstance().now();
+ long nextHarvest = now;
+ long nextExport = now + _monitor.getExportDelay() * 1000;
+ while (_monitor.isRunning()) {
+ now = Clock.getInstance().now();
+ _monitor.coallesceData();
+ if (now >= nextHarvest) {
+ runHarvest();
+ nextHarvest = now + _monitor.getHarvestDelay() * 1000;
+ }
+ if (now >= nextExport) {
+ runExport();
+ nextExport = now + _monitor.getExportDelay() * 1000;
+ }
+ pauseHarvesting();
+ }
+ }
+
+ private void runHarvest() {
+ List routers = getRouters();
+ DataHarvester.getInstance().harvestData(_monitor, routers);
+ }
+
+ /**
+ * Fetch all of the available RouterInfo structures
+ *
+ */
+ private List getRouters() {
+ File routers[] = listRouters();
+ List rv = new ArrayList(64);
+ if (routers != null) {
+ for (int i = 0; i < routers.length; i++) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(routers[i]);
+ RouterInfo ri = new RouterInfo();
+ ri.readBytes(fis);
+ rv.add(ri);
+ } catch (DataFormatException dfe) {
+ _log.warn("Unable to parse the routerInfo from " + routers[i].getAbsolutePath(), dfe);
+ } catch (IOException ioe) {
+ _log.warn("Unable to read the routerInfo from " + routers[i].getAbsolutePath(), ioe);
+ } finally {
+ if (fis != null) try { fis.close(); } catch (IOException ioe) {}
+ }
+ }
+ }
+ return rv;
+ }
+
+ /**
+ * dump the data to the filesystem
+ */
+ private void runExport() {
+ _log.info("Export");
+ List peers = _monitor.getPeers();
+ File exportDir = new File(_monitor.getExportDir());
+ if (!exportDir.exists())
+ exportDir.mkdirs();
+ for (int i = 0; i < peers.size(); i++) {
+ String peerName = (String)peers.get(i);
+ PeerSummary summary = (PeerSummary)_monitor.getSummary(peerName);
+ FileOutputStream fos = null;
+ try {
+ File summaryFile = new File(exportDir, peerName + ".txt");
+ fos = new FileOutputStream(summaryFile);
+ PeerSummaryWriter.getInstance().write(summary, fos);
+ _log.debug("Peer summary written to " + summaryFile.getAbsolutePath());
+ } catch (IOException ioe) {
+ _log.error("Error exporting the peer summary for " + peerName, ioe);
+ } finally {
+ if (fos != null) try { fos.close(); } catch (IOException ioe) {}
+ }
+ }
+ }
+
+ /**
+ * Read in all the peer summaries we had previously exported, overwriting any
+ * existing ones in memory
+ *
+ */
+ private void runImport() {
+ _monitor.importData();
+ }
+
+ /**
+ * Find all of the routers to load
+ *
+ * @return list of File objects pointing at the routers around
+ */
+ private File[] listRouters() {
+ File dbDir = new File(_monitor.getNetDbDir());
+ File files[] = dbDir.listFiles(new FilenameFilter() {
+ public boolean accept(File f, String name) {
+ return name.startsWith("routerInfo-");
+ }
+ });
+ return files;
+ }
+
+ /**
+ * Wait the correct amount of time before harvesting again
+ *
+ */
+ private void pauseHarvesting() {
+ try {
+ Thread.sleep(_monitor.getHarvestDelay());
+ } catch (InterruptedException ie) {}
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java
new file mode 100644
index 000000000..d19e31dd2
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerStat.java
@@ -0,0 +1,46 @@
+package net.i2p.netmonitor;
+
+/**
+ * Actual data point (though its not fully normalized, but KISS)
+ *
+ */
+public class PeerStat {
+ private String _statName;
+ private String _description;
+ private String _valueDescriptions[];
+ private long _sampleDate;
+ private long _lvalues[];
+ private double _dvalues[];
+
+ public PeerStat(String name, String description, String valueDescriptions[], long sampleDate, double values[]) {
+ this(name, description, valueDescriptions, sampleDate, null, values);
+ }
+ public PeerStat(String name, String description, String valueDescriptions[], long sampleDate, long values[]) {
+ this(name, description, valueDescriptions, sampleDate, values, null);
+ }
+ private PeerStat(String name, String description, String valueDescriptions[], long sampleDate, long lvalues[], double dvalues[]) {
+ _statName = name;
+ _description = description;
+ _valueDescriptions = valueDescriptions;
+ _sampleDate = sampleDate;
+ _lvalues = lvalues;
+ _dvalues = dvalues;
+ }
+
+ /** unique name of the stat */
+ public String getStatName() { return _statName; }
+ /** one line summary of the stat */
+ public String getDescription() { return _description; }
+ /** description of each value */
+ public String getValueDescription(int index) { return _valueDescriptions[index]; }
+ /** description of all values */
+ public String[] getValueDescriptions() { return _valueDescriptions; }
+ /** when did the router publish the info being sampled? */
+ public long getSampleDate() { return _sampleDate; }
+ /** if the values are integers, this contains their values */
+ public long[] getLongValues() { return _lvalues; }
+ /** if the values are floating point numbers, this contains their values */
+ public double[] getDoubleValues() { return _dvalues; }
+ /** are the values floating poing numbers? */
+ public boolean getIsDouble() { return _dvalues != null; }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java
new file mode 100644
index 000000000..8f3493cda
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummary.java
@@ -0,0 +1,119 @@
+package net.i2p.netmonitor;
+
+import net.i2p.util.Clock;
+import net.i2p.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.Set;
+
+/**
+ * coordinate the data points summarizing the performance of a particular peer
+ * within the network
+ */
+public class PeerSummary {
+ private static final Log _log = new Log(PeerSummary.class);
+ private String _peer;
+ /** statName to a List of PeerStat elements (sorted by sample date, earliest first) */
+ private Map _stats;
+ /** lock on this when accessing stat data */
+ private Object _coallesceLock = new Object();
+
+ public PeerSummary(String peer) {
+ _peer = peer;
+ _stats = new HashMap(16);
+ }
+
+ /**
+ * Track a data point
+ *
+ * @param stat what data are we tracking?
+ * @param description what does this data mean? (and what are the values?)
+ * @param when what data set is this sample based off?
+ * @param val actual data harvested
+ */
+ public void addData(String stat, String description, String valueDescriptions[], long when, double val[]) {
+ synchronized (_coallesceLock) {
+ TreeMap stats = locked_getData(stat);
+ stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val));
+ }
+ }
+
+ /**
+ * Track a data point
+ *
+ * @param stat what data are we tracking?
+ * @param description what does this data mean? (and what are the values?)
+ * @param when what data set is this sample based off?
+ * @param val actual data harvested
+ */
+ public void addData(String stat, String description, String valueDescriptions[], long when, long val[]) {
+ synchronized (_coallesceLock) {
+ TreeMap stats = locked_getData(stat);
+ stats.put(new Long(when), new PeerStat(stat, description, valueDescriptions, when, val));
+ }
+ }
+
+ /** get the peer's name (H(routerIdentity).toBase64()) */
+ public String getPeer() { return _peer; }
+
+ /**
+ * fetch the ordered list of PeerStat objects for the given stat (or null if
+ * isn't being tracked or has no data)
+ *
+ */
+ public List getData(String statName) {
+ synchronized (_coallesceLock) {
+ return new ArrayList(((TreeMap)_stats.get(statName)).values());
+ }
+ }
+
+ /**
+ * Get the names of all of the stats that are being tracked
+ *
+ */
+ public Set getStatNames() {
+ synchronized (_coallesceLock) {
+ return new HashSet(_stats.keySet());
+ }
+ }
+
+ /** drop old data points */
+ public void coallesceData(long summaryDurationMs) {
+ long earliest = Clock.getInstance().now() - summaryDurationMs;
+ synchronized (_coallesceLock) {
+ locked_coallesce(earliest);
+ }
+ }
+
+ /** go through all the stats and remove ones from before the given date */
+ private void locked_coallesce(long earliestSampleDate) {
+ if (true) return;
+ for (Iterator iter = _stats.keySet().iterator(); iter.hasNext(); ) {
+ String statName = (String)iter.next();
+ TreeMap stats = (TreeMap)_stats.get(statName);
+ while (stats.size() > 0) {
+ Long when = (Long)stats.keySet().iterator().next();
+ if (when.longValue() < earliestSampleDate) {
+ stats.remove(when);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * @return PeerStat elements, ordered by sample date (earliest first)
+ */
+ private TreeMap locked_getData(String statName) {
+ if (!_stats.containsKey(statName))
+ _stats.put(statName, new TreeMap());
+ return (TreeMap)_stats.get(statName);
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java
new file mode 100644
index 000000000..d1cf2042a
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryReader.java
@@ -0,0 +1,103 @@
+package net.i2p.netmonitor;
+
+import net.i2p.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.TreeSet;
+import java.util.Set;
+import java.util.Locale;
+import java.util.Date;
+import java.util.StringTokenizer;
+import java.text.SimpleDateFormat;
+import java.text.ParseException;
+
+/**
+ * Load up the peer summary
+ *
+ */
+class PeerSummaryReader {
+ private static final Log _log = new Log(PeerSummaryReader.class);
+ private static final PeerSummaryReader _instance = new PeerSummaryReader();
+ public static final PeerSummaryReader getInstance() { return _instance; }
+ private PeerSummaryReader() {}
+
+ /** */
+ public void read(NetMonitor monitor, InputStream in) throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ String line = null;
+ PeerSummary summary = null;
+ String curDescription = null;
+ List curArgs = null;
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith("peer\t")) {
+ String name = line.substring("peer\t".length()).trim();
+ summary = monitor.getSummary(name);
+ if (summary == null)
+ summary = new PeerSummary(name);
+ } else if (line.startsWith("## ")) {
+ curDescription = line.substring("## ".length()).trim();
+ curArgs = new ArrayList(4);
+ } else if (line.startsWith("# param ")) {
+ int start = line.indexOf(':');
+ String arg = line.substring(start+1).trim();
+ curArgs.add(arg);
+ } else {
+ StringTokenizer tok = new StringTokenizer(line);
+ String name = tok.nextToken();
+ try {
+ long when = getTime(tok.nextToken());
+ boolean isDouble = false;
+ List argVals = new ArrayList(curArgs.size());
+ while (tok.hasMoreTokens()) {
+ String val = (String)tok.nextToken();
+ if (val.indexOf('.') >= 0) {
+ argVals.add(new Double(val));
+ isDouble = true;
+ } else {
+ argVals.add(new Long(val));
+ }
+ }
+ String valDescriptions[] = new String[curArgs.size()];
+ for (int i = 0; i < curArgs.size(); i++)
+ valDescriptions[i] = (String)curArgs.get(i);
+ if (isDouble) {
+ double values[] = new double[argVals.size()];
+ for (int i = 0; i < argVals.size(); i++)
+ values[i] = ((Double)argVals.get(i)).doubleValue();
+ summary.addData(name, curDescription, valDescriptions, when, values);
+ } else {
+ long values[] = new long[argVals.size()];
+ for (int i = 0; i < argVals.size(); i++)
+ values[i] = ((Long)argVals.get(i)).longValue();
+ summary.addData(name, curDescription, valDescriptions, when, values);
+ }
+ } catch (ParseException pe) {
+ _log.error("Error parsing the data line [" + line + "]", pe);
+ } catch (NumberFormatException nfe) {
+ _log.error("Error parsing the data line [" + line + "]", nfe);
+ }
+ }
+ }
+
+ summary.coallesceData(monitor.getSummaryDurationHours() * 60*60*1000);
+ monitor.addSummary(summary);
+ }
+
+ private static final SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd.HH:mm:ss.SSS", Locale.UK);
+
+ /**
+ * Converts a time (long) to text
+ * @param when the time to convert
+ * @return the textual representation
+ */
+ public long getTime(String when) throws ParseException {
+ synchronized (_fmt) {
+ return _fmt.parse(when).getTime();
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java
new file mode 100644
index 000000000..be9b9bbbe
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/PeerSummaryWriter.java
@@ -0,0 +1,80 @@
+package net.i2p.netmonitor;
+
+import net.i2p.util.Log;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.Set;
+import java.util.Locale;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+
+/**
+ * Dump various peer summaries to disk (so external apps (or good ol' vi) can
+ * peek into what we're harvesting
+ *
+ */
+class PeerSummaryWriter {
+ private static final Log _log = new Log(PeerSummaryWriter.class);
+ private static final PeerSummaryWriter _instance = new PeerSummaryWriter();
+ public static final PeerSummaryWriter getInstance() { return _instance; }
+ private PeerSummaryWriter() {}
+
+ /** write out the peer summary to the stream specified */
+ public void write(PeerSummary summary, OutputStream out) throws IOException {
+ StringBuffer buf = new StringBuffer(4*1024);
+ buf.append("peer\t").append(summary.getPeer()).append('\n');
+ TreeSet names = new TreeSet(summary.getStatNames());
+ for (Iterator iter = names.iterator(); iter.hasNext(); ) {
+ String statName = (String)iter.next();
+ List stats = summary.getData(statName);
+ for (int i = 0; i < stats.size(); i++) {
+ PeerStat stat = (PeerStat)stats.get(i);
+ if (i == 0) {
+ buf.append("## ").append(stat.getDescription()).append('\n');
+ String descr[] = stat.getValueDescriptions();
+ if (descr != null) {
+ for (int j = 0; j < descr.length; j++)
+ buf.append("# param ").append(j).append(": ").append(descr[j]).append('\n');
+ }
+ }
+ buf.append(statName).append('\t');
+ buf.append(getTime(stat.getSampleDate())).append('\t');
+
+ if (stat.getIsDouble()) {
+ double vals[] = stat.getDoubleValues();
+ if (vals != null) {
+ for (int j = 0; j < vals.length; j++)
+ buf.append(vals[j]).append('\t');
+ }
+ } else {
+ long vals[] = stat.getLongValues();
+ if (vals != null) {
+ for (int j = 0; j < vals.length; j++)
+ buf.append(vals[j]).append('\t');
+ }
+ }
+ buf.append('\n');
+ }
+ }
+ String data = buf.toString();
+ if (_log.shouldLog(Log.DEBUG))
+ _log.debug("Stat: \n" + data);
+ out.write(data.getBytes());
+ }
+
+ private static final SimpleDateFormat _fmt = new SimpleDateFormat("yyyyMMdd.HH:mm:ss.SSS", Locale.UK);
+
+ /**
+ * Converts a time (long) to text
+ * @param when the time to convert
+ * @return the textual representation
+ */
+ public String getTime(long when) {
+ synchronized (_fmt) {
+ return _fmt.format(new Date(when));
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java
new file mode 100644
index 000000000..a9fbebebb
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartAdapter.java
@@ -0,0 +1,113 @@
+package net.i2p.netmonitor.gui;
+
+import net.i2p.netmonitor.PeerSummary;
+import net.i2p.netmonitor.PeerStat;
+import net.i2p.util.Log;
+
+import org.jfree.data.XYSeries;
+import org.jfree.data.XYSeriesCollection;
+import org.jfree.data.MovingAverage;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.plot.Plot;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.axis.DateAxis;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.renderer.XYLineAndShapeRenderer;
+import org.jfree.chart.renderer.XYItemRenderer;
+import org.jfree.chart.renderer.XYDotRenderer;
+import java.util.List;
+import java.util.Set;
+import java.util.Iterator;
+
+import javax.swing.JPanel;
+import java.awt.Font;
+import java.awt.Color;
+
+class JFreeChartAdapter {
+ private final static Log _log = new Log(JFreeChartAdapter.class);
+ private final static Color WHITE = new Color(255, 255, 255);
+
+ ChartPanel createPanel(NetViewer state) {
+ ChartPanel panel = new ChartPanel(createChart(state));
+ panel.setDisplayToolTips(true);
+ panel.setEnforceFileExtensions(true);
+ panel.setHorizontalZoom(true);
+ panel.setVerticalZoom(true);
+ panel.setMouseZoomable(true, true);
+ panel.getChart().setBackgroundPaint(WHITE);
+ return panel;
+ }
+
+ JFreeChart createChart(NetViewer state) {
+ Plot plot = createPlot(state);
+ JFreeChart chart = new JFreeChart("I2P network performance", Font.getFont("arial"), plot, true);
+ return chart;
+ }
+
+ void updateChart(ChartPanel panel, NetViewer state) {
+ XYPlot plot = (XYPlot)panel.getChart().getPlot();
+ plot.setDataset(getCollection(state));
+ }
+
+ Plot createPlot(NetViewer state) {
+ XYItemRenderer renderer = new XYLineAndShapeRenderer(); // new XYDotRenderer(); //
+ XYPlot plot = new XYPlot(getCollection(state), new DateAxis(), new NumberAxis("val"), renderer);
+ return plot;
+ }
+
+ XYSeriesCollection getCollection(NetViewer state) {
+ XYSeriesCollection col = new XYSeriesCollection();
+ if (state != null) {
+ List names = state.getConfigNames();
+ for (int i = 0; i < names.size(); i++) {
+ addPeer(col, state.getConfig((String)names.get(i)));
+ }
+ } else {
+ XYSeries series = new XYSeries("latency", false, false);
+ series.add(System.currentTimeMillis(), 0);
+ col.addSeries(series);
+ }
+ return col;
+ }
+
+ void addPeer(XYSeriesCollection col, PeerPlotConfig config) {
+ Set statNames = config.getSummary().getStatNames();
+ for (Iterator iter = statNames.iterator(); iter.hasNext(); ) {
+ String statName = (String)iter.next();
+ List statData = config.getSummary().getData(statName);
+ if (statData.size() > 0) {
+ PeerStat data = (PeerStat)statData.get(0);
+ String args[] = data.getValueDescriptions();
+ if (args != null) {
+ for (int i = 0; i < args.length; i++) {
+ if (config.getSeriesConfig().getShouldPlotValue(statName, args[i], false))
+ addLine(col, config.getPeerName(), statName, args[i], statData);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ */
+ void addLine(XYSeriesCollection col, String peer, String statName, String argName, List dataPoints) {
+ XYSeries series = new XYSeries(peer.substring(0, 4) + ": " + argName, false, false);
+ for (int i = 0; i < dataPoints.size(); i++) {
+ PeerStat data = (PeerStat)dataPoints.get(i);
+ String argNames[] = data.getValueDescriptions();
+ int argIndex = -1;
+ for (int j = 0; j < argNames.length; j++) {
+ if (argNames[j].equals(argName)) {
+ argIndex = j;
+ break;
+ }
+ }
+ if (data.getIsDouble())
+ series.add(data.getSampleDate(), data.getDoubleValues()[argIndex]);
+ else
+ series.add(data.getSampleDate(), data.getLongValues()[argIndex]);
+ }
+ col.addSeries(series);
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java
new file mode 100644
index 000000000..bfe864ac8
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/JFreeChartHeartbeatPlotPane.java
@@ -0,0 +1,58 @@
+package net.i2p.netmonitor.gui;
+
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import javax.swing.JScrollPane;
+import javax.swing.JLabel;
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import net.i2p.util.Log;
+
+import org.jfree.chart.ChartPanel;
+
+/**
+ * Render the graph and legend
+ *
+ */
+class JFreeChartHeartbeatPlotPane extends NetViewerPlotPane {
+ private final static Log _log = new Log(JFreeChartHeartbeatPlotPane.class);
+ private ChartPanel _panel;
+ private JFreeChartAdapter _adapter;
+
+ public JFreeChartHeartbeatPlotPane(NetViewerGUI gui) {
+ super(gui);
+ }
+
+ public void stateUpdated() {
+ if (_panel == null) {
+ remove(0); // remove the dummy
+
+ _adapter = new JFreeChartAdapter();
+ _panel = _adapter.createPanel(_gui.getViewer());
+ _panel.setBackground(_gui.getBackground());
+ JScrollPane pane = new JScrollPane(_panel);
+ pane.setBackground(_gui.getBackground());
+ add(pane, BorderLayout.CENTER);
+ } else {
+ _adapter.updateChart(_panel, _gui.getViewer());
+ //_gui.pack();
+ }
+ }
+
+ protected void initializeComponents() {
+ // noop
+ setLayout(new BorderLayout());
+ add(new JLabel(), BorderLayout.CENTER);
+ //dummy.setBackground(_gui.getBackground());
+ //dummy.setPreferredSize(new Dimension(800,600));
+ //add(dummy);
+
+ //add(_panel);
+
+ }
+}
\ No newline at end of file
diff --git a/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java
new file mode 100644
index 000000000..1f90057fa
--- /dev/null
+++ b/apps/netmonitor/java/src/net/i2p/netmonitor/gui/NetViewer.java
@@ -0,0 +1,86 @@
+package net.i2p.netmonitor.gui;
+
+import net.i2p.netmonitor.NetMonitor;
+import net.i2p.netmonitor.PeerSummary;
+import net.i2p.netmonitor.PeerStat;
+import net.i2p.util.Log;
+import net.i2p.util.I2PThread;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Coordinate the visualization of the network monitor.
+ *
+ * Usage: NetViewer [exportDir]