| /******************************************************************************* |
| * 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 2.0 which |
| * accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| *******************************************************************************/ |
| |
| 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); |
| |
| } |
| |
| } |