Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9f46b112 authored by Chris Craik's avatar Chris Craik Committed by Android (Google) Code Review
Browse files

Merge "Allow profiling of animation performance"

parents 37e800d6 702c6fdc
Loading
Loading
Loading
Loading
+12 −4
Original line number Diff line number Diff line
@@ -49,8 +49,9 @@
    <!-- Drop down menu entry - automatically scroll to the end of the page
    with scrollBy() [CHAR LIMIT=15] -->
    <string name="movement_auto_scroll">Auto-scroll</string>
    <!-- Drop down menu entry -  [CHAR LIMIT=15] -->
    <string name="movement_auto_fling">Auto-fling</string>
    <!-- Drop down menu entry - automatically record for a set time before
    stopping [CHAR LIMIT=15] -->
    <string name="movement_timed">Timed</string>
    <!-- Drop down menu entry - manually navigate the page(s), hit 'capture'
    button [CHAR LIMIT=15] -->
    <string name="movement_manual">Manual</string>
@@ -67,14 +68,21 @@
    <!-- 75th percentile - 75% of frames fall below this value [CHAR LIMIT=12]
    -->
    <string name="percentile_75">75%ile</string>
    <!-- standard deviation [CHAR LIMIT=12] -->
    <string name="std_dev">StdDev</string>
    <!-- mean [CHAR LIMIT=12] -->
    <string name="mean">mean</string>



    <!-- Frame rate [CHAR LIMIT=15] -->
    <string name="frames_per_second">Frames/sec</string>
    <!-- Portion of viewport covered by good tiles [CHAR LIMIT=15] -->
    <string name="viewport_coverage">Coverage</string>
    <!-- Milliseconds taken to inval, and re-render the page [CHAR LIMIT=15] -->
    <string name="render_millis">RenderMillis</string>
    <!-- Number of rendering stalls while running the test [CHAR LIMIT=15] -->
    <string name="render_stalls">Stalls</string>
    <!-- Animation Framerate [CHAR LIMIT=15] -->
    <string name="animation_framerate">AnimFramerate</string>
    <!-- Format string for stat value overlay [CHAR LIMIT=15] -->
    <string name="format_stat">%4.4f</string>

+171 −34
Original line number Diff line number Diff line
@@ -27,17 +27,57 @@ import android.os.Bundle;
import android.os.Environment;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import android.webkit.WebSettings;
import android.widget.Spinner;

