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);
     }
 }