| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| |
| package org.eclipse.jetty.spdy.server.http; |
| |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.util.Collections; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicReference; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import org.eclipse.jetty.client.HttpClient; |
| import org.eclipse.jetty.client.api.ContentResponse; |
| import org.eclipse.jetty.client.api.Response; |
| import org.eclipse.jetty.client.api.Result; |
| import org.eclipse.jetty.http.HttpHeader; |
| import org.eclipse.jetty.server.ConnectionFactory; |
| import org.eclipse.jetty.server.HttpConfiguration; |
| import org.eclipse.jetty.server.HttpConnectionFactory; |
| import org.eclipse.jetty.server.Request; |
| import org.eclipse.jetty.server.handler.AbstractHandler; |
| import org.eclipse.jetty.spdy.api.DataInfo; |
| import org.eclipse.jetty.spdy.api.GoAwayInfo; |
| import org.eclipse.jetty.spdy.api.SPDY; |
| import org.eclipse.jetty.spdy.api.Session; |
| import org.eclipse.jetty.spdy.api.SessionFrameListener; |
| import org.eclipse.jetty.spdy.api.Stream; |
| import org.eclipse.jetty.spdy.api.StreamFrameListener; |
| import org.eclipse.jetty.spdy.api.SynInfo; |
| import org.eclipse.jetty.spdy.http.HTTPSPDYHeader; |
| import org.eclipse.jetty.util.Fields; |
| import org.junit.Assert; |
| import org.junit.Ignore; |
| import org.junit.Test; |
| |
| @Ignore("make this test pass") // TODO |
| public class PushStrategyBenchmarkTest extends AbstractHTTPSPDYTest |
| { |
| // Sample resources size from webtide.com home page |
| private final int[] htmlResources = new int[] |
| {8 * 1024}; |
| private final int[] cssResources = new int[] |
| {12 * 1024, 2 * 1024}; |
| private final int[] jsResources = new int[] |
| {75 * 1024, 24 * 1024, 36 * 1024}; |
| private final int[] pngResources = new int[] |
| {1024, 45 * 1024, 6 * 1024, 2 * 1024, 2 * 1024, 2 * 1024, 3 * 1024, 512, 512, 19 * 1024, 512, 128, 32}; |
| private final Set<String> pushedResources = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); |
| private final AtomicReference<CountDownLatch> latch = new AtomicReference<>(); |
| private final long roundtrip = 100; |
| private final int runs = 10; |
| |
| public PushStrategyBenchmarkTest(short version) |
| { |
| super(version); |
| } |
| |
| @Test |
| public void benchmarkPushStrategy() throws Exception |
| { |
| InetSocketAddress address = startHTTPServer(version, new PushStrategyBenchmarkHandler(), 30000); |
| |
| // Plain HTTP |
| ConnectionFactory factory = new HttpConnectionFactory(new HttpConfiguration()); |
| connector.setDefaultProtocol(factory.getProtocol()); |
| HttpClient httpClient = new HttpClient(); |
| // Simulate browsers, that open 6 connection per origin |
| httpClient.setMaxConnectionsPerDestination(6); |
| httpClient.start(); |
| benchmarkHTTP(httpClient); |
| httpClient.stop(); |
| |
| // First push strategy |
| PushStrategy pushStrategy = new PushStrategy.None(); |
| factory = new HTTPSPDYServerConnectionFactory(version, new HttpConfiguration(), pushStrategy); |
| connector.setDefaultProtocol(factory.getProtocol()); |
| Session session = startClient(version, address, new ClientSessionFrameListener()); |
| benchmarkSPDY(pushStrategy, session); |
| session.goAway(new GoAwayInfo(5, TimeUnit.SECONDS)); |
| |
| // Second push strategy |
| pushStrategy = new ReferrerPushStrategy(); |
| factory = new HTTPSPDYServerConnectionFactory(version, new HttpConfiguration(), pushStrategy); |
| connector.setDefaultProtocol(factory.getProtocol()); |
| session = startClient(version, address, new ClientSessionFrameListener()); |
| benchmarkSPDY(pushStrategy, session); |
| session.goAway(new GoAwayInfo(5, TimeUnit.SECONDS)); |
| } |
| |
| private void benchmarkHTTP(HttpClient httpClient) throws Exception |
| { |
| // Warm up |
| performHTTPRequests(httpClient); |
| performHTTPRequests(httpClient); |
| |
| long total = 0; |
| for (int i = 0; i < runs; ++i) |
| { |
| long begin = System.nanoTime(); |
| int requests = performHTTPRequests(httpClient); |
| long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin); |
| total += elapsed; |
| System.err.printf("HTTP: run %d, %d request(s), roundtrip delay %d ms, elapsed = %d%n", |
| i, requests, roundtrip, elapsed); |
| } |
| System.err.printf("HTTP: roundtrip delay %d ms, average = %d%n%n", |
| roundtrip, total / runs); |
| } |
| |
| private int performHTTPRequests(HttpClient httpClient) throws Exception |
| { |
| int result = 0; |
| |
| for (int j = 0; j < htmlResources.length; ++j) |
| { |
| latch.set(new CountDownLatch(cssResources.length + jsResources.length + pngResources.length)); |
| |
| String primaryPath = "/" + j + ".html"; |
| String referrer = "http://localhost:" + connector.getLocalPort() + primaryPath; |
| ++result; |
| ContentResponse response = httpClient.newRequest("localhost", connector.getLocalPort()) |
| .path(primaryPath) |
| .timeout(5, TimeUnit.SECONDS) |
| .send(); |
| Assert.assertEquals(200, response.getStatus()); |
| |
| for (int i = 0; i < cssResources.length; ++i) |
| { |
| String path = "/" + i + ".css"; |
| ++result; |
| httpClient.newRequest("localhost", connector.getLocalPort()) |
| .path(path) |
| .header(HttpHeader.REFERER, referrer) |
| .send(new TestListener()); |
| } |
| for (int i = 0; i < jsResources.length; ++i) |
| { |
| String path = "/" + i + ".js"; |
| ++result; |
| httpClient.newRequest("localhost", connector.getLocalPort()) |
| .path(path) |
| .header(HttpHeader.REFERER, referrer) |
| .send(new TestListener()); |
| } |
| for (int i = 0; i < pngResources.length; ++i) |
| { |
| String path = "/" + i + ".png"; |
| ++result; |
| httpClient.newRequest("localhost", connector.getLocalPort()) |
| .path(path) |
| .header(HttpHeader.REFERER, referrer) |
| .send(new TestListener()); |
| } |
| |
| Assert.assertTrue(latch.get().await(5, TimeUnit.SECONDS)); |
| } |
| |
| return result; |
| } |
| |
| private void benchmarkSPDY(PushStrategy pushStrategy, Session session) throws Exception |
| { |
| // Warm up PushStrategy |
| performRequests(session); |
| performRequests(session); |
| |
| long total = 0; |
| for (int i = 0; i < runs; ++i) |
| { |
| long begin = System.nanoTime(); |
| int requests = performRequests(session); |
| long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - begin); |
| total += elapsed; |
| System.err.printf("SPDY(%s): run %d, %d request(s), roundtrip delay %d ms, elapsed = %d%n", |
| pushStrategy.getClass().getSimpleName(), i, requests, roundtrip, elapsed); |
| } |
| System.err.printf("SPDY(%s): roundtrip delay %d ms, average = %d%n%n", |
| pushStrategy.getClass().getSimpleName(), roundtrip, total / runs); |
| } |
| |
| private int performRequests(Session session) throws Exception |
| { |
| int result = 0; |
| |
| for (int j = 0; j < htmlResources.length; ++j) |
| { |
| latch.set(new CountDownLatch(cssResources.length + jsResources.length + pngResources.length)); |
| pushedResources.clear(); |
| |
| String primaryPath = "/" + j + ".html"; |
| String referrer = "http://localhost:" + connector.getLocalPort() + primaryPath; |
| Fields headers = new Fields(); |
| headers.put(HTTPSPDYHeader.METHOD.name(version), "GET"); |
| headers.put(HTTPSPDYHeader.URI.name(version), primaryPath); |
| headers.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1"); |
| headers.put(HTTPSPDYHeader.SCHEME.name(version), "http"); |
| headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + connector.getLocalPort()); |
| // Wait for the HTML to simulate browser's behavior |
| ++result; |
| final CountDownLatch htmlLatch = new CountDownLatch(1); |
| session.syn(new SynInfo(headers, true), new StreamFrameListener.Adapter() |
| { |
| @Override |
| public void onData(Stream stream, DataInfo dataInfo) |
| { |
| dataInfo.consume(dataInfo.length()); |
| if (dataInfo.isClose()) |
| htmlLatch.countDown(); |
| } |
| }); |
| Assert.assertTrue(htmlLatch.await(5, TimeUnit.SECONDS)); |
| |
| for (int i = 0; i < cssResources.length; ++i) |
| { |
| String path = "/" + i + ".css"; |
| if (pushedResources.contains(path)) |
| continue; |
| headers = createRequestHeaders(referrer, path); |
| ++result; |
| session.syn(new SynInfo(headers, true), new DataListener()); |
| } |
| for (int i = 0; i < jsResources.length; ++i) |
| { |
| String path = "/" + i + ".js"; |
| if (pushedResources.contains(path)) |
| continue; |
| headers = createRequestHeaders(referrer, path); |
| ++result; |
| session.syn(new SynInfo(headers, true), new DataListener()); |
| } |
| for (int i = 0; i < pngResources.length; ++i) |
| { |
| String path = "/" + i + ".png"; |
| if (pushedResources.contains(path)) |
| continue; |
| headers = createRequestHeaders(referrer, path); |
| ++result; |
| session.syn(new SynInfo(headers, true), new DataListener()); |
| } |
| |
| Assert.assertTrue(latch.get().await(5, TimeUnit.SECONDS)); |
| } |
| |
| return result; |
| } |
| |
| private Fields createRequestHeaders(String referrer, String path) |
| { |
| Fields headers; |
| headers = new Fields(); |
| headers.put(HTTPSPDYHeader.METHOD.name(version), "GET"); |
| headers.put(HTTPSPDYHeader.URI.name(version), path); |
| headers.put(HTTPSPDYHeader.VERSION.name(version), "HTTP/1.1"); |
| headers.put(HTTPSPDYHeader.SCHEME.name(version), "http"); |
| headers.put(HTTPSPDYHeader.HOST.name(version), "localhost:" + connector.getLocalPort()); |
| headers.put("referer", referrer); |
| return headers; |
| } |
| |
| private void sleep(long delay) throws ServletException |
| { |
| try |
| { |
| TimeUnit.MILLISECONDS.sleep(delay); |
| } |
| catch (InterruptedException x) |
| { |
| throw new ServletException(x); |
| } |
| } |
| |
| private class PushStrategyBenchmarkHandler extends AbstractHandler |
| { |
| @Override |
| public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException |
| { |
| baseRequest.setHandled(true); |
| |
| // Sleep half of the roundtrip time, to simulate the delay of responses, even for pushed resources |
| sleep(roundtrip / 2); |
| // If it's not a pushed resource, sleep half of the roundtrip time, to simulate the delay of requests |
| if (request.getHeader("x-spdy-push") == null) |
| sleep(roundtrip / 2); |
| |
| String suffix = target.substring(target.indexOf('.') + 1); |
| int index = Integer.parseInt(target.substring(1, target.length() - suffix.length() - 1)); |
| |
| int contentLength; |
| String contentType; |
| switch (suffix) |
| { |
| case "html": |
| contentLength = htmlResources[index]; |
| contentType = "text/html"; |
| break; |
| case "css": |
| contentLength = cssResources[index]; |
| contentType = "text/css"; |
| break; |
| case "js": |
| contentLength = jsResources[index]; |
| contentType = "text/javascript"; |
| break; |
| case "png": |
| contentLength = pngResources[index]; |
| contentType = "image/png"; |
| break; |
| default: |
| throw new ServletException(); |
| } |
| |
| response.setContentType(contentType); |
| response.setContentLength(contentLength); |
| response.getOutputStream().write(new byte[contentLength]); |
| } |
| } |
| |
| private void addPushedResource(String pushedURI) |
| { |
| switch (version) |
| { |
| case SPDY.V2: |
| { |
| Matcher matcher = Pattern.compile("https?://[^:]+:\\d+(/.*)").matcher(pushedURI); |
| Assert.assertTrue(matcher.matches()); |
| pushedResources.add(matcher.group(1)); |
| break; |
| } |
| case SPDY.V3: |
| { |
| pushedResources.add(pushedURI); |
| break; |
| } |
| default: |
| { |
| throw new IllegalStateException(); |
| } |
| } |
| } |
| |
| private class ClientSessionFrameListener extends SessionFrameListener.Adapter |
| { |
| @Override |
| public StreamFrameListener onSyn(Stream stream, SynInfo synInfo) |
| { |
| String path = synInfo.getHeaders().get(HTTPSPDYHeader.URI.name(version)).getValue(); |
| addPushedResource(path); |
| return new DataListener(); |
| } |
| } |
| |
| private class DataListener extends StreamFrameListener.Adapter |
| { |
| @Override |
| public void onData(Stream stream, DataInfo dataInfo) |
| { |
| dataInfo.consume(dataInfo.length()); |
| if (dataInfo.isClose()) |
| latch.get().countDown(); |
| } |
| } |
| |
| private class TestListener extends Response.Listener.Adapter |
| { |
| @Override |
| public void onComplete(Result result) |
| { |
| if (!result.isFailed()) |
| latch.get().countDown(); |
| } |
| } |
| } |