diff --git a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java index 485ef3da58e9065d354137a8fe6b2cef36f9e7fd..7ee8b069d4852dfff4d98fc2f6524354ef516c47 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/StatSummarizer.java @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Semaphore; import javax.imageio.ImageIO; @@ -44,20 +45,24 @@ public class StatSummarizer implements Runnable { private static StatSummarizer _instance; private static final int MAX_CONCURRENT_PNG = 3; private final Semaphore _sem; + private volatile boolean _isRunning = true; + private Thread _thread; public StatSummarizer() { _context = (RouterContext)RouterContext.listContexts().get(0); // fuck it, only summarize one per jvm _log = _context.logManager().getLog(getClass()); - _listeners = new ArrayList(16); + _listeners = new CopyOnWriteArrayList(); _instance = this; _sem = new Semaphore(MAX_CONCURRENT_PNG, true); + _context.addShutdownTask(new Shutdown()); } public static StatSummarizer instance() { return _instance; } public void run() { + _thread = Thread.currentThread(); String specs = ""; - while (_context.router().isAlive()) { + while (_isRunning && _context.router().isAlive()) { specs = adjustDatabases(specs); try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} } @@ -236,6 +241,20 @@ public class StatSummarizer implements Runnable { private boolean locked_renderRatePng(OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, boolean showCredit) throws IOException { + + // go to some trouble to see if we have the data for the combined bw graph + SummaryListener txLsnr = null; + SummaryListener rxLsnr = null; + for (SummaryListener lsnr : StatSummarizer.instance().getListeners()) { + String title = lsnr.getRate().getRateStat().getName(); + if (title.equals("bw.sendRate")) + txLsnr = lsnr; + else if (title.equals("bw.recvRate")) + rxLsnr = lsnr; + } + if (txLsnr == null || rxLsnr == null) + throw new IOException("no rates for combined graph"); + long end = _context.clock().now() - 60*1000; if (width > GraphHelper.MAX_X) width = GraphHelper.MAX_X; @@ -260,10 +279,13 @@ public class StatSummarizer implements Runnable { String title = _("Bandwidth usage"); if (!hideTitle) def.setTitle(title); + long started = _context.router().getWhenStarted(); + if (started > start && started < end) + def.vrule(started / 1000, Color.BLACK, null, 4.0f); // no room for legend String sendName = SummaryListener.createName(_context, "bw.sendRate.60000"); String recvName = SummaryListener.createName(_context, "bw.recvRate.60000"); - def.datasource(sendName, sendName, sendName, "AVERAGE", "MEMORY"); - def.datasource(recvName, recvName, recvName, "AVERAGE", "MEMORY"); + def.datasource(sendName, txLsnr.getData().getPath(), sendName, "AVERAGE", txLsnr.getBackendName()); + def.datasource(recvName, rxLsnr.getData().getPath(), recvName, "AVERAGE", rxLsnr.getBackendName()); def.area(sendName, Color.BLUE, _("Outbound Bytes/sec")); //def.line(sendName, Color.BLUE, "Outbound bytes/sec", 3); def.line(recvName, Color.RED, _("Inbound Bytes/sec") + "\\r", 3); @@ -271,7 +293,7 @@ public class StatSummarizer implements Runnable { if (!hideLegend) { def.gprint(sendName, "AVERAGE", _("Out average") + ": %.2f %s" + _("Bps")); def.gprint(sendName, "MAX", ' ' + _("max") + ": %.2f %S" + _("Bps") + "\\r"); - def.gprint(recvName, "AVERAGE", _("In average") + ": %.2f %S" + _("Bps")); + def.gprint(recvName, "AVERAGE", _("In average") + ": %.2f %S" + _("Bps")); def.gprint(recvName, "MAX", ' ' + _("max") + ": %.2f %S" + _("Bps") + "\\r"); } if (!showCredit) @@ -347,8 +369,25 @@ public class StatSummarizer implements Runnable { /** translate a string */ private String _(String s) { // the RRD font doesn't have zh chars, at least on my system - if ("zh".equals(Messages.getLanguage(_context))) - return s; + // Works on 1.5.9 + //if ("zh".equals(Messages.getLanguage(_context))) + // return s; return Messages.getString(s, _context); } + + /** + * Make sure any persistent RRDs are closed + * @since 0.8.6 + */ + private class Shutdown implements Runnable { + public void run() { + _isRunning = false; + if (_thread != null) + _thread.interrupt(); + for (SummaryListener lsnr : _listeners) { + lsnr.stopListening(); + } + _listeners.clear(); + } + } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java index cba9553737a06f1f8d4ceed3fcb67e2a2af93654..f338e684811d51997fb563bb1fd5c8c6f160a059 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryListener.java @@ -1,5 +1,6 @@ package net.i2p.router.web; +import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -9,12 +10,15 @@ import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.stat.RateSummaryListener; import net.i2p.util.Log; +import net.i2p.util.SecureFile; +import net.i2p.util.SecureFileOutputStream; import org.jrobin.core.RrdBackendFactory; import org.jrobin.core.RrdDb; import org.jrobin.core.RrdDef; import org.jrobin.core.RrdException; import org.jrobin.core.RrdMemoryBackendFactory; +import org.jrobin.core.RrdNioBackendFactory; import org.jrobin.core.Sample; import org.jrobin.graph.RrdGraph; import org.jrobin.graph.RrdGraphDef; @@ -27,9 +31,16 @@ import org.jrobin.graph.RrdGraphDefTemplate; * @since 0.6.1.13 */ class SummaryListener implements RateSummaryListener { + private static final String PROP_PERSISTENT = "routerconsole.graphPersistent"; + /** note that .jrb files are NOT compatible with .rrd files */ + private static final String RRD_DIR = "rrd"; + private static final String RRD_PREFIX = "rrd-"; + private static final String RRD_SUFFIX = ".jrb"; + private final I2PAppContext _context; private final Log _log; private final Rate _rate; + private final boolean _isPersistent; private String _name; private String _eventName; private RrdDb _db; @@ -39,18 +50,11 @@ class SummaryListener implements RateSummaryListener { static final int PERIODS = 1440; - static { - try { - RrdBackendFactory.setDefaultFactory("MEMORY"); - } catch (RrdException re) { - re.printStackTrace(); - } - } - public SummaryListener(Rate r) { _context = I2PAppContext.getGlobalContext(); _rate = r; _log = _context.logManager().getLog(SummaryListener.class); + _isPersistent = _context.getBooleanProperty(PROP_PERSISTENT); } public void add(double totalValue, long eventCount, double totalEventTime, long period) { @@ -99,24 +103,44 @@ class SummaryListener implements RateSummaryListener { _name = createName(_context, baseName); _eventName = createName(_context, baseName + ".events"); try { - RrdDef def = new RrdDef(_name, now()/1000, period/1000); - // for info on the heartbeat, xff, steps, etc, see the rrdcreate man page, aka - // http://www.jrobin.org/support/man/rrdcreate.html - long heartbeat = period*10/1000; - def.addDatasource(_name, "GAUGE", heartbeat, Double.NaN, Double.NaN); - def.addDatasource(_eventName, "GAUGE", heartbeat, 0, Double.NaN); - double xff = 0.9; - int steps = 1; - int rows = PERIODS; - def.addArchive("AVERAGE", xff, steps, rows); - _factory = (RrdMemoryBackendFactory)RrdBackendFactory.getDefaultFactory(); - _db = new RrdDb(def, _factory); + RrdBackendFactory factory = RrdBackendFactory.getFactory(getBackendName()); + String rrdDefName; + if (_isPersistent) { + // generate full path for persistent RRD files + File rrdDir = new SecureFile(_context.getRouterDir(), RRD_DIR); + File rrdFile = new File(rrdDir, RRD_PREFIX + _name + RRD_SUFFIX); + rrdDefName = rrdFile.getAbsolutePath(); + if (rrdFile.exists()) { + _db = new RrdDb(rrdDefName, factory); + if (_log.shouldLog(Log.INFO)) + _log.info("Existing RRD " + baseName + " (" + rrdDefName + ") consuming " + _db.getRrdBackend().getLength() + " bytes"); + } else { + rrdDir.mkdir(); + } + } else { + rrdDefName = _name; + } + if (_db == null) { + // not persistent or not previously existing + RrdDef def = new RrdDef(rrdDefName, now()/1000, period/1000); + // for info on the heartbeat, xff, steps, etc, see the rrdcreate man page, aka + // http://www.jrobin.org/support/man/rrdcreate.html + long heartbeat = period*10/1000; + def.addDatasource(_name, "GAUGE", heartbeat, Double.NaN, Double.NaN); + def.addDatasource(_eventName, "GAUGE", heartbeat, 0, Double.NaN); + double xff = 0.9; + int steps = 1; + int rows = PERIODS; + def.addArchive("AVERAGE", xff, steps, rows); + _db = new RrdDb(def, factory); + if (_isPersistent) + SecureFileOutputStream.setPerms(new File(rrdDefName)); + if (_log.shouldLog(Log.INFO)) + _log.info("New RRD " + baseName + " (" + rrdDefName + ") consuming " + _db.getRrdBackend().getLength() + " bytes"); + } _sample = _db.createSample(); _renderer = new SummaryRenderer(_context, this); _rate.setSummaryListener(this); - // Typical usage is 23456 bytes ~= 1440 * 16 - if (_log.shouldLog(Log.INFO)) - _log.info("New RRD " + baseName + " consuming " + _db.getRrdBackend().getLength() + " bytes"); } catch (RrdException re) { _log.error("Error starting", re); } catch (IOException ioe) { @@ -132,7 +156,12 @@ class SummaryListener implements RateSummaryListener { _log.error("Error closing", ioe); } _rate.setSummaryListener(null); - _factory.delete(_db.getPath()); + if (!_isPersistent) { + // close() does not release resources for memory backend + try { + ((RrdMemoryBackendFactory)RrdBackendFactory.getFactory(RrdMemoryBackendFactory.NAME)).delete(_db.getPath()); + } catch (RrdException re) {} + } _db = null; } @@ -150,6 +179,11 @@ class SummaryListener implements RateSummaryListener { long now() { return _context.clock().now(); } + /** @since 0.8.6 */ + String getBackendName() { + return _isPersistent ? RrdNioBackendFactory.NAME : RrdMemoryBackendFactory.NAME; + } + @Override public boolean equals(Object obj) { return ((obj instanceof SummaryListener) && ((SummaryListener)obj)._rate.equals(_rate)); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java index a5fd3dcbff6551271e3a6b131864a3c47b1bbce1..37d41719e936f25a3ffb96503d905f3df61b6cf9 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryRenderer.java @@ -13,6 +13,7 @@ import javax.imageio.stream.MemoryCacheImageOutputStream; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; +import net.i2p.router.RouterContext; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.stat.RateSummaryListener; @@ -130,7 +131,10 @@ class SummaryRenderer { // Strings.java descr = _(_listener.getRate().getRateStat().getDescription()); } - def.datasource(plotName, path, plotName, "AVERAGE", "MEMORY"); + long started = ((RouterContext)_context).router().getWhenStarted(); + if (started > start && started < end) + def.vrule(started / 1000, Color.BLACK, _("Restart"), 4.0f); + def.datasource(plotName, path, plotName, "AVERAGE", _listener.getBackendName()); def.area(plotName, Color.BLUE, descr + "\\r"); if (!hideLegend) { def.gprint(plotName, "AVERAGE", _("avg") + ": %.2f %s"); @@ -189,8 +193,9 @@ class SummaryRenderer { /** translate a string */ private String _(String s) { // the RRD font doesn't have zh chars, at least on my system - if ("zh".equals(Messages.getLanguage(_context))) - return s; + // Works on 1.5.9 + //if ("zh".equals(Messages.getLanguage(_context))) + // return s; return Messages.getString(s, _context); } @@ -199,8 +204,9 @@ class SummaryRenderer { */ private String _(String s, String o) { // the RRD font doesn't have zh chars, at least on my system - if ("zh".equals(Messages.getLanguage(_context))) - return s.replace("{0}", o); + // Works on 1.5.9 + //if ("zh".equals(Messages.getLanguage(_context))) + // return s.replace("{0}", o); return Messages.getString(s, o, _context); } }