diff --git a/core/src/main/java/com/google/bitcoin/core/Peer.java b/core/src/main/java/com/google/bitcoin/core/Peer.java index 56ee342f..a6eacec6 100644 --- a/core/src/main/java/com/google/bitcoin/core/Peer.java +++ b/core/src/main/java/com/google/bitcoin/core/Peer.java @@ -88,7 +88,8 @@ public class Peer { // Outstanding pings against this peer and how long the last one took to complete. Locked under the Peer lock. private List pendingPings; - private long lastPingTime; + private long[] lastPingTimes; + private static final int PING_MOVING_AVERAGE_WINDOW = 20; private Channel channel; private VersionMessage peerVersionMessage; @@ -110,7 +111,7 @@ public class Peer { this.isAcked = false; this.handler = new PeerHandler(); this.pendingPings = Lists.newLinkedList(); - this.lastPingTime = Long.MAX_VALUE; + this.lastPingTimes = null; } /** @@ -775,15 +776,27 @@ public class Peer { public void complete() { Preconditions.checkNotNull(future, "Already completed"); Long elapsed = Long.valueOf(Utils.now().getTime() - startTimeMsec); - synchronized (Peer.this) { - Peer.this.lastPingTime = elapsed.longValue(); - } + Peer.this.addPingTimeData(elapsed.longValue()); log.debug("{}: ping time is {} msec", Peer.this.toString(), elapsed); future.set(elapsed); future = null; } } + /** Adds a ping time sample to the averaging window. */ + private synchronized void addPingTimeData(long sample) { + if (lastPingTimes == null) { + lastPingTimes = new long[PING_MOVING_AVERAGE_WINDOW]; + // Initialize the averaging window to the first sample. + Arrays.fill(lastPingTimes, sample); + } else { + // Shift all elements backwards by one. + System.arraycopy(lastPingTimes, 1, lastPingTimes, 0, lastPingTimes.length - 1); + // And append the new sample to the end. + lastPingTimes[lastPingTimes.length - 1] = sample; + } + } + /** * Sends the peer a ping message and returns a future that will be invoked when the pong is received back. * The future provides a number which is the number of milliseconds elapsed between the ping and the pong. @@ -806,7 +819,22 @@ public class Peer { * been called or we did not hear back the "pong" message yet, returns {@link Long#MAX_VALUE}. */ public long getLastPingTime() { - return lastPingTime; + if (lastPingTimes == null) + return Long.MAX_VALUE; + return lastPingTimes[lastPingTimes.length - 1]; + } + + /** + * Returns a moving average of the last N ping/pong cycles. If {@link com.google.bitcoin.core.Peer#ping()} has never + * been called or we did not hear back the "pong" message yet, returns {@link Long#MAX_VALUE}. The moving average + * window is 5 buckets. + */ + public long getPingTime() { + if (lastPingTimes == null) + return Long.MAX_VALUE; + long sum = 0; + for (long i : lastPingTimes) sum += i; + return (long)((double) sum / lastPingTimes.length); } private void processPong(Pong m) { diff --git a/core/src/test/java/com/google/bitcoin/core/PeerTest.java b/core/src/test/java/com/google/bitcoin/core/PeerTest.java index b8de1e3f..6e3d80ca 100644 --- a/core/src/test/java/com/google/bitcoin/core/PeerTest.java +++ b/core/src/test/java/com/google/bitcoin/core/PeerTest.java @@ -446,16 +446,28 @@ public class PeerTest extends TestWithNetworkConnections { Utils.rollMockClock(0); // No ping pong happened yet. assertEquals(Long.MAX_VALUE, peer.getLastPingTime()); + assertEquals(Long.MAX_VALUE, peer.getPingTime()); ListenableFuture future = peer.ping(); Ping pingMsg = (Ping) outbound(); assertEquals(Long.MAX_VALUE, peer.getLastPingTime()); + assertEquals(Long.MAX_VALUE, peer.getPingTime()); assertFalse(future.isDone()); Utils.rollMockClock(5); + // The pong is returned. inbound(peer, new Pong(pingMsg.getNonce())); assertTrue(future.isDone()); long elapsed = future.get(); assertTrue("" + elapsed, elapsed > 1000); assertEquals(elapsed, peer.getLastPingTime()); + assertEquals(elapsed, peer.getPingTime()); + // Do it again and make sure it affects the average. + future = peer.ping(); + outbound(); + Utils.rollMockClock(50); + inbound(peer, new Pong(pingMsg.getNonce())); + elapsed = future.get(); + assertEquals(elapsed, peer.getLastPingTime()); + assertEquals(14000, peer.getPingTime()); } private Message outbound() { diff --git a/examples/src/main/java/com/google/bitcoin/examples/PeerMonitor.java b/examples/src/main/java/com/google/bitcoin/examples/PeerMonitor.java index 03d6cbba..b03a1f14 100644 --- a/examples/src/main/java/com/google/bitcoin/examples/PeerMonitor.java +++ b/examples/src/main/java/com/google/bitcoin/examples/PeerMonitor.java @@ -100,6 +100,7 @@ public class PeerMonitor { peerTableModel = new PeerTableModel(); JTable peerTable = new JTable(peerTableModel); + peerTable.setAutoCreateRowSorter(true); JScrollPane scrollPane = new JScrollPane(peerTable); window.getContentPane().add(scrollPane, BorderLayout.CENTER); window.pack(); @@ -120,6 +121,7 @@ public class PeerMonitor { private final int USER_AGENT = 2; private final int CHAIN_HEIGHT = 3; private final int PING_TIME = 4; + private final int LAST_PING_TIME = 5; public int getRowCount() { return peerGroup.numConnectedPeers(); @@ -132,13 +134,27 @@ public class PeerMonitor { case PROTOCOL_VERSION: return "Protocol version"; case USER_AGENT: return "User Agent"; case CHAIN_HEIGHT: return "Chain height"; - case PING_TIME: return "Ping time"; + case PING_TIME: return "Average ping"; + case LAST_PING_TIME: return "Last ping"; default: throw new RuntimeException(); } } public int getColumnCount() { - return 5; + return 6; + } + + public Class getColumnClass(int column) { + switch (column) { + case PROTOCOL_VERSION: + return Integer.class; + case CHAIN_HEIGHT: + case PING_TIME: + case LAST_PING_TIME: + return Long.class; + default: + return String.class; + } } public Object getValueAt(int row, int col) { @@ -154,11 +170,12 @@ public class PeerMonitor { case CHAIN_HEIGHT: return peer.getBestHeight(); case PING_TIME: - long time = peer.getLastPingTime(); + case LAST_PING_TIME: + long time = col == PING_TIME ? peer.getPingTime() : peer.getLastPingTime(); if (time == Long.MAX_VALUE) - return ""; + return 0L; else - return String.format("%d ms", time); + return time; default: throw new RuntimeException(); }