timing: Extract statistics to a generic class
authorGeneviève Bastien <gbastien+lttng@versatic.net>
Mon, 16 Jan 2017 20:56:02 +0000 (15:56 -0500)
committerGenevieve Bastien <gbastien+lttng@versatic.net>
Fri, 20 Jan 2017 19:41:21 +0000 (14:41 -0500)
These statistics receive any object in parameter and allow to specify a
function that returns the value of the object on which to do the stats.

Change-Id: I16fb1801dd8639fa8cd4da02c095163e53e6709b
Signed-off-by: Geneviève Bastien <gbastien+lttng@versatic.net>
Reviewed-on: https://git.eclipse.org/r/88689
Reviewed-by: Matthew Khouzam <matthew.khouzam@ericsson.com>
Tested-by: Matthew Khouzam <matthew.khouzam@ericsson.com>
Reviewed-by: Hudson CI
analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/AbstractStatisticsTest.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/LongStatisticsTest.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/ObjectStatisticsTest.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/OfflineStatisticsCalculator.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/StatObjectStub.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core/META-INF/MANIFEST.MF
analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/IStatistics.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/Statistics.java [new file with mode: 0644]
analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/package-info.java [new file with mode: 0644]

diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/AbstractStatisticsTest.java b/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/AbstractStatisticsTest.java
new file mode 100644 (file)
index 0000000..1de97a6
--- /dev/null
@@ -0,0 +1,535 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.analysis.timing.core.statistics.IStatistics;
+import org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Base class to test statistics for different object types. This is done with
+ * two tests.
+ * <ol>
+ * <li>test the values vs some sample points calculated by hand (sanity test)
+ * </li>
+ * <li>2- test exhaustively vs a reference implementation.</li>
+ * </ol>
+ *
+ * Each implementation will need to provide the object type to use for the test
+ * and implement a method to get objects that will return the values to test.
+ * Any additional dataset that should be tested for a specific object should be
+ * implemented in a concrete class for that object type.
+ *
+ * This test class tests statistics with positive and negative values. If for certain objects, negative values are not supported, the following test methods should be overridden, to be ignored:
+ * <ol>
+ * <li>{@link #testLimitDataset2()}</li>
+ * <li>{@link #testLargeDatasetNegative()}<li>
+ * </ol>
+ *
+ * @author Matthew Khouzam
+ * @author Geneviève Bastien
+ * @param <E>
+ *            The type of object to calculate statistics on
+ */
+public abstract class AbstractStatisticsTest<@NonNull E> {
+
+    private static final int MEDIUM_AMOUNT_OF_SEGMENTS = 100;
+    private static final int LARGE_AMOUNT_OF_SEGMENTS = 1000000;
+
+    private static final double ERROR = 0.000001;
+    private static final double APPROX_ERROR = 0.0001;
+
+    private final @Nullable Function<@NonNull E, @NonNull Long> fMapper;
+
+    /**
+     * Constructor
+     *
+     * @param mapper
+     *            A mapper function that takes an object to computes statistics
+     *            for and returns the value to use for the statistics. If the
+     *            mapper is <code>null</code>, a default Long identity function
+     *            will be used
+     */
+    public AbstractStatisticsTest(Function<E, @NonNull Long> mapper) {
+        fMapper = mapper;
+    }
+
+    /**
+     * Return the default mapper, or if it is null, a default mapper that casts to Long
+     * @return
+     */
+    private @NonNull Function<@NonNull E, @NonNull Long> getMapper() {
+        Function<@NonNull E, @NonNull Long> mapper = fMapper;
+        if (mapper == null) {
+            // Data type should be long, so define a default mapper
+            return e -> (Long) e;
+        }
+        return mapper;
+    }
+
+    private void testOnlineVsOffline(Collection<E> fixture) {
+
+        validate(new OfflineStatisticsCalculator<>(fixture, getMapper()), buildStats(fixture));
+    }
+
+    private Statistics<E> buildStats(Collection<E> fixture) {
+        Statistics<E> sss = createStatistics();
+        for (E seg : fixture) {
+            sss.update(seg);
+        }
+        return sss;
+    }
+
+    private static <@NonNull E> void validate(IStatistics<E> expected, IStatistics<E> toBeTested) {
+        assertEquals("# of elements", expected.getNbElements(), toBeTested.getNbElements());
+        assertEquals("Sum of values", expected.getTotal(), toBeTested.getTotal(), ERROR * expected.getTotal());
+        assertEquals("Mean", expected.getMean(), toBeTested.getMean(), ERROR * expected.getMean());
+        assertEquals("Min", expected.getMin(), toBeTested.getMin());
+        assertEquals("Max", expected.getMax(), toBeTested.getMax());
+        assertEquals("Min Element", expected.getMinObject(), toBeTested.getMinObject());
+        assertEquals("Max Element", expected.getMaxObject(), toBeTested.getMaxObject());
+        assertEquals("Standard Deviation", expected.getStdDev(), toBeTested.getStdDev(), APPROX_ERROR * expected.getStdDev());
+    }
+
+    /**
+     * Create a statistics object by calling the appropriate constructor whether the mapper function is null or not
+     */
+    private @NonNull Statistics<E> createStatistics() {
+        Function<@NonNull E, @NonNull Long> mapper = fMapper;
+        if (mapper == null) {
+            return new Statistics<>();
+        }
+        return new Statistics<>(mapper);
+    }
+
+    /**
+     * Create the fixture of elements of the generic type from the expected
+     * values for a test. For instance, if the test wants to test values {2, 4,
+     * 6} for a Statistics object for class Foo, then this method will return a
+     * Collection of Foo objects whose mapper function will map respectively to
+     * {2, 4, 6}
+     *
+     * @param longFixture
+     *            The long values that objects should map to for this test
+     * @return A collection of E elements that map to the long values
+     */
+    protected abstract Collection<E> createElementsWithValues(Collection<@NonNull Long> longFixture);
+
+    /**
+     * Test statistics on empty dataset
+     */
+    @Test
+    public void testEmpty() {
+        // Verify the expected default values
+        Statistics<E> stats = createStatistics();
+        assertEquals("Mean", 0, stats.getMean(), ERROR);
+        assertEquals("Min", Long.MAX_VALUE, stats.getMin());
+        assertEquals("Max", Long.MIN_VALUE, stats.getMax());
+        assertEquals("Standard Deviation", Double.NaN, stats.getStdDev(), ERROR);
+        assertNull(stats.getMinObject());
+        assertNull(stats.getMaxObject());
+        assertEquals("Nb objects", 0, stats.getNbElements());
+        assertEquals("Total", 0, stats.getTotal(), ERROR);
+    }
+
+    /**
+     * Test statistics with values added in ascending order
+     */
+    @Test
+    public void testAscending() {
+        // Create a fixture of long values in ascending order
+        List<@NonNull Long> longFixture = new ArrayList<>(MEDIUM_AMOUNT_OF_SEGMENTS);
+        for (long i = 0; i <= MEDIUM_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(i);
+        }
+
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        Statistics<E> sss = buildStats(fixture);
+        assertEquals("Mean", 50, sss.getMean(), ERROR);
+        assertEquals("Min", 0, sss.getMin());
+        assertEquals("Max", MEDIUM_AMOUNT_OF_SEGMENTS, sss.getMax());
+        assertEquals("Standard Deviation", 29.3, sss.getStdDev(), 0.02);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+    }
+
+    /**
+     * Test statistics with values added in descending order
+     */
+    @Test
+    public void testDescending() {
+        // Create a fixture of long values in descending order.
+        List<@NonNull Long> longFixture = new ArrayList<>(MEDIUM_AMOUNT_OF_SEGMENTS);
+        for (long i = MEDIUM_AMOUNT_OF_SEGMENTS; i >= 0; i--) {
+            longFixture.add(i);
+        }
+
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        Statistics<E> sss = buildStats(fixture);
+        assertEquals("Mean", 50, sss.getMean(), ERROR);
+        assertEquals("Min", 0, sss.getMin());
+        assertEquals("Max", MEDIUM_AMOUNT_OF_SEGMENTS, sss.getMax());
+        assertEquals("Standard Deviation", 29.3, sss.getStdDev(), 0.02);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+
+    }
+
+    /**
+     * Test a data set with a small number of objects
+     */
+    @Test
+    public void testSmallDataset() {
+        // Create fixture with only 1 element
+        List<@NonNull Long> longFixture = new ArrayList<>(1);
+        longFixture.add(1L);
+
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+    }
+
+    /**
+     * Test a dataset with positive limit values
+     */
+    @Test
+    public void testLimitDataset() {
+        // Create a fixture with max values
+        List<@NonNull Long> longFixture = new ArrayList<>(1);
+        longFixture.add(Long.MAX_VALUE);
+        longFixture.add(Long.MAX_VALUE);
+
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        Statistics<E> sss = buildStats(fixture);
+        // Test some values
+        assertEquals("Mean", Long.MAX_VALUE, sss.getMean(), ERROR);
+        assertEquals("Total", (double) 2 * Long.MAX_VALUE, sss.getTotal(), ERROR);
+        assertEquals("Standard deviation", Double.NaN, sss.getStdDev(), ERROR);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+    }
+
+    /**
+     * Test a dataset with negative limit values
+     *
+     * NOTE: This test has negative values
+     */
+    @Test
+    public void testLimitDataset2() {
+        // Create a fixture with min values
+        List<@NonNull Long> longFixture = new ArrayList<>(1);
+        longFixture.add(Long.MIN_VALUE);
+        longFixture.add(Long.MIN_VALUE);
+        longFixture.add(Long.MIN_VALUE);
+
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        Statistics<E> sss = buildStats(fixture);
+        // Test some values
+        assertEquals("Mean", Long.MIN_VALUE, sss.getMean(), ERROR);
+        assertEquals("Total", (double) 3 * Long.MIN_VALUE, sss.getTotal(), ERROR);
+        assertEquals("Standard deviation", 0, sss.getStdDev(), ERROR);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+    }
+
+    /**
+     * Test a data set with a large number of objects of random values
+     */
+    @Test
+    public void testLargeDataset() {
+        // Create a fixture of a large number of random values
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        Random rng = new Random(10);
+        for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(Math.abs(rng.nextLong()));
+        }
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+
+    }
+
+    /**
+     * Test a data set with a large number of objects of random values
+     *
+     * NOTE: This test contains negative values
+     */
+    @Test
+    public void testLargeDatasetNegative() {
+        // Create a fixture of a large number of random values
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        Random rng = new Random(10);
+        for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(rng.nextLong());
+        }
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+
+    }
+
+    /**
+     * Test a random dataset where the distribution follows white noise
+     */
+    @Test
+    public void testNoiseDataset() {
+        // Create a fixture of a large number of random values
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        Random rng = new Random(1234);
+        for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(Long.valueOf(Math.abs(rng.nextInt(1000000))));
+        }
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+
+    }
+
+    /**
+     * Test a random dataset where the distribution follows gaussian noise
+     */
+    @Test
+    public void gaussianNoiseTest() {
+        // Create a fixture of a large number of random values
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        Random rng = new Random(1234);
+        for (int i = 1; i <= LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(Long.valueOf(Math.abs(rng.nextInt(1000))));
+        }
+        // Create the statistics object for the objects that will return those
+        // values
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+
+        // Compare with an offline algorithm
+        testOnlineVsOffline(fixture);
+
+    }
+
+    /**
+     * Test building a statistics store with streams
+     */
+    @Test
+    public void streamBuildingTest() {
+        Statistics<E> expected = createStatistics();
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        for (long i = 0; i < LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(i);
+        }
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        fixture.forEach(e -> expected.update(e));
+        Statistics<E> actual = fixture.stream()
+                .<org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics<E>> collect(() -> createStatistics(),
+                        Statistics<E>::update, Statistics<E>::merge);
+        validate(expected, actual);
+    }
+
+    /**
+     * Test building a statistics store with parallel streams
+     */
+    @Test
+    public void parallelStreamBuildingTest() {
+        Statistics<E> expected = createStatistics();
+        List<@NonNull Long> longFixture = new ArrayList<>(LARGE_AMOUNT_OF_SEGMENTS);
+        for (long i = 0; i < LARGE_AMOUNT_OF_SEGMENTS; i++) {
+            longFixture.add(i);
+        }
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        fixture.forEach(e -> expected.update(e));
+        Statistics<E> actual = fixture.parallelStream()
+                .<org.eclipse.tracecompass.analysis.timing.core.statistics.Statistics<E>> collect(() -> createStatistics(),
+                        Statistics<E>::update, Statistics<E>::merge);
+        validate(expected, actual);
+    }
+
+    /**
+     * Test statistics nodes being merged. Two identical blocks.
+     */
+    @Test
+    public void testMergeStatisticsNodes() {
+        // Create a fixture of a few values
+        int nbElements = 10;
+        List<@NonNull Long> longFixture = new ArrayList<>(nbElements);
+        for (long i = 0; i < nbElements; i++) {
+            longFixture.add(i);
+        }
+
+        // Get an object fixture
+        Collection<@NonNull E> fixture = createElementsWithValues(longFixture);
+        IStatistics<@NonNull E> expected = createStatistics();
+        IStatistics<@NonNull E> statsA = createStatistics();
+        IStatistics<@NonNull E> statsB = createStatistics();
+
+        Collection<@NonNull E> allElements = new ArrayList<>(2 * nbElements);
+
+        fixture.stream().forEach(obj -> {
+            // Since we will merge the statistics, the object should be added
+            // twice to the expected statistics and the allElements collection
+            expected.update(obj);
+            expected.update(obj);
+            statsA.update(obj);
+            statsB.update(obj);
+            allElements.add(obj);
+            allElements.add(obj);
+        });
+        // Merge the 2 statistics
+        statsA.merge(statsB);
+        assertEquals("Merged size", 2 * nbElements, statsA.getNbElements());
+
+        // Compare the results of the merge with the expected results
+        validate(expected, statsA);
+
+        // Compare with the offline comparator
+        IStatistics<@NonNull E> offline = new OfflineStatisticsCalculator<>(allElements, getMapper());
+        validate(offline, statsA);
+    }
+
+    /**
+     * Test statistics nodes being merged. Two random blocks.
+     */
+    @Test
+    public void testMergeStatisticsRandomNodes() {
+        Random rnd = new Random();
+        rnd.setSeed(1234);
+        // Create 2 fixtures of a random sizes
+        int size = 2 + rnd.nextInt(1000);
+        int size2 = 2 + rnd.nextInt(1000);
+
+        List<@NonNull Long> longFixture1 = new ArrayList<>(size);
+        for (long i = 0; i < size; i++) {
+            longFixture1.add(Long.valueOf(Math.abs(rnd.nextInt(1000))));
+        }
+        Collection<@NonNull E> fixture1 = createElementsWithValues(longFixture1);
+
+        List<@NonNull Long> longFixture2 = new ArrayList<>(size2);
+        for (long i = 0; i < size2; i++) {
+            longFixture2.add(Long.valueOf(Math.abs(rnd.nextInt(1000))));
+        }
+        Collection<@NonNull E> fixture2 = createElementsWithValues(longFixture2);
+
+        // Create the statistics objects to merge
+        IStatistics<@NonNull E> expected = createStatistics();
+        IStatistics<@NonNull E> statsA = createStatistics();
+        IStatistics<@NonNull E> statsB = createStatistics();
+        Collection<@NonNull E> allElements = new ArrayList<>(size + size2);
+
+        fixture1.stream().forEach(obj -> {
+            expected.update(obj);
+            statsA.update(obj);
+            allElements.add(obj);
+        });
+
+        fixture2.stream().forEach(obj -> {
+            expected.update(obj);
+            statsB.update(obj);
+            allElements.add(obj);
+        });
+
+        // Make sure statsA and statsB have the expected size
+        assertEquals("size of statsA", size, statsA.getNbElements());
+        assertEquals("size of statsB", size2, statsB.getNbElements());
+
+        // Merge the 2 statistics
+        statsA.merge(statsB);
+        assertEquals("Merged size", size + size2, statsA.getNbElements());
+
+        // Compare the results of the merge with the expected results
+        validate(expected, statsA);
+
+        // Compare with the offline comparator
+        IStatistics<@NonNull E> offline = new OfflineStatisticsCalculator<>(allElements, getMapper());
+        validate(offline, statsA);
+    }
+
+    /**
+     * Test corner cases when merging statistics nodes
+     */
+    @Test
+    public void mergeStatisticsCornerCaseNodesTest() {
+        // Create a fixtures of one element
+        Collection<@NonNull E> oneFixture = createElementsWithValues(ImmutableList.of(10L));
+        // Create a small fixtures of a few elements
+        Collection<@NonNull E> smallFixture = createElementsWithValues(ImmutableList.of(0L, 10L, 5L, 12L, 7L, 1234L));
+
+        // Control statistics, not to be modified
+        Statistics<E> noElements = createStatistics();
+        Statistics<E> oneElement = createStatistics();
+        oneElement.update(oneFixture.iterator().next());
+        Statistics<E> allElements = createStatistics();
+        oneFixture.stream().forEach(obj -> allElements.update(obj));
+        smallFixture.stream().forEach(obj -> allElements.update(obj));
+
+        // The statistics objects to test
+        Statistics<E> testStats = createStatistics();
+        Statistics<E> testStats2 = createStatistics();
+
+        // Test merging empty stats on a non-empty one
+        testStats.update(oneFixture.iterator().next());
+        testStats.merge(testStats2);
+        validate(oneElement, testStats);
+        validate(noElements, testStats2);
+
+        // Test merging a one element statistics an empty stats
+        testStats2.merge(testStats);
+        validate(oneElement, testStats);
+        validate(oneElement, testStats2);
+
+        // Test merging stats with only 1 segment
+        Statistics<E> testStats3 = createStatistics();
+        smallFixture.stream().forEach(obj -> testStats3.update(obj));
+        testStats3.merge(testStats2);
+        validate(oneElement, testStats2);
+        validate(allElements, testStats3);
+
+        // Test merging on stats with only 1 segment
+        Statistics<E> testStats4 = createStatistics();
+        smallFixture.stream().forEach(obj -> testStats4.update(obj));
+        testStats2.merge(testStats4);
+        validate(allElements, testStats2);
+
+    }
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/LongStatisticsTest.java b/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/LongStatisticsTest.java
new file mode 100644 (file)
index 0000000..7474069
--- /dev/null
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import java.util.Collection;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Statistics test with values of Long type
+ *
+ * @author Geneviève Bastien
+ */
+public class LongStatisticsTest extends AbstractStatisticsTest<@NonNull Long> {
+
+    /**
+     * Constructor
+     */
+    public LongStatisticsTest() {
+        super(null);
+    }
+
+    @Override
+    protected Collection<@NonNull Long> createElementsWithValues(Collection<@NonNull Long> longFixture) {
+        return longFixture;
+    }
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/ObjectStatisticsTest.java b/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/ObjectStatisticsTest.java
new file mode 100644 (file)
index 0000000..24e0183
--- /dev/null
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Ericsson
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Test the segment statistics class with a dummy object and a mapper function
+ *
+ * @author Geneviève Bastien
+ */
+public class ObjectStatisticsTest extends AbstractStatisticsTest<@NonNull StatObjectStub> {
+
+    /**
+     * Constructor
+     */
+    public ObjectStatisticsTest() {
+        super(e -> e.getValue());
+    }
+
+    @Override
+    protected Collection<@NonNull StatObjectStub> createElementsWithValues(Collection<@NonNull Long> longFixture) {
+        return longFixture.stream()
+                .map(l -> new StatObjectStub(l))
+                .collect(Collectors.toList());
+    }
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/OfflineStatisticsCalculator.java b/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/OfflineStatisticsCalculator.java
new file mode 100644 (file)
index 0000000..25cf426
--- /dev/null
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Ericsson
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import java.util.Collection;
+import java.util.NoSuchElementException;
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.analysis.timing.core.statistics.IStatistics;
+import org.eclipse.tracecompass.common.core.NonNullUtils;
+
+/**
+ * This calculates the statistics of a segment store in an offline manner to
+ * validate online calculations.
+ *
+ * @author Matthew Khouzam
+ * @param <E>
+ *            The type of object to calculate statistics on
+ */
+public class OfflineStatisticsCalculator<@NonNull E> implements IStatistics<E> {
+
+    private final Collection<E> fElements;
+    private final @NonNull Function<E, @NonNull Long> fMapper;
+
+    /**
+     * Constructor
+     *
+     * @param elements
+     *            collection of elements
+     * @param mapper
+     *            A mapper function that takes an object to computes statistics
+     *            for and returns the value to use for the statistics
+     */
+    public OfflineStatisticsCalculator(Collection<E> elements, @NonNull Function<E, @NonNull Long> mapper) {
+        fElements = elements;
+        fMapper = mapper;
+    }
+
+    /**
+     * Get the max value
+     *
+     * @return the max value
+     */
+    @Override
+    public long getMax() {
+        long max = Long.MIN_VALUE;
+        for (E element : fElements) {
+            max = Math.max(max, fMapper.apply(element));
+        }
+        return max;
+    }
+
+    /**
+     * Get the min value
+     *
+     * @return the min value
+     */
+    @Override
+    public long getMin() {
+        long min = Long.MAX_VALUE;
+        for (E element : fElements) {
+            min = Math.min(min, fMapper.apply(element));
+        }
+        return min;
+    }
+
+    @Override
+    public @NonNull E getMinObject() {
+        @Nullable E obj = null;
+        for (E element : fElements) {
+            if (obj == null) {
+                obj = element;
+                continue;
+            }
+            Long value = fMapper.apply(element);
+            if (value <= fMapper.apply(obj)) {
+                obj = element;
+            }
+        }
+        if (obj == null) {
+            throw new NoSuchElementException("There are no elements in the collection");
+        }
+        return obj;
+    }
+
+    @Override
+    public @NonNull E getMaxObject() {
+        @Nullable E obj = null;
+        for (E element : fElements) {
+            if (obj == null) {
+                obj = element;
+                continue;
+            }
+            Long value = fMapper.apply(element);
+            if (value >= fMapper.apply(obj)) {
+                obj = element;
+            }
+        }
+        if (obj == null) {
+            throw new NoSuchElementException("There are no elements in the collection");
+        }
+        return obj;
+    }
+
+    /**
+     * Get the average value
+     *
+     * @return the average value
+     */
+    @Override
+    public double getMean() {
+        double total = 0;
+        for (E element : fElements) {
+            total += (double) NonNullUtils.checkNotNull(fMapper.apply(element)) / (double) fElements.size();
+        }
+        return total;
+    }
+
+    /**
+     * Get the standard deviation.
+     *
+     * @return the standard deviation
+     */
+    @Override
+    public double getStdDev() {
+        if (fElements.size() < 3) {
+            return Double.NaN;
+        }
+        double mean = getMean();
+
+        double totalVariance = 0;
+        for (E element : fElements) {
+            double result = NonNullUtils.checkNotNull(fMapper.apply(element)) - mean;
+            totalVariance += result * result / (fElements.size() - 1);
+        }
+        return Math.sqrt(totalVariance);
+    }
+
+    /**
+     * Get the total
+     *
+     * @return the total
+     */
+    @Override
+    public double getTotal() {
+        double total = 0.0;
+        for (E element : fElements) {
+            total += fMapper.apply(element);
+        }
+        return total;
+    }
+
+    @Override
+    public long getNbElements() {
+        return fElements.size();
+    }
+
+    @Override
+    public void update(@NonNull E object) {
+        throw new UnsupportedOperationException("Offline statistics should not update"); //$NON-NLS-1$
+    }
+
+    @Override
+    public void merge(@NonNull IStatistics<@NonNull E> other) {
+        throw new UnsupportedOperationException("Offline statistics should not merge"); //$NON-NLS-1$
+    }
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/StatObjectStub.java b/analysis/org.eclipse.tracecompass.analysis.timing.core.tests/src/org/eclipse/tracecompass/analysis/timing/core/tests/statistics/StatObjectStub.java
new file mode 100644 (file)
index 0000000..3840d76
--- /dev/null
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.tests.statistics;
+
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * Test class used for statistics test with object and mapper
+ *
+ * @author Geneviève Bastien
+ */
+public class StatObjectStub {
+
+    private final @NonNull Long fValue;
+
+    /**
+     * Constructor
+     *
+     * @param value
+     *            A long value
+     */
+    public StatObjectStub(@NonNull Long value) {
+        fValue = value;
+    }
+
+    /**
+     * Get the long value of this object
+     *
+     * @return The value of this object
+     */
+    public @NonNull Long getValue() {
+        return fValue;
+    }
+}
index 082e6c7805e4cbe0a1643ea070ae393f5810f6f4..2f64ca05c9ffe5b6b8a544124b062905ac4d9254 100644 (file)
@@ -2,7 +2,7 @@ Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-Vendor: %Bundle-Vendor
-Bundle-Version: 1.2.0.qualifier
+Bundle-Version: 1.3.0.qualifier
 Bundle-Localization: plugin
 Bundle-SymbolicName: org.eclipse.tracecompass.analysis.timing.core;singleton:=true
 Bundle-Activator: org.eclipse.tracecompass.internal.analysis.timing.core.Activator
@@ -16,8 +16,9 @@ Require-Bundle: org.eclipse.ui,
  org.eclipse.tracecompass.segmentstore.core
 Export-Package: org.eclipse.tracecompass.analysis.timing.core.segmentstore,
  org.eclipse.tracecompass.analysis.timing.core.segmentstore.statistics,
+ org.eclipse.tracecompass.analysis.timing.core.statistics,
  org.eclipse.tracecompass.internal.analysis.timing.core,
  org.eclipse.tracecompass.internal.analysis.timing.core.callgraph;x-friends:="org.eclipse.tracecompass.analysis.timing.ui,org.eclipse.tracecompass.analysis.timing.core.tests"
-Import-Package: com.google.common.annotations;version="15.0.0",
+Import-Package: com.google.common.annotations,
  com.google.common.collect,
  com.google.common.hash
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/IStatistics.java b/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/IStatistics.java
new file mode 100644 (file)
index 0000000..18b6290
--- /dev/null
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.statistics;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Interface for classes implementing statistics. These statistics take a
+ * generic object type, so that they can keep information on the objects that
+ * have the minimum and maximum value. Implementations will need to be told how
+ * to transform the objects of generic type into a Number that can be used for
+ * statistics calculations.
+ *
+ * @author Geneviève Bastien
+ * @param <E>
+ *            The type of object to calculate statistics on
+ * @since 1.3
+ */
+public interface IStatistics<@NonNull E> {
+
+    /**
+     * Get minimum value
+     *
+     * @return minimum value
+     */
+    long getMin();
+
+    /**
+     * Get maximum value
+     *
+     * @return maximum value
+     */
+    long getMax();
+
+    /**
+     * Get element with minimum value, or <code>null</code> if
+     * {@link #getNbElements()} is 0.
+     *
+     * @return element with minimum value
+     */
+    @Nullable E getMinObject();
+
+    /**
+     * Get element with maximum value, or <code>null</code> if
+     * {@link #getNbElements()} is 0.
+     *
+     * @return element with maximum value
+     */
+    @Nullable E getMaxObject();
+
+    /**
+     * Get number of elements analyzed
+     *
+     * @return number of elements analyzed
+     */
+    long getNbElements();
+
+    /**
+     * Gets the arithmetic mean
+     *
+     * @return arithmetic mean
+     */
+    double getMean();
+
+    /**
+     * Gets the standard deviation of the values
+     *
+     * @return the standard deviation of the segment store, will return NaN if
+     *         there are less than 3 elements
+     */
+    double getStdDev();
+
+    /**
+     * Get total value
+     *
+     * @return total value
+     */
+    double getTotal();
+
+    /**
+     * Update the statistics based on a given object
+     * <p>
+     * This is an online algorithm and must retain a complexity of O(1)
+     *
+     * @param object
+     *            the object used for the update
+     */
+    void update(E object);
+
+    /**
+     * Merge 2 statistics classes for the same object type
+     *
+     * @param other
+     *            The other statistics object
+     */
+    void merge(IStatistics<E> other);
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/Statistics.java b/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/Statistics.java
new file mode 100644 (file)
index 0000000..85f0143
--- /dev/null
@@ -0,0 +1,232 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials are
+ * made available under the terms of the Eclipse Public License v1.0 which
+ * accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+package org.eclipse.tracecompass.analysis.timing.core.statistics;
+
+import java.util.function.Function;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.tracecompass.common.core.NonNullUtils;
+
+/**
+ * Class that calculates statistics on a certain type of object. If the object
+ * is not a {@link Long}, a mapper function should be passed in the constructor
+ * to retrieve the long value to make statistics on from an object.
+ *
+ * @author Bernd Hufmann
+ * @author Geneviève Bastien
+ *
+ * @param <E>
+ *            The type of object to calculate statistics on
+ * @since 1.3
+ */
+public class Statistics<@NonNull E> implements IStatistics<E> {
+
+    private final Function<E, @NonNull Long> fMapper;
+
+    private @Nullable E fMin = null;
+    private @Nullable E fMax = null;
+    private long fNbElements;
+    private double fMean;
+    /**
+     * reminder, this is the variance * nb elem, as per the online algorithm
+     */
+    private double fVariance;
+    private double fTotal;
+
+    /**
+     * Constructor
+     */
+    public Statistics() {
+        this(e -> {
+            if (!(e instanceof Long)) {
+                throw new IllegalStateException("The object " + e + " is not a number"); //$NON-NLS-1$//$NON-NLS-2$
+            }
+            return (Long) e;
+        });
+    }
+
+    /**
+     * Constructor
+     *
+     * @param mapper
+     *            A mapper function that takes an object to computes statistics
+     *            for and returns the value to use for the statistics
+     */
+    public Statistics(Function<E, Long> mapper) {
+        fNbElements = 0;
+        fMean = 0.0;
+        fVariance = 0.0;
+        fTotal = 0.0;
+        fMapper = mapper;
+    }
+
+    @Override
+    public long getMin() {
+        @Nullable
+        E min = fMin;
+        if (min == null) {
+            return Long.MAX_VALUE;
+        }
+        return NonNullUtils.checkNotNull(fMapper.apply(min));
+    }
+
+    @Override
+    public long getMax() {
+        @Nullable
+        E max = fMax;
+        if (max == null) {
+            return Long.MIN_VALUE;
+        }
+        return NonNullUtils.checkNotNull(fMapper.apply(max));
+    }
+
+    @Override
+    public @Nullable E getMinObject() {
+        return fMin;
+    }
+
+    @Override
+    public @Nullable E getMaxObject() {
+        return fMax;
+    }
+
+    @Override
+    public long getNbElements() {
+        return fNbElements;
+    }
+
+    @Override
+    public double getMean() {
+        return fMean;
+    }
+
+    /**
+     * Gets the standard deviation of the elements. It uses the online algorithm
+     * shown here <a href=
+     * "https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm">
+     * Wikipedia article of dec 3 2015 </a>
+     *
+     * @return the standard deviation of the elements, will return NaN if there
+     *         are less than 3 elements
+     */
+    @Override
+    public double getStdDev() {
+        return fNbElements > 2 ? Math.sqrt(fVariance / (fNbElements - 1)) : Double.NaN;
+    }
+
+    @Override
+    public double getTotal() {
+        return fTotal;
+    }
+
+    @Override
+    public void update(E object) {
+        Long value = NonNullUtils.checkNotNull(fMapper.apply(object));
+        /*
+         * Min and max are trivial, as well as number of segments
+         */
+        fMin = value <= getMin() ? object : fMin;
+        fMax = value >= getMax() ? object : fMax;
+
+        fNbElements++;
+        /*
+         * The running mean is not trivial, see proof in javadoc.
+         *
+         * TODO: Check if saturated math would be required here
+         */
+        double delta = value - fMean;
+        fMean += delta / fNbElements;
+        fVariance += delta * (value - fMean);
+        fTotal += value;
+    }
+
+    @Override
+    public void merge(IStatistics<E> o) {
+        if (!(o instanceof Statistics)) {
+            throw new IllegalArgumentException("Can only merge statistics of the same class"); //$NON-NLS-1$
+        }
+        Statistics<E> other = (Statistics<E>) o;
+        if (other.fNbElements == 0) {
+            return;
+        } else if (fNbElements == 0) {
+            copy(other);
+        } else if (other.fNbElements == 1) {
+            update(NonNullUtils.checkNotNull(other.getMaxObject()));
+        } else if (fNbElements == 1) {
+            Statistics<E> copyOther = new Statistics<>(fMapper);
+            copyOther.copy(other);
+            copyOther.update(NonNullUtils.checkNotNull(getMaxObject()));
+            copy(copyOther);
+        } else {
+            internalMerge(other);
+        }
+    }
+
+    private void internalMerge(Statistics<E> other) {
+        /*
+         * TODO: Check if saturated math would be required in this method
+         *
+         * Min and max are trivial, as well as number of segments
+         */
+        long min = getMin();
+        long max = getMax();
+        fMin = other.getMin() <= min ? other.getMinObject() : fMin;
+        fMax = other.getMax() >= max ? other.getMaxObject() : fMax;
+
+        long oldNbSeg = fNbElements;
+        double oldAverage = fMean;
+        long otherSegments = other.getNbElements();
+        double otherAverage = other.getMean();
+        fNbElements += otherSegments;
+        fTotal += other.getTotal();
+
+        /*
+         * Average is a weighted average
+         */
+        fMean = ((oldNbSeg * oldAverage) + (otherAverage * otherSegments)) / fNbElements;
+
+        /*
+         * This one is a bit tricky.
+         *
+         * The variance is the sum of the deltas from a mean squared.
+         *
+         * So if we add the old mean squared back to to variance and remove the
+         * new mean, the standard deviation can be easily calculated.
+         */
+        double avg1Sq = oldAverage * oldAverage;
+        double avg2sq = otherAverage * otherAverage;
+        double avgtSq = fMean * fMean;
+        /*
+         * This is a tricky part, bear in mind that the set is not continuous
+         * but discrete, Therefore, we have for n elements, n-1 intervals
+         * between them. Ergo, n-1 intervals are used for divisions and
+         * multiplications.
+         */
+        double variance1 = fVariance / (oldNbSeg - 1);
+        double variance2 = other.fVariance / (otherSegments - 1);
+        fVariance = ((variance1 + avg1Sq - avgtSq) * (oldNbSeg - 1) + (variance2 + avg2sq - avgtSq) * (otherSegments - 1));
+    }
+
+    private void copy(Statistics<E> copyOther) {
+        fMean = copyOther.fMean;
+        fMax = copyOther.fMax;
+        fMin = copyOther.fMin;
+        fNbElements = copyOther.fNbElements;
+        fTotal = copyOther.fTotal;
+        fVariance = copyOther.fVariance;
+    }
+
+    @Override
+    public String toString() {
+        return this.getClass() + ": Avg: " + getMean() + " on " + getNbElements() + " elements"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+    }
+
+}
diff --git a/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/package-info.java b/analysis/org.eclipse.tracecompass.analysis.timing.core/src/org/eclipse/tracecompass/analysis/timing/core/statistics/package-info.java
new file mode 100644 (file)
index 0000000..75a85ec
--- /dev/null
@@ -0,0 +1,11 @@
+/*******************************************************************************
+ * Copyright (c) 2017 École Polytechnique de Montréal
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *******************************************************************************/
+
+@org.eclipse.jdt.annotation.NonNullByDefault
+package org.eclipse.tracecompass.analysis.timing.core.statistics;
This page took 0.039726 seconds and 5 git commands to generate.