package net.i2p.router.web; import java.awt.Color; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; import java.util.concurrent.Semaphore; import net.i2p.router.RouterContext; import net.i2p.stat.Rate; import net.i2p.stat.RateStat; import net.i2p.util.Log; import org.jrobin.core.RrdException; import org.jrobin.graph.RrdGraph; import org.jrobin.graph.RrdGraphDef; /** * */ public class StatSummarizer implements Runnable { private final RouterContext _context; private final Log _log; /** list of SummaryListener instances */ private final List _listeners; private static StatSummarizer _instance; private static final int MAX_CONCURRENT_PNG = 3; private final Semaphore _sem; public StatSummarizer() { _context = (RouterContext)RouterContext.listContexts().get(0); // fuck it, only summarize one per jvm _log = _context.logManager().getLog(getClass()); _listeners = new ArrayList(16); _instance = this; _sem = new Semaphore(MAX_CONCURRENT_PNG, true); } public static StatSummarizer instance() { return _instance; } public void run() { String specs = ""; while (_context.router().isAlive()) { specs = adjustDatabases(specs); try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} } } /** list of SummaryListener instances */ List getListeners() { return _listeners; } private static final String DEFAULT_DATABASES = "bw.sendRate.60000" + ",bw.recvRate.60000" + // ",tunnel.testSuccessTime.60000" + // ",udp.outboundActiveCount.60000" + // ",udp.receivePacketSize.60000" + // ",udp.receivePacketSkew.60000" + // ",udp.sendConfirmTime.60000" + // ",udp.sendPacketSize.60000" + ",router.memoryUsed.60000" + ",router.activePeers.60000"; // ",router.activeSendPeers.60000" + // ",tunnel.acceptLoad.60000" + // ",tunnel.dropLoadProactive.60000" + // ",tunnel.buildExploratorySuccess.60000" + // ",tunnel.buildExploratoryReject.60000" + // ",tunnel.buildExploratoryExpire.60000" + // ",client.sendAckTime.60000" + // ",client.dispatchNoACK.60000" + // ",ntcp.sendTime.60000" + // ",ntcp.transmitTime.60000" + // ",ntcp.sendBacklogTime.60000" + // ",ntcp.receiveTime.60000" + // ",transport.sendMessageFailureLifetime.60000" + // ",transport.sendProcessingTime.60000"; private String adjustDatabases(String oldSpecs) { String spec = _context.getProperty("stat.summaries", DEFAULT_DATABASES); if ( ( (spec == null) && (oldSpecs == null) ) || ( (spec != null) && (oldSpecs != null) && (oldSpecs.equals(spec))) ) return oldSpecs; List old = parseSpecs(oldSpecs); List newSpecs = parseSpecs(spec); // remove old ones for (Rate r : old) { if (!newSpecs.contains(r)) removeDb(r); } // add new ones StringBuilder buf = new StringBuilder(); boolean comma = false; for (Rate r : newSpecs) { if (!old.contains(r)) addDb(r); if (comma) buf.append(','); else comma = true; buf.append(r.getRateStat().getName()).append(".").append(r.getPeriod()); } return buf.toString(); } private void removeDb(Rate r) { for (int i = 0; i < _listeners.size(); i++) { SummaryListener lsnr = _listeners.get(i); if (lsnr.getRate().equals(r)) { _listeners.remove(i); lsnr.stopListening(); return; } } } private void addDb(Rate r) { SummaryListener lsnr = new SummaryListener(r); _listeners.add(lsnr); lsnr.startListening(); //System.out.println("Start listening for " + r.getRateStat().getName() + ": " + r.getPeriod()); } public boolean renderPng(Rate rate, OutputStream out) throws IOException { return renderPng(rate, out, -1, -1, false, false, false, false, -1, true); } /** * This does the single data graphs. * For the two-data bandwidth graph see renderRatePng(). * Synchronized to conserve memory. * @return success */ public boolean renderPng(Rate rate, OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, boolean showCredit) throws IOException { try { try { _sem.acquire(); } catch (InterruptedException ie) {} return locked_renderPng(rate, out, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, showCredit); } finally { _sem.release(); } } private boolean locked_renderPng(Rate rate, OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, boolean showCredit) throws IOException { if (width > GraphHelper.MAX_X) width = GraphHelper.MAX_X; if (height > GraphHelper.MAX_Y) height = GraphHelper.MAX_Y; for (int i = 0; i < _listeners.size(); i++) { SummaryListener lsnr = _listeners.get(i); if (lsnr.getRate().equals(rate)) { lsnr.renderPng(out, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, showCredit); return true; } } return false; } public boolean renderPng(OutputStream out, String templateFilename) throws IOException { SummaryRenderer.render(_context, out, templateFilename); return true; } public boolean getXML(Rate rate, OutputStream out) throws IOException { for (int i = 0; i < _listeners.size(); i++) { SummaryListener lsnr = _listeners.get(i); if (lsnr.getRate().equals(rate)) { lsnr.getData().exportXml(out); out.write(("\n").getBytes()); out.write(("\n").getBytes()); return true; } } return false; } /** * This does the two-data bandwidth graph only. * For all other graphs see SummaryRenderer * Synchronized to conserve memory. * @return success */ public boolean renderRatePng(OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, boolean showCredit) throws IOException { try { try { _sem.acquire(); } catch (InterruptedException ie) {} return locked_renderRatePng(out, width, height, hideLegend, hideGrid, hideTitle, showEvents, periodCount, showCredit); } finally { _sem.release(); } } private boolean locked_renderRatePng(OutputStream out, int width, int height, boolean hideLegend, boolean hideGrid, boolean hideTitle, boolean showEvents, int periodCount, boolean showCredit) throws IOException { long end = _context.clock().now() - 60*1000; if (width > GraphHelper.MAX_X) width = GraphHelper.MAX_X; if (height > GraphHelper.MAX_Y) height = GraphHelper.MAX_Y; if (periodCount <= 0) periodCount = SummaryListener.PERIODS; if (periodCount > SummaryListener.PERIODS) periodCount = SummaryListener.PERIODS; long period = 60*1000; long start = end - period*periodCount; //long begin = System.currentTimeMillis(); try { RrdGraphDef def = new RrdGraphDef(); def.setTimePeriod(start/1000, 0); def.setLowerLimit(0d); def.setBaseValue(1024); // Note to translators: all runtime zh translation disabled in this file, no font available in RRD String title = _("Bandwidth usage"); if (!hideTitle) def.setTitle(title); 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.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); //def.area(recvName, Color.RED, "Inbound bytes/sec@r"); if (!hideLegend) { def.gprint(sendName, "AVERAGE", _("out average") + ": @2@s" + _("bytes/sec")); def.gprint(sendName, "MAX", ' ' + _("max") + ": @2@s" + _("bytes/sec") + "@r"); def.gprint(recvName, "AVERAGE", _("in average") + ": @2@s" + _("bytes/sec")); def.gprint(recvName, "MAX", ' ' + _("max") + ": @2@s" + _("bytes/sec") + "@r"); } if (!showCredit) def.setShowSignature(false); if (hideLegend) def.setShowLegend(false); if (hideGrid) { def.setGridX(false); def.setGridY(false); } //System.out.println("rendering: path=" + path + " dsNames[0]=" + dsNames[0] + " dsNames[1]=" + dsNames[1] + " lsnr.getName=" + _listener.getName()); def.setAntiAliasing(false); //System.out.println("Rendering: \n" + def.exportXmlTemplate()); //System.out.println("*****************\nData: \n" + _listener.getData().dump()); RrdGraph graph = new RrdGraph(def); //System.out.println("Graph created"); byte data[] = null; if ( (width <= 0) || (height <= 0) ) data = graph.getPNGBytes(); else data = graph.getPNGBytes(width, height); //long timeToPlot = System.currentTimeMillis() - begin; out.write(data); //File t = File.createTempFile("jrobinData", ".xml"); //_listener.getData().dumpXml(new FileOutputStream(t)); //System.out.println("plotted: " + (data != null ? data.length : 0) + " bytes in " + timeToPlot // ); // + ", data written to " + t.getAbsolutePath()); return true; } catch (RrdException re) { _log.error("Error rendering", re); throw new IOException("Error plotting: " + re.getMessage()); } catch (IOException ioe) { _log.error("Error rendering", ioe); throw ioe; } catch (OutOfMemoryError oom) { _log.error("Error rendering", oom); throw new IOException("Error plotting: " + oom.getMessage()); } } /** * @param specs statName.period,statName.period,statName.period * @return list of Rate objects */ private List parseSpecs(String specs) { StringTokenizer tok = new StringTokenizer(specs, ","); List rv = new ArrayList(); while (tok.hasMoreTokens()) { String spec = tok.nextToken(); int split = spec.lastIndexOf('.'); if ( (split <= 0) || (split + 1 >= spec.length()) ) continue; String name = spec.substring(0, split); String per = spec.substring(split+1); long period = -1; try { period = Long.parseLong(per); RateStat rs = _context.statManager().getRate(name); if (rs != null) { Rate r = rs.getRate(period); if (r != null) rv.add(r); } } catch (NumberFormatException nfe) {} } return rv; } /** 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; return Messages.getString(s, _context); } }