Merge "Allow profiling of animation performance"
diff --git a/tests/TileBenchmark/res/values/strings.xml b/tests/TileBenchmark/res/values/strings.xml
index 5af52dc..6c7055b 100644
--- a/tests/TileBenchmark/res/values/strings.xml
+++ b/tests/TileBenchmark/res/values/strings.xml
@@ -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>
 
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
index cc39b75..6356cc1 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PerformanceTest.java
@@ -27,17 +27,57 @@
 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 @@
                     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 @@
     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 @@
             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 @@
         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 @@
         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 @@
             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);
+        }
+    }
+
+    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;
+    }
 
-        for (int i = 0; i < MAX_ITERATIONS; i++)
-            if (!runIteration()) {
-                getInstrumentation().sendStatus(1, null);
-                return;
-            }
-        getInstrumentation().sendStatus(0, mStats.getBundle());
+    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);
+        }
     }
 }
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
index 9ea90f8..a3ae9be 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
@@ -80,7 +80,7 @@
                     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 @@
 
                 @Override
                 public double getMax() {
-                    return 1;
+                    return 100;
                 }
 
                 @Override
@@ -108,6 +108,9 @@
     }
 
     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 @@
                 + 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 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,40 +207,47 @@
     }
 
     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;
 
+    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++) {
+            TileData frame[] = data.frames[frameIndex];
+            int newBar = (frame[0].top + frame[0].bottom) / 2;
+
+            MetricGen s = Metrics[metricIndex];
+            double absoluteValue = s.getValue(frame);
+            double relativeValue = absoluteValue / s.getMax();
+            relativeValue = Math.min(1,relativeValue);
+            relativeValue = Math.max(0,relativeValue);
+            int rightPos = (int) (-BAR_WIDTH * metricIndex);
+            int leftPos = (int) (-BAR_WIDTH * (metricIndex + relativeValue));
+
+            ShapeDrawable graphBar = new ShapeDrawable();
+            graphBar.getPaint().setColor(Color.BLUE);
+            graphBar.setBounds(leftPos, lastBar, rightPos, newBar);
+
+            mShapes.add(graphBar);
+            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++) {
-            // create graph out of rectangles, one per frame
-            int lastBar = 0;
-            for (int frameIndex = 0; frameIndex < data.frames.length; frameIndex++) {
-                TileData frame[] = data.frames[frameIndex];
-                int newBar = (frame[0].top + frame[0].bottom) / 2;
-
-                MetricGen s = Metrics[metricIndex];
-                double absoluteValue = s.getValue(frame);
-                double relativeValue = absoluteValue / s.getMax();
-                relativeValue = Math.min(1,relativeValue);
-                relativeValue = Math.max(0,relativeValue);
-                int rightPos = (int) (-BAR_WIDTH * metricIndex);
-                int leftPos = (int) (-BAR_WIDTH * (metricIndex + relativeValue));
-
-                ShapeDrawable graphBar = new ShapeDrawable();
-                graphBar.getPaint().setColor(Color.BLUE);
-                graphBar.setBounds(leftPos, lastBar, rightPos, newBar);
-
-                mShapes.add(graphBar);
-                metricValues[frameIndex] = absoluteValue;
-                lastBar = newBar;
-            }
+            // 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 @@
                 mStats[metricIndex][statIndex] =
                         Stats[statIndex].getValue(metricValues);
             }
-
-            mSingleStats = data.singleStats;
         }
     }
 
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
index d38d006..2e77157 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
@@ -22,11 +22,12 @@
 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 @@
  */
 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 @@
 
     LoggingWebViewClient mLoggingWebViewClient = new LoggingWebViewClient();
     AutoLoggingWebViewClient mAutoLoggingWebViewClient = new AutoLoggingWebViewClient();
+    TimedLoggingWebViewClient mTimedLoggingWebViewClient = new TimedLoggingWebViewClient();
 
     private enum TestingState {
         NOT_TESTING,
@@ -93,18 +97,18 @@
         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,15 +128,19 @@
             super.onPageStarted(view, url, favicon);
             mUrl.setText(url);
         }
-    }
-
-    private class AutoLoggingWebViewClient extends LoggingWebViewClient {
 
         @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);
         }
 
@@ -143,6 +151,32 @@
         }
     }
 
