Adding intent performance testing app

Change-Id: I1b3a8b6e5b9230066d31f8f520e212973b6f703e
diff --git a/apps/intent-perf/pom.xml b/apps/intent-perf/pom.xml
new file mode 100644
index 0000000..038ecd8
--- /dev/null
+++ b/apps/intent-perf/pom.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-apps</artifactId>
+        <version>1.1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-app-intent-perf</artifactId>
+    <packaging>bundle</packaging>
+
+    <description>ONOS intent perf app bundle</description>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <version>2.5.3</version>
+                <configuration>
+                    <descriptor>src/assembly/bin.xml</descriptor>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/apps/intent-perf/src/assembly/app.xml b/apps/intent-perf/src/assembly/app.xml
new file mode 100644
index 0000000..dcc6ec8
--- /dev/null
+++ b/apps/intent-perf/src/assembly/app.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<app name="org.onosproject.intentperf" origin="ON.Lab" version="1.1.0"
+     features="onos-app-intent-perf">
+    <description>Intent performance application</description>
+</app>
\ No newline at end of file
diff --git a/apps/intent-perf/src/assembly/bin.xml b/apps/intent-perf/src/assembly/bin.xml
new file mode 100644
index 0000000..b92fa72
--- /dev/null
+++ b/apps/intent-perf/src/assembly/bin.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2015 Open Networking Laboratory
+  ~
+  ~ 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.
+  -->
+<assembly
+        xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
+    <formats>
+        <format>zip</format>
+    </formats>
+    <id>onos</id>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <files>
+        <file>
+            <source>src/assembly/app.xml</source>
+            <destName>app.xml</destName>
+        </file>
+        <file>
+            <source>target/${project.artifactId}-${project.version}.jar</source>
+            <destName>m2/org/onosproject/${project.artifactId}/${project.version}/${project.artifactId}-${project.version}.jar</destName>
+        </file>
+    </files>
+</assembly>
\ No newline at end of file
diff --git a/apps/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java b/apps/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java
new file mode 100644
index 0000000..19cc06d
--- /dev/null
+++ b/apps/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * 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.onosproject.intentperf;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.util.Counter;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentListener;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.Key;
+import org.onosproject.net.intent.PointToPointIntent;
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import static org.onlab.util.Tools.delay;
+import static org.onlab.util.Tools.groupedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Application to set up demos.
+ */
+@Component(immediate = true)
+public class IntentPerfInstaller {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentService intentService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    private ExecutorService worker;
+    private ApplicationId appId;
+    private Listener listener;
+    private Set<Intent> intents;
+    private Set<Intent> submitted;
+    private Set<Intent> withdrawn;
+    private boolean stopped;
+
+    private static final long REPORT_PERIOD = 5000L; //ms
+    private Timer reportTimer;
+
+    //FIXME make this configurable
+    private static final int NUM_KEYS = 10000;
+
+    @Activate
+    public void activate() {
+        String nodeId = clusterService.getLocalNode().ip().toString();
+        appId = coreService.registerApplication("org.onosproject.intentperf."
+                                                        + nodeId);
+        intents = Sets.newHashSet();
+        submitted = Sets.newHashSet();
+        withdrawn = Sets.newHashSet();
+
+        worker = Executors.newFixedThreadPool(1, groupedThreads("onos/intent-perf", "worker"));
+        log.info("Started with Application ID {}", appId.id());
+        start(); //FIXME
+    }
+
+    @Deactivate
+    public void deactivate() {
+        stop();
+        log.info("Stopped");
+    }
+
+    public void start() {
+        // perhaps we want to prime before listening...
+        // we will need to discard the first few results for priming and warmup
+        listener = new Listener();
+        intentService.addListener(listener);
+        reportTimer = new Timer("intent-perf-reporter");
+        reportTimer.scheduleAtFixedRate(new TimerTask() {
+            @Override
+            public void run() {
+                listener.report();
+            }
+        }, REPORT_PERIOD, REPORT_PERIOD);
+
+        stopped = false;
+        worker.submit(() -> {
+            delay(2000);
+            createIntents(NUM_KEYS, 2); //FIXME
+            prime();
+            while (!stopped) {
+                cycle();
+                // TODO delay if required
+            }
+        });
+
+    }
+
+    public void stop() {
+        if (listener != null) {
+            reportTimer.cancel();
+            intentService.removeListener(listener);
+            listener = null;
+            reportTimer = null;
+        }
+        stopped = true;
+        try {
+            worker.awaitTermination(5, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            log.warn("Failed to stop worker.");
+        }
+    }
+
+
+    private void cycle() {
+        subset(submitted).forEach(this::withdraw);
+        subset(withdrawn).forEach(this::submit);
+    }
+
+    private Iterable<Intent> subset(Set<Intent> intents) {
+        List<Intent> subset = Lists.newArrayList(intents);
+        Collections.shuffle(subset);
+        return subset.subList(0, subset.size() / 2);
+    }
+
+    private void submit(Intent intent) {
+        intentService.submit(intent);
+        submitted.add(intent);
+        withdrawn.remove(intent); //TODO could check result here...
+    }
+
+    private void withdraw(Intent intent) {
+        intentService.withdraw(intent);
+        withdrawn.add(intent);
+        submitted.remove(intent); //TODO could check result here...
+    }
+
+    private void createIntents(int numberOfKeys, int pathLength) {
+
+        Iterator<Device> deviceItr = deviceService.getAvailableDevices().iterator();
+
+        if (!deviceItr.hasNext()) {
+            throw new IllegalStateException("There are no devices");
+        }
+
+        Device ingressDevice = deviceItr.next();
+
+        for (int i = 0; i < numberOfKeys; i++) {
+            Key key = Key.of(i, appId);
+            TrafficSelector selector = DefaultTrafficSelector.builder().build();
+
+            TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+            //FIXME
+            ConnectPoint ingress = new ConnectPoint(ingressDevice.id(), PortNumber.portNumber(1));
+            ConnectPoint egress = new ConnectPoint(ingressDevice.id(), PortNumber.portNumber(2));
+
+            Intent intent = new PointToPointIntent(appId, key,
+                                                   selector, treatment,
+                                                   ingress, egress,
+                                                   Collections.emptyList());
+            intents.add(intent);
+        }
+    }
+
+    private void prime() {
+        int i = 0;
+        withdrawn.addAll(intents);
+        for (Intent intent : intents) {
+            submit(intent);
+            // only submit half of the intents to start
+            if (i++ >= intents.size() / 2) {
+                break;
+            }
+        }
+    }
+
+    class Listener implements IntentListener {
+
+
+        private Map<IntentEvent.Type, Counter> counters;
+
+        public Listener() {
+            counters = initCounters();
+
+        }
+
+        private Map<IntentEvent.Type, Counter> initCounters() {
+            Map<IntentEvent.Type, Counter> map = Maps.newHashMap();
+            for (IntentEvent.Type type : IntentEvent.Type.values()) {
+                map.put(type, new Counter());
+            }
+            return map;
+        }
+
+        @Override
+        public void event(IntentEvent event) {
+            if (event.subject().appId().equals(appId)) {
+                counters.get(event.type()).add(1);
+            }
+        }
+
+        public void report() {
+            StringBuilder stringBuilder = new StringBuilder();
+            for (IntentEvent.Type type : IntentEvent.Type.values()) {
+                stringBuilder.append(printCounter(type)).append("; ");
+            }
+            log.info("Intent Throughput:\n{}", stringBuilder);
+        }
+
+        private String printCounter(IntentEvent.Type event) {
+            Counter counter = counters.get(event);
+            String result = String.format("%s=%.2f", event, counter.throughput());
+            counter.reset();
+            return result;
+        }
+    }
+}
diff --git a/apps/pom.xml b/apps/pom.xml
index 167e64c..ebf1df7 100644
--- a/apps/pom.xml
+++ b/apps/pom.xml
@@ -48,6 +48,7 @@
         <module>routing</module>
         <module>routing-api</module>
         <module>bgprouter</module>
+        <module>intent-perf</module>
     </modules>
 
     <properties>
diff --git a/features/features.xml b/features/features.xml
index 23e833a..3a5d5e5 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -257,6 +257,12 @@
         <bundle>mvn:org.onosproject/onos-app-demo/@ONOS-VERSION</bundle>
     </feature>
 
+    <feature name="onos-app-intent-perf" version="@FEATURE-VERSION"
+             description="ONOS intent perf applications">
+        <feature>onos-api</feature>
+        <bundle>mvn:org.onosproject/onos-app-intent-perf/@ONOS-VERSION</bundle>
+    </feature>
+
     <feature name="onos-app-election" version="@FEATURE-VERSION"
              description="ONOS app leadership election test">
         <feature>onos-api</feature>