package edu.washington.cs.oneswarm.f2f.datagram; import static edu.washington.cs.oneswarm.f2f.datagram.DatagramConnection.MAX_DATAGRAM_PAYLOAD_SIZE; import static edu.washington.cs.oneswarm.f2f.datagram.DatagramConnection.MAX_DATAGRAM_SIZE; import static org.testng.Assert.assertEquals; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.LinkedList; import org.gudy.azureus2.core3.util.DirectByteBuffer; import org.gudy.azureus2.core3.util.DirectByteBufferPool; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.testng.Assert; import com.aelitis.azureus.core.peermanager.messaging.Message; import edu.washington.cs.oneswarm.f2f.datagram.DatagramConnection.ReceiveState; import edu.washington.cs.oneswarm.f2f.datagram.DatagramConnection.SendState; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FDatagramInit; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FDatagramOk; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessage; import edu.washington.cs.oneswarm.f2f.messaging.OSF2FMessageFactory; import edu.washington.cs.oneswarm.f2f.servicesharing.OSF2FServiceDataMsg; import edu.washington.cs.oneswarm.test.util.OneSwarmTestBase; import edu.washington.cs.oneswarm.test.util.TestUtils; public class DatagramConnectionTest extends OneSwarmTestBase { public static final byte AL = DirectByteBuffer.AL_MSG; public static final byte SS = DirectByteBuffer.SS_MSG; public static final int LOTS_OF_TOKENS = 100 * 1000 * 1000; // Leave room for 1 word channel_id + 2 word service header public static final int MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE = MAX_DATAGRAM_PAYLOAD_SIZE - 12; private static class MockDatagramConnectionManager implements DatagramConnectionManager { DatagramSocket socket; final String desc; private DatagramConnection conn; private final DatagramRateLimiter rateLimiter = new DatagramRateLimiter(); public MockDatagramConnectionManager(String desc) throws SocketException { this.socket = new DatagramSocket(); socket.setSoTimeout(500); this.desc = desc; rateLimiter.setTokenBucketSize(LOTS_OF_TOKENS); } @Override public void send(DatagramPacket packet, boolean lan) throws IOException { // System.out.println(desc + ": sending packet: " + // packet.getSocketAddress()); socket.send(packet); } @Override public void register(DatagramConnection connection) { conn = connection; rateLimiter.addQueue(connection); } @Override public int getPort() { return socket.getLocalPort(); } @Override public void deregister(DatagramConnection conn) { rateLimiter.removeQueue(conn); this.conn = null; } void receive() throws IOException { DatagramPacket p = new DatagramPacket(new byte[1450], 0, 1450); socket.receive(p); // System.out.println(desc + ": received packet: " + // p.getSocketAddress()); conn.messageReceived(p); } public void socketUpdated() throws IOException { int oldPort = this.socket.getLocalPort(); this.socket.close(); this.socket = new DatagramSocket(); System.out.println("port change: " + oldPort + "->" + socket.getLocalPort()); conn.reInitialize(); } @Override public DatagramRateLimiter getMainRateLimiter() { return rateLimiter; } @SuppressWarnings("unused") public String getDesc() { return this.desc; } } MockDatagramConnectionManager manager1; MockDatagramConnectionManager manager2; DatagramConnection conn1; DatagramConnection conn2; LinkedList<Message> conn1Incoming; LinkedList<Message> conn2Incoming; boolean skipOkPackets; public void setupLogging() { logFinest(DatagramEncrypter.logger); logFinest(DatagramDecrypter.logger); logFinest(DatagramConnection.logger); logFinest(DatagramRateLimitedChannelQueue.logger); logFinest(DatagramRateLimiter.logger); } @BeforeClass public static void setUpClass() throws Exception { OSF2FMessageFactory.init(); } @After public void tearDown() throws Exception { manager1.socket.close(); manager2.socket.close(); } @Before public void setUp() throws Exception { setupLogging(); skipOkPackets = true; final InetAddress localhost = InetAddress.getByName("127.0.0.1"); manager1 = new MockDatagramConnectionManager("1"); conn1Incoming = new LinkedList<Message>(); conn1 = new DatagramConnection(manager1, new DatagramListener() { @Override public void sendDatagramOk(OSF2FDatagramOk osf2fDatagramOk) { // Fake instant reception at conn2. conn2.okMessageReceived(); } @Override public InetAddress getRemoteIp() { return localhost; } @Override public void datagramDecoded(Message message, int size) { if (skipOkPackets && message instanceof OSF2FDatagramOk) { return; } conn1Incoming.add(message); } @Override public String toString() { return "1"; } @Override public void initDatagramConnection() { OSF2FDatagramInit init1 = conn1.createInitMessage(); conn2.initMessageReceived(init1); } @Override public boolean isLanLocal() { return false; } }); manager2 = new MockDatagramConnectionManager("2"); conn2Incoming = new LinkedList<Message>(); conn2 = new DatagramConnection(manager2, new DatagramListener() { @Override public void sendDatagramOk(OSF2FDatagramOk osf2fDatagramOk) { conn1.okMessageReceived(); } @Override public InetAddress getRemoteIp() { return localhost; } @Override public void datagramDecoded(Message message, int size) { if (skipOkPackets && message instanceof OSF2FDatagramOk) { return; } conn2Incoming.add(message); } @Override public String toString() { return "2"; } @Override public void initDatagramConnection() { OSF2FDatagramInit init2 = conn2.createInitMessage(); conn1.initMessageReceived(init2); } @Override public boolean isLanLocal() { return false; } }); // Send init packet from 1 to 2. conn1.reInitialize(); // And from 2 to 1. conn2.reInitialize(); // This should eventually result in an UDP packet getting sent from conn // 2 to conn 1. manager1.receive(); // When using a real friend connection the init message must arrive // before the OK message, but in the test the ok message will arrive // before so we need to trigger a manual ok message since the first one // was dropped. conn1.sendUdpOK(); manager2.receive(); Assert.assertEquals(conn1.sendState, SendState.ACTIVE); Assert.assertEquals(conn2.sendState, SendState.ACTIVE); Assert.assertEquals(conn1.receiveState, ReceiveState.ACTIVE); Assert.assertEquals(conn2.receiveState, ReceiveState.ACTIVE); } @Test public void testSendReceiveSimple() throws Exception { byte[] testData = "hello".getBytes(); OSF2FServiceDataMsg msg = createPacket(testData); conn1.sendMessage(msg); allocateRateLimitTokens(MAX_DATAGRAM_SIZE); manager2.receive(); checkPacket(testData); } private void allocateRateLimitTokens(int amount) { manager1.rateLimiter.refillBucket(amount); manager1.rateLimiter.allocateTokens(); } private void checkPacket(byte[] testData) { Assert.assertTrue(conn2Incoming.size() > 0, "No packets left."); OSF2FServiceDataMsg incoming = (OSF2FServiceDataMsg) conn2Incoming.removeFirst(); byte[] inData = new byte[incoming.getPayload().remaining(SS)]; incoming.getPayload().get(SS, inData); Assert.assertEquals(inData, testData); incoming.destroy(); } private int sequenceNumber = 0; private OSF2FServiceDataMsg createPacket(byte[] testData) { return createPacket(testData, 0); } private OSF2FServiceDataMsg createPacket(byte[] testData, int channel) { DirectByteBuffer data = DirectByteBufferPool.getBuffer(AL, MAX_DATAGRAM_PAYLOAD_SIZE); data.put(SS, testData); data.flip(SS); return createPacket(data, channel); } private OSF2FServiceDataMsg createPacket(DirectByteBuffer data) { return createPacket(data, 0); } private OSF2FServiceDataMsg createPacket(DirectByteBuffer data, int channel) { OSF2FServiceDataMsg msg = new OSF2FServiceDataMsg((byte) 0, channel, sequenceNumber++, (short) 1000, new int[0], data); return msg; } @Test public void testMultipleSimple() throws Exception { byte[] testData1 = "hello1".getBytes(); OSF2FServiceDataMsg msg1 = createPacket(testData1); byte[] testData2 = "hello2".getBytes(); OSF2FServiceDataMsg msg2 = createPacket(testData2); // Make sure that both are sent together. synchronized (conn1.encrypter) { conn1.sendMessage(msg1); conn1.sendMessage(msg2); } allocateRateLimitTokens(MAX_DATAGRAM_SIZE); // Only one call to receive (both messages must be in one packet). manager2.receive(); checkPacket(testData1); checkPacket(testData2); } @Test public void testMultipleOverfull() throws Exception { // Queue up 3 packets, the last should not fit in the first datagram. int saveRoomFor = 2 * (OSF2FMessage.MESSAGE_HEADER_LEN + OSF2FServiceDataMsg.BASE_LENGTH) - 1; byte[] testData1 = new byte[MAX_DATAGRAM_PAYLOAD_SIZE - OSF2FServiceDataMsg.BASE_LENGTH - saveRoomFor]; System.out.println(testData1.length); OSF2FServiceDataMsg msg1 = createPacket(testData1); OSF2FServiceDataMsg msg2 = createPacket(new byte[0]); OSF2FServiceDataMsg msg3 = createPacket(new byte[0]); // Make sure that all are sent together. synchronized (conn1.encrypter) { conn1.sendMessage(msg1); conn1.sendMessage(msg2); conn1.sendMessage(msg3); } allocateRateLimitTokens(2 * MAX_DATAGRAM_SIZE); // Only one call to receive (both messages must be in one packet). manager2.receive(); checkPacket(testData1); checkPacket(new byte[0]); Assert.assertEquals(conn2Incoming.size(), 0); allocateRateLimitTokens(MAX_DATAGRAM_SIZE); manager2.receive(); checkPacket(new byte[0]); } @Test public void testAllMinSize() throws Exception { // Make sure that all are sent together. skipOkPackets = false; int packets = MAX_DATAGRAM_PAYLOAD_SIZE / (OSF2FMessage.MESSAGE_HEADER_LEN) + 1 + 1; synchronized (conn1.encrypter) { for (int i = 0; i < packets; i++) { conn1.sendMessage(new OSF2FDatagramOk(0)); } } // Only one call to receive (both messages must be in one packet). manager2.receive(); Assert.assertEquals(conn2Incoming.size(), packets - 1); manager2.receive(); Assert.assertEquals(conn2Incoming.size(), packets); } @Test public void testSocketChange() throws Exception { byte[] testData1 = "hello1".getBytes(); OSF2FServiceDataMsg msg1 = createPacket(testData1); conn1.sendMessage(msg1); allocateRateLimitTokens(MAX_DATAGRAM_SIZE); manager2.receive(); checkPacket(testData1); System.err.println("NEW CONN INIT START"); manager1.socketUpdated(); manager2.receive(); System.err.println("NEW CONN INIT DONE"); byte[] testData2 = "hello22".getBytes(); OSF2FServiceDataMsg msg2 = createPacket(testData2); conn1.sendMessage(msg2); allocateRateLimitTokens(MAX_DATAGRAM_SIZE); manager2.receive(); checkPacket(testData2); } /** * done: time=5.67s speed=23.53MB/s ,17.65kpps * * @throws Exception */ @Test public void testSendPerformance() throws Exception { logInfo(DatagramEncrypter.logger); logInfo(DatagramDecrypter.logger); logInfo(DatagramConnection.logger); logInfo(DatagramRateLimiter.logger); logInfo(DatagramRateLimitedChannelQueue.logger); long startTime = System.currentTimeMillis(); final int packets = 200000; int length = MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE; final int TOKEN_BATCH_SIZE = 1000; for (int i = 0; i < packets; i++) { DirectByteBuffer data = DirectByteBufferPool.getBuffer(AL, length); OSF2FMessage message = createPacket(data); conn1.sendMessage(message); if (i % TOKEN_BATCH_SIZE == 0) { allocateRateLimitTokens(TOKEN_BATCH_SIZE * MAX_DATAGRAM_SIZE); } } double mb = length * packets / (1024 * 1024.0); double elapsed = (System.currentTimeMillis() - startTime) / 1000.0; while (conn1.queues.size() == 0 || !((DatagramRateLimitedChannelQueue) conn1.queues.get(0)).isEmpty()) { Thread.sleep(10); } while (conn1.sendThread.messageQueue.peek() != null) { Thread.sleep(10); } System.out.println(String.format("done: time=%.2fs speed=%.2fMB/s ,%.2fkpps", elapsed, mb / elapsed, packets / elapsed / 1000)); } /** * received 96.8 percent * done: time=12.77s speed=20.22MB/s ,15.66kpps * * @throws Exception */ @Test public void testReceivePerformance() throws Exception { logInfo(DatagramEncrypter.logger); logInfo(DatagramDecrypter.logger); logInfo(DatagramConnection.logger); logInfo(DatagramRateLimiter.logger); logInfo(DatagramRateLimitedChannelQueue.logger); long startTime = System.currentTimeMillis(); final int packets = 200000; final int length = MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE; final int TOKEN_BATCH_SIZE = 1000; Thread sendThread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < packets; i++) { DirectByteBuffer data = DirectByteBufferPool.getBuffer(AL, length); OSF2FMessage message = createPacket(data); conn1.sendMessage(message); if (i % TOKEN_BATCH_SIZE == 0) { allocateRateLimitTokens(TOKEN_BATCH_SIZE * MAX_DATAGRAM_SIZE); } } } }); sendThread.start(); int packet = 0; try { for (; packet < packets; packet++) { manager2.receive(); conn2Incoming.removeFirst().destroy(); } } catch (java.net.SocketTimeoutException e) { // Expected } double mb = length * packet / (1024 * 1024.0); double elapsed = (System.currentTimeMillis() - startTime) / 1000.0; double percentReceived = packet * 100.0 / packets; System.out.println(String.format("received %.1f percent", percentReceived)); System.out.println(String.format("done: time=%.2fs speed=%.2fMB/s ,%.2fkpps", elapsed, mb / elapsed, packets / elapsed / 1000)); Assert.assertTrue(percentReceived > 0.8, String.format("high packet loss (%.1f) percent", percentReceived)); } @Test public void testChannelRateLimitSimple() throws Exception { byte[] testData1 = "hello1".getBytes(); byte[] testData2 = "hello22".getBytes(); OSF2FServiceDataMsg msg1 = createPacket(testData1, 1); // Queue 2 packets, channel 1 before 2. conn1.sendMessage(msg1); OSF2FServiceDataMsg msg2 = createPacket(testData2, 2); conn1.sendMessage(msg2); // Give tokens to channel 2 first. conn1.queueMap.get(2).refillBucket(MAX_DATAGRAM_SIZE); // The give tokens to channel 1. conn1.queueMap.get(1).refillBucket(MAX_DATAGRAM_SIZE); // The packet on channel 2 should arrive first. manager2.receive(); checkPacket(testData2); manager2.receive(); checkPacket(testData1); // Check that it won't eat tokens if full. conn1.setTokenBucketSize(MAX_DATAGRAM_SIZE); conn1.refillBucket(MAX_DATAGRAM_SIZE); // No more tokens should be added. assertEquals(conn1.refillBucket(1), 0); conn1.allocateTokens(); conn1.refillBucket(MAX_DATAGRAM_SIZE); assertEquals(conn1.refillBucket(1), 0); } @Test public void testChannelFairSharing() throws Exception { int PACKET_NUM = 100; // Queue 2 packets to create channel 0 and 1 conn1.sendMessage(createPacket(new byte[MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE], 0)); conn1.sendMessage(createPacket(new byte[MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE], 1)); allocateRateLimitTokens(2 * MAX_DATAGRAM_SIZE); checkPacketChannel(0); checkPacketChannel(1); DatagramRateLimitedChannelQueue channel0RateLimiter = conn1.queueMap.get(0); DatagramRateLimitedChannelQueue channel1RateLimiter = conn1.queueMap.get(1); // After the queues are created we can change parameters. // Increase max queue size. channel0RateLimiter.maxQueueLength = PACKET_NUM * MAX_DATAGRAM_SIZE; channel1RateLimiter.maxQueueLength = PACKET_NUM * MAX_DATAGRAM_SIZE; // Fill channel 0 followed by channel 1. for (int i = 0; i < PACKET_NUM; i++) { if (i < PACKET_NUM / 2) { conn1.sendMessage(createPacket(new byte[MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE], 0)); } else { conn1.sendMessage(createPacket(new byte[MAX_CHANNEL_MESSAGE_PAYLOAD_SIZE], 1)); } } // Packets should arrive interleaved. for (int i = 0; i < PACKET_NUM; i++) { if (i % 2 == 0) { allocateRateLimitTokens(2 * MAX_DATAGRAM_SIZE); } if (!checkPacketChannel(i % 2)) { String msg = "Error at packet " + i + ", expected " + i % 2; System.err.println(msg); Assert.fail(msg); } } } private boolean checkPacketChannel(int channelId) throws IOException { manager2.receive(); OSF2FServiceDataMsg incoming = (OSF2FServiceDataMsg) conn2Incoming.removeFirst(); return incoming.getChannelId() == channelId; } /** Boilerplate code for running as executable. */ public static void main(String[] args) throws Exception { TestUtils.swtCompatibleTestRunner(DatagramConnectionTest.class); } }