public class PerformanceTest extends
        ActivityInstrumentationTestCase2<ProfileActivity> {

    public static class AnimStat {
        double aggVal = 0;
        double aggSqrVal = 0;
        double count = 0;
    }

    private class StatAggregator extends PlaybackGraphs {
        private HashMap<String, Double> mDataMap = new HashMap<String, Double>();
        private HashMap<String, AnimStat> mAnimDataMap = new HashMap<String, AnimStat>();
        private int mCount = 0;


        public void aggregate() {
            boolean inAnimTests = mAnimTests != null;
            Resources resources = mWeb.getResources();
            String animFramerateString = resources.getString(R.string.animation_framerate);
            for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
                String name = e.getKey();
                if (inAnimTests) {
                    if (name.equals(animFramerateString)) {
                        // in animation testing phase, record animation framerate and aggregate
                        // stats, differentiating on values of mAnimTestNr and mDoubleBuffering
                        String fullName = ANIM_TEST_NAMES[mAnimTestNr] + " " + name;
                        fullName += mDoubleBuffering ? " tiled" : " webkit";

                        if (!mAnimDataMap.containsKey(fullName)) {
                            mAnimDataMap.put(fullName, new AnimStat());
                        }
                        AnimStat statVals = mAnimDataMap.get(fullName);
                        statVals.aggVal += e.getValue();
                        statVals.aggSqrVal += e.getValue() * e.getValue();
                        statVals.count += 1;
                    }
                } else {
                    double aggVal = mDataMap.containsKey(name)
                            ? mDataMap.get(name) : 0;
                    mDataMap.put(name, aggVal + e.getValue());
                }
            }

            if (inAnimTests) {
                return;
            }

            mCount++;
            Resources resources = mView.getResources();
            for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
                for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
                    String metricLabel = resources.getString(
@@ -53,34 +93,47 @@ public class PerformanceTest extends
                    mDataMap.put(label, aggVal);
                }
            }
            for (Map.Entry<String, Double> e : mSingleStats.entrySet()) {
                double aggVal = mDataMap.containsKey(e.getKey())
                        ? mDataMap.get(e.getKey()) : 0;
                mDataMap.put(e.getKey(), aggVal + e.getValue());
            }

        }

        // build the final bundle of results
        public Bundle getBundle() {
            Bundle b = new Bundle();
            int count = 0 == mCount ? Integer.MAX_VALUE : mCount;
            int count = (0 == mCount) ? Integer.MAX_VALUE : mCount;
            for (Map.Entry<String, Double> e : mDataMap.entrySet()) {
                b.putDouble(e.getKey(), e.getValue() / count);
            }

            for (Map.Entry<String, AnimStat> e : mAnimDataMap.entrySet()) {
                String statName = e.getKey();
                AnimStat statVals = e.getValue();

                double avg = statVals.aggVal/statVals.count;
                double stdDev = Math.sqrt((statVals.aggSqrVal / statVals.count) - avg * avg);

                b.putDouble(statName, avg);
                b.putDouble(statName + " STD DEV", stdDev);
            }

            return b;
        }
    }

    ProfileActivity mActivity;
    ProfiledWebView mView;
    StatAggregator mStats = new StatAggregator();
    ProfiledWebView mWeb;
    Spinner mMovementSpinner;
    StatAggregator mStats;

    private static final String LOGTAG = "PerformanceTest";
    private static final String TEST_LOCATION = "webkit/page_cycler";
    private static final String URL_PREFIX = "file://";
    private static final String URL_POSTFIX = "/index.html?skip=true";
    private static final int MAX_ITERATIONS = 4;
    private static final String TEST_DIRS[] = {
        "alexa25_2011"//, "alexa_us", "android", "dom", "intl2", "moz", "moz2"
    private static final String SCROLL_TEST_DIRS[] = {
        "alexa25_2011"
    };
    private static final String ANIM_TEST_DIRS[] = {
        "dhtml"
    };

    public PerformanceTest() {
@@ -91,7 +144,22 @@ public class PerformanceTest extends
    protected void setUp() throws Exception {
        super.setUp();
        mActivity = getActivity();
        mView = (ProfiledWebView) mActivity.findViewById(R.id.web);
        mWeb = (ProfiledWebView) mActivity.findViewById(R.id.web);
        mMovementSpinner = (Spinner) mActivity.findViewById(R.id.movement);
        mStats = new StatAggregator();

        // use mStats as a condition variable between the UI thread and
        // this(the testing) thread
        mActivity.setCallback(new ProfileCallback() {
            @Override
            public void profileCallback(RunData data) {
                mStats.setData(data);
                synchronized (mStats) {
                    mStats.notify();
                }
            }
        });

    }

    private boolean loadUrl(final String url) {
@@ -100,12 +168,13 @@ public class PerformanceTest extends
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mView.loadUrl(url);
                    mWeb.loadUrl(url);
                }
            });
            synchronized (mStats) {
                mStats.wait();
            }

            mStats.aggregate();
        } catch (InterruptedException e) {
            e.printStackTrace();
@@ -114,15 +183,30 @@ public class PerformanceTest extends
        return true;
    }

    private boolean runIteration() {
    private boolean validTest(String nextTest) {
        // if testing animations, test must be in mAnimTests
        if (mAnimTests == null)
            return true;

        for (String test : mAnimTests) {
            if (test.equals(nextTest)) {
                return true;
            }
        }
        return false;
    }

    private boolean runIteration(String[] testDirs) {
        File sdFile = Environment.getExternalStorageDirectory();
        for (String testDirName : TEST_DIRS) {
        for (String testDirName : testDirs) {
            File testDir = new File(sdFile, TEST_LOCATION + "/" + testDirName);
            Log.d(LOGTAG, "Testing dir: '" + testDir.getAbsolutePath()
                    + "', exists=" + testDir.exists());

            for (File siteDir : testDir.listFiles()) {
                if (!siteDir.isDirectory())
                if (!siteDir.isDirectory() || !validTest(siteDir.getName())) {
                    continue;
                }

                if (!loadUrl(URL_PREFIX + siteDir.getAbsolutePath()
                        + URL_POSTFIX)) {
@@ -133,7 +217,44 @@ public class PerformanceTest extends
        return true;
    }

    public void testMetrics() {
    private boolean  runTestDirs(String[] testDirs) {
        for (int i = 0; i < MAX_ITERATIONS; i++)
            if (!runIteration(testDirs)) {
                return false;
            }
        return true;
    }

    private void pushDoubleBuffering() {
        getInstrumentation().runOnMainSync(new Runnable() {
            public void run() {
                mWeb.setDoubleBuffering(mDoubleBuffering);
            }
        });
    }

    private void setScrollingTestingMode(final boolean scrolled) {
        getInstrumentation().runOnMainSync(new Runnable() {
            public void run() {
                mMovementSpinner.setSelection(scrolled ? 0 : 2);
            }
        });
    }


    private String[] mAnimTests = null;
    private int mAnimTestNr = -1;
    private boolean mDoubleBuffering = true;
    private static final String[] ANIM_TEST_NAMES = {
        "slow", "fast"
    };
    private static final String[][] ANIM_TESTS = {
        {"scrolling", "replaceimages", "layers5", "layers1"},
        {"slidingballs", "meter", "slidein", "fadespacing", "colorfade",
                "mozilla", "movingtext", "diagball", "zoom", "imageslide"},
    };

    private boolean checkMedia() {
        String state = Environment.getExternalStorageState();

        if (!Environment.MEDIA_MOUNTED.equals(state)
@@ -141,27 +262,43 @@ public class PerformanceTest extends
            Log.d(LOGTAG, "ARG Can't access sd card!");
            // Can't read the SD card, fail and die!
            getInstrumentation().sendStatus(1, null);
            return;
            return false;
        }
        return true;
    }

        // use mGraphs as a condition variable between the UI thread and
        // this(the testing) thread
        mActivity.setCallback(new ProfileCallback() {
            @Override
            public void profileCallback(RunData data) {
                Log.d(LOGTAG, "test completion callback");
                mStats.setData(data);
                synchronized (mStats) {
                    mStats.notify();
    public void testMetrics() {
        setScrollingTestingMode(true);
        if (checkMedia() && runTestDirs(SCROLL_TEST_DIRS)) {
            getInstrumentation().sendStatus(0, mStats.getBundle());
        } else {
            getInstrumentation().sendStatus(1, null);
        }
    }
        });

        for (int i = 0; i < MAX_ITERATIONS; i++)
            if (!runIteration()) {
                getInstrumentation().sendStatus(1, null);
                return;
    private boolean runAnimationTests() {
        for (int doubleBuffer = 0; doubleBuffer <= 1; doubleBuffer++) {
            mDoubleBuffering = doubleBuffer == 1;
            pushDoubleBuffering();
            for (mAnimTestNr = 0; mAnimTestNr < ANIM_TESTS.length; mAnimTestNr++) {
                mAnimTests = ANIM_TESTS[mAnimTestNr];
                if (!runTestDirs(ANIM_TEST_DIRS)) {
                    return false;
                }
            }
        }
        return true;
    }

    public void testAnimations() {
        // instead of autoscrolling, load each page until either an timer fires,
        // or the animation signals complete via javascript
        setScrollingTestingMode(false);

        if (checkMedia() && runAnimationTests()) {
            getInstrumentation().sendStatus(0, mStats.getBundle());
        } else {
            getInstrumentation().sendStatus(1, null);
        }
    }
}
+80 −27
Original line number Diff line number Diff line
@@ -80,7 +80,7 @@ public class PlaybackGraphs {
                    for (int tileID = 1; tileID < frame.length; tileID++) {
                        TileData data = frame[tileID];
                        double coverage = viewportCoverage(frame[0], data);
                        total += coverage * (data.isReady ? 1 : 0);
                        total += coverage * (data.isReady ? 100 : 0);
                        totalCount += coverage;
                    }
                    if (totalCount == 0) {
@@ -91,7 +91,7 @@ public class PlaybackGraphs {

                @Override
                public double getMax() {
                    return 1;
                    return 100;
                }

                @Override
@@ -108,6 +108,9 @@ public class PlaybackGraphs {
    }

    public static double getPercentile(double sortedValues[], double ratioAbove) {
        if (sortedValues.length == 0)
            return -1;

        double index = ratioAbove * (sortedValues.length - 1);
        int intIndex = (int) Math.floor(index);
        if (index == intIndex) {
@@ -118,6 +121,31 @@ public class PlaybackGraphs {
                + sortedValues[intIndex + 1] * (alpha);
    }

    public static double getMean(double sortedValues[]) {
        if (sortedValues.length == 0)
            return -1;

        double agg = 0;
        for (double val : sortedValues) {
            agg += val;
        }
        return agg / sortedValues.length;
    }

    public static double getStdDev(double sortedValues[]) {
        if (sortedValues.length == 0)
            return -1;

        double agg = 0;
        double sqrAgg = 0;
        for (double val : sortedValues) {
            agg += val;
            sqrAgg += val*val;
        }
        double mean = agg / sortedValues.length;
        return Math.sqrt((sqrAgg / sortedValues.length) - (mean * mean));
    }

    protected static StatGen[] Stats = new StatGen[] {
            new StatGen() {
                @Override
@@ -149,6 +177,26 @@ public class PlaybackGraphs {
                public int getLabelId() {
                    return R.string.percentile_75;
                }
            }, new StatGen() {
                @Override
                public double getValue(double[] sortedValues) {
                    return getStdDev(sortedValues);
                }

                @Override
                public int getLabelId() {
                    return R.string.std_dev;
                }
            }, new StatGen() {
                @Override
                public double getValue(double[] sortedValues) {
                    return getMean(sortedValues);
                }

                @Override
                public int getLabelId() {
                    return R.string.mean;
                }
            },
    };

@@ -159,18 +207,10 @@ public class PlaybackGraphs {
    }

    private ArrayList<ShapeDrawable> mShapes = new ArrayList<ShapeDrawable>();
    protected double[][] mStats = new double[Metrics.length][Stats.length];
    protected final double[][] mStats = new double[Metrics.length][Stats.length];
    protected HashMap<String, Double> mSingleStats;

    public void setData(RunData data) {
        mShapes.clear();
        double metricValues[] = new double[data.frames.length];

        if (data.frames.length == 0) {
            return;
        }

        for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
    private void gatherFrameMetric(int metricIndex, double metricValues[], RunData data) {
        // create graph out of rectangles, one per frame
        int lastBar = 0;
        for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
@@ -193,6 +233,21 @@ public class PlaybackGraphs {
            metricValues[frameIndex] = absoluteValue;
            lastBar = newBar;
        }
    }

    public void setData(RunData data) {
        mShapes.clear();
        double metricValues[] = new double[data.frames.length];

        mSingleStats = data.singleStats;

        if (data.frames.length == 0) {
            return;
        }

        for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
            // calculate metric based on list of frames
            gatherFrameMetric(metricIndex, metricValues, data);

            // store aggregate statistics per metric (median, and similar)
            Arrays.sort(metricValues);
@@ -200,8 +255,6 @@ public class PlaybackGraphs {
                mStats[metricIndex][statIndex] =
                        Stats[statIndex].getValue(metricValues);
            }

            mSingleStats = data.singleStats;
        }
    }

+49 −20
Original line number Diff line number Diff line
@@ -22,11 +22,12 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.AdapterView;
@@ -49,6 +50,8 @@ import java.io.ObjectOutputStream;
 */
public class ProfileActivity extends Activity {

    private static final int TIMED_RECORD_MILLIS = 2000;

    public interface ProfileCallback {
        public void profileCallback(RunData data);
    }
@@ -65,6 +68,7 @@ public class ProfileActivity extends Activity {

    LoggingWebViewClient mLoggingWebViewClient = new LoggingWebViewClient();
    AutoLoggingWebViewClient mAutoLoggingWebViewClient = new AutoLoggingWebViewClient();
    TimedLoggingWebViewClient mTimedLoggingWebViewClient = new TimedLoggingWebViewClient();

    private enum TestingState {
        NOT_TESTING,
@@ -93,18 +97,18 @@ public class ProfileActivity extends Activity {
        public void onItemSelected(AdapterView<?> parent, View view,
                int position, long id) {
            String movementStr = parent.getItemAtPosition(position).toString();
            if (movementStr == getResources().getString(
                    R.string.movement_auto_scroll)
                    || movementStr == getResources().getString(
                            R.string.movement_auto_fling)) {
            if (movementStr == getResources().getString(R.string.movement_auto_scroll)) {
                mWeb.setWebViewClient(mAutoLoggingWebViewClient);
                mCaptureButton.setEnabled(false);
                mVelocitySpinner.setEnabled(true);
            } else if (movementStr == getResources().getString(
                    R.string.movement_manual)) {
            } else if (movementStr == getResources().getString(R.string.movement_manual)) {
                mWeb.setWebViewClient(mLoggingWebViewClient);
                mCaptureButton.setEnabled(true);
                mVelocitySpinner.setEnabled(false);
            } else if (movementStr == getResources().getString(R.string.movement_timed)) {
                mWeb.setWebViewClient(mTimedLoggingWebViewClient);
                mCaptureButton.setEnabled(false);
                mVelocitySpinner.setEnabled(false);
            }
        }

@@ -124,16 +128,46 @@ public class ProfileActivity extends Activity {
            super.onPageStarted(view, url, favicon);
            mUrl.setText(url);
        }

        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            view.requestFocus();
            ((ProfiledWebView)view).onPageFinished();
        }
    }

    private class AutoLoggingWebViewClient extends LoggingWebViewClient {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            startViewProfiling(true);
        }

        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon) {
            super.onPageStarted(view, url, favicon);
            setTestingState(TestingState.PRE_TESTING);
        }
    }

    private class TimedLoggingWebViewClient extends LoggingWebViewClient {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            view.requestFocus();
            startViewProfiling(false);

            startViewProfiling(true);
            // after a fixed time after page finished, stop testing
            new CountDownTimer(TIMED_RECORD_MILLIS, TIMED_RECORD_MILLIS) {
                @Override
                public void onTick(long millisUntilFinished) {
                }

                @Override
                public void onFinish() {
                    mWeb.stopScrollTest();
                }
            }.start();
        }

        @Override
@@ -178,11 +212,13 @@ public class ProfileActivity extends Activity {
                mMovementSpinner.setEnabled(false);
                break;
            case START_TESTING:
                mCaptureButton.setChecked(true);
                mUrl.setBackgroundResource(R.color.background_start_testing);
                mInspectButton.setEnabled(false);
                mMovementSpinner.setEnabled(false);
                break;
            case STOP_TESTING:
                mCaptureButton.setChecked(false);
                mUrl.setBackgroundResource(R.color.background_stop_testing);
                break;
            case SAVED_TESTING:
@@ -195,7 +231,6 @@ public class ProfileActivity extends Activity {
    /** auto - automatically scroll. */
    private void startViewProfiling(boolean auto) {
        // toggle capture button to indicate capture state to user
        mCaptureButton.setChecked(true);
        mWeb.startScrollTest(mCallback, auto);
        setTestingState(TestingState.START_TESTING);
    }
@@ -217,7 +252,7 @@ public class ProfileActivity extends Activity {
            public void profileCallback(RunData data) {
                new StoreFileTask().execute(new Pair<String, RunData>(
                        TEMP_FILENAME, data));
                mCaptureButton.setChecked(false);
                Log.d("ProfileActivity", "stored " + data.frames.length + " frames in file");
                setTestingState(TestingState.STOP_TESTING);
            }
        });
@@ -245,8 +280,8 @@ public class ProfileActivity extends Activity {
        // Movement spinner
        String content[] = {
                getResources().getString(R.string.movement_auto_scroll),
                getResources().getString(R.string.movement_auto_fling),
                getResources().getString(R.string.movement_manual)
                getResources().getString(R.string.movement_manual),
                getResources().getString(R.string.movement_timed)
        };
        adapter = new ArrayAdapter<CharSequence>(this,
                android.R.layout.simple_spinner_item, content);
@@ -270,13 +305,7 @@ public class ProfileActivity extends Activity {
        });

        // Custom profiling WebView
        WebSettings settings = mWeb.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setSupportZoom(true);
        settings.setEnableSmoothTransition(true);
        settings.setBuiltInZoomControls(true);
        settings.setLoadWithOverviewMode(true);
        settings.setProperty("use_minimal_memory", "false"); // prefetch tiles, as browser does
        mWeb.init(this);
        mWeb.setWebViewClient(new LoggingWebViewClient());

        // URL text entry
+88 −15

File changed.

Preview size limit exceeded, changes collapsed.