OSDN Git Service

Adds test to collect performance metrics
authorDiego Perez <diegoperez@google.com>
Wed, 14 Dec 2016 17:03:15 +0000 (17:03 +0000)
committerDiego Perez <diegoperez@google.com>
Mon, 16 Jan 2017 18:14:34 +0000 (18:14 +0000)
Test: Adds performance tests
Change-Id: I7d112ddad4efb2c1c84ac177d230225cc031f7f3

tools/layoutlib/bridge/tests/Android.mk
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java [new file with mode: 0644]
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTestBase.java
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java [new file with mode: 0644]
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java [new file with mode: 0644]
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java [new file with mode: 0644]
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java [new file with mode: 0644]
tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java [new file with mode: 0644]

index 9ee416a..33d55de 100644 (file)
@@ -30,7 +30,8 @@ LOCAL_JAVA_LIBRARIES := layoutlib \
                        layoutlib_api-prebuilt \
                        tools-common-prebuilt \
                        sdk-common \
-                       junit-host
+                       junit-host \
+                       guavalib
 
 include $(BUILD_HOST_JAVA_LIBRARY)
 
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/PerformanceTests.java
new file mode 100644 (file)
index 0000000..c90c26a
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive;
+
+import com.android.ide.common.rendering.api.SessionParams;
+import com.android.layoutlib.bridge.intensive.setup.ConfigGenerator;
+import com.android.layoutlib.bridge.intensive.util.perf.PerformanceRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.annotation.NonNull;
+
+/**
+ * Set of render tests
+ */
+@RunWith(PerformanceRunner.class)
+public class PerformanceTests extends RenderTestBase {
+
+    @Before
+    public void setUp() {
+        ignoreAllLogging();
+    }
+
+
+    private void render(@NonNull String layoutFileName) throws ClassNotFoundException {
+        SessionParams params = createSessionParams(layoutFileName, ConfigGenerator.NEXUS_5);
+        render(params, 250);
+    }
+
+    @Test
+    public void testActivity() throws ClassNotFoundException {
+        render("activity.xml");
+    }
+
+    @Test
+    public void testAllWidgets() throws ClassNotFoundException {
+        render("allwidgets.xml");
+    }
+}
index f622c21..3e5f9e0 100644 (file)
@@ -35,6 +35,7 @@ import com.android.layoutlib.bridge.intensive.setup.LayoutLibTestCallback;
 import com.android.layoutlib.bridge.intensive.setup.LayoutPullParser;
 import com.android.layoutlib.bridge.intensive.util.ImageUtils;
 import com.android.layoutlib.bridge.intensive.util.ModuleClassLoader;
+import com.android.layoutlib.bridge.intensive.util.TestUtils;
 import com.android.tools.layoutlib.java.System_Delegate;
 import com.android.utils.ILogger;
 
