diff --git a/simple/simple-common/src/main/java/org/simpleframework/common/LeastRecentlyUsedMap.java b/simple/simple-common/src/main/java/org/simpleframework/common/LeastRecentlyUsedMap.java new file mode 100644 index 0000000..ee1c5d1 --- /dev/null +++ b/simple/simple-common/src/main/java/org/simpleframework/common/LeastRecentlyUsedMap.java @@ -0,0 +1,131 @@ +/* + * LeastRecentlyUsedMap.java May 2007 + * + * Copyright (C) 2007, Niall Gallagher + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package org.simpleframework.common; + +import java.util.LinkedHashMap; +import java.util.Map.Entry; + +/** + * The LeastRecentlyUsedMap is a hash map that keeps only + * those entries most recently used. This acts much like a hot spot + * cache for specific keys that are used frequently. It also allows + * for algorithms to keep hot spot values available in the cache + * without the risk of running out of memory. + * + * @author Niall Gallagher + */ +public class LeastRecentlyUsedMap extends LinkedHashMap { + + /** + * This is the listener that is called when an entry is removed. + */ + private final RemovalListener listener; + + /** + * This is the number of items to keep within the cache. + */ + private final int capacity; + + /** + * Constructor for the LeastRecentlyUsedMap object. This + * creates a hash container that keeps only those entries that have + * been recently added or accessed available within the collection. + */ + public LeastRecentlyUsedMap() { + this(null); + } + + /** + * Constructor for the LeastRecentlyUsedMap object. This + * creates a hash container that keeps only those entries that have + * been recently added or accessed available within the collection. + * + * @param capacity this is the capacity of the hash container + */ + public LeastRecentlyUsedMap(int capacity) { + this(null, capacity); + } + + /** + * Constructor for the LeastRecentlyUsedMap object. This + * creates a hash container that keeps only those entries that have + * been recently added or accessed available within the collection. + * + * @param listener this listens for entries that are removed + */ + public LeastRecentlyUsedMap(RemovalListener listener) { + this(listener, 100); + } + + /** + * Constructor for the LeastRecentlyUsedMap object. This + * creates a hash container that keeps only those entries that have + * been recently added or accessed available within the collection. + * + * @param listener this listens for entries that are removed + * @param capacity this is the capacity of the hash container + */ + public LeastRecentlyUsedMap(RemovalListener listener, int capacity) { + this.listener = listener; + this.capacity = capacity; + } + + /** + * This is used to determine if an entry should be removed from the + * cache. If the cache has reached its capacity then the listener, + * if one was specified is given a callback to tell any other + * participating objects the entry has been removed. + * + * @param eldest this is the candidate for removal + */ + @Override + protected boolean removeEldestEntry(Entry eldest) { + int size = size(); + + if (size <= capacity) { + return false; + } + if (listener != null) { + V value = eldest.getValue(); + K key = eldest.getKey(); + + listener.notifyRemoved(key, value); + } + return true; + } + + /** + * The RemovalListener is used with the least recently + * used hash map to listen for removals. A callback is issued if + * an entry has been removed from the container because it was + * the least recently used entry. + */ + public static interface RemovalListener { + + /** + * This method is called when the entry has been removed due + * to the capacity having been reached. On removal any + * implementation can take action using the key or value. + * + * @param key this is the key of the removed entry + * @param value this is the value of the removed entry + */ + public void notifyRemoved(K key, V value); + } +} diff --git a/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java b/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java index 5c994ce..bf2a97a 100644 --- a/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java +++ b/simple/simple-http/src/main/java/org/simpleframework/http/message/SegmentConsumer.java @@ -448,7 +448,9 @@ protected void cookie(String value) { * @param value this is the value that is to be parsed */ protected void language(String value) { - language = new LanguageParser(value); + if(language == null) { + language = new LanguageParser(value); + } } /** @@ -460,7 +462,9 @@ protected void language(String value) { * @param value this is the content type value to parse */ protected void type(String value) { - type = new ContentTypeParser(value); + if(type == null) { + type = new ContentTypeParser(value); + } } /** @@ -472,7 +476,9 @@ protected void type(String value) { * @param value this is the content type value to parse */ protected void disposition(String value) { - disposition = new ContentDispositionParser(value); + if(disposition == null) { + disposition = new ContentDispositionParser(value); + } } /** @@ -483,7 +489,9 @@ protected void disposition(String value) { * @param value this is the value representing the encoding */ protected void encoding(String value) { - encoding = value; + if(encoding == null) { + encoding = value; + } } /** diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/ContainerPerformanceTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/ContainerPerformanceTest.java new file mode 100644 index 0000000..b558a6d --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/ContainerPerformanceTest.java @@ -0,0 +1,278 @@ +package org.simpleframework.http.message; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SocketChannel; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import junit.framework.TestCase; + +import org.simpleframework.http.Request; +import org.simpleframework.http.Response; +import org.simpleframework.http.core.Container; +import org.simpleframework.http.core.ContainerSocketProcessor; +import org.simpleframework.http.core.ThreadDumper; +import org.simpleframework.transport.connect.Connection; +import org.simpleframework.transport.connect.SocketConnection; +import org.simpleframework.transport.trace.Trace; +import org.simpleframework.transport.trace.TraceAnalyzer; + +public class ContainerPerformanceTest extends TestCase { + + private static final int ITERATIONS = 100000; + private static final int THREADS = 50; + + private static final byte[] SOURCE_1 = + ("GET /index.html HTTP/1.1\r\n"+ + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_2 = + ("GET /tmp/amazon_files/21lP7I1XB5L.jpg HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n" + + "\r\n").getBytes(); + + private static final byte[] SOURCE_3 = + ("GET /tmp/amazon_files/in-your-city-blue-large._V256095983_.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_4 = + ("GET /tmp/amazon_files/narrowtimer_transparent._V47062518_.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + private static final byte[] CLOSE = + ("GET /final_resource.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "Conection: close\r\n"+ + "\r\n").getBytes(); + + private static final byte[] RESPONSE_1 = + ("{'product': {\r\n"+ + " 'id': '1234',\r\n"+ + " 'name': 'AU3TB00001256',\r\n"+ + " 'values': {\r\n"+ + " 'best': [\r\n"+ + " {'bid': '13.344'},\r\n"+ + " {'offer': '12.1'},\r\n"+ + " {'volume': '100000'}\r\n"+ + " ]\r\n"+ + " }\r\n"+ + "}}").getBytes(); + + // push through as many valid HTTP/1.1 requests as possible + public void testPerformance() throws Exception { + final AtomicInteger counter = new AtomicInteger(ITERATIONS * THREADS * 4); + final CountDownLatch latch = new CountDownLatch(1); + List threads = new ArrayList(); + ThreadDumper dumper = new ThreadDumper(); + //TraceAnalyzer analyzer = new DebugTraceAnalyzer(counter, true); + TraceAnalyzer analyzer = new DebugTraceAnalyzer(counter, false); + CounterContainer container = new CounterContainer(counter, latch); + ContainerSocketProcessor processor = new ContainerSocketProcessor(container, 50, 1); + Connection connection = new SocketConnection(processor, analyzer); + InetSocketAddress address = (InetSocketAddress)connection.connect(null); // ephemeral port + Thread.sleep(1000); + + for(int i = 0; i < THREADS; i++) { + final SocketChannel client = SocketChannel.open(new InetSocketAddress("localhost", address.getPort())); + client.configureBlocking(true); + client.finishConnect(); + Thread.sleep(10); + + while(!client.finishConnect()) { + Thread.sleep(10); + } + System.err.println("connected="+client.isConnected()+" blocking="+client.isBlocking()); + + // read the HTTP/1.1 responses from the TCP stream so it does not fill the TCP window + Thread readThread = new Thread() { + public void run() { + try { + byte[] data = new byte[8192]; + + while(client.isConnected()) { + client.read(ByteBuffer.wrap(data)); + } + }catch(Exception e){ + e.printStackTrace(); + } + } + }; + + // write the HTTP/1.1 requests down the socket for the server to parse and dispatch + Thread writeThread = new Thread() { + public void run() { + try { + for(int i = 0; i < ITERATIONS; i++) { + client.write(ByteBuffer.wrap(SOURCE_1)); + client.write(ByteBuffer.wrap(SOURCE_2)); + client.write(ByteBuffer.wrap(SOURCE_3)); + client.write(ByteBuffer.wrap(SOURCE_4)); + } + client.write(ByteBuffer.wrap(CLOSE)); + client.close(); + } catch(Exception e){ + e.printStackTrace(); + } + } + }; + readThread.start(); + writeThread.start(); + threads.add(readThread); + threads.add(writeThread); + } + dumper.start(); + + // wait for all clients to finish + for(Thread thread : threads){ + thread.join(); + } + latch.await(); + connection.close(); + dumper.kill(); + } + + // This is a container that counts the callbacks/requests it gets + private class CounterContainer implements Container { + + private final AtomicInteger counter; + private final CountDownLatch latch; + private final long start; + private final int require; + + public CounterContainer(AtomicInteger counter, CountDownLatch latch) { + this.start = System.currentTimeMillis(); + this.require = counter.get(); + this.counter = counter; + this.latch = latch; + } + + public void handle(Request req, Response resp) { + try { + OutputStream out = resp.getOutputStream(); + String target = req.getPath().getPath(); // parse the HTTP request URI + + resp.setValue("Content-Type", "application/json"); + resp.setValue("Connection", "keep-alive"); + resp.setValue("X-Request-URI", target); + resp.setContentLength(RESPONSE_1.length); + out.write(RESPONSE_1); + out.close(); + + int count = counter.decrementAndGet(); + int total = require - count; + + if(total % 100000 == 0) { + long duration = System.currentTimeMillis() - start; + DecimalFormat format = new DecimalFormat("###,###,###,###.##"); + + System.err.println("Request: " + format.format(total) + " in " + format.format(duration) + " which is " + format.format(total / duration) + " per ms and "+format.format(total/(Math.max(duration,1.0)/1000.0))+" per second"); + } + if(count == 0){ + latch.countDown(); + } + }catch(Exception e){ + e.printStackTrace(); + } + } + } + + // This is just for debugging the I/O if needed + public class DebugTraceAnalyzer implements TraceAnalyzer { + + private final AtomicInteger counter; + private final boolean debug; + + public DebugTraceAnalyzer(AtomicInteger counter, boolean debug){ + this.counter = counter; + this.debug = debug; + } + + public Trace attach(SelectableChannel channel) { + return new DebugTrace(channel); + } + + public void stop() {} + + private class DebugTrace implements Trace { + + private final SelectableChannel channel; + + public DebugTrace(SelectableChannel channel) { + this.channel = channel; + } + + public void trace(Object event) { + if(debug) { + trace(event, ""); + } + } + + public void trace(Object event, Object value) { + if(debug) { + if(value instanceof Throwable) { + StringWriter writer = new StringWriter(); + PrintWriter out = new PrintWriter(writer); + ((Exception)value).printStackTrace(out); + out.flush(); + value = writer.toString(); + } + if(value != null && !String.valueOf(value).isEmpty()) { + System.err.printf("(%s) %s [%s] %s: %s%n", Thread.currentThread().getName(), channel, counter, event, value); + } else { + System.err.printf("(%s) %s [%s] %s%n", Thread.currentThread().getName(), channel, counter, event); + } + } + } + } + } + + public static void main(String[] list) throws Exception { + new ContainerPerformanceTest().testPerformance(); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerPerformanceTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerPerformanceTest.java new file mode 100644 index 0000000..7e75933 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerPerformanceTest.java @@ -0,0 +1,86 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.transport.ByteCursor; + +public class RequestConsumerPerformanceTest extends TestCase { + + private static final int ITERATIONS = 1000000; + + private static final byte[] SOURCE_1 = + ("POST /index.html HTTP/1.0\r\n"+ + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "Content-Length: 42\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_2 = + ("GET /tmp/amazon_files/21lP7I1XB5L.jpg HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n" + + "\r\n").getBytes(); + + private static final byte[] SOURCE_3 = + ("GET /tmp/amazon_files/in-your-city-blue-large._V256095983_.gif HTTP/1.1Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_4 = + ("GET /tmp/amazon_files/narrowtimer_transparent._V47062518_.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + public void testPerformance() throws Exception { + testPerformance(SOURCE_1, "/index.html"); + testPerformance(SOURCE_2, "/tmp/amazon_files/21lP7I1XB5L.jpg"); + testPerformance(SOURCE_3, "/tmp/amazon_files/in-your-city-blue-large._V256095983_.gif"); + testPerformance(SOURCE_4, "/tmp/amazon_files/narrowtimer_transparent._V47062518_.gif"); + } + + public void testPerformance(byte[] request, String path) throws Exception { + long start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + RequestConsumer header = new RequestConsumer(); + ByteCursor cursor = new StreamCursor(request); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getPath().getPath(), path); + } + long finish = System.currentTimeMillis(); + long duration = finish - start; + + System.err.printf("time=%s performance=%s (per/ms) performance=%s (per/sec) path=%s%n", duration, ITERATIONS/duration, ITERATIONS/(duration/1000), path); + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerTest.java similarity index 97% rename from simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java rename to simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerTest.java index ae9672f..a0fe422 100644 --- a/simple/simple-http/src/test/java/org/simpleframework/http/core/RequestConsumerTest.java +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/RequestConsumerTest.java @@ -1,5 +1,7 @@ -package org.simpleframework.http.core; +package org.simpleframework.http.message; +import org.simpleframework.http.core.DribbleCursor; +import org.simpleframework.http.core.StreamCursor; import org.simpleframework.http.message.RequestConsumer; import org.simpleframework.transport.ByteCursor; diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerPerformanceTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerPerformanceTest.java new file mode 100644 index 0000000..0bcc685 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/SegmentConsumerPerformanceTest.java @@ -0,0 +1,101 @@ +package org.simpleframework.http.message; + +import junit.framework.TestCase; + +import org.simpleframework.http.core.StreamCursor; +import org.simpleframework.transport.ByteCursor; + +public class SegmentConsumerPerformanceTest extends TestCase { + + private static final int ITERATIONS = 1000000; + + private static final String SOURCE_1 = + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "User-Agent:\r\n" + + "Content-Length: 42\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n"; + + private static final String SOURCE_2 = + "Host: ajax.googleapis.com\r\n"+ + "Connection: keep-alive\r\n"+ + "Pragma: no-cache\r\n"+ + "Cache-Control: no-cache\r\n"+ + "Accept: */*\r\n"+ + "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36\r\n"+ + "X-Client-Data: CKK2yQEIqbbJAQjEtskBCPCIygEI/ZXKAQi9mMoB\r\n"+ + "Referer: http://stackoverflow.com/questions/25033458/memory-consumed-by-a-thread\r\n"+ + "Accept-Encoding: gzip, deflate, sdch\r\n"+ + "Accept-Language: en-GB,en-US;q=0.8,en;q=0.6\r\n"+ + "\r\n"; + + + public void testEmptyHeaderComplex() throws Exception { + byte[] data = SOURCE_1.getBytes("UTF-8"); + + for(int i = 0; i < 4; i++) { + SegmentConsumer header = new SegmentConsumer(); + ByteCursor cursor = new StreamCursor(data); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + assertEquals(header.getValue("Pragma"), null); + assertEquals(header.getValue("User-Agent"), ""); + assertEquals(header.getValue("Content-Length"), "42"); + assertEquals(header.getValue("Content-Type"), "application/x-www-form-urlencoded"); + assertEquals(header.getValue("Host"), "some.host.com"); + assertEquals(header.getValues("Accept").size(), 4); + assertEquals(header.getValues("Accept").get(0), "image/gif"); + assertEquals(header.getValues("Accept").get(1), "image/png"); + assertEquals(header.getValues("Accept").get(2), "image/jpeg"); + assertEquals(header.getValues("Accept").get(3), "*"); + assertEquals(header.getContentType().getPrimary(), "application"); + assertEquals(header.getContentType().getSecondary(), "x-www-form-urlencoded"); + assertEquals(header.getTransferEncoding(), "chunked"); + } + for(int x = 0; x < 4; x++) { + data = SOURCE_1.getBytes("UTF-8"); + long start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + SegmentConsumer header = new SegmentConsumer(); + ByteCursor cursor = new StreamCursor(data); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + } + long finish = System.currentTimeMillis(); + long duration = finish - start; + + System.err.println("SOURCE_1 time: " + duration + " ms, performance: " + (ITERATIONS/duration)+" headers per ms and " + (ITERATIONS/(duration/1000)) + " per second"); + + data = SOURCE_2.getBytes("UTF-8"); + start = System.currentTimeMillis(); + + for(int i = 0; i < ITERATIONS; i++) { + SegmentConsumer header = new SegmentConsumer(); + ByteCursor cursor = new StreamCursor(data); + + while(!header.isFinished()) { + header.consume(cursor); + } + assertEquals(cursor.ready(), -1); + } + finish = System.currentTimeMillis(); + duration = finish - start; + + System.err.println("SOURCE_2 time: " + duration + " ms, performance: " + (ITERATIONS/duration)+" headers per ms and " + (ITERATIONS/(duration/1000)) + " per second"); + } + } +} diff --git a/simple/simple-http/src/test/java/org/simpleframework/http/message/SocketTransportPerformanceTest.java b/simple/simple-http/src/test/java/org/simpleframework/http/message/SocketTransportPerformanceTest.java new file mode 100644 index 0000000..0599618 --- /dev/null +++ b/simple/simple-http/src/test/java/org/simpleframework/http/message/SocketTransportPerformanceTest.java @@ -0,0 +1,152 @@ +package org.simpleframework.http.message; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.text.DecimalFormat; + +import junit.framework.TestCase; + +import org.simpleframework.common.thread.ConcurrentExecutor; +import org.simpleframework.http.core.ThreadDumper; +import org.simpleframework.transport.SocketTransport; +import org.simpleframework.transport.SocketWrapper; +import org.simpleframework.transport.TransportCursor; +import org.simpleframework.transport.reactor.ExecutorReactor; +import org.simpleframework.transport.reactor.Reactor; +import org.simpleframework.transport.trace.Trace; + +public class SocketTransportPerformanceTest extends TestCase { + + private static final int ITERATIONS = 1000000; + + private static final byte[] SOURCE_1 = + ("POST /index.html HTTP/1.0\r\n"+ + "Content-Type: application/x-www-form-urlencoded\r\n"+ + "Content-Length: 42\r\n"+ + "Transfer-Encoding: chunked\r\n"+ + "Accept: image/gif;q=1.0,\r\n image/jpeg;q=0.8,\r\n"+ + " \t\t image/png;\t\r\n\t"+ + " q=1.0,*;q=0.1\r\n"+ + "Accept-Language: fr;q=0.1, en-us;q=0.4, en-gb; q=0.8, en;q=0.7\r\n"+ + "Host: some.host.com \r\n"+ + "Cookie: $Version=1; UID=1234-5678; $Path=/; $Domain=.host.com\r\n"+ + "Cookie: $Version=1; NAME=\"Niall Gallagher\"; $path=\"/\"\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_2 = + ("GET /tmp/amazon_files/21lP7I1XB5L.jpg HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n" + + "\r\n").getBytes(); + + private static final byte[] SOURCE_3 = + ("GET /tmp/amazon_files/in-your-city-blue-large._V256095983_.gif HTTP/1.1Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + private static final byte[] SOURCE_4 = + ("GET /tmp/amazon_files/narrowtimer_transparent._V47062518_.gif HTTP/1.1\r\n"+ + "Accept-Encoding: gzip, deflate\r\n"+ + "Connection: keep-alive\r\n"+ + "Referer: http://localhost:9090/tmp/amazon.htm\r\n"+ + "Cache-Control: max-age=0\r\n"+ + "Host: localhost:9090\r\n"+ + "Accept-Language: en-US\r\n"+ + "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Version/3.1 Safari/525.13\r\n"+ + "Accept: */*\r\n"+ + "\r\n").getBytes(); + + public void testPerformance() throws Exception { + SocketAddress address = new InetSocketAddress("localhost", 44455); + ServerSocketChannel server = ServerSocketChannel.open(); + server.configureBlocking(false); + server.bind(address); + final SocketChannel writer = SocketChannel.open(address); + writer.configureBlocking(true); + writer.finishConnect(); + Thread.sleep(1000); + SocketChannel reader = server.accept(); + while(!reader.finishConnect()) { + Thread.sleep(1000); + } + while(!writer.finishConnect()) { + Thread.sleep(1000); + } + DecimalFormat format = new DecimalFormat("###,###,###,###,###.###"); + ConcurrentExecutor executor = new ConcurrentExecutor(SocketTransport.class, 10); + Reactor reactor = new ExecutorReactor(executor); + Trace trace = new MockTrace(); + SocketWrapper wrapper = new SocketWrapper(reader, trace); + SocketTransport transport = new SocketTransport(wrapper, reactor); + TransportCursor cursor = new TransportCursor(transport); + ThreadDumper dumper = new ThreadDumper(); + Thread thread = new Thread() { + public void run() { + try { + for(int i = 0; i < ITERATIONS; i++) { + writer.write(ByteBuffer.wrap(SOURCE_1)); + writer.write(ByteBuffer.wrap(SOURCE_2)); + writer.write(ByteBuffer.wrap(SOURCE_3)); + writer.write(ByteBuffer.wrap(SOURCE_4)); + } + writer.close(); + } catch(Exception e){ + e.printStackTrace(); + } + } + }; + dumper.start(); + thread.start(); + long start = System.currentTimeMillis(); + int count = 0; + + while(true) { + RequestConsumer header = new RequestConsumer(); + + while(!header.isFinished()) { + header.consume(cursor); + } + header.getPath().getPath(); // parse address also + int ready = cursor.ready(); + + if(count++ % 50000 == 0) { + System.err.println("Done: " +format.format(count) + " in " + (System.currentTimeMillis()-start) + " ms"); + } + if(ready == -1) { + break; + } + } + long finish = System.currentTimeMillis(); + long duration = finish - start; + + System.err.printf("count=%s time=%s performance=%s (per/ms) performance=%s (per/sec)s%n", format.format(count), duration, format.format(count/duration), format.format(count/(duration/1000))); + thread.join(); + executor.stop(); + reactor.stop(); + dumper.kill(); + } + + private static class MockTrace implements Trace { + public void trace(Object event) {} + public void trace(Object event, Object value) {} + } + + public static void main(String[] list) throws Exception { + new SocketTransportPerformanceTest().testPerformance(); + } +}