/*
* Copyright 2016 higherfrequencytrading.com
*
* 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 net.openhft.chronicle.engine.mit;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.OS;
import net.openhft.chronicle.core.threads.ThreadDump;
import net.openhft.chronicle.engine.api.map.KeyValueStore;
import net.openhft.chronicle.engine.api.map.MapEvent;
import net.openhft.chronicle.engine.api.map.MapEventListener;
import net.openhft.chronicle.engine.api.map.MapView;
import net.openhft.chronicle.engine.api.pubsub.InvalidSubscriberException;
import net.openhft.chronicle.engine.api.pubsub.Subscriber;
import net.openhft.chronicle.engine.api.pubsub.SubscriptionCollection;
import net.openhft.chronicle.engine.api.pubsub.TopicSubscriber;
import net.openhft.chronicle.engine.api.tree.Asset;
import net.openhft.chronicle.engine.map.ChronicleMapKeyValueStore;
import net.openhft.chronicle.engine.map.KVSSubscription;
import net.openhft.chronicle.engine.map.VanillaMapView;
import net.openhft.chronicle.engine.server.ServerEndpoint;
import net.openhft.chronicle.engine.tree.VanillaAssetTree;
import net.openhft.chronicle.network.TCPRegistry;
import net.openhft.chronicle.network.connection.TcpChannelHub;
import net.openhft.chronicle.wire.WireType;
import net.openhft.chronicle.wire.YamlLogging;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.IntStream;
@Ignore("Long running test")
public class RemoteSubscriptionModelPerformanceTest {
//TODO DS test having the server side on another machine
private static final int _noOfPuts = 50;
private static final int _noOfRunsToAverage = Boolean.parseBoolean(System.getProperty("quick", "true")) ? 2 : 100;
private static final long _secondInNanos = 1_000_000_000L;
private static final AtomicInteger counter = new AtomicInteger();
private static String _twoMbTestString;
private static int _twoMbTestStringLength;
private static Map<String, String> _testMap;
private static VanillaAssetTree serverAssetTree, clientAssetTree;
private static ServerEndpoint serverEndpoint;
@NotNull
private static AtomicReference<Throwable> t = new AtomicReference();
private final String _mapName = "PerfTestMap" + counter.incrementAndGet();
private ThreadDump threadDump;
@BeforeClass
public static void setUpBeforeClass() throws IOException {
YamlLogging.setAll(false);
@NotNull char[] chars = new char[2 << 20];
Arrays.fill(chars, '~');
_twoMbTestString = new String(chars);
_twoMbTestStringLength = _twoMbTestString.length();
serverAssetTree = new VanillaAssetTree(1).forTesting();
//The following line doesn't add anything and breaks subscriptions
serverAssetTree.root().addWrappingRule(MapView.class, "map directly to KeyValueStore", VanillaMapView::new, KeyValueStore.class);
serverAssetTree.root().addLeafRule(KeyValueStore.class, "use Chronicle Map", (context, asset) ->
new ChronicleMapKeyValueStore(context.basePath(OS.TARGET).entries(_noOfPuts).averageValueSize(_twoMbTestStringLength), asset));
TCPRegistry.createServerSocketChannelFor("RemoteSubscriptionModelPerformanceTest.port");
serverEndpoint = new ServerEndpoint("RemoteSubscriptionModelPerformanceTest.port", serverAssetTree);
clientAssetTree = new VanillaAssetTree(13).forRemoteAccess("RemoteSubscriptionModelPerformanceTest.port", WireType.BINARY);
}
@AfterClass
public static void tearDownAfterClass() {
clientAssetTree.close();
serverEndpoint.close();
serverAssetTree.close();
TcpChannelHub.closeAllHubs();
TCPRegistry.reset();
}
@After
public void afterMethod() {
final Throwable th = t.getAndSet(null);
if (th != null) throw Jvm.rethrow(th);
}
@Before
public void threadDump() {
threadDump = new ThreadDump();
}
@After
public void checkThreadDump() {
threadDump.assertNoNewThreads();
}
@Before
public void setUp() throws IOException {
Files.deleteIfExists(Paths.get(OS.TARGET, _mapName));
_testMap = clientAssetTree.acquireMap(_mapName + "?putReturnsNull=true", String.class, String.class);
_testMap.clear();
}
@After
public void tearDown() throws IOException {
// System.out.println("Native memory used "+OS.memory().nativeMemoryUsed());
// System.gc();
}
/**
* Test that listening to events for a given key can handle 50 updates per second of 2 MB string values.
*/
@Test
public void testGetPerformance() {
_testMap.clear();
IntStream.range(0, _noOfPuts).forEach(i ->
_testMap.put(TestUtils.getKey(_mapName, i), _twoMbTestString));
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> _testMap.size(), () -> {
IntStream.range(0, _noOfPuts).forEach(i ->
_testMap.get(TestUtils.getKey(_mapName, i)));
}, _noOfRunsToAverage, _secondInNanos * 3 / 2);
}
/**
* Test that 50 updates per second of 2 MB string values completes in 1 second.
*/
@Test
public void testPutPerformance() {
_testMap.clear();
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> _testMap.size(), () -> {
IntStream.range(0, _noOfPuts).forEach(i ->
_testMap.put(TestUtils.getKey(_mapName, i), _twoMbTestString));
}, _noOfRunsToAverage, _secondInNanos);
}
/**
* Test that listening to events for a given key can handle 50 updates per second of 2 MB string
* values.
*/
@Test
public void testSubscriptionMapEventOnKeyPerformance() {
_testMap.clear();
String key = TestUtils.getKey(_mapName, 0);
//Create subscriber and register
//Add 4 for the number of puts that is added to the string
@NotNull TestChronicleKeyEventSubscriber keyEventSubscriber = new TestChronicleKeyEventSubscriber(_twoMbTestStringLength);
clientAssetTree.registerSubscriber(_mapName + "/" + key + "?bootstrap=false", String.class, keyEventSubscriber);
Jvm.pause(100);
Asset child = serverAssetTree.getAsset(_mapName).getChild(key);
Assert.assertNotNull(child);
@Nullable SubscriptionCollection subscription = child.subscription(false);
Assert.assertEquals(1, subscription.subscriberCount());
long start = System.nanoTime();
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(() -> {
IntStream.range(0, _noOfPuts).forEach(i ->
{
_testMap.put(key, _twoMbTestString);
});
}, _noOfRunsToAverage, 3 * _secondInNanos);
waitFor(() -> keyEventSubscriber.getNoOfEvents().get() >= _noOfPuts * _noOfRunsToAverage);
long time = System.nanoTime() - start;
System.out.printf("Took %.3f seconds to receive all events%n", time / 1e9);
//Test that the correct number of events was triggered on event listener
Assert.assertEquals(_noOfPuts * _noOfRunsToAverage, keyEventSubscriber.getNoOfEvents().get());
clientAssetTree.unregisterSubscriber(_mapName + "/" + key, keyEventSubscriber);
Jvm.pause(100);
Assert.assertEquals(0, subscription.subscriberCount());
}
/**
* Test that listening to events for a given map can handle 50 updates per second of 2 MB string
* values and are triggering events which contain both the key and value (topic).
*/
@Test
public void testSubscriptionMapEventOnTopicPerformance() {
_testMap.clear();
String key = TestUtils.getKey(_mapName, 0);
//Create subscriber and register
@NotNull TestChronicleTopicSubscriber topicSubscriber = new TestChronicleTopicSubscriber(key, _twoMbTestStringLength);
clientAssetTree.registerTopicSubscriber(_mapName, String.class, String.class, topicSubscriber);
Jvm.pause(100);
@NotNull KVSSubscription subscription = (KVSSubscription) serverAssetTree.getAsset(_mapName).subscription(false);
Assert.assertEquals(1, subscription.topicSubscriberCount());
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> {
System.out.println("test");
int events = _noOfPuts * i;
waitFor(() -> events == topicSubscriber.getNoOfEvents().get());
Assert.assertEquals(events, topicSubscriber.getNoOfEvents().get());
}, () -> {
IntStream.range(0, _noOfPuts).forEach(i ->
{
_testMap.put(key, _twoMbTestString);
});
}, _noOfRunsToAverage, 3 * _secondInNanos
);
//Test that the correct number of events was triggered on event listener
int events = _noOfPuts * _noOfRunsToAverage;
waitFor(() -> events == topicSubscriber.getNoOfEvents().get());
Assert.assertEquals(events, topicSubscriber.getNoOfEvents().get());
clientAssetTree.unregisterTopicSubscriber(_mapName, topicSubscriber);
waitFor(() -> 0 == subscription.topicSubscriberCount());
Assert.assertEquals(0, subscription.topicSubscriberCount());
}
/**
* Tests the performance of an event listener on the map for Insert events of 2 MB strings.
* Expect it to handle at least 50 2 MB updates per second.
*/
@Test
public void testSubscriptionMapEventListenerInsertPerformance() {
_testMap.clear();
YamlLogging.setAll(false);
//Create subscriber and register
@NotNull TestChronicleMapEventListener mapEventListener = new TestChronicleMapEventListener(_mapName, _twoMbTestStringLength);
@NotNull Subscriber<MapEvent> mapEventSubscriber = e -> e.apply(mapEventListener);
clientAssetTree.registerSubscriber(_mapName, MapEvent.class, mapEventSubscriber);
Jvm.pause(100);
@Nullable KVSSubscription subscription = (KVSSubscription) serverAssetTree.getAsset(_mapName).subscription(false);
Assert.assertEquals(1, subscription.entrySubscriberCount());
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> {
if (i > 0) {
waitFor(() -> mapEventListener.getNoOfInsertEvents().get() >= _noOfPuts);
Assert.assertEquals(_noOfPuts, mapEventListener.getNoOfInsertEvents().get());
}
//Test that the correct number of events were triggered on event listener
Assert.assertEquals(0, mapEventListener.getNoOfRemoveEvents().get());
Assert.assertEquals(0, mapEventListener.getNoOfUpdateEvents().get());
_testMap.clear();
mapEventListener.resetCounters();
}, () -> {
IntStream.range(0, _noOfPuts).forEach(i ->
{
_testMap.put(TestUtils.getKey(_mapName, i), _twoMbTestString);
});
}, _noOfRunsToAverage, 2 * _secondInNanos
);
clientAssetTree.unregisterSubscriber(_mapName, mapEventSubscriber);
Jvm.pause(100);
Assert.assertEquals(0, subscription.entrySubscriberCount());
}
/**
* Tests the performance of an event listener on the map for Update events of 2 MB strings.
* Expect it to handle at least 50 2 MB updates per second.
*/
@Test
public void testSubscriptionMapEventListenerUpdatePerformance() {
_testMap.clear();
//Put values before testing as we want to ignore the insert events
@NotNull Function<Integer, Object> putFunction = a -> _testMap.put(TestUtils.getKey(_mapName, a), _twoMbTestString);
IntStream.range(0, _noOfPuts).forEach(i ->
{
putFunction.apply(i);
});
Jvm.pause(100);
//Create subscriber and register
@NotNull TestChronicleMapEventListener mapEventListener = new TestChronicleMapEventListener(_mapName, _twoMbTestStringLength);
@NotNull Subscriber<MapEvent> mapEventSubscriber = e -> e.apply(mapEventListener);
clientAssetTree.registerSubscriber(_mapName + "?bootstrap=false", MapEvent.class, mapEventSubscriber);
@NotNull KVSSubscription subscription = (KVSSubscription) serverAssetTree.getAsset(_mapName).subscription(false);
waitFor(() -> subscription.entrySubscriberCount() == 1);
Assert.assertEquals(1, subscription.entrySubscriberCount());
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> {
if (i > 0) {
waitFor(() -> mapEventListener.getNoOfUpdateEvents().get() >= _noOfPuts);
//Test that the correct number of events were triggered on event listener
Assert.assertEquals(_noOfPuts, mapEventListener.getNoOfUpdateEvents().get());
}
Assert.assertEquals(0, mapEventListener.getNoOfInsertEvents().get());
Assert.assertEquals(0, mapEventListener.getNoOfRemoveEvents().get());
mapEventListener.resetCounters();
}, () -> {
IntStream.range(0, _noOfPuts).forEach(i ->
{
putFunction.apply(i);
});
}, _noOfRunsToAverage, 3 * _secondInNanos
);
clientAssetTree.unregisterSubscriber(_mapName, mapEventSubscriber);
waitFor(() -> subscription.entrySubscriberCount() == 0);
Assert.assertEquals(0, subscription.entrySubscriberCount());
}
private void waitFor(@NotNull BooleanSupplier b) {
for (int i = 1; i <= 40; i++)
if (!b.getAsBoolean())
Jvm.pause(i * i);
}
/**
* Tests the performance of an event listener on the map for Remove events of 2 MB strings.
* Expect it to handle at least 50 2 MB updates per second.
*/
@Test
public void testSubscriptionMapEventListenerRemovePerformance() {
_testMap.clear();
//Put values before testing as we want to ignore the insert and update events
//Create subscriber and register
@NotNull TestChronicleMapEventListener mapEventListener = new TestChronicleMapEventListener(_mapName, _twoMbTestStringLength);
@NotNull Subscriber<MapEvent> mapEventSubscriber = e -> e.apply(mapEventListener);
clientAssetTree.registerSubscriber(_mapName, MapEvent.class, mapEventSubscriber);
//Perform test a number of times to allow the JVM to warm up, but verify runtime against average
long runtimeInNanos = 0;
for (int i = 0; i < _noOfRunsToAverage; i++) {
//Put values before testing as we want to ignore the insert and update events
IntStream.range(0, _noOfPuts).forEach(c ->
{
_testMap.put(TestUtils.getKey(_mapName, c), _twoMbTestString);
});
waitFor(() -> mapEventListener.getNoOfInsertEvents().get() >= _noOfPuts);
mapEventListener.resetCounters();
long startTime = System.nanoTime();
IntStream.range(0, _noOfPuts).forEach(c ->
{
_testMap.remove(TestUtils.getKey(_mapName, c));
});
runtimeInNanos += System.nanoTime() - startTime;
waitFor(() -> mapEventListener.getNoOfRemoveEvents().get() >= _noOfPuts);
//Test that the correct number of events were triggered on event listener
Assert.assertEquals(0, mapEventListener.getNoOfInsertEvents().get());
Assert.assertEquals(_noOfPuts, mapEventListener.getNoOfRemoveEvents().get());
Assert.assertEquals(0, mapEventListener.getNoOfUpdateEvents().get());
}
Assert.assertTrue((runtimeInNanos / (_noOfPuts * _noOfRunsToAverage)) <= 2 * _secondInNanos);
clientAssetTree.unregisterSubscriber(_mapName, mapEventSubscriber);
}
/**
* Checks that all updates triggered are for the key specified in the constructor and increments
* the number of updates.
*/
class TestChronicleKeyEventSubscriber implements Subscriber<String> {
private int _stringLength;
@NotNull
private AtomicInteger _noOfEvents = new AtomicInteger(0);
public TestChronicleKeyEventSubscriber(int stringLength) {
_stringLength = stringLength;
}
@NotNull
public AtomicInteger getNoOfEvents() {
return _noOfEvents;
}
@Override
public void onMessage(@Nullable String newValue) {
if (newValue == null) {
System.out.println("No value");
} else {
Assert.assertEquals(_stringLength, newValue.length());
_noOfEvents.incrementAndGet();
}
}
}
/**
* Topic subscriber checking for each message that it is for the right key (in constructor) and
* the expected size value. Increments event counter which can be checked at the end of the
* test.
*/
class TestChronicleTopicSubscriber implements TopicSubscriber<String, String> {
private String _keyName;
private int _stringLength;
@NotNull
private AtomicInteger _noOfEvents = new AtomicInteger(0);
public TestChronicleTopicSubscriber(String keyName, int stringLength) {
_keyName = keyName;
_stringLength = stringLength;
}
/**
* Test that the topic/key is the one specified in constructor and the message is the
* expected size.
*
* @throws InvalidSubscriberException
*/
@Override
public void onMessage(String topic, @NotNull String message) throws InvalidSubscriberException {
Assert.assertEquals(_keyName, topic);
Assert.assertEquals(_stringLength, message.length());
_noOfEvents.incrementAndGet();
}
@NotNull
public AtomicInteger getNoOfEvents() {
return _noOfEvents;
}
}
/**
* Map event listener for performance testing. Checks that the key is the one expected and the
* size of the value is as expected. Increments event specific counters that can be used to
* check against the expected number of events.
*/
class TestChronicleMapEventListener implements MapEventListener<String, String> {
@NotNull
private AtomicInteger _noOfInsertEvents = new AtomicInteger(0);
@NotNull
private AtomicInteger _noOfUpdateEvents = new AtomicInteger(0);
@NotNull
private AtomicInteger _noOfRemoveEvents = new AtomicInteger(0);
private String _mapName;
private int _stringLength;
public TestChronicleMapEventListener(String mapName, int stringLength) {
_mapName = mapName;
_stringLength = stringLength;
}
@Override
public void update(String assetName, String key, String oldValue, @NotNull String newValue) {
testKeyAndValue(key, newValue, _noOfUpdateEvents);
}
@Override
public void insert(String assetName, String key, @NotNull String value) {
testKeyAndValue(key, value, _noOfInsertEvents);
}
@Override
public void remove(String assetName, String key, @NotNull String value) {
testKeyAndValue(key, value, _noOfRemoveEvents);
}
@NotNull
public AtomicInteger getNoOfInsertEvents() {
return _noOfInsertEvents;
}
@NotNull
public AtomicInteger getNoOfUpdateEvents() {
return _noOfUpdateEvents;
}
@NotNull
public AtomicInteger getNoOfRemoveEvents() {
return _noOfRemoveEvents;
}
public void resetCounters() {
_noOfInsertEvents = new AtomicInteger(0);
_noOfUpdateEvents = new AtomicInteger(0);
_noOfRemoveEvents = new AtomicInteger(0);
}
private void testKeyAndValue(String key, @NotNull String value, @NotNull AtomicInteger counterToIncrement) {
int counter = counterToIncrement.getAndIncrement();
Assert.assertEquals(TestUtils.getKey(_mapName, counter), key);
Assert.assertEquals(_stringLength, value.length());
}
}
}