Merge "Added TileBenchmark"
diff --git a/tests/TileBenchmark/Android.mk b/tests/TileBenchmark/Android.mk
new file mode 100644
index 0000000..430f0f1
--- /dev/null
+++ b/tests/TileBenchmark/Android.mk
@@ -0,0 +1,32 @@
+# Copyright (C) 2011 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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := TileBenchmark
+
+include $(BUILD_PACKAGE)
+
+##################################################
+include $(CLEAR_VARS)
+
+include $(BUILD_MULTI_PREBUILT)
+
+# Use the folloing include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/TileBenchmark/AndroidManifest.xml b/tests/TileBenchmark/AndroidManifest.xml
new file mode 100644
index 0000000..663cc0d
--- /dev/null
+++ b/tests/TileBenchmark/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      android:versionCode="1"
+      android:versionName="1.0" package="com.test.tilebenchmark">
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <application android:icon="@drawable/icon"
+                 android:label="@string/app_name"
+                 android:hardwareAccelerated="true">
+        <activity android:name=".ProfileActivity"
+                  android:label="@string/profile_activity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity android:name=".PlaybackActivity"
+                  android:label="@string/playback_activity">
+        </activity>
+    </application>
+</manifest>
diff --git a/tests/TileBenchmark/res/drawable-hdpi/icon.png b/tests/TileBenchmark/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..8074c4c
--- /dev/null
+++ b/tests/TileBenchmark/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/tests/TileBenchmark/res/drawable-ldpi/icon.png b/tests/TileBenchmark/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..1095584
--- /dev/null
+++ b/tests/TileBenchmark/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/tests/TileBenchmark/res/drawable-mdpi/icon.png b/tests/TileBenchmark/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..a07c69f
--- /dev/null
+++ b/tests/TileBenchmark/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/tests/TileBenchmark/res/layout/main.xml b/tests/TileBenchmark/res/layout/main.xml
new file mode 100644
index 0000000..4a81da6
--- /dev/null
+++ b/tests/TileBenchmark/res/layout/main.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+    <LinearLayout
+        android:id="@+id/top"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        >
+        <Button
+            android:id="@+id/inspect"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/inspect_log"
+            />
+        <Spinner
+            android:id="@+id/velocity"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:prompt="@string/desired_scroll_velocity"
+            />
+        <EditText
+            android:id="@+id/url"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:inputType="textUri"
+            android:imeOptions="actionGo"
+            android:layout_weight="1"
+            />
+    </LinearLayout>
+    <com.test.tilebenchmark.ProfiledWebView
+        android:id="@+id/web"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        />
+</LinearLayout>
diff --git a/tests/TileBenchmark/res/layout/playback.xml b/tests/TileBenchmark/res/layout/playback.xml
new file mode 100644
index 0000000..aa1c8a4
--- /dev/null
+++ b/tests/TileBenchmark/res/layout/playback.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    >
+    <LinearLayout
+        android:id="@+id/top"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        >
+        <Button
+            android:id="@+id/backward"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/backward"
+            />
+        <TextView
+            android:id="@+id/frame_display"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:layout_weight="1"
+            />
+        <Button
+            android:id="@+id/forward"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/forward"
+            />
+        <SeekBar
+            android:id="@+id/seek_bar"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="10"
+            />
+    </LinearLayout>
+    <com.test.tilebenchmark.PlaybackView
+        android:id="@+id/playback"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        />
+</LinearLayout>
diff --git a/tests/TileBenchmark/res/values/colors.xml b/tests/TileBenchmark/res/values/colors.xml
new file mode 100644
index 0000000..3958083
--- /dev/null
+++ b/tests/TileBenchmark/res/values/colors.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<resources>
+    <!-- The color of tiles with valid textures -->
+    <color name="ready_tile">#ff4ac230</color>
+    <!-- The color of tiles with stale / invalid textures -->
+    <color name="unready_tile">#ff744400</color>
+    <!-- Background color for logged URLs -->
+    <color name="finished_url">#ff004000</color>
+    <!-- Background color for URLs with logging in progress -->
+    <color name="unfinished_url">#ff400000</color>
+</resources>
diff --git a/tests/TileBenchmark/res/values/strings.xml b/tests/TileBenchmark/res/values/strings.xml
new file mode 100644
index 0000000..f70ee2c
--- /dev/null
+++ b/tests/TileBenchmark/res/values/strings.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<resources>
+    <!-- Button, steps back a single frame [CHAR LIMIT=15] -->
+    <string name="backward">Backward</string>
+    <!-- Button, steps forward a single frame [CHAR LIMIT=15] -->
+    <string name="forward">Forward</string>
+    <!-- The name of the application [CHAR LIMIT=20] -->
+    <string name="app_name">TileBenchmark</string>
+    <!-- name of the auto-scroller / tile logger activity [CHAR LIMIT=100] -->
+    <string name="profile_activity">Webview Profiler</string>
+    <!-- name of the tile log playback activity [CHAR LIMIT=100] -->
+    <string name="playback_activity">Webview Tile Playback</string>
+    <!-- Button, loads another tile log [CHAR LIMIT=30] -->
+    <string name="loadbutton">Load</string>
+    <!-- Button, opens the playback activity [CHAR LIMIT=20] -->
+    <string name="inspect_log">Inspect Log</string>
+    <!-- The speed of auto-scrolling [CHAR LIMIT=30] -->
+    <string name="desired_scroll_velocity">Choose Scroll Velocity</string>
+    <!-- Pixels moved per frame [CHAR LIMIT=10] -->
+    <string-array name="velocity_array">
+        <item>1</item>
+        <item>25</item>
+        <item>50</item>
+        <item>100</item>
+        <item>200</item>
+        <item>400</item>
+    </string-array>
+    <!-- 25th percentile - 25% of frames fall below this value [CHAR LIMIT=12]
+    -->
+    <string name="percentile_25">25%ile</string>
+    <!-- 50th percentile - 50% of frames fall below this value (aka median)
+    [CHAR LIMIT=12] -->
+    <string name="percentile_50">median</string>
+    <!-- 75th percentile - 75% of frames fall below this value [CHAR LIMIT=12]
+    -->
+    <string name="percentile_75">75%ile</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>
+    <!-- Format string for stat value overlay [CHAR LIMIT=15] -->
+    <string name="format_stat">%4.4f</string>
+    <!-- Format string for displaying aggregate stats+values (nr of valid tiles,
+    etc.) [CHAR LIMIT=20] -->
+    <string name="format_stat_name">%1$9s %2$3d</string>
+    <!-- Text hovering over canvas, number of tiles ready [CHAR LIMIT=15] -->
+    <string name="ready_tiles">Ready Tiles</string>
+    <!-- Text hovering over canvas, number tiles not ready [CHAR LIMIT=15] -->
+    <string name="unready_tiles">Unready Tiles</string>
+    <!-- Text hovering over canvas, number of tiles that haven't been
+    allocated to a place on the page [CHAR LIMIT=15] -->
+    <string name="unplaced_tiles">Unplaced Tiles</string>
+</resources>
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
new file mode 100644
index 0000000..5130f5d
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackActivity.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+
+/**
+ * Interface for playing back WebView tile rendering status. Draws viewport and
+ * states of tiles and statistics for off-line analysis.
+ */
+public class PlaybackActivity extends Activity {
+    private static final float SCROLL_SCALER = 0.125f;
+
+    PlaybackView mPlaybackView;
+    SeekBar mSeekBar;
+    Button mForward;
+    Button mBackward;
+    TextView mFrameDisplay;
+
+    private int mFrame = -1;
+    private int mFrameMax;
+
+    private class TouchFrameChangeListener extends SimpleOnGestureListener {
+        float mDist = 0;
+
+        @Override
+        public boolean onScroll(MotionEvent e1, MotionEvent e2,
+                float distanceX, float distanceY) {
+            // aggregate scrolls so that small ones can add up
+            mDist += distanceY * SCROLL_SCALER;
+            int intComponent = (int) Math.floor(Math.abs(mDist));
+            if (intComponent >= 1) {
+                int scrollDist = (mDist > 0) ? intComponent : -intComponent;
+                setFrame(null, mFrame + scrollDist);
+                mDist -= scrollDist;
+            }
+            return super.onScroll(e1, e2, distanceX, distanceY);
+        }
+    };
+
+    private class SeekFrameChangeListener implements OnSeekBarChangeListener {
+        @Override
+        public void onStopTrackingTouch(SeekBar seekBar) {
+        }
+
+        @Override
+        public void onStartTrackingTouch(SeekBar seekBar) {
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress,
+                boolean fromUser) {
+            setFrame(seekBar, progress);
+        }
+    };
+
+    private class LoadFileTask extends AsyncTask<String, Void, TileData[][]> {
+        @Override
+        protected TileData[][] doInBackground(String... params) {
+            TileData[][] data = null;
+            try {
+                FileInputStream fis = openFileInput(params[0]);
+                ObjectInputStream in = new ObjectInputStream(fis);
+                data = (TileData[][]) in.readObject();
+                in.close();
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            } catch (ClassNotFoundException ex) {
+                ex.printStackTrace();
+            }
+            return data;
+        }
+
+        @Override
+        protected void onPostExecute(TileData data[][]) {
+            if (data == null) {
+                data = genTestPattern();
+            }
+            mPlaybackView.setData(data);
+
+            mFrameMax = data.length - 1;
+            mSeekBar.setMax(mFrameMax);
+
+            setFrame(null, 0);
+        }
+    }
+
+    private void setFrame(View changer, int f) {
+        if (f < 0) {
+            f = 0;
+        } else if (f > mFrameMax) {
+            f = mFrameMax;
+        }
+
+        if (mFrame == f) {
+            return;
+        }
+
+        mFrame = f;
+        mForward.setEnabled(mFrame != mFrameMax);
+        mBackward.setEnabled(mFrame != 0);
+        if (changer != mSeekBar) {
+            mSeekBar.setProgress(mFrame);
+        }
+        mFrameDisplay.setText(Integer.toString(mFrame));
+        mPlaybackView.setFrame(mFrame);
+    };
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.playback);
+
+        mPlaybackView = (PlaybackView) findViewById(R.id.playback);
+        mSeekBar = (SeekBar) findViewById(R.id.seek_bar);
+        mForward = (Button) findViewById(R.id.forward);
+        mBackward = (Button) findViewById(R.id.backward);
+        mFrameDisplay = (TextView) findViewById(R.id.frame_display);
+
+        mForward.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setFrame(v, mFrame + 1);
+            }
+        });
+
+        mBackward.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                setFrame(v, mFrame - 1);
+            }
+        });
+
+        mSeekBar.setOnSeekBarChangeListener(new SeekFrameChangeListener());
+
+        mPlaybackView.setOnGestureListener(new TouchFrameChangeListener());
+
+        new LoadFileTask().execute(ProfileActivity.TEMP_FILENAME);
+    }
+
+    private TileData[][] genTestPattern() {
+        final int XMAX = 5;
+        final int FRAMEMAX = 99;
+
+        TileData example[][] = new TileData[FRAMEMAX][];
+        for (int frame = 0; frame < FRAMEMAX; frame++) {
+            int numTiles = frame + 10;
+
+            example[frame] = new TileData[numTiles];
+            for (int t = 0; t < numTiles; t++) {
+                int x = t % XMAX;
+                int y = t / XMAX;
+                boolean isReady = y * 10 < frame;
+                example[frame][t] = new TileData(x, y, isReady, 0);
+            }
+        }
+        return example;
+    }
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
new file mode 100644
index 0000000..db4a341
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackGraphs.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.ShapeDrawable;
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+public class PlaybackGraphs {
+    private static final int BAR_WIDTH = PlaybackView.TILEX * 3;
+    private static final float CANVAS_SCALE = 0.2f;
+    private static final double IDEAL_FRAMES = 60;
+    private static final int LABELOFFSET = 100;
+    private static Paint whiteLabels;
+
+    private static double viewportCoverage(int l, int b, int r, int t,
+            int tileIndexX,
+            int tileIndexY) {
+        if (tileIndexX * PlaybackView.TILEX < r
+                && (tileIndexX + 1) * PlaybackView.TILEX >= l
+                && tileIndexY * PlaybackView.TILEY < t
+                && (tileIndexY + 1) * PlaybackView.TILEY >= b) {
+            return 1.0f;
+        }
+        return 0.0f;
+    }
+
+    private interface MetricGen {
+        public double getValue(TileData[] frame);
+
+        public double getMax();
+
+        public int getLabelId();
+    };
+
+    private static MetricGen[] Metrics = new MetricGen[] {
+            new MetricGen() {
+                // framerate graph
+                @Override
+                public double getValue(TileData[] frame) {
+                    int renderTimeUS = frame[0].level;
+                    return 1.0e6f / renderTimeUS;
+                }
+
+                @Override
+                public double getMax() {
+                    return IDEAL_FRAMES;
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.frames_per_second;
+                }
+            }, new MetricGen() {
+                // coverage graph
+                @Override
+                public double getValue(TileData[] frame) {
+                    int l = frame[0].x, b = frame[0].y;
+                    int r = frame[1].x, t = frame[1].y;
+                    double total = 0, totalCount = 0;
+                    for (int tileID = 2; tileID < frame.length; tileID++) {
+                        TileData data = frame[tileID];
+                        double coverage = viewportCoverage(l, b, r, t, data.x,
+                                data.y);
+                        total += coverage * (data.isReady ? 1 : 0);
+                        totalCount += coverage;
+                    }
+                    if (totalCount == 0) {
+                        return -1;
+                    }
+                    return total / totalCount;
+                }
+
+                @Override
+                public double getMax() {
+                    return 1;
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.viewport_coverage;
+                }
+            }
+    };
+
+    private interface StatGen {
+        public double getValue(double sortedValues[]);
+
+        public int getLabelId();
+    }
+
+    public static double getPercentile(double sortedValues[], double ratioAbove) {
+        double index = ratioAbove * (sortedValues.length - 1);
+        int intIndex = (int) Math.floor(index);
+        if (index == intIndex) {
+            return sortedValues[intIndex];
+        }
+        double alpha = index - intIndex;
+        return sortedValues[intIndex] * (1 - alpha)
+                + sortedValues[intIndex + 1] * (alpha);
+    }
+
+    private static StatGen[] Stats = new StatGen[] {
+            new StatGen() {
+                @Override
+                public double getValue(double[] sortedValues) {
+                    return getPercentile(sortedValues, 0.25);
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.percentile_25;
+                }
+            }, new StatGen() {
+                @Override
+                public double getValue(double[] sortedValues) {
+                    return getPercentile(sortedValues, 0.5);
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.percentile_50;
+                }
+            }, new StatGen() {
+                @Override
+                public double getValue(double[] sortedValues) {
+                    return getPercentile(sortedValues, 0.75);
+                }
+
+                @Override
+                public int getLabelId() {
+                    return R.string.percentile_75;
+                }
+            },
+    };
+
+    public PlaybackGraphs() {
+        whiteLabels = new Paint();
+        whiteLabels.setColor(Color.WHITE);
+        whiteLabels.setTextSize(PlaybackView.TILEY / 3);
+    }
+
+    private ArrayList<ShapeDrawable> mShapes = new ArrayList<ShapeDrawable>();
+    private double[][] mStats = new double[Metrics.length][Stats.length];
+
+    public void setData(TileData[][] tileProfilingData) {
+        mShapes.clear();
+        double metricValues[] = new double[tileProfilingData.length];
+
+        if (tileProfilingData.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 < tileProfilingData.length; frameIndex++) {
+                TileData frame[] = tileProfilingData[frameIndex];
+                int newBar = (frame[0].y + frame[1].y) / 2;
+
+                MetricGen s = Metrics[metricIndex];
+                double absoluteValue = s.getValue(frame);
+                double relativeValue = absoluteValue / s.getMax();
+                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;
+            }
+
+            // store aggregate statistics per metric (median, and similar)
+            Arrays.sort(metricValues);
+            for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
+                mStats[metricIndex][statIndex] = Stats[statIndex]
+                        .getValue(metricValues);
+            }
+        }
+    }
+
+    public void drawVerticalShiftedShapes(Canvas canvas,
+            ArrayList<ShapeDrawable> shapes) {
+        // Shapes drawn here are drawn relative to the viewRect
+        Rect viewRect = shapes.get(shapes.size() - 1).getBounds();
+        canvas.translate(0, 5 * PlaybackView.TILEY - viewRect.top);
+
+        for (ShapeDrawable shape : mShapes) {
+            shape.draw(canvas);
+        }
+        for (ShapeDrawable shape : shapes) {
+            shape.draw(canvas);
+        }
+    }
+
+    public void draw(Canvas canvas, ArrayList<ShapeDrawable> shapes,
+            String[] strings, Resources resources) {
+        canvas.scale(CANVAS_SCALE, CANVAS_SCALE);
+
+        canvas.translate(BAR_WIDTH * Metrics.length, 0);
+
+        canvas.save();
+        drawVerticalShiftedShapes(canvas, shapes);
+        canvas.restore();
+
+        for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
+            String label = resources.getString(
+                    Metrics[metricIndex].getLabelId());
+            int xPos = (metricIndex + 1) * -BAR_WIDTH;
+            int yPos = LABELOFFSET;
+            canvas.drawText(label, xPos, yPos, whiteLabels);
+            for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
+                label = resources.getString(R.string.format_stat, mStats[metricIndex][statIndex]);
+                yPos = LABELOFFSET + (1 + statIndex) * PlaybackView.TILEY / 2;
+                canvas.drawText(label, xPos, yPos, whiteLabels);
+            }
+        }
+        for (int stringIndex = 0; stringIndex < strings.length; stringIndex++) {
+            int yPos = LABELOFFSET + stringIndex * PlaybackView.TILEY / 2;
+            canvas.drawText(strings[stringIndex], 0, yPos, whiteLabels);
+        }
+    }
+
+    public Bundle getStatBundle(Resources resources) {
+        Bundle b = new Bundle();
+
+        for (int metricIndex = 0; metricIndex < Metrics.length; metricIndex++) {
+            for (int statIndex = 0; statIndex < Stats.length; statIndex++) {
+                String metricLabel = resources.getString(
+                        Metrics[metricIndex].getLabelId());
+                String statLabel = resources.getString(
+                        Stats[statIndex].getLabelId());
+                double value = mStats[metricIndex][statIndex];
+                b.putDouble(metricLabel + " " + statLabel, value);
+            }
+        }
+
+        return b;
+    }
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
new file mode 100644
index 0000000..f104eac
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/PlaybackView.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.ShapeDrawable;
+import android.util.AttributeSet;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+
+public class PlaybackView extends View {
+    public static final int TILEX = 300;
+    public static final int TILEY = 300;
+
+    private Paint levelPaint = null, coordPaint = null, goldPaint = null;
+    private PlaybackGraphs mGraphs;
+
+    private ArrayList<ShapeDrawable> mTempShapes = new ArrayList<ShapeDrawable>();
+    private TileData mProfData[][] = null;
+    private GestureDetector mGestureDetector = null;
+    private String mRenderStrings[] = new String[3];
+
+    private class TileDrawable extends ShapeDrawable {
+        TileData tile;
+
+        public TileDrawable(TileData t) {
+            int tileColorId = t.isReady ? R.color.ready_tile
+                    : R.color.unready_tile;
+            getPaint().setColor(getResources().getColor(tileColorId));
+
+            setBounds(t.x * TILEX, t.y * TILEY, (t.x + 1) * TILEX, (t.y + 1)
+                    * TILEY);
+            this.tile = t;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            super.draw(canvas);
+            canvas.drawText(Integer.toString(tile.level), getBounds().left,
+                    getBounds().bottom, levelPaint);
+            canvas.drawText(tile.x + "," + tile.y, getBounds().left,
+                    ((getBounds().bottom + getBounds().top) / 2), coordPaint);
+        }
+    }
+
+    public PlaybackView(Context context) {
+        super(context);
+        init();
+    }
+
+    public PlaybackView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public PlaybackView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    public void setOnGestureListener(OnGestureListener gl) {
+        mGestureDetector = new GestureDetector(getContext(), gl);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        mGestureDetector.onTouchEvent(event);
+        return true;
+    }
+
+    private void init() {
+        levelPaint = new Paint();
+        levelPaint.setColor(Color.WHITE);
+        levelPaint.setTextSize(TILEY / 2);
+        coordPaint = new Paint();
+        coordPaint.setColor(Color.BLACK);
+        coordPaint.setTextSize(TILEY / 3);
+        goldPaint = new Paint();
+        goldPaint.setColor(0xffa0e010);
+        mGraphs = new PlaybackGraphs();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mTempShapes == null || mTempShapes.isEmpty()) {
+            return;
+        }
+
+        mGraphs.draw(canvas, mTempShapes, mRenderStrings, getResources());
+    }
+
+    public int setFrame(int frame) {
+        if (mProfData == null || mProfData.length == 0) {
+            return 0;
+        }
+
+        int readyTiles = 0, unreadyTiles = 0, unplacedTiles = 0;
+        mTempShapes.clear();
+
+        // draw actual tiles
+        for (int tileID = 2; tileID < mProfData[frame].length; tileID++) {
+            TileData t = mProfData[frame][tileID];
+            mTempShapes.add(new TileDrawable(t));
+            if (t.isReady) {
+                readyTiles++;
+            } else {
+                unreadyTiles++;
+            }
+            if (t.x < 0 || t.y < 0) {
+                unplacedTiles++;
+            }
+        }
+        mRenderStrings[0] = getResources().getString(R.string.format_stat_name,
+                getResources().getString(R.string.ready_tiles), readyTiles);
+        mRenderStrings[1] = getResources().getString(R.string.format_stat_name,
+                getResources().getString(R.string.unready_tiles), unreadyTiles);
+        mRenderStrings[2] = getResources().getString(R.string.format_stat_name,
+                getResources().getString(R.string.unplaced_tiles), unplacedTiles);
+
+        // draw view rect (using first two TileData objects)
+        ShapeDrawable viewShape = new ShapeDrawable();
+        viewShape.getPaint().setColor(0xff0000ff);
+        viewShape.setAlpha(64);
+        viewShape.setBounds(mProfData[frame][0].x, mProfData[frame][0].y,
+                mProfData[frame][1].x, mProfData[frame][1].y);
+        mTempShapes.add(viewShape);
+        this.invalidate();
+        return frame;
+    }
+
+    public void setData(TileData[][] tileProfilingData) {
+        mProfData = tileProfilingData;
+
+        mGraphs.setData(mProfData);
+    }
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
new file mode 100644
index 0000000..23b6275
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfileActivity.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+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;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+
+/**
+ * Interface for profiling the webview's scrolling, with simple controls on how
+ * to scroll, and what content to load.
+ */
+public class ProfileActivity extends Activity {
+
+    public interface ProfileCallback {
+        public void profileCallback(TileData data[][]);
+    }
+
+    public static final String TEMP_FILENAME = "profile.tiles";
+    private static final int LOAD_TEST_DELAY = 2000; // nr of millis after load,
+                                                     // before test
+
+    Button mInspectButton;
+    Spinner mVelocitySpinner;
+    EditText mUrl;
+    ProfiledWebView mWeb;
+    ProfileCallback mCallback;
+
+    private class VelocitySelectedListener implements OnItemSelectedListener {
+        @Override
+        public void onItemSelected(AdapterView<?> parent, View view,
+                int position, long id) {
+            String speedStr = parent.getItemAtPosition(position).toString();
+            int speedInt = Integer.parseInt(speedStr);
+            mWeb.setAutoScrollSpeed(speedInt);
+        }
+
+        @Override
+        public void onNothingSelected(AdapterView<?> parent) {
+        }
+    }
+
+    private class LoggingWebViewClient extends WebViewClient {
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, String url) {
+            return false;
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            mUrl.setText(url);
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            view.requestFocus();
+            new CountDownTimer(LOAD_TEST_DELAY, LOAD_TEST_DELAY) {
+                @Override
+                public void onTick(long millisUntilFinished) {
+                }
+
+                @Override
+                public void onFinish() {
+                    mWeb.startScrollTest(mCallback);
+                }
+            }.start();
+        }
+    }
+
+    private class StoreFileTask extends
+            AsyncTask<Pair<String, TileData[][]>, Void, Void> {
+
+        @Override
+        protected Void doInBackground(Pair<String, TileData[][]>... params) {
+            try {
+                FileOutputStream fos = openFileOutput(params[0].first,
+                        Context.MODE_PRIVATE);
+                ObjectOutputStream out = new ObjectOutputStream(fos);
+                out.writeObject(params[0].second);
+                out.close();
+            } catch (IOException ex) {
+                ex.printStackTrace();
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Void v) {
+            mUrl.setBackgroundResource(R.color.finished_url);
+        }
+    }
+
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+        mInspectButton = (Button) findViewById(R.id.inspect);
+        mVelocitySpinner = (Spinner) findViewById(R.id.velocity);
+        mUrl = (EditText) findViewById(R.id.url);
+        mWeb = (ProfiledWebView) findViewById(R.id.web);
+        mCallback = new ProfileCallback() {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void profileCallback(TileData[][] data) {
+                new StoreFileTask().execute(new Pair<String, TileData[][]>(TEMP_FILENAME, data));
+            }
+        };
+
+        // Inspect button (opens PlaybackActivity)
+        mInspectButton.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                startActivity(new Intent(ProfileActivity.this,
+                        PlaybackActivity.class));
+            }
+        });
+
+        // Velocity spinner
+        ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
+                this, R.array.velocity_array,
+                android.R.layout.simple_spinner_item);
+        adapter.setDropDownViewResource(
+                android.R.layout.simple_spinner_dropdown_item);
+        mVelocitySpinner.setAdapter(adapter);
+        mVelocitySpinner.setOnItemSelectedListener(
+                new VelocitySelectedListener());
+        mVelocitySpinner.setSelection(3);
+
+        // Custom profiling WebView
+        WebSettings settings = mWeb.getSettings();
+        settings.setJavaScriptEnabled(true);
+        settings.setSupportZoom(true);
+        settings.setEnableSmoothTransition(true);
+        settings.setBuiltInZoomControls(true);
+        settings.setLoadWithOverviewMode(true);
+        mWeb.setWebViewClient(new LoggingWebViewClient());
+
+        // URL text entry
+        mUrl.setOnEditorActionListener(new OnEditorActionListener() {
+            public boolean onEditorAction(TextView v, int actionId,
+                    KeyEvent event) {
+                String url = mUrl.getText().toString();
+                mUrl.setBackgroundResource(R.color.unfinished_url);
+                mWeb.loadUrl(url);
+                mWeb.requestFocus();
+                return true;
+            }
+        });
+    }
+
+    public void setCallback(ProfileCallback callback) {
+        mCallback = callback;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if ((keyCode == KeyEvent.KEYCODE_BACK) && mWeb.canGoBack()) {
+            mWeb.goBack();
+            return true;
+        }
+        return super.onKeyDown(keyCode, event);
+    }
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
new file mode 100644
index 0000000..6560624
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/ProfiledWebView.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.webkit.WebView;
+
+import com.test.tilebenchmark.ProfileActivity.ProfileCallback;
+
+public class ProfiledWebView extends WebView {
+    private int mSpeed;
+
+    private boolean isScrolling = false;
+    private ProfileCallback mCallback;
+
+    public ProfiledWebView(Context context) {
+        super(context);
+    }
+
+    public ProfiledWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public ProfiledWebView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public ProfiledWebView(Context context, AttributeSet attrs, int defStyle,
+            boolean privateBrowsing) {
+        super(context, attrs, defStyle, privateBrowsing);
+    }
+
+    @Override
+    protected void onDraw(android.graphics.Canvas canvas) {
+        if (isScrolling) {
+            if (canScrollVertically(1)) {
+                scrollBy(0, mSpeed);
+            } else {
+                stopScrollTest();
+                isScrolling = false;
+            }
+        }
+        super.onDraw(canvas);
+    }
+
+    /*
+     * Called once the page is loaded to start scrolling for evaluating tiles
+     */
+    public void startScrollTest(ProfileCallback callback) {
+        isScrolling = true;
+        mCallback = callback;
+        super.tileProfilingStart();
+        invalidate();
+    }
+
+    /*
+     * Called once the page has stopped scrolling
+     */
+    public void stopScrollTest() {
+        float testRatio = super.tileProfilingStop();
+
+        TileData data[][] = new TileData[super.tileProfilingNumFrames()][];
+        for (int frame = 0; frame < data.length; frame++) {
+            data[frame] = new TileData[
+                    super.tileProfilingNumTilesInFrame(frame)];
+            for (int tile = 0; tile < data[frame].length; tile++) {
+                int x = super.tileProfilingGetX(frame, tile);
+                int y = super.tileProfilingGetY(frame, tile);
+                boolean isReady = super.tileProfilingGetReady(frame, tile);
+                int level = super.tileProfilingGetLevel(frame, tile);
+
+                data[frame][tile] = new TileData(x, y, isReady, level);
+            }
+        }
+        super.tileProfilingClear();
+
+        mCallback.profileCallback(data);
+    }
+
+    @Override
+    public void loadUrl(String url) {
+        if (!url.startsWith("http://")) {
+            url = "http://" + url;
+        }
+        super.loadUrl(url);
+    }
+
+    public void setAutoScrollSpeed(int speedInt) {
+        mSpeed = speedInt;
+    }
+}
diff --git a/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java b/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
new file mode 100644
index 0000000..7d4bb9f
--- /dev/null
+++ b/tests/TileBenchmark/src/com/test/tilebenchmark/TileData.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2011 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.test.tilebenchmark;
+
+import java.io.Serializable;
+
+public class TileData implements Serializable {
+    public int x, y;
+    public boolean isReady;
+    public int level;
+
+    public TileData(int x, int y, boolean isReady, int level) {
+        this.x = x;
+        this.y = y;
+        this.isReady = isReady;
+        this.level = level;
+    }
+}