@@ -50,7 +51,6 @@ import android.annotation.Nullable;
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.ref.WeakReference;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -272,7 +272,7 @@ public class RenderTestBase {
      * Initialize the bridge and the resource maps.
      */
     @BeforeClass
-    public static void setUp() {
+    public static void beforeClass() {
         File data_dir = new File(PLATFORM_DIR, "data");
         File res = new File(data_dir, "res");
         sFrameworkRepo = new FrameworkResources(new FolderWrapper(res));
@@ -304,21 +304,6 @@ public class RenderTestBase {
         }
     }
 
-    private static void gc() {
-        // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
-        Object obj = new Object();
-        WeakReference ref = new WeakReference<>(obj);
-        //noinspection UnusedAssignment
-        obj = null;
-        while (ref.get() != null) {
-            System.gc();
-            System.runFinalization();
-        }
-
-        System.gc();
-        System.runFinalization();
-    }
-
     @AfterClass
     public static void tearDown() {
         sLayoutLibLog = null;
@@ -327,7 +312,7 @@ public class RenderTestBase {
         sLogger = null;
         sBridge = null;
 
-        gc();
+        TestUtils.gc();
 
         System.out.println("Objects still linked from the DelegateManager:");
         DelegateManager.dump(System.out);
@@ -435,6 +420,27 @@ public class RenderTestBase {
         return sLayoutLibLog;
     }
 
+    protected static void ignoreAllLogging() {
+        sLayoutLibLog = new LayoutLog();
+        sLogger = new ILogger() {
+            @Override
+            public void error(Throwable t, String msgFormat, Object... args) {
+            }
+
+            @Override
+            public void warning(String msgFormat, Object... args) {
+            }
+
+            @Override
+            public void info(String msgFormat, Object... args) {
+            }
+
+            @Override
+            public void verbose(String msgFormat, Object... args) {
+            }
+        };
+    }
+
     protected static ILogger getLogger() {
         if (sLogger == null) {
             sLogger = new ILogger() {
@@ -500,7 +506,7 @@ public class RenderTestBase {
         return renderAndVerify(params, goldenFileName);
     }
 
-    private SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
+    protected SessionParams createSessionParams(String layoutFileName, ConfigGenerator deviceConfig)
             throws ClassNotFoundException {
         // Create the layout pull parser.
         LayoutPullParser parser = createLayoutPullParser(layoutFileName);
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/TestUtils.java
new file mode 100644 (file)
index 0000000..1df8e79
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive.util;
+
+import java.lang.ref.WeakReference;
+
+public class TestUtils {
+    public static void gc() {
+        // See RuntimeUtil#gc in jlibs (http://jlibs.in/)
+        Object obj = new Object();
+        WeakReference ref = new WeakReference<>(obj);
+        //noinspection UnusedAssignment
+        obj = null;
+        while (ref.get() != null) {
+            System.gc();
+            System.runFinalization();
+        }
+
+        System.gc();
+        System.runFinalization();
+    }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/LongStatsCollector.java
new file mode 100644 (file)
index 0000000..ee98b4b
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive.util.perf;
+
+import android.annotation.NonNull;
+import android.util.LongArray;
+
+import java.util.Arrays;
+import java.util.function.LongConsumer;
+
+/**
+ * Class that collect a series of longs and produces the median, min and max values.
+ */
+public class LongStatsCollector implements LongConsumer {
+    private final LongArray mAllValues;
+    private long mMin = Long.MAX_VALUE;
+    private long mMax = Long.MIN_VALUE;
+    public LongStatsCollector(int estimatedRuns) {
+        mAllValues = new LongArray(estimatedRuns);
+    }
+
+    public int size() {
+        return mAllValues.size();
+    }
+
+    @NonNull
+    public Stats getStats() {
+        if (mAllValues.size() == 0) {
+            throw new IndexOutOfBoundsException("No data");
+        }
+
+        double median;
+        int size = mAllValues.size();
+        long[] buffer = new long[size];
+        for (int i = 0; i < size; i++) {
+            buffer[i] = mAllValues.get(i);
+        }
+
+        Arrays.sort(buffer);
+
+        int midPoint = size / 2;
+        median = (size % 2 == 0) ? (buffer[midPoint - 1] + buffer[midPoint]) / 2 : buffer[midPoint];
+
+        return new Stats(mAllValues.size(), mMin, mMax, median);
+    }
+
+    @Override
+    public void accept(long value) {
+        mMin = Math.min(mMin, value);
+        mMax = Math.max(mMax, value);
+        mAllValues.add(value);
+    }
+
+    public static class Stats {
+        private final int mSamples;
+        private final long mMin;
+        private final long mMax;
+        private final double mMedian;
+
+        private Stats(int samples, long min, long max, double median) {
+            mSamples = samples;
+            mMin = min;
+            mMax = max;
+            mMedian = median;
+        }
+
+        public int getSampleCount() {
+            return mSamples;
+        }
+
+        public long getMin() {
+            return mMin;
+        }
+
+        public long getMax() {
+            return mMax;
+        }
+
+        public double getMedian() {
+            return mMedian;
+        }
+    }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/PerformanceRunner.java
new file mode 100644 (file)
index 0000000..7225a10
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive.util.perf;
+
+import org.junit.runner.Runner;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * JUnit {@link Runner} that times the test execution and produces some stats.
+ */
+public class PerformanceRunner extends BlockJUnit4ClassRunner {
+    private static final int DEFAULT_WARMUP_ITERATIONS = 50;
+    private static final int DEFAULT_RUNS = 100;
+
+    private final int mWarmUpIterations;
+    private final int mRuns;
+
+    public PerformanceRunner(Class<?> testClass) throws InitializationError {
+        super(testClass);
+
+        Configuration classConfig = testClass.getAnnotation(Configuration.class);
+        mWarmUpIterations = classConfig != null && classConfig.warmUpIterations() != -1 ?
+                classConfig.warmUpIterations() :
+                DEFAULT_WARMUP_ITERATIONS;
+        mRuns = classConfig != null && classConfig.runs() != -1 ?
+                classConfig.runs() :
+                DEFAULT_RUNS;
+    }
+
+    @Override
+    protected Statement methodInvoker(FrameworkMethod method, Object test) {
+        int warmUpIterations;
+        int runs;
+
+        Configuration methodConfig = method.getAnnotation(Configuration.class);
+        warmUpIterations = methodConfig != null && methodConfig.warmUpIterations() != -1 ?
+                methodConfig.warmUpIterations() :
+                mWarmUpIterations;
+        runs = methodConfig != null && methodConfig.runs() != -1 ?
+                methodConfig.runs() :
+                mRuns;
+        return new TimedStatement(super.methodInvoker(method, test), warmUpIterations, runs,
+                (result) -> System.out.println(result.toString()));
+    }
+
+    @Override
+    public void run(RunNotifier notifier) {
+        super.run(notifier);
+    }
+
+    @Retention(RetentionPolicy.RUNTIME)
+    @Inherited
+    public @interface Configuration {
+        int warmUpIterations() default -1;
+
+        int runs() default -1;
+    }
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatement.java
new file mode 100644 (file)
index 0000000..77a2b0e
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive.util.perf;
+
+import com.android.layoutlib.bridge.intensive.util.TestUtils;
+
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+/**
+ * JUnit {@link Statement} used to measure some statistics about the test method.
+ */
+public class TimedStatement extends Statement {
+    private static final int CALIBRATION_WARMUP_ITERATIONS = 50;
+    private static final int CALIBRATION_RUNS = 100;
+
+    private static boolean sIsCalibrated;
+    private static double sCalibrated;
+
+    private final Statement mStatement;
+    private final int mWarmUpIterations;
+    private final int mRuns;
+    private final Runtime mRuntime = Runtime.getRuntime();
+    private final Consumer<TimedStatementResult> mCallback;
+
+    TimedStatement(Statement statement, int warmUpIterations, int runs,
+            Consumer<TimedStatementResult> finishedCallback) {
+        mStatement = statement;
+        mWarmUpIterations = warmUpIterations;
+        mRuns = runs;
+        mCallback = finishedCallback;
+    }
+
+    /**
+     * The calibrate method tries to do some work that involves IO, memory allocations and some
+     * operations on the randomly generated data to calibrate the speed of the machine with
+     * something that resembles the execution of a test case.
+     */
+    private static void calibrateMethod() throws IOException {
+        File tmpFile = File.createTempFile("test", "file");
+        Random rnd = new Random();
+        HashFunction hashFunction = Hashing.sha512();
+        for (int i = 0; i < 5 + rnd.nextInt(5); i++) {
+            FileOutputStream stream = new FileOutputStream(tmpFile);
+            int bytes = 30000 + rnd.nextInt(60000);
+            byte[] buffer = new byte[bytes];
+
+            rnd.nextBytes(buffer);
+            byte acc = 0;
+            for (int j = 0; j < bytes; j++) {
+                acc += buffer[i];
+            }
+            buffer[0] = acc;
+            stream.write(buffer);
+            System.gc();
+            stream.close();
+            FileInputStream input = new FileInputStream(tmpFile);
+            byte[] readBuffer = new byte[bytes];
+            //noinspection ResultOfMethodCallIgnored
+            input.read(readBuffer);
+            buffer = readBuffer;
+            HashCode code1 = hashFunction.hashBytes(buffer);
+            Arrays.sort(buffer);
+            HashCode code2 = hashFunction.hashBytes(buffer);
+            input.close();
+
+            FileOutputStream hashStream = new FileOutputStream(tmpFile);
+            hashStream.write(code1.asBytes());
+            hashStream.write(code2.asBytes());
+            hashStream.close();
+        }
+    }
+
+    /**
+     * Runs the calibration process and sets the calibration measure in {@link #sCalibrated}
+     */
+    private static void doCalibration() throws IOException {
+        System.out.println("Calibrating ...");
+        TestUtils.gc();
+        for (int i = 0; i < CALIBRATION_WARMUP_ITERATIONS; i++) {
+            calibrateMethod();
+        }
+
+        LongStatsCollector stats = new LongStatsCollector(CALIBRATION_RUNS);
+        for (int i = 0; i < CALIBRATION_RUNS; i++) {
+            TestUtils.gc();
+            long start = System.currentTimeMillis();
+            calibrateMethod();
+            stats.accept(System.currentTimeMillis() - start);
+        }
+
+        sCalibrated = stats.getStats().getMedian();
+        sIsCalibrated = true;
+        System.out.printf("  DONE %fms\n", sCalibrated);
+    }
+
+    private long getUsedMemory() {
+        return mRuntime.totalMemory() - mRuntime.freeMemory();
+    }
+
+
+    @Override
+    public void evaluate() throws Throwable {
+        if (!sIsCalibrated) {
+            doCalibration();
+        }
+
+        for (int i = 0; i < mWarmUpIterations; i++) {
+            mStatement.evaluate();
+        }
+
+        LongStatsCollector timeStats = new LongStatsCollector(mRuns);
+        LongStatsCollector memoryUseStats = new LongStatsCollector(mRuns);
+        AtomicBoolean collectSamples = new AtomicBoolean(false);
+
+        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
+        TestUtils.gc();
+        executorService.scheduleAtFixedRate(() -> {
+            if (!collectSamples.get()) {
+                return;
+            }
+            memoryUseStats.accept(getUsedMemory());
+        }, 0, 200, TimeUnit.MILLISECONDS);
+
+        try {
+            for (int i = 0; i < mRuns; i++) {
+                TestUtils.gc();
+                collectSamples.set(true);
+                long startTimeMs = System.currentTimeMillis();
+                mStatement.evaluate();
+                long stopTimeMs = System.currentTimeMillis();
+                collectSamples.set(true);
+                timeStats.accept(stopTimeMs - startTimeMs);
+
+            }
+        } finally {
+            executorService.shutdownNow();
+        }
+
+        TimedStatementResult result = new TimedStatementResult(
+                mWarmUpIterations,
+                mRuns,
+                sCalibrated,
+                timeStats.getStats(),
+                memoryUseStats.getStats());
+        mCallback.accept(result);
+    }
+
+}
diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/util/perf/TimedStatementResult.java
new file mode 100644 (file)
index 0000000..59f90d2
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 com.android.layoutlib.bridge.intensive.util.perf;
+
+import com.android.layoutlib.bridge.intensive.util.perf.LongStatsCollector.Stats;
+
+import java.text.DecimalFormat;
+
+/**
+ * Result value of a {@link TimedStatement}
+ */
+public class TimedStatementResult {
+    private static final DecimalFormat UNITS_FORMAT = new DecimalFormat("#.##");
+
+    private final int mWarmUpIterations;
+    private final int mRuns;
+    private final double mCalibrationTimeMs;
+    private final Stats mTimeStats;
+    private final Stats mMemoryStats;
+
+    TimedStatementResult(int warmUpIterations, int runs,
+            double calibrationTimeMs,
+            Stats timeStats,
+            Stats memoryStats) {
+        mWarmUpIterations = warmUpIterations;
+        mRuns = runs;
+        mCalibrationTimeMs = calibrationTimeMs;
+        mTimeStats = timeStats;
+        mMemoryStats = memoryStats;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "Warm up %d. Runs %d\n" + "Time:             %s ms (min: %s, max %s)\n" +
+                        "Calibration Time: %f ms\n" +
+                        "Calibrated Time:  %s units (min: %s, max %s)\n" +
+                        "Sampled %d times\n" +
+                        "   Memory used:  %d bytes (max %d)\n\n",
+                mWarmUpIterations, mRuns,
+                mTimeStats.getMedian(), mTimeStats.getMin(), mTimeStats.getMax(),
+                mCalibrationTimeMs,
+                UNITS_FORMAT.format((mTimeStats.getMedian() / mCalibrationTimeMs) * 100000),
+                UNITS_FORMAT.format((mTimeStats.getMin() / mCalibrationTimeMs) * 100000),
+                UNITS_FORMAT.format((mTimeStats.getMax() / mCalibrationTimeMs) * 100000),
+                mMemoryStats.getSampleCount(),
+                (long)mMemoryStats.getMedian() - mMemoryStats.getMin(),
+                mMemoryStats.getMax() - mMemoryStats.getMin());
+    }
+}