package org.limewire.rudp; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.LongHashMap; import org.limewire.rudp.messages.DataMessage; /**<p> * A variable sized block of packets to send or receive using UDP with * a possible out of order data. Data is accepted within a certain window size. * </p> * <p>Acknowledged data is released, whereas un-acknowledge data is maintained in * the <code>DataWindow</code>. * </p><p> * For readers, the data can be forwarded once missing data (aka holes) within the * window is received. * </p><p> * For the writer, if the round trip time (RTT) for ACK messages of the older * data is greatly exceeded ({@link #getRTTVar()}), the data can be resent to * try to receive an ACK message. *</p> * All methods in this class rely on external synchronization of access. */ /* TODO: DataMessage timing still requires work. */ public class DataWindow { private static final Log LOG = LogFactory.getLog(DataWindow.class); public static final int MAX_SEQUENCE_NUMBER = 0xFFFF; private static final int HIST_SIZE = 4; private static final float RTT_GAIN = 1.0f / 8.0f; private static final float DEVIATION_GAIN = 1.0f / 4.0f; private final LongHashMap<DataRecord> window; private long windowStart; private int windowSize; private long averageRTT; private long averageLowRTT; private int lowRTTCount; private float srtt; private float rttvar; private float rto; /** * optimization: use this instead of iterating through data to see * if there is any readable data. * must be set/unset when data is added/removed. */ private boolean readableData; /* * Define a data window for sending or receiving multiple udp packets * The size defines how much look ahead there is. Start is normally zero * or one. */ public DataWindow(int size, long start) { if (size < 1) { throw new IllegalArgumentException("size must be > 0"); } windowStart = start; windowSize = size; window = new LongHashMap<DataRecord>(size+2); } /** * Adds a new message to the window. */ public DataRecord addData(DataMessage msg) { if (windowStart > msg.getSequenceNumber()) { throw new IllegalStateException("message is not in current window: " + windowStart + " > " + msg.getSequenceNumber()); } long seqNo = msg.getSequenceNumber(); if (seqNo == windowStart) readableData = true; DataRecord d = window.get(seqNo); if (d != null) { if (LOG.isDebugEnabled()) LOG.debug("received duplicate message seq: " + msg.getSequenceNumber() + ", window start: " + windowStart); return d; } if (LOG.isDebugEnabled()) LOG.debug("adding message seq: " + msg.getSequenceNumber() + ", window start: " + windowStart); d = new DataRecord(msg); window.put(seqNo, d); return d; } /** * Get the block based on the sequenceNumber. */ public DataRecord getBlock(long pnum) { return window.get(pnum); } /** * Get the start of the data window. The start will generally be the * sequence number of the lowest un-ACK'ed message. */ public long getWindowStart() { return windowStart; } /** * Get the size of the data window. */ public int getWindowSize() { return windowSize; } /** * Get the number of slots in use. This excludes read data. */ public int getUsedSpots() { DataRecord d; int count = 0; for (long i = windowStart; i < windowStart+windowSize+3; i++) { // Count the spots that are full and not written if ( (d = window.get(i)) != null && (!d.read || i != windowStart)) count++; } return(count); } /** * Get the number of slots available to be used. */ public int getWindowSpace() { return(windowSize - getUsedSpots()); } /** * Calculate the average wait time of the N lowest unresponded to * blocks */ // public int calculateWaitTime(long time, int n) { // DataRecord d; // int count = 0; // long totalDelta = 0; // for (long i = windowStart; i < windowStart+windowSize+1; i++) { // d = window.get(i); // if ( d != null && d.acks == 0 ) { // count++; // totalDelta += time - d.sentTime; // if (count >= n) // break; // } // } // if (count > 0) // return(((int)totalDelta)/count); // else // return 0; // } /** * Clear out the acknowledged blocks at the beginning and advance the * window forward. Return the number of un-ACK'ed blocks. */ public int clearLowAckedBlocks(ChunkReleaser releaser) { DataRecord d; int count = 0; for (long i = windowStart; i < windowStart+windowSize+1; i++) { d = window.get(i); if ( d != null && d.acks > 0 ) { window.remove(i); count++; if(releaser != null) releaser.releaseChunk(d.msg.getChunk()); } else { break; } } windowStart += count; return count; } /** * From the window, find the number for the next block. * i.e. sequenceNumber */ public long getLowestUnsentBlock() { for (long i = windowStart; i < windowStart+windowSize+1; i++) { if (window.get(i) == null) return(i); } return -1; } /** * Count the number of ACKs from higher number blocks. * This should give you a hint that a block went missing. * Note that this assumes that the low block isn't ACK'ed since * it would get cleared if it was ACK'ed. */ public int countHigherAckBlocks() { DataRecord d; int count = 0; for (long i = windowStart+1; i < windowStart+windowSize+1; i++) { d = window.get(i); if ( d != null && d.acks > 0 ) { count++; } } return(count); } /** * If the sent data has not been ACK'ed for some multiple of * the RTO it looks like a message was lost. */ //For more information on Computing TCP's Retransmission Timer, see //http://www.ietf.org/rfc/rfc2988.txt //DataWindow is not TCP, but the rfc document is a good //resource to find similar methods. public boolean acksAppearToBeMissing(long time, int multiple) { int irto = (int) rto; // Check for first record being old DataRecord drec = getBlock(windowStart); if (irto > 0 && drec != null && drec.acks < 1 && drec.sentTime + (multiple * irto) < time) { return true; } return false; } /** * Return the RTO based on window data and ACKs. * <p> * The RTO is the duration of the retransmission timer which ensures data * delivery in the absence of any feedback from the remote data receiver. */ public int getRTO() { return (int)rto; } /** * Return the Round-Trip Time Variation (RTTVar) which is a measure of the * range of Round-Trip Time (RTT) values. */ public float getRTTVar() { return rttvar; } /** * Returns the SRRT estimate. The "smoothed" round-trip time estimate * is an attempt to predict future round-trip times by sampling * the behavior of packets sent over a connection and averaging those * samples. */ public float getSRTT() { return srtt; } /** * Return the current measure of low round trip time. */ public int lowRoundTripTime() { return (int) averageLowRTT; } /** * Record that a block was ACK'ed and calculate the * round trip time and averages from it. */ public void ackBlock(long pnum) { if (LOG.isDebugEnabled()) LOG.debug("entered ackBlock with # " + pnum); DataRecord drec = getBlock(pnum); if (drec != null) { drec.acks++; drec.ackTime = System.currentTimeMillis(); // delta = measuredRTT - srtt // srtt = srtt + g * delta // rttvar = rttvar + h*(abs(delta) - rttvar) // RTO = srtt + 4 * rttvar // delta is the difference between the measured RTT // and the current smoothed RTT estimator (srtt). // g is the gain applied to the RTT estimator and equals // 1/8. h is the gain applied to the mean deviation estimator // and equals 1/4. // Add to the averageRTT if (drec.acks == 1 && drec.sends == 1) { long rtt = (drec.ackTime - drec.sentTime); float delta = rtt - srtt; if (rtt > 0) { // Compute RTO if (srtt <= 0.1) srtt = delta; else srtt = srtt + RTT_GAIN * delta; rttvar = rttvar + DEVIATION_GAIN * (Math.abs(delta) - rttvar); rto = (float) (srtt + 4 * rttvar + 0.5); // Compute the average RTT if (averageRTT == 0) averageRTT = rtt; else { float avgRTT = ((float) (averageRTT * (HIST_SIZE - 1) + rtt)) / HIST_SIZE; averageRTT = (long) avgRTT; } // Compute a measure of the lowest RTT if (lowRTTCount < 10 || rtt < averageLowRTT) { if (averageLowRTT == 0) averageLowRTT = rtt; else { float lowRtt = ((float) (averageLowRTT * (HIST_SIZE - 1) + rtt)) / HIST_SIZE; averageLowRTT = (long) lowRtt; } lowRTTCount++; } } } } } /** * Record an ACK if not yet present for blocks up to the receiving * windowStart sent from the receiving connection. */ public void pseudoAckToReceiverWindow(long wStart) { // If the windowStart is old, just ignore it if (wStart <= windowStart) return; DataRecord drec; for (long i = windowStart; i < wStart; i++) { drec = getBlock(i); if (drec != null && drec.acks == 0) { // Presumably the ack got lost or is still incoming so ack it drec.acks++; // Create a fake ackTime since we don't know when it should be drec.ackTime = drec.sentTime + (int) rto; } } } /** * Get the oldest un-ACK'ed block. */ public DataRecord getOldestUnackedBlock() { DataRecord d; // Find the oldest block. DataRecord oldest = null; for (long i = windowStart; i < windowStart+windowSize+1; i++) { d = getBlock(i); if ( d != null ) { if ( d.acks == 0 && (oldest == null || d.sentTime < oldest.sentTime) ) { oldest = d; } } } return oldest; } /** * Checks if we have at least one block that can be read (in order). * Specifically, this will return false if there is at least one null or * already-read block nearer to the windowStart than the first non-null non-read * block. */ public boolean hasReadableData() { return readableData; } /** * Get a readable block. This will return the first unread block starting from * windowStart. */ public DataRecord getReadableBlock() { if (LOG.isDebugEnabled()) LOG.debug("wStart " + windowStart+" wSize "+windowSize); DataRecord d; // Find a readable block for (long i = windowStart; i < windowStart+windowSize+1; i++) { d = getBlock(i); if ( d != null ) { if (d.read) continue; else return d; } else { break; } } return null; } /** * To advance the window of the reader, higher blocks need to come in. * Once they do, older read blocks below the new window can be cleared. * Return the size of the window advancement. */ public int clearEarlyReadBlocks() { DataRecord d; int count = 0; // long maxBlock = windowStart+windowSize; // long newMaxBlock = maxBlock+windowSize; // long lastBlock = -1; // Find the last block /* for (int i = maxBlock; i < newMaxBlock; i++) { d = getBlock(i); if ( d != null ) lastBlock = i; } */ // Advance the window up to windowSize before lastBlock and clear old // blocks - This ensures that the data is successfully acked before // it is removed. Note: windowSpace must reflect the true // potential space. //for (int i = windowStart; i < lastBlock - windowSize + 1; i++) { for (long i = windowStart; i < windowStart + windowSize + 1; i++) { d = window.get(i); if ( d != null && d.read) { window.remove(i); count++; } else { if(d == null) readableData = false; else readableData = true; break; } } windowStart += count; return(count); } /** * Find the record that has been ACK'ed the most. */ // public DataRecord findMostAcked() { // DataRecord d; // DataRecord mostAcked = null; // // // Compare ack numbers // for (long i = windowStart; i < windowStart+windowSize+1; i++) { // d = getBlock(i); // if ( mostAcked == null ) { // mostAcked = d; // } else if ( d != null ) { // if (mostAcked.acks < d.acks) // mostAcked = d; // } // } // return mostAcked; // } /** * Find the number of unread records */ public int numNotRead() { DataRecord d; int count = 0; // Count the number of records not written for (long i = windowStart; i < windowStart + windowSize + 1; i++) { d = getBlock(i); if (d != null && !d.read) { count++; } } return count; } /** * Find the number of un-ACK'ed records */ // public int numNotAcked() { // DataRecord d; // int count = 0; // // // Count the number of records not acked // for (long i = windowStart; i < windowStart+windowSize+1; i++) { // d = getBlock(i); // if ( d != null && d.acks <=0) { // count++; // } // } // return count; // } } /** * Record information about data messages either getting written to the * network or getting read from the network. In the first case, the * ACKs is important. In the second case, the read state is important. * For writing, the sentTime and the ackTime form the basis for the * round trip time and a calculation for timeout resends. */ class DataRecord { final DataMessage msg; // the actual data message int sends; // count of the sends boolean read; // whether the data was read int acks; // count of the number of acks long sentTime; // when it was sent long ackTime; // when it was acked DataRecord(DataMessage msg) { this.msg=msg; } }