/* * Mojito Distributed Hash Table (Mojito DHT) * Copyright (C) 2006-2007 LimeWire LLC * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.limewire.mojito.util; import java.math.BigInteger; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.mojito.KUID; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.routing.RouteTable; import org.limewire.mojito.routing.RouteTable.SelectMode; import org.limewire.mojito.settings.ContextSettings; import org.limewire.mojito.settings.KademliaSettings; /** * An utility class to compute the approximate DHT size. * <p> * http://azureus.cvs.sourceforge.net/azureus/azureus2/com/aelitis/azureus/core/dht/control/impl/DHTControlImpl.java */ public class DHTSizeEstimator { private static final Log LOG = LogFactory.getLog(DHTSizeEstimator.class); private static final BigInteger MAXIMUM = KUID.MAXIMUM.toBigInteger(); private static final int MIN_NODE_COUNT = 3; /** History of local estimations. */ private List<BigInteger> localSizeHistory = new LinkedList<BigInteger>(); /** History of remote estimations (sizes we received with pongs). */ private List<BigInteger> remoteSizeHistory = new LinkedList<BigInteger>(); /** Current estimated size. */ private BigInteger estimatedSize = BigInteger.ZERO; /** The time when we made the last estimation */ private long localEstimateTime = 0L; /** The time when we updated the estimated DHT size. */ private long updateEstimatedSizeTime = 0L; /** * Clears the history and sets everything to * its initial state. */ public synchronized void clear() { estimatedSize = BigInteger.ZERO; localEstimateTime = 0L; updateEstimatedSizeTime = 0L; localSizeHistory.clear(); remoteSizeHistory.clear(); } /** * Returns the approximate DHT size. */ public synchronized BigInteger getEstimatedSize(RouteTable routeTable) { if (routeTable != null && (System.currentTimeMillis() - localEstimateTime) >= ContextSettings.ESTIMATE_NETWORK_SIZE_EVERY.getValue()) { SelectMode mode = SelectMode.ALL; if (ContextSettings.ESTIMATE_WITH_LIVE_NODES_ONLY.getValue()) { mode = SelectMode.ALIVE; } KUID localNodeId = routeTable.getLocalNode().getNodeID(); Collection<Contact> nodes = routeTable.select(localNodeId, KademliaSettings.REPLICATION_PARAMETER.getValue(), mode); updateSize(nodes); localEstimateTime = System.currentTimeMillis(); } return estimatedSize; } /** * Adds the approximate DHT size as returned by a remote Node. * The average of the remote DHT sizes is incorporated into into * our local computation. */ public synchronized void addEstimatedRemoteSize(BigInteger remoteSize) { if (!ContextSettings.COUNT_REMOTE_SIZE.getValue()) { // Clear the list of remotely estimated DHT sizes as they're // no longer needed. remoteSizeHistory.clear(); return; } if (remoteSize.compareTo(BigInteger.ZERO) == 0) { return; } if (remoteSize.compareTo(BigInteger.ZERO) < 0 || remoteSize.compareTo(MAXIMUM) > 0) { if (LOG.isWarnEnabled()) { LOG.warn(remoteSize + " is an illegal argument"); } return; } remoteSizeHistory.add(remoteSize); // Adjust the size of the List. The Setting is SIMPP-able // and may change! int maxRemoteHistorySize = ContextSettings.MAX_REMOTE_HISTORY_SIZE.getValue(); while(remoteSizeHistory.size() > maxRemoteHistorySize && !remoteSizeHistory.isEmpty()) { remoteSizeHistory.remove(0); } } /** * Updates the estimated DHT size with the given List of Contacts. * If <tt>nodes</tt> is null it will use the local RouteTable to * estimate the DHT size. */ public synchronized void updateSize(Collection<? extends Contact> nodes) { if ((System.currentTimeMillis() - updateEstimatedSizeTime) >= ContextSettings.UPDATE_NETWORK_SIZE_EVERY.getValue()) { if (nodes.size() >= MIN_NODE_COUNT) { estimatedSize = computeSize(nodes); updateEstimatedSizeTime = System.currentTimeMillis(); } } } /** * Computes and returns the approximate DHT size based * on the given List of Contacts. */ public synchronized BigInteger computeSize(Collection<? extends Contact> nodes) { // Works only with more than two Nodes if (nodes.size() < MIN_NODE_COUNT) { // There's always us! return BigInteger.ONE.max(BigInteger.valueOf(nodes.size())); } // Get the Iterator. We assume the Contacts are sorted by // their xor distance! Iterator<? extends Contact> contacts = nodes.iterator(); // See Azureus DHTControlImpl.estimateDHTSize() // Di = nearestId xor NodeIDi // Dc = sum(i * Di) / sum(i * i) // Size = 2**160 / Dc BigInteger sum1 = BigInteger.ZERO; BigInteger sum2 = BigInteger.ZERO; // The algorithm works relative to the ID space. KUID nearestId = contacts.next().getNodeID(); // We start 1 because the nearest Node is the 0th item! for (int i = 1; contacts.hasNext(); i++) { Contact node = contacts.next(); BigInteger distance = nearestId.xor(node.getNodeID()).toBigInteger(); BigInteger j = BigInteger.valueOf(i); sum1 = sum1.add(j.multiply(distance)); sum2 = sum2.add(j.pow(2)); } BigInteger estimatedSize = BigInteger.ZERO; if (!sum1.equals(BigInteger.ZERO)) { estimatedSize = KUID.MAXIMUM.toBigInteger().multiply(sum2).divide(sum1); } // And there is always us! estimatedSize = BigInteger.ONE.max(estimatedSize); // Get the average of the local estimations BigInteger localSize = BigInteger.ZERO; localSizeHistory.add(estimatedSize); // Adjust the size of the List. The Setting is SIMPP-able // and may change! int maxLocalHistorySize = ContextSettings.MAX_LOCAL_HISTORY_SIZE.getValue(); while(localSizeHistory.size() > maxLocalHistorySize && !localSizeHistory.isEmpty()) { localSizeHistory.remove(0); } if (!localSizeHistory.isEmpty()) { BigInteger localSizeSum = BigInteger.ZERO; for (BigInteger size : localSizeHistory) { localSizeSum = localSizeSum.add(size); } localSize = localSizeSum.divide(BigInteger.valueOf(localSizeHistory.size())); } // Get the combined average // S = (localEstimation + sum(remoteEstimation[i]))/count BigInteger combinedSize = localSize; if (ContextSettings.COUNT_REMOTE_SIZE.getValue()) { // Prune all duplicates and sort the values Set<BigInteger> remoteSizeSet = new TreeSet<BigInteger>(remoteSizeHistory); if (remoteSizeSet.size() >= 3) { BigInteger[] remote = remoteSizeSet.toArray(new BigInteger[0]); // Skip the smallest and largest values int count = 1; int skip = ContextSettings.SKIP_REMOTE_ESTIMATES.getValue(); for (int i = skip; (skip >= 0) && (i < (remote.length-skip)); i++) { combinedSize = combinedSize.add(remote[i]); count++; } combinedSize = combinedSize.divide(BigInteger.valueOf(count)); // Make sure we didn't exceed the MAXIMUM number as // we made an addition with the local estimation which // might be already 2**160 bit! combinedSize = combinedSize.min(MAXIMUM); } } // There is always us! return BigInteger.ONE.max(combinedSize); } }