/* * Copyright 2013, The Sporting Exchange Limited * * 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 com.betfair.application.performance; import com.betfair.application.util.BaselineClientConstants; import com.betfair.application.util.HttpBodyBuilder; import com.betfair.application.util.HttpCallLogEntry; import com.betfair.application.util.HttpCallable; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.client.DefaultHttpClient; import java.io.*; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; public class BaselinePerformanceTester implements BaselineClientConstants { // REQUEST MODELLING private static final int SOAP_COUNT; private static final int REST_XML_COUNT; private static final int REST_JSON_COUNT; private static final int SIMPLE_GET; private static final int LARGE_GET; private static final int COMPLEX_MUTATOR; private static final int LARGE_POST; private static final int EXCEPTION; private static final int STYLES; private static final int GET_TIMEOUT; private static final int DATES; private static final int MAPS; private static int TOTAL_CALL_NUMBER; private static final int NUM_THREADS; private static final int NUM_CALLS; private static final int TRACE_EVERY; private static final String HOST; private static final int PORT; private static final String SOAP_ENDPOINT; private static final String REST_BASE; private static final boolean CHECK_LOG; private static final Map<String, String> METHOD_NAMES = new HashMap<String, String>(); static { // Read in the properties. try { Properties props = new Properties(); InputStream is = BaselinePerformanceTester.class.getResourceAsStream("/perftester/perftest.properties"); props.load(is); is.close(); HOST = props.getProperty("HOST"); PORT = Integer.parseInt(props.getProperty("PORT")); String nullPart = ""; if (Boolean.valueOf(props.getProperty("NULLTEST"))) { nullPart = "/null"; } SOAP_ENDPOINT = "http://"+HOST+":"+PORT+nullPart+"/BaselineService/v2.0"; REST_BASE = "http://"+HOST+":"+PORT+nullPart+"/baseline/v2.0/"; NUM_THREADS = Integer.parseInt(props.getProperty("NUM_THREADS")); NUM_CALLS = Integer.parseInt(props.getProperty("NUM_CALLS")); TRACE_EVERY = Integer.parseInt(props.getProperty("TRACE_EVERY")); SOAP_COUNT = Integer.parseInt(props.getProperty(SOAP)); REST_XML_COUNT = Integer.parseInt(props.getProperty("REST_XML")); REST_JSON_COUNT = Integer.parseInt(props.getProperty("REST_JSON")); SIMPLE_GET = Integer.parseInt(props.getProperty("SIMPLE_GET")); TOTAL_CALL_NUMBER += SIMPLE_GET; METHOD_NAMES.put("SIMPLE_GET", "testSimpleGet"); LARGE_GET = Integer.parseInt(props.getProperty("LARGE_GET")); TOTAL_CALL_NUMBER += LARGE_GET; METHOD_NAMES.put("LARGE_GET", "testLargeGet"); COMPLEX_MUTATOR = Integer.parseInt(props.getProperty("COMPLEX_MUTATOR")); TOTAL_CALL_NUMBER += COMPLEX_MUTATOR; METHOD_NAMES.put("COMPLEX_MUTATOR", "testComplexMutator"); LARGE_POST = Integer.parseInt(props.getProperty("LARGE_POST")); TOTAL_CALL_NUMBER += LARGE_POST; METHOD_NAMES.put("LARGE_POST", "testLargePost"); EXCEPTION = Integer.parseInt(props.getProperty("EXCEPTION")); TOTAL_CALL_NUMBER += EXCEPTION; METHOD_NAMES.put("EXCEPTION", "testException"); STYLES = Integer.parseInt(props.getProperty("STYLES")); TOTAL_CALL_NUMBER += STYLES; METHOD_NAMES.put("STYLES", "testParameterStyles"); GET_TIMEOUT = Integer.parseInt(props.getProperty("GET_TIMEOUT")); TOTAL_CALL_NUMBER += GET_TIMEOUT; METHOD_NAMES.put("GET_TIMEOUT", "testGetTimeout"); DATES = Integer.parseInt(props.getProperty("DATES")); TOTAL_CALL_NUMBER += DATES; METHOD_NAMES.put("DATES", "testDateRetrieval"); MAPS = Integer.parseInt(props.getProperty("MAPS")); TOTAL_CALL_NUMBER += MAPS; METHOD_NAMES.put("MAPS", "testMapRetrieval"); CHECK_LOG = Boolean.valueOf(props.getProperty("CHECK_LOG")); } catch (Exception e) { throw new RuntimeException("Failed to initialise tester", e); } } private static String CHARSET = "utf-8"; private static final Map<HttpCallLogEntry, CallLogCount> LOG = new TreeMap<HttpCallLogEntry, CallLogCount>(); private static final ExecutorService executor = (ExecutorService)Executors.newFixedThreadPool(NUM_THREADS); private static ThreadLocal<HttpClient> client = new ThreadLocal<HttpClient>(); private static RangeFinder<HttpCallable> requests = new RangeFinder<HttpCallable>(); private static RangeFinder<String> protocols = new RangeFinder<String>(); private static AtomicLong callsRemaining = new AtomicLong(0); private static AtomicLong callsMade = new AtomicLong(0); private static AtomicLong avgRandomiser = new AtomicLong(0); private static AtomicLong bytesReceived = new AtomicLong(0); private static AtomicLong totalLogRetries = new AtomicLong(0); private static void setup() { requests.addValue(SIMPLE_GET, new HttpCallable("SIMPLE_GET", REST_BASE+SIMPLE_GET_PATH, SOAP_ENDPOINT, HttpStatus.SC_OK, new HttpBodyBuilder(SIMPLE_GET_SOAP))); requests.addValue(LARGE_GET, new HttpCallable( "LARGE_GET", REST_BASE+LARGE_GET_PATH, SOAP_ENDPOINT, HttpStatus.SC_OK, new HttpBodyBuilder(LARGE_GET_SOAP))); requests.addValue(GET_TIMEOUT, new HttpCallable("TIMEOUT", REST_BASE+SIMPLE_TIMEOUT_PATH, SOAP_ENDPOINT, HttpStatus.SC_OK, new HttpBodyBuilder(TIMEOUT_SOAP))); requests.addValue(EXCEPTION, new HttpCallable( "EXCEPTION", REST_BASE+EXCEPTION_PATH_UNAUTHORISED, SOAP_ENDPOINT, HttpStatus.SC_UNAUTHORIZED, new HttpBodyBuilder(EXC_GET_SOAP))); requests.addValue(STYLES, new HttpCallable( "STYLES", REST_BASE+STYLES_PATH, SOAP_ENDPOINT, HttpStatus.SC_OK, new HttpBodyBuilder(STYLES_SOAP))); requests.addValue(DATES, new HttpCallable("DATES", REST_BASE+"dates", SOAP_ENDPOINT, new HttpBodyBuilder(DATES_BODY_JSON), new HttpBodyBuilder(DATES_BODY_XML), new HttpBodyBuilder(DATES_SOAP))); requests.addValue(COMPLEX_MUTATOR, new HttpCallable("COMPLEX_MUTATOR", REST_BASE+"complex", SOAP_ENDPOINT, new HttpBodyBuilder(COMPLEX_MUTATOR_BODY_JSON), new HttpBodyBuilder(COMPLEX_MUTATOR_BODY_XML), new HttpBodyBuilder(COMPLEX_MUTATOR_SOAP))); requests.addValue(LARGE_POST, new HttpCallable("LARGE_POST", REST_BASE+"large", SOAP_ENDPOINT, new HttpBodyBuilder(LARGE_POST_BODY_JSON_START, LARGE_POST_BODY_JSON_REPEAT, LARGE_POST_BODY_JSON_SEPARATOR, LARGE_POST_BODY_JSON_END), new HttpBodyBuilder(LARGE_POST_BODY_XML_START, LARGE_POST_BODY_XML_REPEAT, "", LARGE_POST_BODY_XML_END), new HttpBodyBuilder(LARGE_POST_SOAP_START, LARGE_POST_SOAP_REPEAT, "", LARGE_POST_SOAP_END))); requests.addValue(MAPS, new HttpCallable("MAPS", REST_BASE+"map1", SOAP_ENDPOINT, new HttpBodyBuilder(MAPS_BODY_JSON), new HttpBodyBuilder(MAPS_BODY_XML), new HttpBodyBuilder(MAPS_SOAP))); protocols.addValue(SOAP_COUNT, SOAP); protocols.addValue(SOAP_COUNT, SOAP); protocols.addValue(REST_XML_COUNT, APPLICATION_XML); protocols.addValue(REST_XML_COUNT, TEXT_XML); protocols.addValue(REST_JSON_COUNT, APPLICATION_JSON); protocols.addValue(REST_JSON_COUNT, TEXT_JSON); } public static void main(String[] args) { setup(); // Create an instance of HttpClient. Long time = System.currentTimeMillis(); final Random rnd = new Random(1); for (int i = 0; i < NUM_CALLS; i++) { callsRemaining.incrementAndGet(); executor.execute(new Runnable() { public void run() { makeRequest( getRequest(rnd), getContentType(rnd), rnd); } }); } long lastTime = callsRemaining.longValue(); while (callsRemaining.longValue() > 0) { try { Thread.sleep(1000); } catch (Exception ignored) {} if (lastTime - 1000 > callsRemaining.longValue()){ lastTime = callsRemaining.get(); System.out.print("."); } } time = System.currentTimeMillis() - time; System.out.println("Done."); executor.shutdown(); analyseCalls(time); } private static HttpCallable getRequest(Random rnd) { return requests.getValue(rnd.nextInt(requests.getMaxRange())); } private static String getContentType(Random rnd) { return protocols.getValue(rnd.nextInt(protocols.getMaxRange())); } private static void analyseCalls(long time) { Map<String, Map<HttpCallLogEntry, CallLogCount>> maps = new TreeMap<String, Map<HttpCallLogEntry,CallLogCount>>(); maps.put("SOAP ", new HashMap<HttpCallLogEntry, CallLogCount>()); maps.put("REST_XML ", new HashMap<HttpCallLogEntry, CallLogCount>()); maps.put("REST_JSON ", new HashMap<HttpCallLogEntry, CallLogCount>()); long totalCalls = 0; long totalCallTime = 0; for (Map.Entry<HttpCallLogEntry, CallLogCount> entry: LOG.entrySet()) { HttpCallLogEntry e = entry.getKey(); CallLogCount c = entry.getValue(); double callTimeMilli = c.aveCallTime() / 1000000d; System.out.format("%16s called %6d times with %2d failures in average time of %8.3f ms (%16s)\n", e.getMethod(), c.numCalls.get(), c.failures.get(), callTimeMilli, e.getProtocol()); totalCalls += c.numCalls.get(); totalCallTime += c.callTime.get(); if (e.getProtocol().equals(SOAP)) maps.get("SOAP ").put(e, c); if (e.getProtocol().endsWith("xml")) maps.get("REST_XML ").put(e, c); if (e.getProtocol().endsWith("json")) maps.get("REST_JSON ").put(e, c); } System.out.println(); for (Map.Entry<String, Map<HttpCallLogEntry, CallLogCount>> me: maps.entrySet()) { simpleSummary(me.getKey(), me.getValue()); } System.out.println(); System.out.format("Average randomiser : %d\n", avgRandomiser.longValue()/totalCalls); System.out.format("Average bytes/msg : %d\n", bytesReceived.longValue()/totalCalls); System.out.format("HTTP Threads : %d\n", NUM_THREADS); System.out.format("Total Log Retries : %d\n", totalLogRetries.longValue()); System.out.format("Total Calls made : %d\n", totalCalls); System.out.format("Average time per call : %3.3f ms\n", totalCallTime / (totalCalls * 1000000d)); System.out.format("Total Time taken : %3.3f seconds\n", time / 1000.0d); System.out.format("TPS : %d\n", (int) (totalCalls / (time / 1000.0d))); } public static void simpleSummary(String ident, Map<HttpCallLogEntry, CallLogCount> calls) { if (calls.isEmpty()) return; long totalCalls = 0; long totalCallTime = 0; for (Map.Entry<HttpCallLogEntry, CallLogCount> entry: calls.entrySet()) { CallLogCount c = entry.getValue(); totalCalls += c.numCalls.get(); totalCallTime += c.callTime.get(); } System.out.format("%20s: Total Calls: %6d, Ave time per call: %3.3f ms\n", ident, totalCalls,totalCallTime / (totalCalls * 1000000d)); } private static void makeRequest(HttpCallable call, String contentType, Random rnd) { HttpClient httpc = client.get(); if (httpc == null) { httpc = new DefaultHttpClient(); client.set(httpc); } HttpCallLogEntry cle = new HttpCallLogEntry(); int randomiser = rnd.nextInt(50); avgRandomiser.addAndGet(randomiser); HttpUriRequest method = call.getMethod(contentType, new Object[] { randomiser }, randomiser, cle); method.addHeader("Authority", "CoUGARUK"); CallLogCount loggedCount = null; // Execute the method. synchronized (LOG) { loggedCount = LOG.get(cle); if (loggedCount == null) { loggedCount = new CallLogCount(); LOG.put(cle, loggedCount); } } long nanoTime = System.nanoTime(); InputStream is = null; try { if (TRACE_EVERY > 0 && callsMade.incrementAndGet() % TRACE_EVERY == 0) { method.addHeader("X-Trace-Me", "true"); } loggedCount.numCalls.incrementAndGet(); final HttpResponse httpResponse = httpc.execute(method); int statusCode = httpResponse.getStatusLine().getStatusCode(); boolean failed = false; int expectedHTTPCode = call.expectedResult(); if (contentType.equals(SOAP) && expectedHTTPCode != HttpStatus.SC_OK) { // All SOAP errors are 500. expectedHTTPCode = HttpStatus.SC_INTERNAL_SERVER_ERROR; } if (statusCode != expectedHTTPCode) { System.err.println("Method failed: " + httpResponse.getStatusLine().getReasonPhrase()); failed = true; } // Read the response body. is = httpResponse.getEntity().getContent(); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); int b; while ((b = is.read()) != -1) { baos.write(b); } is.close(); String result = new String(baos.toByteArray(), CHARSET); if (result.length() == 0) { System.err.println("FAILURE: Empty buffer returned"); failed = true; } if (failed) { loggedCount.failures.incrementAndGet(); } bytesReceived.addAndGet(baos.toByteArray().length); if (CHECK_LOG && NUM_THREADS == 1) { File logFile = new File("C:\\perforce\\se\\development\\HEAD\\cougar\\cougar-framework\\baseline\\baseline-launch\\logs\\request-Baseline.log"); String lastLine = getLastLine(logFile); int tries = 0; while (!lastLine.contains(METHOD_NAMES.get(call.getName()))) { if (++tries > 5) { System.err.println("LOG FAIL: Call: "+METHOD_NAMES.get(call.getName())+", Line: "+lastLine); } else { try { Thread.sleep(1); } catch (InterruptedException e) {} } lastLine = getLastLine(logFile); } totalLogRetries.addAndGet(tries); } } catch (Exception e) { System.err.println("Fatal transport error: " + e.getMessage()); e.printStackTrace(); loggedCount.failures.incrementAndGet(); } finally { // Release the connection. if (is != null) { try { is.close(); } catch (IOException e) { /* ignore */} } callsRemaining.decrementAndGet(); nanoTime = System.nanoTime() - nanoTime; loggedCount.callTime.addAndGet(nanoTime); } } private static String getLastLine(File file) throws IOException { InputStreamReader streamReader = new InputStreamReader(new FileInputStream(file)); BufferedReader br = new BufferedReader(streamReader); String line = null; while (br.ready()) { line = br.readLine(); } return line; } private static class CallLogCount { private AtomicLong numCalls = new AtomicLong(0); private AtomicLong failures = new AtomicLong(0); private AtomicLong callTime = new AtomicLong(0); private long aveCallTime() { if (numCalls.longValue() == 0) return 0; return callTime.longValue() / (numCalls.longValue()); } } private static class RangeFinder<T> { TreeMap<Integer, T> map = new TreeMap<Integer, T>(); int rangeVal=0; public void addValue(int range, T t) { if (range == 0) return; rangeVal += range; map.put(rangeVal, t); } public int getMaxRange() { return rangeVal; } public T getValue(int point) { if (point < 0 || point >= rangeVal) { throw new IllegalArgumentException(); } for (Map.Entry<Integer, T> entry: map.entrySet()) { if (point < entry.getKey()) { return entry.getValue(); } } throw new IllegalStateException("Tits"); } } }