blob: 4a36ebea062dc1b13c30bc47939b85b6d9ce3896 [file] [log] [blame]
Jordan Halterman29718e62017-07-20 13:24:33 -07001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2017-present Open Networking Foundation
Jordan Halterman29718e62017-07-20 13:24:33 -07003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.primitiveperf;
17
Jordan Halterman73296092018-03-08 16:06:28 -050018import java.util.ArrayList;
Jordan Halterman29718e62017-07-20 13:24:33 -070019import java.util.Dictionary;
20import java.util.List;
21import java.util.Map;
Jordan Halterman73296092018-03-08 16:06:28 -050022import java.util.Random;
Jordan Halterman29718e62017-07-20 13:24:33 -070023import java.util.Timer;
24import java.util.TimerTask;
25import java.util.TreeMap;
Jordan Halterman29718e62017-07-20 13:24:33 -070026import java.util.concurrent.ExecutorService;
27import java.util.concurrent.Executors;
28import java.util.concurrent.TimeUnit;
29import java.util.concurrent.atomic.AtomicLong;
Jordan Halterman29718e62017-07-20 13:24:33 -070030
31import com.google.common.collect.Lists;
32import org.apache.felix.scr.annotations.Activate;
33import org.apache.felix.scr.annotations.Component;
34import org.apache.felix.scr.annotations.Deactivate;
35import org.apache.felix.scr.annotations.Modified;
36import org.apache.felix.scr.annotations.Property;
37import org.apache.felix.scr.annotations.Reference;
Jordan Halterman29718e62017-07-20 13:24:33 -070038import org.apache.felix.scr.annotations.Service;
39import org.onosproject.cfg.ComponentConfigService;
40import org.onosproject.cluster.ClusterService;
41import org.onosproject.cluster.ControllerNode;
42import org.onosproject.cluster.NodeId;
Jordan Halterman29718e62017-07-20 13:24:33 -070043import org.onosproject.store.serializers.KryoNamespaces;
Jordan Halterman73296092018-03-08 16:06:28 -050044import org.onosproject.store.service.AtomicValue;
45import org.onosproject.store.service.AtomicValueEventListener;
Jordan Halterman29718e62017-07-20 13:24:33 -070046import org.onosproject.store.service.ConsistentMap;
47import org.onosproject.store.service.Serializer;
48import org.onosproject.store.service.StorageService;
49import org.osgi.service.component.ComponentContext;
50import org.slf4j.Logger;
51
52import static com.google.common.base.Strings.isNullOrEmpty;
53import static java.lang.System.currentTimeMillis;
54import static org.apache.felix.scr.annotations.ReferenceCardinality.MANDATORY_UNARY;
55import static org.onlab.util.Tools.get;
56import static org.onlab.util.Tools.groupedThreads;
57import static org.slf4j.LoggerFactory.getLogger;
58
59/**
60 * Application to test sustained primitive throughput.
61 */
62@Component(immediate = true)
63@Service(value = PrimitivePerfApp.class)
64public class PrimitivePerfApp {
65
66 private final Logger log = getLogger(getClass());
67
Jordan Halterman73296092018-03-08 16:06:28 -050068 private static final int DEFAULT_NUM_CLIENTS = 8;
Jordan Halterman29718e62017-07-20 13:24:33 -070069 private static final int DEFAULT_WRITE_PERCENTAGE = 100;
70
Jordan Halterman73296092018-03-08 16:06:28 -050071 private static final int DEFAULT_NUM_KEYS = 100_000;
72 private static final int DEFAULT_KEY_LENGTH = 32;
73 private static final int DEFAULT_NUM_UNIQUE_VALUES = 100;
74 private static final int DEFAULT_VALUE_LENGTH = 1024;
75 private static final boolean DEFAULT_INCLUDE_EVENTS = false;
76 private static final boolean DEFAULT_DETERMINISTIC = true;
77
Jordan Halterman29718e62017-07-20 13:24:33 -070078 private static final int REPORT_PERIOD = 1_000; //ms
79
Jordan Halterman73296092018-03-08 16:06:28 -050080 private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
Jordan Halterman29718e62017-07-20 13:24:33 -070081
Jordan Halterman73296092018-03-08 16:06:28 -050082 @Property(
83 name = "numClients",
84 intValue = DEFAULT_NUM_CLIENTS,
85 label = "Number of clients to use to submit writes")
Jordan Halterman29718e62017-07-20 13:24:33 -070086 private int numClients = DEFAULT_NUM_CLIENTS;
87
Jordan Halterman73296092018-03-08 16:06:28 -050088 @Property(
89 name = "writePercentage",
90 intValue = DEFAULT_WRITE_PERCENTAGE,
91 label = "Percentage of operations to perform as writes")
Jordan Halterman29718e62017-07-20 13:24:33 -070092 private int writePercentage = DEFAULT_WRITE_PERCENTAGE;
93
Jordan Halterman73296092018-03-08 16:06:28 -050094 @Property(
95 name = "numKeys",
96 intValue = DEFAULT_NUM_KEYS,
97 label = "Number of unique keys to write")
98 private int numKeys = DEFAULT_NUM_KEYS;
99
100 @Property(
101 name = "keyLength",
102 intValue = DEFAULT_KEY_LENGTH,
103 label = "Key length")
104 private int keyLength = DEFAULT_KEY_LENGTH;
105
106 @Property(
107 name = "numValues",
108 intValue = DEFAULT_NUM_UNIQUE_VALUES,
109 label = "Number of unique values to write")
110 private int numValues = DEFAULT_NUM_UNIQUE_VALUES;
111
112 @Property(
113 name = "valueLength",
114 intValue = DEFAULT_VALUE_LENGTH,
115 label = "Value length")
116 private int valueLength = DEFAULT_VALUE_LENGTH;
117
118 @Property(
119 name = "includeEvents",
120 boolValue = DEFAULT_INCLUDE_EVENTS,
121 label = "Whether to include events in test")
122 private boolean includeEvents = DEFAULT_INCLUDE_EVENTS;
123
124 @Property(
125 name = "deterministic",
126 boolValue = DEFAULT_DETERMINISTIC,
127 label = "Whether to deterministically populate entries")
128 private boolean deterministic = DEFAULT_DETERMINISTIC;
129
Jordan Halterman29718e62017-07-20 13:24:33 -0700130 @Reference(cardinality = MANDATORY_UNARY)
131 protected ClusterService clusterService;
132
133 @Reference(cardinality = MANDATORY_UNARY)
134 protected StorageService storageService;
135
136 @Reference(cardinality = MANDATORY_UNARY)
137 protected ComponentConfigService configService;
138
139 @Reference(cardinality = MANDATORY_UNARY)
140 protected PrimitivePerfCollector sampleCollector;
141
Jordan Halterman29718e62017-07-20 13:24:33 -0700142 private ExecutorService messageHandlingExecutor;
143
144 private ExecutorService workers;
Jordan Halterman73296092018-03-08 16:06:28 -0500145
146 // Tracks whether the test has been started in the cluster.
147 private AtomicValue<Boolean> started;
148 private AtomicValueEventListener<Boolean> startedListener = event -> {
149 if (event.newValue()) {
150 startTestRun();
151 } else {
152 stopTestRun();
153 }
154 };
155
156 // Tracks whether local clients are running.
157 private volatile boolean running;
Jordan Halterman29718e62017-07-20 13:24:33 -0700158
159 private Timer reportTimer;
160
161 private NodeId nodeId;
162 private TimerTask reporterTask;
163
164 private long startTime;
165 private long currentStartTime;
166 private AtomicLong overallCounter;
167 private AtomicLong currentCounter;
168
169 @Activate
170 public void activate(ComponentContext context) {
171 configService.registerProperties(getClass());
172
173 nodeId = clusterService.getLocalNode().id();
Jordan Halterman73296092018-03-08 16:06:28 -0500174 started = storageService.<Boolean>atomicValueBuilder()
175 .withName("perf-test-started")
176 .withSerializer(Serializer.using(KryoNamespaces.BASIC))
177 .build()
178 .asAtomicValue();
179 started.addListener(startedListener);
Jordan Halterman29718e62017-07-20 13:24:33 -0700180
181 reportTimer = new Timer("onos-primitive-perf-reporter");
182
183 messageHandlingExecutor = Executors.newSingleThreadExecutor(
184 groupedThreads("onos/perf", "command-handler"));
185
Jordan Halterman29718e62017-07-20 13:24:33 -0700186 // TODO: investigate why this seems to be necessary for configs to get picked up on initial activation
187 modify(context);
188 }
189
190 @Deactivate
191 public void deactivate() {
192 stopTestRun();
193
194 configService.unregisterProperties(getClass(), false);
195 messageHandlingExecutor.shutdown();
Jordan Halterman73296092018-03-08 16:06:28 -0500196 started.removeListener(startedListener);
Jordan Halterman29718e62017-07-20 13:24:33 -0700197
198 if (reportTimer != null) {
199 reportTimer.cancel();
200 reportTimer = null;
201 }
202 }
203
Jordan Halterman73296092018-03-08 16:06:28 -0500204 private int parseInt(Dictionary<?, ?> properties, String name, int currentValue, int defaultValue) {
205 try {
206 String s = get(properties, name);
207 return isNullOrEmpty(s) ? currentValue : Integer.parseInt(s.trim());
208 } catch (NumberFormatException | ClassCastException e) {
209 log.warn("Malformed configuration detected; using defaults", e);
210 return defaultValue;
211 }
212 }
213
Jordan Halterman29718e62017-07-20 13:24:33 -0700214 @Modified
215 public void modify(ComponentContext context) {
216 if (context == null) {
217 logConfig("Reconfigured");
218 return;
219 }
220
221 Dictionary<?, ?> properties = context.getProperties();
Jordan Halterman73296092018-03-08 16:06:28 -0500222 int newNumClients = parseInt(properties, "numClients", numClients, DEFAULT_NUM_CLIENTS);
223 int newWritePercentage = parseInt(properties, "writePercentage", writePercentage, DEFAULT_WRITE_PERCENTAGE);
224 int newNumKeys = parseInt(properties, "numKeys", numKeys, DEFAULT_NUM_KEYS);
225 int newKeyLength = parseInt(properties, "keyLength", keyLength, DEFAULT_KEY_LENGTH);
226 int newNumValues = parseInt(properties, "numValues", numValues, DEFAULT_NUM_UNIQUE_VALUES);
227 int newValueLength = parseInt(properties, "valueLength", valueLength, DEFAULT_VALUE_LENGTH);
Jordan Halterman29718e62017-07-20 13:24:33 -0700228
Jordan Halterman73296092018-03-08 16:06:28 -0500229 String includeEventsString = get(properties, "includeEvents");
230 boolean newIncludeEvents = isNullOrEmpty(includeEventsString)
231 ? includeEvents
232 : Boolean.parseBoolean(includeEventsString.trim());
Jordan Halterman29718e62017-07-20 13:24:33 -0700233
Jordan Halterman73296092018-03-08 16:06:28 -0500234 String deterministicString = get(properties, "deterministic");
235 boolean newDeterministic = isNullOrEmpty(deterministicString)
236 ? includeEvents
237 : Boolean.parseBoolean(deterministicString.trim());
238
239 if (newNumClients != numClients
240 || newWritePercentage != writePercentage
241 || newNumKeys != numKeys
242 || newKeyLength != keyLength
243 || newNumValues != numValues
244 || newValueLength != valueLength
245 || newIncludeEvents != includeEvents
246 || newDeterministic != deterministic) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700247 numClients = newNumClients;
248 writePercentage = newWritePercentage;
Jordan Halterman73296092018-03-08 16:06:28 -0500249 numKeys = newNumKeys;
250 keyLength = newKeyLength;
251 numValues = newNumValues;
252 valueLength = newValueLength;
253 includeEvents = newIncludeEvents;
254 deterministic = newDeterministic;
Jordan Halterman29718e62017-07-20 13:24:33 -0700255 logConfig("Reconfigured");
Jordan Halterman73296092018-03-08 16:06:28 -0500256 Boolean started = this.started.get();
257 if (started != null && started) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700258 stop();
259 start();
260 }
Jordan Halterman73296092018-03-08 16:06:28 -0500261 } else {
262 Boolean started = this.started.get();
263 if (started != null && started) {
264 if (running) {
265 stopTestRun();
266 }
267 startTestRun();
268 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700269 }
270 }
271
Jordan Halterman73296092018-03-08 16:06:28 -0500272 /**
273 * Starts a new test.
274 */
Jordan Halterman29718e62017-07-20 13:24:33 -0700275 public void start() {
Jordan Halterman73296092018-03-08 16:06:28 -0500276 // To stop the test, we simply update the "started" value. Events from the change will notify all
277 // nodes to start the test.
278 Boolean started = this.started.get();
279 if (started == null || !started) {
280 this.started.set(true);
Jordan Halterman29718e62017-07-20 13:24:33 -0700281 }
282 }
283
Jordan Halterman73296092018-03-08 16:06:28 -0500284 /**
285 * Stops a test.
286 */
Jordan Halterman29718e62017-07-20 13:24:33 -0700287 public void stop() {
Jordan Halterman73296092018-03-08 16:06:28 -0500288 // To stop the test, we simply update the "started" value. Events from the change will notify all
289 // nodes to stop the test.
290 Boolean started = this.started.get();
291 if (started != null && started) {
292 this.started.set(false);
Jordan Halterman29718e62017-07-20 13:24:33 -0700293 }
294 }
295
296 private void logConfig(String prefix) {
Jordan Halterman73296092018-03-08 16:06:28 -0500297 log.info("{} with " +
298 "numClients = {}; " +
299 "writePercentage = {}; " +
300 "numKeys = {}; " +
301 "keyLength = {}; " +
302 "numValues = {}; " +
303 "valueLength = {}; " +
304 "includeEvents = {}; " +
305 "deterministic = {}",
306 prefix,
307 numClients,
308 writePercentage,
309 numKeys,
310 keyLength,
311 numValues,
312 valueLength,
313 includeEvents,
314 deterministic);
Jordan Halterman29718e62017-07-20 13:24:33 -0700315 }
316
317 private void startTestRun() {
Jordan Halterman73296092018-03-08 16:06:28 -0500318 if (running) {
319 return;
320 }
321
Jordan Halterman29718e62017-07-20 13:24:33 -0700322 sampleCollector.clearSamples();
323
324 startTime = System.currentTimeMillis();
325 currentStartTime = startTime;
326 currentCounter = new AtomicLong();
327 overallCounter = new AtomicLong();
328
329 reporterTask = new ReporterTask();
Jordan Halterman73296092018-03-08 16:06:28 -0500330 reportTimer.scheduleAtFixedRate(
331 reporterTask, REPORT_PERIOD - currentTimeMillis() % REPORT_PERIOD, REPORT_PERIOD);
Jordan Halterman29718e62017-07-20 13:24:33 -0700332
Jordan Halterman73296092018-03-08 16:06:28 -0500333 running = true;
Jordan Halterman29718e62017-07-20 13:24:33 -0700334
335 Map<String, ControllerNode> nodes = new TreeMap<>();
336 for (ControllerNode node : clusterService.getNodes()) {
337 nodes.put(node.id().id(), node);
338 }
339
340 // Compute the index of the local node in a sorted list of nodes.
341 List<String> sortedNodes = Lists.newArrayList(nodes.keySet());
342 int nodeCount = nodes.size();
343 int index = sortedNodes.indexOf(nodeId.id());
344
345 // Count the number of workers assigned to this node.
346 int workerCount = 0;
347 for (int i = 1; i <= numClients; i++) {
348 if (i % nodeCount == index) {
349 workerCount++;
350 }
351 }
352
353 // Create a worker pool and start the workers for this node.
Jordan Haltermanf5333892017-08-29 10:42:13 -0700354 if (workerCount > 0) {
Jordan Halterman73296092018-03-08 16:06:28 -0500355 String[] keys = createStrings(keyLength, numKeys);
356 String[] values = createStrings(valueLength, numValues);
357
Jordan Haltermanf5333892017-08-29 10:42:13 -0700358 workers = Executors.newFixedThreadPool(workerCount, groupedThreads("onos/primitive-perf", "worker-%d"));
359 for (int i = 0; i < workerCount; i++) {
Jordan Halterman73296092018-03-08 16:06:28 -0500360 if (deterministic) {
361 workers.submit(new DeterministicRunner(keys, values));
362 } else {
363 workers.submit(new NonDeterministicRunner(keys, values));
364 }
Jordan Haltermanf5333892017-08-29 10:42:13 -0700365 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700366 }
367 log.info("Started test run");
368 }
369
Jordan Halterman73296092018-03-08 16:06:28 -0500370 /**
371 * Creates a deterministic array of strings to write to the cluster.
372 *
373 * @param length the string lengths
374 * @param count the string count
375 * @return a deterministic array of strings
376 */
377 private String[] createStrings(int length, int count) {
378 Random random = new Random(length);
379 List<String> stringsList = new ArrayList<>(count);
380 for (int i = 0; i < count; i++) {
381 stringsList.add(randomString(length, random));
382 }
383 return stringsList.toArray(new String[stringsList.size()]);
384 }
385
386 /**
387 * Creates a deterministic string based on the given seed.
388 *
389 * @param length the seed from which to create the string
390 * @param random the random object from which to create the string characters
391 * @return the string
392 */
393 private String randomString(int length, Random random) {
394 char[] buffer = new char[length];
395 for (int i = 0; i < length; i++) {
396 buffer[i] = CHARS[random.nextInt(CHARS.length)];
397 }
398 return new String(buffer);
399 }
400
Jordan Halterman29718e62017-07-20 13:24:33 -0700401 private void stopTestRun() {
Jordan Halterman73296092018-03-08 16:06:28 -0500402 if (!running) {
403 return;
404 }
405
Jordan Halterman29718e62017-07-20 13:24:33 -0700406 if (reporterTask != null) {
407 reporterTask.cancel();
408 reporterTask = null;
409 }
410
Jordan Haltermanf5333892017-08-29 10:42:13 -0700411 if (workers != null) {
412 workers.shutdown();
413 try {
414 workers.awaitTermination(10, TimeUnit.MILLISECONDS);
415 } catch (InterruptedException e) {
416 log.warn("Failed to stop worker", e);
Ray Milkey5c7d4882018-02-05 14:50:39 -0800417 Thread.currentThread().interrupt();
Jordan Haltermanf5333892017-08-29 10:42:13 -0700418 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700419 }
420
421 sampleCollector.recordSample(0, 0);
422 sampleCollector.recordSample(0, 0);
Jordan Halterman73296092018-03-08 16:06:28 -0500423
424 running = false;
Jordan Halterman29718e62017-07-20 13:24:33 -0700425
426 log.info("Stopped test run");
427 }
428
429 // Submits primitive operations.
Jordan Halterman73296092018-03-08 16:06:28 -0500430 abstract class Runner implements Runnable {
431 final String[] keys;
432 final String[] values;
433 final Random random = new Random();
434 ConsistentMap<String, String> map;
Jordan Halterman29718e62017-07-20 13:24:33 -0700435
Jordan Halterman73296092018-03-08 16:06:28 -0500436 Runner(String[] keys, String[] values) {
437 this.keys = keys;
438 this.values = values;
Jordan Halterman29718e62017-07-20 13:24:33 -0700439 }
440
441 @Override
442 public void run() {
443 setup();
Jordan Halterman73296092018-03-08 16:06:28 -0500444 while (running) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700445 try {
446 submit();
447 } catch (Exception e) {
448 log.warn("Exception during cycle", e);
449 }
450 }
451 teardown();
452 }
453
Jordan Halterman73296092018-03-08 16:06:28 -0500454 void setup() {
Jordan Halterman29718e62017-07-20 13:24:33 -0700455 map = storageService.<String, String>consistentMapBuilder()
456 .withName("perf-test")
457 .withSerializer(Serializer.using(KryoNamespaces.BASIC))
458 .build();
Jordan Halterman73296092018-03-08 16:06:28 -0500459 if (includeEvents) {
460 map.addListener(event -> {
461 });
462 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700463 }
464
Jordan Halterman73296092018-03-08 16:06:28 -0500465 abstract void submit();
466
467 void teardown() {
468 //map.destroy();
469 }
470 }
471
472 private class NonDeterministicRunner extends Runner {
473 NonDeterministicRunner(String[] keys, String[] values) {
474 super(keys, values);
475 }
476
477 @Override
478 void submit() {
479 currentCounter.incrementAndGet();
480 String key = keys[random.nextInt(keys.length)];
481 if (random.nextInt(100) < writePercentage) {
482 map.put(key, values[random.nextInt(values.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700483 } else {
484 map.get(key);
485 }
486 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700487 }
488
Jordan Halterman73296092018-03-08 16:06:28 -0500489 private class DeterministicRunner extends Runner {
490 private int index;
491
492 DeterministicRunner(String[] keys, String[] values) {
493 super(keys, values);
494 }
495
Jordan Halterman29718e62017-07-20 13:24:33 -0700496 @Override
Jordan Halterman73296092018-03-08 16:06:28 -0500497 void submit() {
498 currentCounter.incrementAndGet();
499 if (random.nextInt(100) < writePercentage) {
500 map.put(keys[index++ % keys.length], values[random.nextInt(values.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700501 } else {
Jordan Halterman73296092018-03-08 16:06:28 -0500502 map.get(keys[random.nextInt(keys.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700503 }
504 }
505 }
506
507 private class ReporterTask extends TimerTask {
508 @Override
509 public void run() {
510 long endTime = System.currentTimeMillis();
511 long overallTime = endTime - startTime;
512 long currentTime = endTime - currentStartTime;
513 long currentCount = currentCounter.getAndSet(0);
514 long overallCount = overallCounter.addAndGet(currentCount);
515 sampleCollector.recordSample(overallTime > 0 ? overallCount / (overallTime / 1000d) : 0,
516 currentTime > 0 ? currentCount / (currentTime / 1000d) : 0);
517 currentStartTime = System.currentTimeMillis();
518 }
519 }
520
521}