From 538dd07e7baee524458ac8b79a62c9539b5265f1 Mon Sep 17 00:00:00 2001
From: jrandom <jrandom>
Date: Thu, 17 Mar 2005 05:29:55 +0000
Subject: [PATCH] 2005-03-16  jrandom     * Adjust the old speed calculator to
 include end to end RTT data in its       estimates, and use that as the
 primary speed calculator again.     * Use the mean of the high capacity
 speeds to determine the fast       threshold, rather than the median. 
 Perhaps we should use the mean of       all active non-failing peers?     *
 Updated the profile page to sort by tier, then alphabetically.     * Added
 some alternative socketManager factories (good call aum!)

---
 .../streaming/I2PSocketManagerFactory.java    | 49 +++++++---
 .../src/net/i2p/router/web/SummaryHelper.java |  2 +-
 history.txt                                   | 11 ++-
 .../src/net/i2p/router/RouterContext.java     |  4 +-
 .../src/net/i2p/router/RouterVersion.java     |  4 +-
 .../OutboundClientMessageJobHelper.java       | 16 +--
 .../OutboundClientMessageOneShotJob.java      | 33 +++++--
 .../i2p/router/peermanager/PeerProfile.java   |  8 ++
 .../peermanager/ProfileManagerImpl.java       |  7 ++
 .../router/peermanager/ProfileOrganizer.java  | 26 +++++
 .../peermanager/ProfileOrganizerRenderer.java | 98 +++++++++++++++----
 .../peermanager/ProfilePersistenceHelper.java |  2 +
 .../router/peermanager/SpeedCalculator.java   | 38 +++++++
 13 files changed, 243 insertions(+), 55 deletions(-)

