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

Commit 702c6fdc authored by Chris Craik's avatar Chris Craik
Browse files

Allow profiling of animation performance

Also fixes manual testing mode
Depends on external/webkit change: https://android-git.corp.google.com/g/#/c/159794/

Change-Id: I169e80f9b5328b1b5a7b0aeaf09652de67febd8d
parent d4e34d61
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.