From 03c8000a59a7a7b2a608ad4ef598f37120340eba Mon Sep 17 00:00:00 2001
From: Michael Zangl <michael.zangl@student.kit.edu>
Date: Mon, 20 Jul 2015 13:04:02 +0200
Subject: [PATCH 1/4] Added new performance test for OsmPrimitive tag handling.

---
 .../openstreetmap/josm/PerformanceTestUtils.java   |  44 +++++
 .../josm/data/osm/KeyValuePerformanceTest.java     | 204 ++++++++++++++++++++
 .../josm/data/osm/OsmDataGenerator.java            | 214 +++++++++++++++++++++
 3 files changed, 462 insertions(+)
 create mode 100644 test/performance/org/openstreetmap/josm/PerformanceTestUtils.java
 create mode 100644 test/performance/org/openstreetmap/josm/data/osm/KeyValuePerformanceTest.java
 create mode 100644 test/performance/org/openstreetmap/josm/data/osm/OsmDataGenerator.java

diff --git a/test/performance/org/openstreetmap/josm/PerformanceTestUtils.java b/test/performance/org/openstreetmap/josm/PerformanceTestUtils.java
new file mode 100644
index 0000000..f2d64ce
--- /dev/null
+++ b/test/performance/org/openstreetmap/josm/PerformanceTestUtils.java
@@ -0,0 +1,44 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm;
+
+/**
+ * Timer utilities for performance tests.
+ * @author Michael Zangl
+ */
+public class PerformanceTestUtils {
+    /**
+     * A timer that measures the time from it's creation to the {@link #done()} call.
+    * @author Michael Zangl
+     */
+    public static class PerformanceTestTimer {
+        private String name;
+        private long time;
+
+        protected PerformanceTestTimer(String name) {
+            this.name = name;
+            time = System.nanoTime();
+        }
+
+        /**
+         * Prints the time since this timer was created.
+         */
+        public void done() {
+            long dTime = System.nanoTime() - time;
+            System.out.println("TIMER " + name + ": " + dTime / 1000000 + "ms");
+        }
+    }
+
+    private PerformanceTestUtils() {
+    }
+
+    /**
+     * Starts a new performance timer.
+     * @param name The name/description of the timer.
+     * @return A {@link PerformanceTestTimer} object of which you can call {@link PerformanceTestTimer#done()} when done.
+     */
+    public static PerformanceTestTimer startTimer(String name) {
+        System.gc();
+        System.runFinalization();
+        return new PerformanceTestTimer(name);
+    }
+}
diff --git a/test/performance/org/openstreetmap/josm/data/osm/KeyValuePerformanceTest.java b/test/performance/org/openstreetmap/josm/data/osm/KeyValuePerformanceTest.java
new file mode 100644
index 0000000..83171b5
--- /dev/null
+++ b/test/performance/org/openstreetmap/josm/data/osm/KeyValuePerformanceTest.java
@@ -0,0 +1,204 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Random;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.PerformanceTestUtils;
+import org.openstreetmap.josm.PerformanceTestUtils.PerformanceTestTimer;
+import org.openstreetmap.josm.data.osm.OsmDataGenerator.KeyValueDataGenerator;
+
+/**
+ * This test measures the performance of {@link OsmPrimitive#get(String)} and related.
+ * @author Michael Zangl
+ */
+public class KeyValuePerformanceTest {
+    private static final int PUT_RUNS = 10000;
+    private static final int GET_RUNS = 100000;
+    private static final int TEST_STRING_COUNT = 10000;
+    private static final int STRING_INTERN_TESTS = 5000000;
+    private static final double[] TAG_NODE_RATIOS = new double[] { .05, .3, 3, 20, 200 };
+    private ArrayList<String> testStrings = new ArrayList<>();
+    private Random random;
+
+    /**
+     * Prepare the test.
+     */
+    @BeforeClass
+    public static void createJOSMFixture() {
+        JOSMFixture.createPerformanceTestFixture().init(true);
+    }
+
+    /**
+     * See if there is a big difference between Strings that are interned and those that are not.
+     */
+    @Test
+    public void meassureStringEqualsIntern() {
+        String str1Interned = "string1";
+        String str1InternedB = "string1";
+        String str1 = new String(str1Interned);
+        String str1B = new String(str1Interned);
+        String str2Interned = "string2";
+        String str2 = new String(str2Interned);
+
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            // warm up
+            assertTrue(str1.equals(str1B));
+        }
+
+        PerformanceTestTimer timer = PerformanceTestUtils.startTimer("Assertion overhead.");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertTrue(true);
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1.equals(str2) succeeds (without intern)");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertTrue(str1.equals(str1B));
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1 == str2 succeeds");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertTrue(str1Interned == str1InternedB);
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1 == str2.intern() succeeds");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertTrue(str1Interned == str1.intern());
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1 == str2.intern() succeeds for interned string");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertTrue(str1Interned == str1InternedB.intern());
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1.equals(str2) = fails (without intern)");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertFalse(str1.equals(str2));
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1 == str2 fails");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertFalse(str1Interned == str2Interned);
+        }
+        timer.done();
+
+        timer = PerformanceTestUtils.startTimer("str1 == str2.intern() fails");
+        for (int i = 0; i < STRING_INTERN_TESTS; i++) {
+            assertFalse(str1Interned == str2.intern());
+        }
+        timer.done();
+    }
+
+    /**
+     * Generate an array of test strings.
+     */
+    @Before
+    public void generateTestStrings() {
+        testStrings.clear();
+        random = new Random(123);
+        for (int i = 0; i < TEST_STRING_COUNT; i++) {
+            testStrings.add(RandomStringUtils.random(10, 0, 0, true, true, null, random));
+        }
+    }
+
+    /**
+     * Measure the speed of {@link OsmPrimitive#put(String, String)}
+     */
+    @Test
+    public void testKeyValuePut() {
+        for (double tagNodeRatio : TAG_NODE_RATIOS) {
+            int nodeCount = (int) (PUT_RUNS / tagNodeRatio);
+            KeyValueDataGenerator generator = OsmDataGenerator.getKeyValue(nodeCount, 0);
+            generator.generateDataSet();
+
+            PerformanceTestTimer timer = PerformanceTestUtils
+                    .startTimer("OsmPrimitive#put(String, String) with put/node ratio " + tagNodeRatio);
+
+            for (int i = 0; i < PUT_RUNS; i++) {
+                String key = generator.randomKey();
+                String value = generator.randomValue();
+                generator.randomNode().put(key, value);
+            }
+
+            timer.done();
+        }
+    }
+
+    /**
+     * Measure the speed of {@link OsmPrimitive#get(String)}
+     */
+    @Test
+    public void testKeyValueGet() {
+        for (double tagNodeRatio : TAG_NODE_RATIOS) {
+            KeyValueDataGenerator generator = OsmDataGenerator.getKeyValue(tagNodeRatio);
+            generator.generateDataSet();
+
+            PerformanceTestTimer timer = PerformanceTestUtils
+                    .startTimer("OsmPrimitive#get(String) with tag/node ratio " + tagNodeRatio);
+            for (int i = 0; i < GET_RUNS; i++) {
+                String key = generator.randomKey();
+                // to make comparison easier.
+                generator.randomValue();
+                generator.randomNode().get(key);
+            }
+            timer.done();
+        }
+    }
+
+    /**
+     * Measure the speed of {@link OsmPrimitive#getKeys()}
+     */
+    @Test
+    public void testKeyValueGetKeys() {
+        for (double tagNodeRatio : TAG_NODE_RATIOS) {
+            KeyValueDataGenerator generator = OsmDataGenerator.getKeyValue(tagNodeRatio);
+            generator.generateDataSet();
+
+            PerformanceTestTimer timer = PerformanceTestUtils.startTimer("OsmPrimitive#getKeys() with tag/node ratio "
+                    + tagNodeRatio);
+
+            for (int i = 0; i < GET_RUNS; i++) {
+                // to make comparison easier.
+                generator.randomKey();
+                generator.randomValue();
+                generator.randomNode().getKeys();
+            }
+            timer.done();
+        }
+    }
+
+    /**
+     * Measure the speed of {@link OsmPrimitive#getKeys()}.get(key)
+     */
+    @Test
+    public void testKeyValueGetKeysGet() {
+        for (double tagNodeRatio : TAG_NODE_RATIOS) {
+            KeyValueDataGenerator generator = OsmDataGenerator.getKeyValue(tagNodeRatio);
+            generator.generateDataSet();
+
+            PerformanceTestTimer timer = PerformanceTestUtils
+                    .startTimer("OsmPrimitive#getKeys().get(key) with tag/node ratio " + tagNodeRatio);
+            for (int i = 0; i < GET_RUNS; i++) {
+                String key = generator.randomKey();
+                // to make comparison easier.
+                generator.randomValue();
+                generator.randomNode().getKeys().get(key);
+            }
+            timer.done();
+        }
+    }
+}
diff --git a/test/performance/org/openstreetmap/josm/data/osm/OsmDataGenerator.java b/test/performance/org/openstreetmap/josm/data/osm/OsmDataGenerator.java
new file mode 100644
index 0000000..fce9c2d
--- /dev/null
+++ b/test/performance/org/openstreetmap/josm/data/osm/OsmDataGenerator.java
@@ -0,0 +1,214 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Random;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+/**
+ * This is an utility class that allows you to generate OSM test data.
+ * @author Michael Zangl
+ */
+public class OsmDataGenerator {
+    private static final int DEFAULT_KEY_VALUE_RATIO = 3;
+    private static final int DEFAULT_NODE_COUNT = 1000;
+    private static final String DATA_DIR = "data_nodist" + File.separator + "osmfiles";
+
+    /**
+     * A generator that generates test data by filling a data set.
+     * @author Michael Zangl
+     */
+    public static abstract class DataGenerator {
+        private String datasetName;
+        protected final Random random;
+        private DataSet ds;
+
+        /**
+         * Create a new generator.
+         * @param datasetName The name for the generator. Only used for human readability.
+         */
+        protected DataGenerator(String datasetName) {
+            this.datasetName = datasetName;
+            this.random = new Random(1234);
+        }
+
+        /**
+         * Generates the data set. If this method is called twice, the same dataset is returned.
+         * @return The generated data set.
+         */
+        public DataSet generateDataSet() {
+            ensureInitialized();
+            return ds;
+        }
+
+        protected void ensureInitialized() {
+            if (ds == null) {
+                ds = new DataSet();
+                fillData(ds);
+            }
+        }
+
+        protected abstract void fillData(DataSet ds);
+
+        /**
+         * Create a random node and add it to the dataset.
+         * @return
+         */
+        protected Node createRandomNode(DataSet ds) {
+            Node node = new Node();
+            node.setEastNorth(new EastNorth(random.nextDouble(), random.nextDouble()));
+            ds.addPrimitive(node);
+            return node;
+        }
+
+        protected String randomString() {
+            return RandomStringUtils.random(12, 0, 0, true, true, null, random);
+        }
+
+        /**
+         * Gets a file path where this data could be stored.
+         * @return A file path.
+         */
+        public File getFile() {
+            return new File(DATA_DIR + File.separator + datasetName + ".osm");
+        }
+
+        /**
+         * Creates a new {@link OsmDataLayer} that uses the underlying dataset of this generator.
+         * @return A new data layer.
+         */
+        public OsmDataLayer createDataLayer() {
+            return new OsmDataLayer(generateDataSet(), datasetName, getFile());
+        }
+
+        @Override
+        public String toString() {
+            return "DataGenerator [datasetName=" + datasetName + "]";
+        }
+    }
+
+    /**
+     * A data generator that generates a bunch of random nodes.
+    * @author Michael Zangl
+     */
+    public static class NodeDataGenerator extends DataGenerator {
+        protected final ArrayList<Node> nodes = new ArrayList<>();
+        private final int nodeCount;
+
+        private NodeDataGenerator(String datasetName, int nodeCount) {
+            super(datasetName);
+            this.nodeCount = nodeCount;
+        }
+
+        @Override
+        public void fillData(DataSet ds) {
+            for (int i = 0; i < nodeCount; i++) {
+                nodes.add(createRandomNode(ds));
+            }
+        }
+
+        /**
+         * Gets a random node of this dataset.
+         * @return A random node.
+         */
+        public Node randomNode() {
+            ensureInitialized();
+            return nodes.get(random.nextInt(nodes.size()));
+        }
+    }
+
+    /**
+     * A data generator that generates a bunch of random nodes and fills them with keys/values.
+    * @author Michael Zangl
+     */
+    public static class KeyValueDataGenerator extends NodeDataGenerator {
+
+        private static final int VALUE_COUNT = 200;
+        private static final int KEY_COUNT = 150;
+        private final double tagNodeRation;
+        private ArrayList<String> keys;
+        private ArrayList<String> values;
+
+        private KeyValueDataGenerator(String datasetName, int nodeCount, double tagNodeRation) {
+            super(datasetName, nodeCount);
+            this.tagNodeRation = tagNodeRation;
+        }
+
+        @Override
+        public void fillData(DataSet ds) {
+            super.fillData(ds);
+            keys = new ArrayList<>();
+            for (int i = 0; i < KEY_COUNT; i++) {
+                keys.add(randomString());
+            }
+            values = new ArrayList<>();
+            for (int i = 0; i < VALUE_COUNT; i++) {
+                values.add(randomString());
+            }
+
+            double tags = nodes.size() * tagNodeRation;
+            for (int i = 0; i < tags; i++) {
+                String key = randomKey();
+                String value = randomValue();
+                nodes.get(random.nextInt(nodes.size())).put(key, value);
+            }
+        }
+
+        /**
+         * Gets a random value that was used to fill the tags.
+         * @return A random String probably used in as value somewhere.
+         */
+        public String randomValue() {
+            ensureInitialized();
+            return values.get(random.nextInt(values.size()));
+        }
+
+        /**
+         * Gets a random key that was used to fill the tags.
+         * @return A random String probably used in as key somewhere.
+         */
+        public String randomKey() {
+            ensureInitialized();
+            return keys.get(random.nextInt(keys.size()));
+        }
+    }
+
+    /**
+     * Generate a generator that creates some nodes and adds random keys and values to it.
+     * @return The generator
+     */
+    public static KeyValueDataGenerator getKeyValue() {
+        return getKeyValue(DEFAULT_KEY_VALUE_RATIO);
+    }
+
+    /**
+     * Generate a generator that creates some nodes and adds random keys and values to it.
+     * @param tagNodeRation How many tags to add per node (on average).
+     * @return The generator
+     */
+    public static KeyValueDataGenerator getKeyValue(double tagNodeRation) {
+        return getKeyValue(DEFAULT_NODE_COUNT, tagNodeRation);
+    }
+
+    /**
+     * Generate a generator that creates some nodes and adds random keys and values to it.
+     * @param nodeCount The number of nodes the dataset should contain.
+     * @param tagNodeRation How many tags to add per node (on average).
+     * @return The generator
+     */
+    public static KeyValueDataGenerator getKeyValue(int nodeCount, double tagNodeRation) {
+        return new KeyValueDataGenerator("key-value", nodeCount, tagNodeRation);
+    }
+
+    /**
+     * Create a generator that generates a bunch of nodes.
+     * @return The generator
+     */
+    public static DataGenerator getNodes() {
+        return new NodeDataGenerator("nodes", DEFAULT_NODE_COUNT);
+    }
+}
-- 
1.9.1