diff --git a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
index 390f61d787..c4b37ec4af 100644
--- a/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
+++ b/apps/ministreaming/java/src/net/i2p/client/streaming/I2PSocketManagerFactory.java
@@ -36,18 +36,7 @@ public class I2PSocketManagerFactory {
      * @return the newly created socket manager, or null if there were errors
      */
     public static I2PSocketManager createManager() {
-        String i2cpHost = System.getProperty(I2PClient.PROP_TCP_HOST, "localhost");
-        int i2cpPort = 7654;
-        String i2cpPortStr = System.getProperty(I2PClient.PROP_TCP_PORT);
-        if (i2cpPortStr != null) {
-            try {
-                i2cpPort = Integer.parseInt(i2cpPortStr);
-            } catch (NumberFormatException nfe) {
-                // gobble gobble
-            }
-        }
-
-        return createManager(i2cpHost, i2cpPort, System.getProperties());
+        return createManager(getHost(), getPort(), System.getProperties());
     }
 
     /**
@@ -72,6 +61,26 @@ public class I2PSocketManagerFactory {
         }
     }
 
+    /**
+     * Create a socket manager using the destination loaded from the given private key
+     * stream and connected to the default I2CP host and port.
+     *
+     * @return the newly created socket manager, or null if there were errors
+     */
+    public static I2PSocketManager createManager(InputStream myPrivateKeyStream) {
+        return createManager(myPrivateKeyStream, getHost(), getPort(), System.getProperties());
+    }
+    
+    /**
+     * Create a socket manager using the destination loaded from the given private key
+     * stream and connected to the default I2CP host and port.
+     *
+     * @return the newly created socket manager, or null if there were errors
+     */
+    public static I2PSocketManager createManager(InputStream myPrivateKeyStream, Properties opts) {
+        return createManager(myPrivateKeyStream, getHost(), getPort(), opts);
+    }
+    
     /**
      * Create a socket manager using the destination loaded from the given private key
      * stream and connected to the I2CP router on the specified machine on the given
@@ -154,4 +163,20 @@ public class I2PSocketManagerFactory {
         }
         
     }
+
+    private static String getHost() {
+        return System.getProperty(I2PClient.PROP_TCP_HOST, "localhost");
+    }
+    private static int getPort() {
+        int i2cpPort = 7654;
+        String i2cpPortStr = System.getProperty(I2PClient.PROP_TCP_PORT);
+        if (i2cpPortStr != null) {
+            try {
+                i2cpPort = Integer.parseInt(i2cpPortStr);
+            } catch (NumberFormatException nfe) {
+                // gobble gobble
+            }
+        }
+        return i2cpPort;
+    }
 }
\ No newline at end of file
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index cfcfe8c0d5..395c6c5060 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -84,7 +84,7 @@ public class SummaryHelper {
         
         if (ms < 60 * 1000) {
             return now + " (" + (ms / 1000) + "s)";
-        } else if (ms < 60 * 1000) {
+        } else if (ms < 60 * 60 * 1000) {
             return now + " (" + (ms / (60 * 1000)) + "m)";
         } else if (ms < 24 * 60 * 60 * 1000) {
             return now + " (" + (ms / (60 * 60 * 1000)) + "h)";
diff --git a/history.txt b/history.txt
index 600dd3e6a6..537d35ffbf 100644
--- a/history.txt
+++ b/history.txt
@@ -1,4 +1,13 @@
-$Id: history.txt,v 1.168 2005/03/11 17:23:41 jrandom Exp $
+$Id: history.txt,v 1.169 2005/03/14 22:47:15 jrandom Exp $
+
+2005-03-16  jrandom
+    * Adjust the old speed calculator to include end to end RTT data in its 
+      estimates, and use that as the primary speed calculator again.
+    * Use the mean of the high capacity speeds to determine the fast 
+      threshold, rather than the median.  Perhaps we should use the mean of
+      all active non-failing peers?
+    * Updated the profile page to sort by tier, then alphabetically.
+    * Added some alternative socketManager factories (good call aum!)
 
 2005-03-14  jrandom
     * New strict speed calculator that goes off the actual number of messages
diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java
index 1eab8727c1..197eead2b2 100644
--- a/router/java/src/net/i2p/router/RouterContext.java
+++ b/router/java/src/net/i2p/router/RouterContext.java
@@ -116,8 +116,8 @@ public class RouterContext extends I2PAppContext {
         _throttle = new RouterDoSThrottle(this);
         _isFailingCalc = new IsFailingCalculator(this);
         _integrationCalc = new IntegrationCalculator(this);
-        _speedCalc = new StrictSpeedCalculator(this);
-        _oldSpeedCalc = new SpeedCalculator(this);
+        _speedCalc = new SpeedCalculator(this);
+        _oldSpeedCalc = new StrictSpeedCalculator(this);
         _reliabilityCalc = new ReliabilityCalculator(this);
         _capacityCalc = new CapacityCalculator(this);
     }
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 1f88e1f61f..69ce2378a1 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
  *
  */
 public class RouterVersion {
-    public final static String ID = "$Revision: 1.162 $ $Date: 2005/03/11 17:23:41 $";
+    public final static String ID = "$Revision: 1.163 $ $Date: 2005/03/14 22:47:15 $";
     public final static String VERSION = "0.5.0.2";
-    public final static long BUILD = 3;
+    public final static long BUILD = 4;
     public static void main(String args[]) {
         System.out.println("I2P Router version: " + VERSION);
         System.out.println("Router ID: " + RouterVersion.ID);
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
index 49800858b1..6857ea74a1 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageJobHelper.java
@@ -52,10 +52,11 @@ class OutboundClientMessageJobHelper {
      * @return garlic, or null if no tunnels were found (or other errors)
      */
     static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, 
-                                             Payload data, Hash from, Destination dest, SessionKey wrappedKey, Set wrappedTags, 
+                                             Payload data, Hash from, Destination dest, TunnelInfo replyTunnel,
+                                             SessionKey wrappedKey, Set wrappedTags, 
                                              boolean requireAck, LeaseSet bundledReplyLeaseSet) {
         PayloadGarlicConfig dataClove = buildDataClove(ctx, data, dest, expiration);
-        return createGarlicMessage(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, wrappedKey, 
+        return createGarlicMessage(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, wrappedKey, 
                                    wrappedTags, requireAck, bundledReplyLeaseSet);
     }
     /**
@@ -65,9 +66,9 @@ class OutboundClientMessageJobHelper {
      * @return garlic, or null if no tunnels were found (or other errors)
      */
     static GarlicMessage createGarlicMessage(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, 
-                                             PayloadGarlicConfig dataClove, Hash from, Destination dest, SessionKey wrappedKey, 
+                                             PayloadGarlicConfig dataClove, Hash from, Destination dest, TunnelInfo replyTunnel, SessionKey wrappedKey, 
                                              Set wrappedTags, boolean requireAck, LeaseSet bundledReplyLeaseSet) {
-        GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, requireAck, bundledReplyLeaseSet);
+        GarlicConfig config = createGarlicConfig(ctx, replyToken, expiration, recipientPK, dataClove, from, dest, replyTunnel, requireAck, bundledReplyLeaseSet);
         if (config == null)
             return null;
         GarlicMessage msg = GarlicMessageBuilder.buildMessage(ctx, config, wrappedKey, wrappedTags);
@@ -75,7 +76,7 @@ class OutboundClientMessageJobHelper {
     }
     
     private static GarlicConfig createGarlicConfig(RouterContext ctx, long replyToken, long expiration, PublicKey recipientPK, 
-                                                   PayloadGarlicConfig dataClove, Hash from, Destination dest, boolean requireAck,
+                                                   PayloadGarlicConfig dataClove, Hash from, Destination dest, TunnelInfo replyTunnel, boolean requireAck,
                                                    LeaseSet bundledReplyLeaseSet) {
         Log log = ctx.logManager().getLog(OutboundClientMessageJobHelper.class);
         if (log.shouldLog(Log.DEBUG))
@@ -85,7 +86,7 @@ class OutboundClientMessageJobHelper {
         config.addClove(dataClove);
         
         if (requireAck) {
-            PayloadGarlicConfig ackClove = buildAckClove(ctx, from, replyToken, expiration);
+            PayloadGarlicConfig ackClove = buildAckClove(ctx, from, replyTunnel, replyToken, expiration);
             if (ackClove == null)
                 return null; // no tunnels
             config.addClove(ackClove);
@@ -122,14 +123,13 @@ class OutboundClientMessageJobHelper {
     /**
      * Build a clove that sends a DeliveryStatusMessage to us
      */
-    private static PayloadGarlicConfig buildAckClove(RouterContext ctx, Hash from, long replyToken, long expiration) {
+    private static PayloadGarlicConfig buildAckClove(RouterContext ctx, Hash from, TunnelInfo replyToTunnel, long replyToken, long expiration) {
         Log log = ctx.logManager().getLog(OutboundClientMessageJobHelper.class);
         PayloadGarlicConfig ackClove = new PayloadGarlicConfig();
         
         Hash replyToTunnelRouter = null; // inbound tunnel gateway
         TunnelId replyToTunnelId = null; // tunnel id on that gateway
         
-        TunnelInfo replyToTunnel = ctx.tunnelManager().selectInboundTunnel(from);
         if (replyToTunnel == null) {
             if (log.shouldLog(Log.ERROR))
                 log.error("Unable to send client message from " + from.toBase64() 
diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
index 2501c162d8..b10f06cf89 100644
--- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
+++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java
@@ -61,6 +61,8 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
     private long _start;
     private boolean _finished;
     private long _leaseSetLookupBegin;
+    private TunnelInfo _outTunnel;
+    private TunnelInfo _inTunnel;
     
     /**
      * final timeout (in milliseconds) that the outbound message will fail in.
@@ -312,10 +314,12 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             replyLeaseSet = getContext().netDb().lookupLeaseSetLocally(_from.calculateHash());
         }
         
+        _inTunnel = selectInboundTunnel();
+        
         GarlicMessage msg = OutboundClientMessageJobHelper.createGarlicMessage(getContext(), token, 
                                                                                _overallExpiration, key, 
                                                                                _clove, _from.calculateHash(), 
-                                                                               _to, 
+                                                                               _to, _inTunnel,
                                                                                sessKey, tags, 
                                                                                true, replyLeaseSet);
         if (msg == null) {
@@ -341,16 +345,16 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
                        + _lease.getTunnelId() + " on " 
                        + _lease.getGateway().toBase64());
         
-        TunnelInfo outTunnel = selectOutboundTunnel();
-        if (outTunnel != null) {
+        _outTunnel = selectOutboundTunnel();
+        if (_outTunnel != null) {
             if (_log.shouldLog(Log.DEBUG))
-                _log.debug(getJobId() + ": Sending tunnel message out " + outTunnel.getSendTunnelId(0) + " to " 
+                _log.debug(getJobId() + ": Sending tunnel message out " + _outTunnel.getSendTunnelId(0) + " to " 
                            + _toString + " at "
                            + _lease.getTunnelId() + " on " 
                            + _lease.getGateway().toBase64());
 
             // dispatch may take 100+ms, so toss it in its own job
-            getContext().jobQueue().addJob(new DispatchJob(getContext(), msg, outTunnel, selector, onReply, onFail, (int)(_overallExpiration-getContext().clock().now())));
+            getContext().jobQueue().addJob(new DispatchJob(getContext(), msg, selector, onReply, onFail, (int)(_overallExpiration-getContext().clock().now())));
         } else {
             if (_log.shouldLog(Log.ERROR))
                 _log.error(getJobId() + ": Could not find any outbound tunnels to send the payload through... wtf?");
@@ -362,15 +366,13 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
 
     private class DispatchJob extends JobImpl {
         private GarlicMessage _msg;
-        private TunnelInfo _outTunnel;
         private ReplySelector _selector;
         private SendSuccessJob _replyFound;
         private SendTimeoutJob _replyTimeout;
         private int _timeoutMs;
-        public DispatchJob(RouterContext ctx, GarlicMessage msg, TunnelInfo out, ReplySelector sel, SendSuccessJob success, SendTimeoutJob timeout, int timeoutMs) {
+        public DispatchJob(RouterContext ctx, GarlicMessage msg, ReplySelector sel, SendSuccessJob success, SendTimeoutJob timeout, int timeoutMs) {
             super(ctx);
             _msg = msg;
-            _outTunnel = out;
             _selector = sel;
             _replyFound = success;
             _replyTimeout = timeout;
@@ -396,6 +398,13 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
     private TunnelInfo selectOutboundTunnel() {
         return getContext().tunnelManager().selectOutboundTunnel(_from.calculateHash());
     }
+    /**
+     * Pick an arbitrary outbound tunnel for any deliveryStatusMessage to come back in
+     *
+     */
+    private TunnelInfo selectInboundTunnel() {
+        return getContext().tunnelManager().selectInboundTunnel(_from.calculateHash());
+    }
     
     /**
      * give up the ghost, this message just aint going through.  tell the client to fuck off.
@@ -540,8 +549,14 @@ public class OutboundClientMessageOneShotJob extends JobImpl {
             
             getContext().statManager().addRateData("client.sendAckTime", sendTime, 0);
             getContext().statManager().addRateData("client.sendMessageSize", _clientMessageSize, sendTime);
+            if (_outTunnel != null)
+                for (int i = 0; i < _outTunnel.getLength(); i++)
+                    getContext().profileManager().tunnelTestSucceeded(_outTunnel.getPeer(i), sendTime);
+            if (_inTunnel != null)
+                for (int i = 0; i < _inTunnel.getLength(); i++)
+                    getContext().profileManager().tunnelTestSucceeded(_inTunnel.getPeer(i), sendTime);
         }
-        
+
         public void setMessage(I2NPMessage msg) {}
     }
     
diff --git a/router/java/src/net/i2p/router/peermanager/PeerProfile.java b/router/java/src/net/i2p/router/peermanager/PeerProfile.java
index 4dadb6c8b0..c97395823a 100644
--- a/router/java/src/net/i2p/router/peermanager/PeerProfile.java
+++ b/router/java/src/net/i2p/router/peermanager/PeerProfile.java
@@ -26,6 +26,7 @@ public class PeerProfile {
     private RateStat _dbResponseTime = null;
     private RateStat _tunnelCreateResponseTime = null;
     private RateStat _tunnelTestResponseTime = null;
+    private RateStat _tunnelTestResponseTimeSlow = null;
     private RateStat _commError = null;
     private RateStat _dbIntroduction = null;
     // calculation bonuses
@@ -143,6 +144,8 @@ public class PeerProfile {
     public RateStat getTunnelCreateResponseTime() { return _tunnelCreateResponseTime; }
     /** how long it takes to successfully test a tunnel this peer participates in (in milliseconds), calculated over a 10 minute, 1 hour, and 1 day period */
     public RateStat getTunnelTestResponseTime() { return _tunnelTestResponseTime; }
+    /** how long it takes to successfully test the peer (in milliseconds) when the time exceeds 5s */
+    public RateStat getTunnelTestResponseTimeSlow() { return _tunnelTestResponseTimeSlow; }
     /** how long between communication errors with the peer (disconnection, etc), calculated over a 1 minute, 1 hour, and 1 day period */
     public RateStat getCommError() { return _commError; }
     /** how many new peers we get from dbSearchReplyMessages or dbStore messages, calculated over a 1 hour, 1 day, and 1 week period */
@@ -224,6 +227,7 @@ public class PeerProfile {
         _dbResponseTime = null;
         _tunnelCreateResponseTime = null;
         _tunnelTestResponseTime = null;
+        _tunnelTestResponseTimeSlow = null;
         _commError = null;
         _dbIntroduction = null;
         _tunnelHistory = null;
@@ -253,6 +257,8 @@ public class PeerProfile {
             _tunnelCreateResponseTime = new RateStat("tunnelCreateResponseTime", "how long it takes to get a tunnel create response from the peer (in milliseconds)", group, new long[] { 10*60*1000l, 30*60*1000l, 60*60*1000l, 24*60*60*1000 } );
         if (_tunnelTestResponseTime == null)
             _tunnelTestResponseTime = new RateStat("tunnelTestResponseTime", "how long it takes to successfully test a tunnel this peer participates in (in milliseconds)", group, new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000 } );
+        if (_tunnelTestResponseTimeSlow == null)
+            _tunnelTestResponseTimeSlow = new RateStat("tunnelTestResponseTimeSlow", "how long it takes to successfully test a peer when the time exceeds 5s", group, new long[] { 10*60*1000l, 60*60*1000l, 24*60*60*1000l, });
         if (_commError == null)
             _commError = new RateStat("commErrorRate", "how long between communication errors with the peer (e.g. disconnection)", group, new long[] { 60*1000l, 10*60*1000l, 60*60*1000l, 24*60*60*1000 } );
         if (_dbIntroduction == null)
@@ -269,6 +275,7 @@ public class PeerProfile {
         _dbResponseTime.setStatLog(_context.statManager().getStatLog());
         _tunnelCreateResponseTime.setStatLog(_context.statManager().getStatLog());
         _tunnelTestResponseTime.setStatLog(_context.statManager().getStatLog());
+        _tunnelTestResponseTimeSlow.setStatLog(_context.statManager().getStatLog());
         _commError.setStatLog(_context.statManager().getStatLog());
         _dbIntroduction.setStatLog(_context.statManager().getStatLog());
         _expanded = true;
@@ -285,6 +292,7 @@ public class PeerProfile {
         _sendSuccessSize.coalesceStats();
         _tunnelCreateResponseTime.coalesceStats();
         _tunnelTestResponseTime.coalesceStats();
+        _tunnelTestResponseTimeSlow.coalesceStats();
         _dbHistory.coalesceStats();
         _tunnelHistory.coalesceStats();
         
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java b/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java
index 57c4f62db4..d7fb125006 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileManagerImpl.java
@@ -112,6 +112,13 @@ public class ProfileManagerImpl implements ProfileManager {
         PeerProfile data = getProfile(peer);
         if (data == null) return;
         data.getTunnelTestResponseTime().addData(responseTimeMs, responseTimeMs);
+        if (responseTimeMs > getSlowThreshold())
+            data.getTunnelTestResponseTimeSlow().addData(responseTimeMs, responseTimeMs);
+    }
+    
+    private int getSlowThreshold() {
+        // perhaps we should have this compare vs. tunnel.testSuccessTime?
+        return 5*1000;
     }
     
     /**
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index 89435f3392..3cc6374a08 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -644,6 +644,10 @@ public class ProfileOrganizer {
      *                  (highest first) for active nonfailing peers
      */
     private void locked_calculateSpeedThreshold(Set reordered) {
+        if (true) {
+            locked_calculateSpeedThresholdMean(reordered);
+            return;
+        }
         Set speeds = new TreeSet();
         for (Iterator iter = reordered.iterator(); iter.hasNext(); ) {
             PeerProfile profile = (PeerProfile)iter.next();
@@ -669,6 +673,28 @@ public class ProfileOrganizer {
             _log.info("Threshold value for speed: " + _thresholdSpeedValue + " out of speeds: " + speeds);
     }
     
+    private void locked_calculateSpeedThresholdMean(Set reordered) {
+        double total = 0;
+        int count = 0;
+        for (Iterator iter = reordered.iterator(); iter.hasNext(); ) {
+            PeerProfile profile = (PeerProfile)iter.next();
+            if (profile.getCapacityValue() >= _thresholdCapacityValue) {
+                // duplicates being clobbered is fine by us
+                total += profile.getSpeedValue();
+                count++;
+            } else {
+                // its ordered
+                break;
+            }
+        }
+
+        if (count > 0)
+            _thresholdSpeedValue = total / count;
+        if (_log.shouldLog(Log.INFO))
+            _log.info("Threshold value for speed: " + _thresholdSpeedValue + " out of speeds: " + count);
+    }
+    
+    
     /** simple average, or 0 if NaN */
     private final static double avg(double total, double quantity) {
         if ( (total > 0) && (quantity > 0) )
diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
index aa9cd02683..588aef96d2 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizerRenderer.java
@@ -6,10 +6,11 @@ import java.io.Writer;
 import java.text.DecimalFormat;
 import java.text.DecimalFormatSymbols;
 
+import java.util.Comparator;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Set;
-import java.util.TreeMap;
+import java.util.TreeSet;
 
 import net.i2p.data.Hash;
 import net.i2p.router.RouterContext;
@@ -21,23 +22,25 @@ import net.i2p.router.RouterContext;
 class ProfileOrganizerRenderer {
     private RouterContext _context;
     private ProfileOrganizer _organizer;
+    private ProfileComparator _comparator;
     
     public ProfileOrganizerRenderer(ProfileOrganizer organizer, RouterContext context) {
         _context = context;
         _organizer = organizer;
+        _comparator = new ProfileComparator();
     }
     public void renderStatusHTML(Writer out) throws IOException {
         Set peers = _organizer.selectAllPeers();
         
         long hideBefore = _context.clock().now() - 3*60*60*1000;
         
-        TreeMap order = new TreeMap();
+        TreeSet order = new TreeSet(_comparator);
         for (Iterator iter = peers.iterator(); iter.hasNext();) {
             Hash peer = (Hash)iter.next();
             if (_organizer.getUs().equals(peer)) continue;
             PeerProfile prof = _organizer.getProfile(peer);
             if (prof.getLastSendSuccessful() <= hideBefore) continue;
-            order.put(peer.toBase64(), prof);
+            order.add(prof);
         }
         
         int fast = 0;
@@ -56,24 +59,11 @@ class ProfileOrganizerRenderer {
         buf.append("<td><b>Failing?</b></td>");
         buf.append("<td>&nbsp;</td>");
         buf.append("</tr>");
-        for (Iterator iter = order.keySet().iterator(); iter.hasNext();) {
-            String name = (String)iter.next();
-            PeerProfile prof = (PeerProfile)order.get(name);
+        int prevTier = 1;
+        for (Iterator iter = order.iterator(); iter.hasNext();) {
+            PeerProfile prof = (PeerProfile)iter.next();
             Hash peer = prof.getPeer();
             
-            buf.append("<tr>");
-            buf.append("<td><code>");
-            if (prof.getIsFailing()) {
-                buf.append("<font color=\"red\">--").append(peer.toBase64().substring(0,6)).append("</font>");
-            } else {
-                if (prof.getIsActive()) {
-                    buf.append("<font color=\"blue\">++").append(peer.toBase64().substring(0,6)).append("</font>");
-                } else {
-                    buf.append("__").append(peer.toBase64().substring(0,6));
-                }
-            }
-            buf.append("</code></td>");
-            buf.append("<td>");
             int tier = 0;
             boolean isIntegrated = false;
             if (_organizer.isFast(peer)) {
@@ -86,7 +76,7 @@ class ProfileOrganizerRenderer {
             } else if (_organizer.isFailing(peer)) {
                 failing++;
             } else {
-                    tier = 3;
+                tier = 3;
             }
             
             if (_organizer.isWellIntegrated(peer)) {
@@ -94,6 +84,24 @@ class ProfileOrganizerRenderer {
                 integrated++;
             }
             
+            if (tier != prevTier)
+                buf.append("<tr><td colspan=\"7\"><hr /></td></tr>\n");
+            prevTier = tier;
+            
+            buf.append("<tr>");
+            buf.append("<td><code>");
+            if (prof.getIsFailing()) {
+                buf.append("<font color=\"red\">--").append(peer.toBase64().substring(0,6)).append("</font>");
+            } else {
+                if (prof.getIsActive()) {
+                    buf.append("<font color=\"blue\">++").append(peer.toBase64().substring(0,6)).append("</font>");
+                } else {
+                    buf.append("__").append(peer.toBase64().substring(0,6));
+                }
+            }
+            buf.append("</code></td>");
+            buf.append("<td>");
+            
             switch (tier) {
                 case 1: buf.append("Fast"); break;
                 case 2: buf.append("High Capacity"); break;
@@ -131,6 +139,56 @@ class ProfileOrganizerRenderer {
         out.flush();
     }
     
+    private class ProfileComparator implements Comparator {
+        public int compare(Object lhs, Object rhs) {
+            if ( (lhs == null) || (rhs == null) ) 
+                throw new NullPointerException("lhs=" + lhs + " rhs=" + rhs);
+            if ( !(lhs instanceof PeerProfile) || !(rhs instanceof PeerProfile) ) 
+                throw new ClassCastException("lhs=" + lhs.getClass().getName() + " rhs=" + rhs.getClass().getName());
+            
+            PeerProfile left = (PeerProfile)lhs;
+            PeerProfile right = (PeerProfile)rhs;
+            
+            if (_context.profileOrganizer().isFast(left.getPeer())) {
+                if (_context.profileOrganizer().isFast(right.getPeer())) {
+                    return compareHashes(left, right);
+                } else {
+                    return -1; // fast comes first
+                }
+            } else if (_context.profileOrganizer().isHighCapacity(left.getPeer())) {
+                if (_context.profileOrganizer().isFast(right.getPeer())) {
+                    return 1; 
+                } else if (_context.profileOrganizer().isHighCapacity(right.getPeer())) {
+                    return compareHashes(left, right);
+                } else {
+                    return -1;
+                }
+            } else if (_context.profileOrganizer().isFailing(left.getPeer())) {
+                if (_context.profileOrganizer().isFailing(right.getPeer())) {
+                    return compareHashes(left, right);
+                } else {
+                    return 1;
+                }
+            } else {
+                // left is not failing
+                if (_context.profileOrganizer().isFast(right.getPeer())) {
+                    return 1;
+                } else if (_context.profileOrganizer().isHighCapacity(right.getPeer())) {
+                    return 1;
+                } else if (_context.profileOrganizer().isFailing(right.getPeer())) {
+                    return -1;
+                } else {
+                    return compareHashes(left, right);
+                }
+            }
+        }
+        
+        private int compareHashes(PeerProfile left, PeerProfile right) {
+            return left.getPeer().toBase64().compareTo(right.getPeer().toBase64());
+        }
+        
+    }
+    
     private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
     private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } }
 }
diff --git a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
index ebb21a482b..c384caab3c 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfilePersistenceHelper.java
@@ -126,6 +126,7 @@ class ProfilePersistenceHelper {
             profile.getSendSuccessSize().store(out, "sendSuccessSize");
             profile.getTunnelCreateResponseTime().store(out, "tunnelCreateResponseTime");
             profile.getTunnelTestResponseTime().store(out, "tunnelTestResponseTime");
+            profile.getTunnelTestResponseTimeSlow().store(out, "tunnelTestResponseTimeSlow");
         }
     }
     
@@ -189,6 +190,7 @@ class ProfilePersistenceHelper {
             profile.getSendSuccessSize().load(props, "sendSuccessSize", true);
             profile.getTunnelCreateResponseTime().load(props, "tunnelCreateResponseTime", true);
             profile.getTunnelTestResponseTime().load(props, "tunnelTestResponseTime", true);
+            profile.getTunnelTestResponseTimeSlow().load(props, "tunnelTestResponseTimeSlow", true);
             
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Loaded the profile for " + peer.toBase64() + " from " + file.getName());
diff --git a/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java b/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java
index 7ee12fbab5..261cab163e 100644
--- a/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java
+++ b/router/java/src/net/i2p/router/peermanager/SpeedCalculator.java
@@ -72,10 +72,35 @@ public class SpeedCalculator extends Calculator {
         double estimateFactor = getEstimateFactor(threshold, events);
         double rv = (1-estimateFactor)*measuredRTPerMinute + (estimateFactor)*estimatedRTPerMinute;
         
+        long slowCount = 0;
+        RateStat rs = profile.getTunnelTestResponseTimeSlow();
+        if (rs != null) {
+            Rate r = rs.getRate(period);
+            if (r != null)
+                slowCount = r.getCurrentEventCount();
+        }
+        long fastCount = 0;
+        rs = profile.getTunnelTestResponseTime();
+        if (rs != null) {
+            Rate r = rs.getRate(period);
+            if (r != null)
+                fastCount = r.getCurrentEventCount();
+        }
+        double slowPct = 0;
+        if (fastCount > 0)
+            slowPct = slowCount / fastCount;
+        else
+            rv /= 100; // if there are no tunnel tests, weigh against it
+        if (slowPct > 0.01) // if 1% of the traffic is dogshit slow, the peer probably sucks
+            rv /= 100.0*slowPct;
+        
+        rv = adjust(period, rv);
+        
         if (_log.shouldLog(Log.DEBUG)) {
             _log.debug("\n\nrv: " + rv + " events: " + events + " threshold: " + threshold + " period: " + period + " useTunnelTestOnly? " + tunnelTestOnly + "\n"
                        + "measuredRTT: " + measuredRoundTripTime + " measured events per minute: " + measuredRTPerMinute + "\n"
                        + "estimateRTT: " + estimatedRoundTripTime + " estimated events per minute: " + estimatedRTPerMinute + "\n" 
+                       + "slow count: " + slowCount + " fast count: " + fastCount + "\n"
                        + "estimateFactor: " + estimateFactor + "\n"
                        + "for peer: " + profile.getPeer().toBase64());
         }
@@ -83,6 +108,19 @@ public class SpeedCalculator extends Calculator {
         rv += profile.getSpeedBonus();
         return rv;
     }
+    
+    private double adjust(long period, double value) {
+        switch ((int)period) {
+            case 10*60*1000:
+                return value;
+            case 60*60*1000:
+                return value * 0.5;
+            case 24*60*60*1000:
+                return value * 0.001;
+            default:
+                return value * 0.0001;
+        }
+    }
 
     /**
      * How much do we want to prefer the measured values more than the estimated 
-- 
GitLab