/* * 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.Chassis; 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.TopicSubscriber; import net.openhft.chronicle.engine.map.AuthenticatedKeyValueStore; import net.openhft.chronicle.engine.map.FilePerKeyValueStore; import net.openhft.chronicle.engine.tree.VanillaAsset; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.*; import java.io.Closeable; import java.io.IOException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.IntStream; @Ignore public class SubscriptionModelFilePerKeyPerformanceTest { static final AtomicInteger counter = new AtomicInteger(); private static final int _noOfPuts = 50; private static final int _noOfRunsToAverage = Boolean.getBoolean("quick") ? 2 : 10; private static final long _secondInNanos = Jvm.isDebug() ? 1_200_000_000L : 1_000_000_000L; private static String _twoMbTestString; private static int _twoMbTestStringLength; private static Map<String, String> _testMap; @NotNull private static String _mapName = "PerfTestMap"; private ThreadDump threadDump; @BeforeClass public static void setUpBeforeClass() { @NotNull char[] chars = new char[2 << 20]; Arrays.fill(chars, '~'); _twoMbTestString = new String(chars); _twoMbTestStringLength = _twoMbTestString.length(); } @Before public void threadDump() { threadDump = new ThreadDump(); } @After public void checkThreadDump() { threadDump.assertNoNewThreads(); } @Before public void setUp() { Chassis.resetChassis(); ((VanillaAsset) Chassis.assetTree().root()).enableTranslatingValuesToBytesStore(); @NotNull String basePath = OS.TARGET + "/fpk/" + counter.getAndIncrement(); System.out.println("Writing to " + basePath); Chassis.assetTree().root().addLeafRule(AuthenticatedKeyValueStore.class, "FilePer Key", (context, asset) -> new FilePerKeyValueStore(context.basePath(basePath), asset)); _testMap = Chassis.acquireMap(_mapName, String.class, String.class); _testMap.clear(); } @After public void tearDown() throws IOException { // System.out.println("Native memory used "+OS.memory().nativeMemoryUsed()); ((Closeable) ((MapView) _testMap).underlying()).close(); 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 testSubscriptionMapEventOnKeyPerformance() { String key = TestUtils.getKey(_mapName, 0); //Create subscriber and register @NotNull TestChronicleKeyEventSubscriber keyEventSubscriber = new TestChronicleKeyEventSubscriber(_twoMbTestStringLength); Chassis.registerSubscriber(_mapName + "?bootstrap=false", MapEvent.class, me -> keyEventSubscriber.onMessage((String) me.getValue())); @NotNull AtomicInteger counter = new AtomicInteger(); //Perform test a number of times to allow the JVM to warm up, but verify runtime against average TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> keyEventSubscriber.waitForEvents(_noOfPuts * i, 0.1), () -> IntStream.range(0, _noOfPuts).forEach( i -> _testMap.put(key + i, counter.incrementAndGet() + _twoMbTestString)), _noOfRunsToAverage, _secondInNanos); //Test that the correct number of events was triggered on event listener keyEventSubscriber.waitForEvents(_noOfPuts * _noOfRunsToAverage, 0.3); } /** * 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() { String key = TestUtils.getKey(_mapName, 0); //Create subscriber and register @NotNull TestChronicleTopicSubscriber topicSubscriber = new TestChronicleTopicSubscriber(key, _twoMbTestStringLength); Chassis.registerTopicSubscriber(_mapName, String.class, String.class, topicSubscriber); //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, i + _twoMbTestString); _testMap.size(); }); }, _noOfRunsToAverage, _secondInNanos); //Test that the correct number of events was triggered on event listener topicSubscriber.waitForEvents(_noOfPuts * _noOfRunsToAverage, 0.7); } /** * 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() { //Create subscriber and register @NotNull TestChronicleMapEventListener mapEventListener = new TestChronicleMapEventListener(_mapName, _twoMbTestStringLength); Chassis.registerSubscriber(_mapName, MapEvent.class, e -> e.apply(mapEventListener)); //Perform test a number of times to allow the JVM to warm up, but verify runtime against average TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> { _testMap.clear(); Jvm.pause(500); mapEventListener.resetCounters(); }, () -> { IntStream.range(0, _noOfPuts).forEach(i -> { _testMap.put(TestUtils.getKey(_mapName, i), _twoMbTestString); }); mapEventListener.waitForNMaps(_noOfPuts); //Test that the correct number of events were triggered on event listener if (_noOfPuts != mapEventListener.getNoOfInsertEvents().get()) Jvm.pause(50); Assert.assertEquals(_noOfPuts, mapEventListener.getNoOfInsertEvents().get()); Assert.assertEquals(0, mapEventListener.getNoOfRemoveEvents().get()); Assert.assertEquals(0, mapEventListener.getNoOfUpdateEvents().get()); }, _noOfRunsToAverage, _secondInNanos); } /** * 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() { //Put values before testing as we want to ignore the insert events @NotNull Function<Integer, Object> putFunction = a -> _testMap.put(TestUtils.getKey(_mapName, a), System.nanoTime() + _twoMbTestString); IntStream.range(0, _noOfPuts).parallel().forEach(i -> { putFunction.apply(i); }); //Create subscriber and register @NotNull TestChronicleMapEventListener mapEventListener = new TestChronicleMapEventListener(_mapName, _twoMbTestStringLength); Chassis.registerSubscriber(_mapName + "?bootstrap=false", MapEvent.class, e -> e.apply(mapEventListener)); //Perform test a number of times to allow the JVM to warm up, but verify runtime against average TestUtils.runMultipleTimesAndVerifyAvgRuntime(i -> { Jvm.pause(200); mapEventListener.resetCounters(); }, () -> { IntStream.range(0, _noOfPuts).forEach(i -> { putFunction.apply(i); }); mapEventListener.waitForNMaps(_noOfPuts); //Test that the correct number of events were triggered on event listener // todo make more reliable on windows. Assert.assertEquals(_noOfPuts, mapEventListener.getNoOfUpdateEvents().get() + mapEventListener.getNoOfInsertEvents().get(), _noOfPuts * 0.4); Assert.assertEquals(0, mapEventListener.getNoOfRemoveEvents().get()); }, _noOfRunsToAverage, _secondInNanos); } /** * 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() throws InterruptedException { //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); Chassis.registerSubscriber(_mapName + "?bootstrap=false", MapEvent.class, e -> e.apply(mapEventListener)); //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++) { Jvm.pause(400); mapEventListener.resetCounters(); //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); }); mapEventListener.waitForNMaps(_noOfPuts); mapEventListener.resetCounters(); long startTime = System.nanoTime(); IntStream.range(0, _noOfPuts).parallel().forEach(c -> { _testMap.remove(TestUtils.getKey(_mapName, c)); }); mapEventListener.waitForNMaps(_noOfPuts); runtimeInNanos += System.nanoTime() - startTime; //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(), 1); } Assert.assertTrue((runtimeInNanos / (_noOfPuts * _noOfRunsToAverage)) <= _secondInNanos); } /** * 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(@NotNull String newValue) { try { Assert.assertEquals(_stringLength + 2, newValue.length(), 1); } catch (Error e) { throw e; } _noOfEvents.incrementAndGet(); } public void waitForEvents(int events, double error) { for (int i = 1; i <= 30; i++) { if (events * (1 - error / 2) <= getNoOfEvents().get()) break; Jvm.pause(i * i); } Jvm.pause(100); Assert.assertEquals(events * (1 - error / 2), getNoOfEvents().get(), error / 2 * events); } } /** * 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, @Nullable String message) throws InvalidSubscriberException { if (message == null) { System.out.println("topic " + topic + " deleted?"); return; } Assert.assertEquals(_keyName, topic); Assert.assertEquals(_stringLength + 2, message.length(), 1); _noOfEvents.incrementAndGet(); } @NotNull public AtomicInteger getNoOfEvents() { return _noOfEvents; } public void waitForEvents(int events, double error) { for (int i = 1; i <= 30; i++) { if (events * (1 - error) <= getNoOfEvents().get()) break; Jvm.pause(i * i); } Jvm.pause(100); Assert.assertEquals(events * (1 - error / 2), getNoOfEvents().get(), error / 2 * events); } } /** * 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 Set<String> mapsUpdated = Collections.synchronizedSet(new TreeSet<>()); 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, String newValue) { testKeyAndValue(key, newValue, _noOfUpdateEvents); } @Override public void insert(String assetName, String key, String value) { testKeyAndValue(key, value, _noOfInsertEvents); } @Override public void remove(String assetName, String key, 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); mapsUpdated.clear(); } private void testKeyAndValue(String key, @Nullable String value, @NotNull AtomicInteger counterToIncrement) { // System.out.println("key: " + key); counterToIncrement.getAndIncrement(); mapsUpdated.add(key); try { if (value != null) Assert.assertEquals(_stringLength + 8, value.length(), 8); } catch (Error e) { throw e; } } public void waitForNMaps(int noOfMaps) { for (int i = 1; i <= 40; i++) { if (mapsUpdated.size() >= noOfMaps) break; Jvm.pause(i * i); } Assert.assertEquals(toString(), noOfMaps, mapsUpdated.size()); } @NotNull @Override public String toString() { return "TestChronicleMapEventListener{" + "_noOfInsertEvents=" + _noOfInsertEvents + ", _noOfUpdateEvents=" + _noOfUpdateEvents + ", _noOfRemoveEvents=" + _noOfRemoveEvents + ", mapsUpdated=" + mapsUpdated.size() + ", _mapName='" + _mapName + '\'' + ", _stringLength=" + _stringLength + '}'; } } }