blob: 9c6271044b7d0c245dc26bec2f690d764e3d1de3 [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
Ray Milkeyd84f89b2018-08-17 14:54:17 -070018import com.google.common.collect.Lists;
19import org.onosproject.cfg.ComponentConfigService;
20import org.onosproject.cluster.ClusterService;
21import org.onosproject.cluster.ControllerNode;
22import org.onosproject.cluster.NodeId;
23import org.onosproject.store.serializers.KryoNamespaces;
24import org.onosproject.store.service.AtomicValue;
25import org.onosproject.store.service.AtomicValueEventListener;
26import org.onosproject.store.service.ConsistentMap;
27import org.onosproject.store.service.Serializer;
28import org.onosproject.store.service.StorageService;
29import org.osgi.service.component.ComponentContext;
30import org.osgi.service.component.annotations.Activate;
31import org.osgi.service.component.annotations.Component;
32import org.osgi.service.component.annotations.Deactivate;
33import org.osgi.service.component.annotations.Modified;
34import org.osgi.service.component.annotations.Reference;
35import org.slf4j.Logger;
36
Jordan Halterman73296092018-03-08 16:06:28 -050037import java.util.ArrayList;
Jordan Halterman29718e62017-07-20 13:24:33 -070038import java.util.Dictionary;
39import java.util.List;
40import java.util.Map;
Jordan Halterman73296092018-03-08 16:06:28 -050041import java.util.Random;
Jordan Halterman29718e62017-07-20 13:24:33 -070042import java.util.Timer;
43import java.util.TimerTask;
44import java.util.TreeMap;
Jordan Halterman29718e62017-07-20 13:24:33 -070045import java.util.concurrent.ExecutorService;
46import java.util.concurrent.Executors;
47import java.util.concurrent.TimeUnit;
48import java.util.concurrent.atomic.AtomicLong;
Jordan Halterman29718e62017-07-20 13:24:33 -070049
Jordan Halterman29718e62017-07-20 13:24:33 -070050import static com.google.common.base.Strings.isNullOrEmpty;
51import static java.lang.System.currentTimeMillis;
Jordan Halterman29718e62017-07-20 13:24:33 -070052import static org.onlab.util.Tools.get;
53import static org.onlab.util.Tools.groupedThreads;
Ray Milkeyd84f89b2018-08-17 14:54:17 -070054import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
Jordan Halterman29718e62017-07-20 13:24:33 -070055import static org.slf4j.LoggerFactory.getLogger;
56
57/**
58 * Application to test sustained primitive throughput.
59 */
Ray Milkeyd84f89b2018-08-17 14:54:17 -070060@Component(immediate = true, service = PrimitivePerfApp.class)
Jordan Halterman29718e62017-07-20 13:24:33 -070061public class PrimitivePerfApp {
62
63 private final Logger log = getLogger(getClass());
64
Jordan Halterman73296092018-03-08 16:06:28 -050065 private static final int DEFAULT_NUM_CLIENTS = 8;
Jordan Halterman29718e62017-07-20 13:24:33 -070066 private static final int DEFAULT_WRITE_PERCENTAGE = 100;
67
Jordan Halterman73296092018-03-08 16:06:28 -050068 private static final int DEFAULT_NUM_KEYS = 100_000;
69 private static final int DEFAULT_KEY_LENGTH = 32;
70 private static final int DEFAULT_NUM_UNIQUE_VALUES = 100;
71 private static final int DEFAULT_VALUE_LENGTH = 1024;
72 private static final boolean DEFAULT_INCLUDE_EVENTS = false;
73 private static final boolean DEFAULT_DETERMINISTIC = true;
74
Jordan Halterman29718e62017-07-20 13:24:33 -070075 private static final int REPORT_PERIOD = 1_000; //ms
76
Jordan Halterman73296092018-03-08 16:06:28 -050077 private static final char[] CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
Jordan Halterman29718e62017-07-20 13:24:33 -070078
Ray Milkeyd84f89b2018-08-17 14:54:17 -070079 //@Property(
80 // name = "numClients",
81 // intValue = DEFAULT_NUM_CLIENTS,
82 // label = "Number of clients to use to submit writes")
Jordan Halterman29718e62017-07-20 13:24:33 -070083 private int numClients = DEFAULT_NUM_CLIENTS;
84
Ray Milkeyd84f89b2018-08-17 14:54:17 -070085 //@Property(
86 // name = "writePercentage",
87 // intValue = DEFAULT_WRITE_PERCENTAGE,
88 // label = "Percentage of operations to perform as writes")
Jordan Halterman29718e62017-07-20 13:24:33 -070089 private int writePercentage = DEFAULT_WRITE_PERCENTAGE;
90
Ray Milkeyd84f89b2018-08-17 14:54:17 -070091 //@Property(
92 // name = "numKeys",
93 // intValue = DEFAULT_NUM_KEYS,
94 // label = "Number of unique keys to write")
Jordan Halterman73296092018-03-08 16:06:28 -050095 private int numKeys = DEFAULT_NUM_KEYS;
96
Ray Milkeyd84f89b2018-08-17 14:54:17 -070097 //@Property(
98 // name = "keyLength",
99 // intValue = DEFAULT_KEY_LENGTH,
100 // label = "Key length")
Jordan Halterman73296092018-03-08 16:06:28 -0500101 private int keyLength = DEFAULT_KEY_LENGTH;
102
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700103 //@Property(
104 // name = "numValues",
105 // intValue = DEFAULT_NUM_UNIQUE_VALUES,
106 // label = "Number of unique values to write")
Jordan Halterman73296092018-03-08 16:06:28 -0500107 private int numValues = DEFAULT_NUM_UNIQUE_VALUES;
108
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700109 //@Property(
110 // name = "valueLength",
111 // intValue = DEFAULT_VALUE_LENGTH,
112 // label = "Value length")
Jordan Halterman73296092018-03-08 16:06:28 -0500113 private int valueLength = DEFAULT_VALUE_LENGTH;
114
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700115 //@Property(
116 // name = "includeEvents",
117 // boolValue = DEFAULT_INCLUDE_EVENTS,
118 // label = "Whether to include events in test")
Jordan Halterman73296092018-03-08 16:06:28 -0500119 private boolean includeEvents = DEFAULT_INCLUDE_EVENTS;
120
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700121 //@Property(
122 // name = "deterministic",
123 // boolValue = DEFAULT_DETERMINISTIC,
124 // label = "Whether to deterministically populate entries")
Jordan Halterman73296092018-03-08 16:06:28 -0500125 private boolean deterministic = DEFAULT_DETERMINISTIC;
126
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700127 @Reference(cardinality = MANDATORY)
Jordan Halterman29718e62017-07-20 13:24:33 -0700128 protected ClusterService clusterService;
129
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700130 @Reference(cardinality = MANDATORY)
Jordan Halterman29718e62017-07-20 13:24:33 -0700131 protected StorageService storageService;
132
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700133 @Reference(cardinality = MANDATORY)
Jordan Halterman29718e62017-07-20 13:24:33 -0700134 protected ComponentConfigService configService;
135
Ray Milkeyd84f89b2018-08-17 14:54:17 -0700136 @Reference(cardinality = MANDATORY)
Jordan Halterman29718e62017-07-20 13:24:33 -0700137 protected PrimitivePerfCollector sampleCollector;
138
Jordan Halterman29718e62017-07-20 13:24:33 -0700139 private ExecutorService messageHandlingExecutor;
140
141 private ExecutorService workers;
Jordan Halterman73296092018-03-08 16:06:28 -0500142
143 // Tracks whether the test has been started in the cluster.
144 private AtomicValue<Boolean> started;
145 private AtomicValueEventListener<Boolean> startedListener = event -> {
146 if (event.newValue()) {
147 startTestRun();
148 } else {
149 stopTestRun();
150 }
151 };
152
153 // Tracks whether local clients are running.
154 private volatile boolean running;
Jordan Halterman29718e62017-07-20 13:24:33 -0700155
156 private Timer reportTimer;
157
158 private NodeId nodeId;
159 private TimerTask reporterTask;
160
161 private long startTime;
162 private long currentStartTime;
163 private AtomicLong overallCounter;
164 private AtomicLong currentCounter;
165
166 @Activate
167 public void activate(ComponentContext context) {
168 configService.registerProperties(getClass());
169
170 nodeId = clusterService.getLocalNode().id();
Jordan Halterman73296092018-03-08 16:06:28 -0500171 started = storageService.<Boolean>atomicValueBuilder()
172 .withName("perf-test-started")
173 .withSerializer(Serializer.using(KryoNamespaces.BASIC))
174 .build()
175 .asAtomicValue();
176 started.addListener(startedListener);
Jordan Halterman29718e62017-07-20 13:24:33 -0700177
178 reportTimer = new Timer("onos-primitive-perf-reporter");
179
180 messageHandlingExecutor = Executors.newSingleThreadExecutor(
181 groupedThreads("onos/perf", "command-handler"));
182
Jordan Halterman29718e62017-07-20 13:24:33 -0700183 // TODO: investigate why this seems to be necessary for configs to get picked up on initial activation
184 modify(context);
185 }
186
187 @Deactivate
188 public void deactivate() {
189 stopTestRun();
190
191 configService.unregisterProperties(getClass(), false);
192 messageHandlingExecutor.shutdown();
Jordan Halterman73296092018-03-08 16:06:28 -0500193 started.removeListener(startedListener);
Jordan Halterman29718e62017-07-20 13:24:33 -0700194
195 if (reportTimer != null) {
196 reportTimer.cancel();
197 reportTimer = null;
198 }
199 }
200
Jordan Halterman73296092018-03-08 16:06:28 -0500201 private int parseInt(Dictionary<?, ?> properties, String name, int currentValue, int defaultValue) {
202 try {
203 String s = get(properties, name);
204 return isNullOrEmpty(s) ? currentValue : Integer.parseInt(s.trim());
205 } catch (NumberFormatException | ClassCastException e) {
206 log.warn("Malformed configuration detected; using defaults", e);
207 return defaultValue;
208 }
209 }
210
Jordan Halterman29718e62017-07-20 13:24:33 -0700211 @Modified
212 public void modify(ComponentContext context) {
213 if (context == null) {
214 logConfig("Reconfigured");
215 return;
216 }
217
218 Dictionary<?, ?> properties = context.getProperties();
Jordan Halterman73296092018-03-08 16:06:28 -0500219 int newNumClients = parseInt(properties, "numClients", numClients, DEFAULT_NUM_CLIENTS);
220 int newWritePercentage = parseInt(properties, "writePercentage", writePercentage, DEFAULT_WRITE_PERCENTAGE);
221 int newNumKeys = parseInt(properties, "numKeys", numKeys, DEFAULT_NUM_KEYS);
222 int newKeyLength = parseInt(properties, "keyLength", keyLength, DEFAULT_KEY_LENGTH);
223 int newNumValues = parseInt(properties, "numValues", numValues, DEFAULT_NUM_UNIQUE_VALUES);
224 int newValueLength = parseInt(properties, "valueLength", valueLength, DEFAULT_VALUE_LENGTH);
Jordan Halterman29718e62017-07-20 13:24:33 -0700225
Jordan Halterman73296092018-03-08 16:06:28 -0500226 String includeEventsString = get(properties, "includeEvents");
227 boolean newIncludeEvents = isNullOrEmpty(includeEventsString)
228 ? includeEvents
229 : Boolean.parseBoolean(includeEventsString.trim());
Jordan Halterman29718e62017-07-20 13:24:33 -0700230
Jordan Halterman73296092018-03-08 16:06:28 -0500231 String deterministicString = get(properties, "deterministic");
232 boolean newDeterministic = isNullOrEmpty(deterministicString)
233 ? includeEvents
234 : Boolean.parseBoolean(deterministicString.trim());
235
236 if (newNumClients != numClients
237 || newWritePercentage != writePercentage
238 || newNumKeys != numKeys
239 || newKeyLength != keyLength
240 || newNumValues != numValues
241 || newValueLength != valueLength
242 || newIncludeEvents != includeEvents
243 || newDeterministic != deterministic) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700244 numClients = newNumClients;
245 writePercentage = newWritePercentage;
Jordan Halterman73296092018-03-08 16:06:28 -0500246 numKeys = newNumKeys;
247 keyLength = newKeyLength;
248 numValues = newNumValues;
249 valueLength = newValueLength;
250 includeEvents = newIncludeEvents;
251 deterministic = newDeterministic;
Jordan Halterman29718e62017-07-20 13:24:33 -0700252 logConfig("Reconfigured");
Jordan Halterman73296092018-03-08 16:06:28 -0500253 Boolean started = this.started.get();
254 if (started != null && started) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700255 stop();
256 start();
257 }
Jordan Halterman73296092018-03-08 16:06:28 -0500258 } else {
259 Boolean started = this.started.get();
260 if (started != null && started) {
261 if (running) {
262 stopTestRun();
263 }
264 startTestRun();
265 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700266 }
267 }
268
Jordan Halterman73296092018-03-08 16:06:28 -0500269 /**
270 * Starts a new test.
271 */
Jordan Halterman29718e62017-07-20 13:24:33 -0700272 public void start() {
Jordan Halterman73296092018-03-08 16:06:28 -0500273 // To stop the test, we simply update the "started" value. Events from the change will notify all
274 // nodes to start the test.
275 Boolean started = this.started.get();
276 if (started == null || !started) {
277 this.started.set(true);
Jordan Halterman29718e62017-07-20 13:24:33 -0700278 }
279 }
280
Jordan Halterman73296092018-03-08 16:06:28 -0500281 /**
282 * Stops a test.
283 */
Jordan Halterman29718e62017-07-20 13:24:33 -0700284 public void stop() {
Jordan Halterman73296092018-03-08 16:06:28 -0500285 // To stop the test, we simply update the "started" value. Events from the change will notify all
286 // nodes to stop the test.
287 Boolean started = this.started.get();
288 if (started != null && started) {
289 this.started.set(false);
Jordan Halterman29718e62017-07-20 13:24:33 -0700290 }
291 }
292
293 private void logConfig(String prefix) {
Jordan Halterman73296092018-03-08 16:06:28 -0500294 log.info("{} with " +
295 "numClients = {}; " +
296 "writePercentage = {}; " +
297 "numKeys = {}; " +
298 "keyLength = {}; " +
299 "numValues = {}; " +
300 "valueLength = {}; " +
301 "includeEvents = {}; " +
302 "deterministic = {}",
303 prefix,
304 numClients,
305 writePercentage,
306 numKeys,
307 keyLength,
308 numValues,
309 valueLength,
310 includeEvents,
311 deterministic);
Jordan Halterman29718e62017-07-20 13:24:33 -0700312 }
313
314 private void startTestRun() {
Jordan Halterman73296092018-03-08 16:06:28 -0500315 if (running) {
316 return;
317 }
318
Jordan Halterman29718e62017-07-20 13:24:33 -0700319 sampleCollector.clearSamples();
320
321 startTime = System.currentTimeMillis();
322 currentStartTime = startTime;
323 currentCounter = new AtomicLong();
324 overallCounter = new AtomicLong();
325
326 reporterTask = new ReporterTask();
Jordan Halterman73296092018-03-08 16:06:28 -0500327 reportTimer.scheduleAtFixedRate(
328 reporterTask, REPORT_PERIOD - currentTimeMillis() % REPORT_PERIOD, REPORT_PERIOD);
Jordan Halterman29718e62017-07-20 13:24:33 -0700329
Jordan Halterman73296092018-03-08 16:06:28 -0500330 running = true;
Jordan Halterman29718e62017-07-20 13:24:33 -0700331
332 Map<String, ControllerNode> nodes = new TreeMap<>();
333 for (ControllerNode node : clusterService.getNodes()) {
334 nodes.put(node.id().id(), node);
335 }
336
337 // Compute the index of the local node in a sorted list of nodes.
338 List<String> sortedNodes = Lists.newArrayList(nodes.keySet());
339 int nodeCount = nodes.size();
340 int index = sortedNodes.indexOf(nodeId.id());
341
342 // Count the number of workers assigned to this node.
343 int workerCount = 0;
344 for (int i = 1; i <= numClients; i++) {
345 if (i % nodeCount == index) {
346 workerCount++;
347 }
348 }
349
350 // Create a worker pool and start the workers for this node.
Jordan Haltermanf5333892017-08-29 10:42:13 -0700351 if (workerCount > 0) {
Jordan Halterman73296092018-03-08 16:06:28 -0500352 String[] keys = createStrings(keyLength, numKeys);
353 String[] values = createStrings(valueLength, numValues);
354
Jordan Haltermanf5333892017-08-29 10:42:13 -0700355 workers = Executors.newFixedThreadPool(workerCount, groupedThreads("onos/primitive-perf", "worker-%d"));
356 for (int i = 0; i < workerCount; i++) {
Jordan Halterman73296092018-03-08 16:06:28 -0500357 if (deterministic) {
358 workers.submit(new DeterministicRunner(keys, values));
359 } else {
360 workers.submit(new NonDeterministicRunner(keys, values));
361 }
Jordan Haltermanf5333892017-08-29 10:42:13 -0700362 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700363 }
364 log.info("Started test run");
365 }
366
Jordan Halterman73296092018-03-08 16:06:28 -0500367 /**
368 * Creates a deterministic array of strings to write to the cluster.
369 *
370 * @param length the string lengths
371 * @param count the string count
372 * @return a deterministic array of strings
373 */
374 private String[] createStrings(int length, int count) {
375 Random random = new Random(length);
376 List<String> stringsList = new ArrayList<>(count);
377 for (int i = 0; i < count; i++) {
378 stringsList.add(randomString(length, random));
379 }
380 return stringsList.toArray(new String[stringsList.size()]);
381 }
382
383 /**
384 * Creates a deterministic string based on the given seed.
385 *
386 * @param length the seed from which to create the string
387 * @param random the random object from which to create the string characters
388 * @return the string
389 */
390 private String randomString(int length, Random random) {
391 char[] buffer = new char[length];
392 for (int i = 0; i < length; i++) {
393 buffer[i] = CHARS[random.nextInt(CHARS.length)];
394 }
395 return new String(buffer);
396 }
397
Jordan Halterman29718e62017-07-20 13:24:33 -0700398 private void stopTestRun() {
Jordan Halterman73296092018-03-08 16:06:28 -0500399 if (!running) {
400 return;
401 }
402
Jordan Halterman29718e62017-07-20 13:24:33 -0700403 if (reporterTask != null) {
404 reporterTask.cancel();
405 reporterTask = null;
406 }
407
Jordan Haltermanf5333892017-08-29 10:42:13 -0700408 if (workers != null) {
409 workers.shutdown();
410 try {
411 workers.awaitTermination(10, TimeUnit.MILLISECONDS);
412 } catch (InterruptedException e) {
413 log.warn("Failed to stop worker", e);
Ray Milkey5c7d4882018-02-05 14:50:39 -0800414 Thread.currentThread().interrupt();
Jordan Haltermanf5333892017-08-29 10:42:13 -0700415 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700416 }
417
418 sampleCollector.recordSample(0, 0);
419 sampleCollector.recordSample(0, 0);
Jordan Halterman73296092018-03-08 16:06:28 -0500420
421 running = false;
Jordan Halterman29718e62017-07-20 13:24:33 -0700422
423 log.info("Stopped test run");
424 }
425
426 // Submits primitive operations.
Jordan Halterman73296092018-03-08 16:06:28 -0500427 abstract class Runner implements Runnable {
428 final String[] keys;
429 final String[] values;
430 final Random random = new Random();
431 ConsistentMap<String, String> map;
Jordan Halterman29718e62017-07-20 13:24:33 -0700432
Jordan Halterman73296092018-03-08 16:06:28 -0500433 Runner(String[] keys, String[] values) {
434 this.keys = keys;
435 this.values = values;
Jordan Halterman29718e62017-07-20 13:24:33 -0700436 }
437
438 @Override
439 public void run() {
440 setup();
Jordan Halterman73296092018-03-08 16:06:28 -0500441 while (running) {
Jordan Halterman29718e62017-07-20 13:24:33 -0700442 try {
443 submit();
444 } catch (Exception e) {
445 log.warn("Exception during cycle", e);
446 }
447 }
448 teardown();
449 }
450
Jordan Halterman73296092018-03-08 16:06:28 -0500451 void setup() {
Jordan Halterman29718e62017-07-20 13:24:33 -0700452 map = storageService.<String, String>consistentMapBuilder()
453 .withName("perf-test")
454 .withSerializer(Serializer.using(KryoNamespaces.BASIC))
455 .build();
Jordan Halterman73296092018-03-08 16:06:28 -0500456 if (includeEvents) {
457 map.addListener(event -> {
458 });
459 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700460 }
461
Jordan Halterman73296092018-03-08 16:06:28 -0500462 abstract void submit();
463
464 void teardown() {
465 //map.destroy();
466 }
467 }
468
469 private class NonDeterministicRunner extends Runner {
470 NonDeterministicRunner(String[] keys, String[] values) {
471 super(keys, values);
472 }
473
474 @Override
475 void submit() {
476 currentCounter.incrementAndGet();
477 String key = keys[random.nextInt(keys.length)];
478 if (random.nextInt(100) < writePercentage) {
479 map.put(key, values[random.nextInt(values.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700480 } else {
481 map.get(key);
482 }
483 }
Jordan Halterman29718e62017-07-20 13:24:33 -0700484 }
485
Jordan Halterman73296092018-03-08 16:06:28 -0500486 private class DeterministicRunner extends Runner {
487 private int index;
488
489 DeterministicRunner(String[] keys, String[] values) {
490 super(keys, values);
491 }
492
Jordan Halterman29718e62017-07-20 13:24:33 -0700493 @Override
Jordan Halterman73296092018-03-08 16:06:28 -0500494 void submit() {
495 currentCounter.incrementAndGet();
496 if (random.nextInt(100) < writePercentage) {
497 map.put(keys[index++ % keys.length], values[random.nextInt(values.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700498 } else {
Jordan Halterman73296092018-03-08 16:06:28 -0500499 map.get(keys[random.nextInt(keys.length)]);
Jordan Halterman29718e62017-07-20 13:24:33 -0700500 }
501 }
502 }
503
504 private class ReporterTask extends TimerTask {
505 @Override
506 public void run() {
507 long endTime = System.currentTimeMillis();
508 long overallTime = endTime - startTime;
509 long currentTime = endTime - currentStartTime;
510 long currentCount = currentCounter.getAndSet(0);
511 long overallCount = overallCounter.addAndGet(currentCount);
512 sampleCollector.recordSample(overallTime > 0 ? overallCount / (overallTime / 1000d) : 0,
513 currentTime > 0 ? currentCount / (currentTime / 1000d) : 0);
514 currentStartTime = System.currentTimeMillis();
515 }
516 }
517
518}