/* * 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.f1x.tools; import org.f1x.SessionIDBean; import org.f1x.api.FixInitiatorSettings; import org.f1x.api.FixVersion; import org.f1x.api.message.MessageBuilder; import org.f1x.api.message.MessageParser; import org.f1x.api.message.Tools; import org.f1x.api.message.fields.*; import org.f1x.api.session.SessionID; import org.f1x.api.session.SessionStatus; import org.f1x.v1.FixSessionInitiator; import org.gflogger.config.xml.XmlLogFactoryConfigurator; import java.io.IOException; import java.util.Arrays; import java.util.concurrent.TimeUnit; public class LatencyTestClient extends FixSessionInitiator { private static final long BASE_NANOTIME = System.nanoTime(); private final MessageBuilder mb; private final int intervalBetweenMessagesInNanos; private int orderCounter = 0; private final int [] LATENCIES; private int signalsReceived = -500; // warmup private static final int MICROS_TIMESTAMP_TAG = 8888; private static final MsgType TEST_MSG_TYPE = (true) ? MsgType.ORDER_SINGLE : MsgType.MARKET_DATA_SNAPSHOT_FULL_REFRESH; /** * @param messageRate messages per second */ public LatencyTestClient(String host, int port, SessionID sessionID, int messageRate, int count) { super(host, port, FixVersion.FIX44, sessionID, new FixInitiatorSettings()); getSettings().setResetSequenceNumbersOnEachLogon(true); LOGGER.info().append("Message rate: ").append(messageRate).append(" msg/sec").commit(); LATENCIES = new int [count]; mb = createMessageBuilder(); intervalBetweenMessagesInNanos = (int) TimeUnit.SECONDS.toNanos(1) / messageRate; } public void sendMessage () throws IOException { assert getSessionStatus() == SessionStatus.ApplicationConnected; if (TEST_MSG_TYPE == MsgType.ORDER_SINGLE) { mb.clear(); mb.setMessageType(MsgType.ORDER_SINGLE); mb.add(MICROS_TIMESTAMP_TAG, getMicrosecondClock()); // timestamp of signal creation mb.add(FixTags.ClOrdID, ++orderCounter); mb.add(FixTags.HandlInst, HandlInst.AUTOMATED_EXECUTION_ORDER_PRIVATE); mb.add(FixTags.OrderQty, 1); mb.add(FixTags.OrdType, OrdType.LIMIT); mb.add(FixTags.Price, 1.43); mb.add(FixTags.Side, Side.BUY); mb.add(FixTags.Symbol, "EUR/USD"); mb.add(FixTags.SecurityType, SecurityType.FOREIGN_EXCHANGE_CONTRACT); mb.add(FixTags.TimeInForce, TimeInForce.DAY); mb.add(76, "MARKET-FEED-SIM"); mb.add(FixTags.ExDestination, "#CANCEL-AFTER-OPEN"); mb.addUTCTimestamp(FixTags.TransactTime, System.currentTimeMillis()); } else { final int ORDER_BOOK_DEPTH = 4; mb.clear(); mb.setMessageType(MsgType.MARKET_DATA_SNAPSHOT_FULL_REFRESH); mb.add(FixTags.Symbol, "EUR/USD"); mb.add(FixTags.MDReqID, "dummyRequestId"); mb.add(MICROS_TIMESTAMP_TAG, getMicrosecondClock()); // timestamp of signal creation mb.add(FixTags.NoMDEntries, 2 * ORDER_BOOK_DEPTH); for (int i = 0; i < ORDER_BOOK_DEPTH; i++) { mb.add(FixTags.MDEntryType, MDEntryType.BID); mb.add(FixTags.MDEntryPx, 1.35); mb.add(FixTags.Currency, "EUR"); mb.add(FixTags.MDEntrySize, 10000); mb.add(FixTags.QuoteCondition, "A"); mb.add(FixTags.MDEntryOriginator, "Originator"); mb.add(FixTags.QuoteEntryID, "BID1"); mb.add(FixTags.MDEntryType, MDEntryType.OFFER); mb.add(FixTags.MDEntryPx, 0.74); mb.add(FixTags.Currency, "EUR"); mb.add(FixTags.MDEntrySize, 15000); mb.add(FixTags.QuoteCondition, "A"); mb.add(FixTags.MDEntryOriginator, "Originator"); mb.add(FixTags.QuoteEntryID, "OFFER1"); } } send(mb); } @Override protected void onSessionStatusChanged(SessionStatus oldStatus, SessionStatus newStatus) { super.onSessionStatusChanged(oldStatus, newStatus); if (newStatus == SessionStatus.ApplicationConnected) new Thread("Message Generator") { @Override public void run() { sendMessages(); } }.start(); } private void sendMessages () { while (getSessionStatus() == SessionStatus.ApplicationConnected) { try { long nextNanoTime = (intervalBetweenMessagesInNanos != 0) ? System.nanoTime() + intervalBetweenMessagesInNanos : 0; while (true) { //TODO: Was: while (active) if (intervalBetweenMessagesInNanos != 0) { if (System.nanoTime() < nextNanoTime) continue; // spin-wait } sendMessage(); nextNanoTime += intervalBetweenMessagesInNanos; } } catch (Throwable e) { e.printStackTrace(); break; } } } @Override protected void processInboundAppMessage(CharSequence msgType, int msgSeqNum, boolean possDup, MessageParser parser) throws IOException { if (Tools.equals(TEST_MSG_TYPE, msgType)) { while(parser.next()) { if (parser.getTagNum() == MICROS_TIMESTAMP_TAG) { recordLatency(parser.getIntValue()); // stored as getMicrosecondClock() break; } } } else { super.processInboundAppMessage(msgType, msgSeqNum, possDup, parser); } } private void recordLatency(int signalCreation) { int signalLatency = getMicrosecondClock() - signalCreation; ++signalsReceived; if (signalsReceived >= 0) { if (signalsReceived < LATENCIES.length) { LATENCIES[signalsReceived] = signalLatency; } else if (signalsReceived == LATENCIES.length) { printStats(); } } } private void printStats() { System.out.println("Experiment complete. Preparing latencies (in microseconds) ..."); Arrays.sort(LATENCIES); System.out.println("MIN: " + LATENCIES[0]); System.out.println("MAX: " + LATENCIES[LATENCIES.length -1]); System.out.println("MEDIAN:" + LATENCIES[LATENCIES.length / 2]); System.out.println("MEDIAN: " + LATENCIES[LATENCIES.length/2]); System.out.println("99.000%: " + LATENCIES[ (int) (99L*LATENCIES.length/100)]); System.out.println("99.900%: " + LATENCIES[ (int) (999L*LATENCIES.length/1000)]); System.out.println("99.990%: " + LATENCIES[ (int) (9999L*LATENCIES.length/10000)]); System.out.println("99.999%: " + LATENCIES[ (int)(99999L*LATENCIES.length/100000)]); System.out.println("99.9999%: " + LATENCIES[ (int)(999999L*LATENCIES.length/1000000)]); System.out.println("99.99999%:" + LATENCIES[ (int)(9999999L*LATENCIES.length/10000000)]); close(); System.exit(0); } public static void main (String [] args) throws InterruptedException, IOException { try { XmlLogFactoryConfigurator.configure(); } catch (Exception e) { e.printStackTrace(); } String host = args[0]; int port = Integer.parseInt(args[1]); int rate = args.length > 2 ? Integer.parseInt(args[2]) : 1000; int count = args.length > 3 ? Integer.parseInt(args[3]) : 1000000; final LatencyTestClient client = new LatencyTestClient(host, port, new SessionIDBean("CLIENT", "SERVER"), rate, count); final Thread acceptorThread = new Thread(client, "EchoClient"); acceptorThread.start(); Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { LOGGER.info().append("Exiting...").commit(); client.close(); } }); } private static int getMicrosecondClock() { return (int) ((System.nanoTime() - BASE_NANOTIME) / 1000L); } }