+    private class TimedLoggingWebViewClient extends LoggingWebViewClient {
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            startViewProfiling(false);
+
+            // 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
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            setTestingState(TestingState.PRE_TESTING);
+        }
+    }
+
     private class StoreFileTask extends
             AsyncTask<Pair<String, RunData>, Void, Void> {
 
@@ -178,11 +212,13 @@
                 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 @@
     /** 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 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 @@
         // 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 @@
         });
 
         // 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
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
index 83f1668..a706f78 100644
--- a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
@@ -20,21 +20,28 @@
 import android.os.CountDownTimer;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.webkit.WebSettings;
 import android.webkit.WebView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
 
 import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
 import com.test.tilebenchmark.RunData.TileData;
 
 public class ProfiledWebView extends WebView {
+    private static final String LOGTAG = "ProfiledWebView";
+
     private int mSpeed;
 
     private boolean mIsTesting = false;
     private boolean mIsScrolling = false;
     private ProfileCallback mCallback;
     private long mContentInvalMillis;
-    private boolean mHadToBeForced = false;
     private static final int LOAD_STALL_MILLIS = 2000; // nr of millis after load,
                                                        // before test is forced
+    private double mLoadTime;
+    private double mAnimationTime;
 
     public ProfiledWebView(Context context) {
         super(context);
@@ -53,6 +60,39 @@
         super(context, attrs, defStyle, privateBrowsing);
     }
 
+    private class JavaScriptInterface {
+        Context mContext;
+
+        /** Instantiate the interface and set the context */
+        JavaScriptInterface(Context c) {
+            mContext = c;
+        }
+
+        /** Show a toast from the web page */
+        public void animationComplete() {
+            Toast.makeText(mContext, "Animation complete!", Toast.LENGTH_SHORT).show();
+            //Log.d(LOGTAG, "anim complete");
+            mAnimationTime = System.currentTimeMillis();
+        }
+    }
+
+    public void init(Context c) {
+        WebSettings settings = 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
+        addJavascriptInterface(new JavaScriptInterface(c), "Android");
+        mAnimationTime = 0;
+        mLoadTime = 0;
+    }
+
+    public void onPageFinished() {
+        mLoadTime = System.currentTimeMillis();
+    }
+
     @Override
     protected void onDraw(android.graphics.Canvas canvas) {
         if (mIsTesting && mIsScrolling) {
@@ -72,9 +112,12 @@
      * scrolling, invalidate all content and redraw it, measuring time taken.
      */
     public void startScrollTest(ProfileCallback callback, boolean autoScrolling) {
-        mIsScrolling = autoScrolling;
         mCallback = callback;
         mIsTesting = false;
+        mIsScrolling = false;
+        WebSettings settings = getSettings();
+        settings.setProperty("tree_updates", "0");
+
 
         if (autoScrolling) {
             // after a while, force it to start even if the pages haven't swapped
@@ -86,13 +129,18 @@
                 @Override
                 public void onFinish() {
                     // invalidate all content, and kick off redraw
+                    Log.d("ProfiledWebView",
+                            "kicking off test with callback registration, and tile discard...");
                     registerPageSwapCallback();
                     discardAllTextures();
                     invalidate();
-
+                    mIsScrolling = true;
                     mContentInvalMillis = System.currentTimeMillis();
                 }
             }.start();
+        } else {
+            mIsTesting = true;
+            tileProfilingStart();
         }
     }
 
@@ -102,13 +150,35 @@
      */
     @Override
     protected void pageSwapCallback(boolean startAnim) {
-        mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
-        super.pageSwapCallback(startAnim);
-        Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis
-                + "millis");
-        mIsTesting = true;
-        invalidate(); // ensure a redraw so that auto-scrolling can occur
-        tileProfilingStart();
+        if (!mIsTesting && mIsScrolling) {
+            // kick off testing
+            mContentInvalMillis = System.currentTimeMillis() - mContentInvalMillis;
+            super.pageSwapCallback(startAnim);
+            Log.d("ProfiledWebView", "REDRAW TOOK " + mContentInvalMillis + "millis");
+            mIsTesting = true;
+            invalidate(); // ensure a redraw so that auto-scrolling can occur
+            tileProfilingStart();
+        }
+    }
+
+    private double animFramerate() {
+        WebSettings settings = getSettings();
+        String updatesString = settings.getProperty("tree_updates");
+        int updates = (updatesString == null) ? -1 : Integer.parseInt(updatesString);
+
+        double animationTime;
+        if (mAnimationTime == 0) {
+            animationTime = System.currentTimeMillis() - mLoadTime;
+        } else {
+            animationTime = mAnimationTime - mLoadTime;
+        }
+
+        return updates * 1000 / animationTime;
+    }
+
+    public void setDoubleBuffering(boolean useDoubleBuffering) {
+        WebSettings settings = getSettings();
+        settings.setProperty("use_double_buffering", useDoubleBuffering ? "true" : "false");
     }
 
     /*
@@ -127,11 +197,12 @@
         // record the time spent (before scrolling) rendering the page
         data.singleStats.put(getResources().getString(R.string.render_millis),
                 (double)mContentInvalMillis);
-        // record if the page render timed out
-        Log.d("ProfiledWebView", "hadtobeforced = " + mHadToBeForced);
-        data.singleStats.put(getResources().getString(R.string.render_stalls),
-                             mHadToBeForced ? 1.0 : 0.0);
-        mHadToBeForced = false;
+
+        // record framerate
+        double framerate = animFramerate();
+        Log.d(LOGTAG, "anim framerate was "+framerate);
+        data.singleStats.put(getResources().getString(R.string.animation_framerate),
+                framerate);
 
         for (int frame = 0; frame < data.frames.length; frame++) {
             data.frames[frame] = new TileData[
@@ -159,6 +230,8 @@
 
     @Override
     public void loadUrl(String url) {
+        mAnimationTime = 0;
+        mLoadTime = 0;
         if (!url.startsWith("http://") && !url.startsWith("file://")) {
             url = "http://" + url;
         }