summaryrefslogtreecommitdiffstats
path: root/tests/HierarchyViewerTest
diff options
context:
space:
mode:
authorSiva Velusamy <vsiva@google.com>2015-04-22 10:23:56 -0700
committerSiva Velusamy <vsiva@google.com>2015-05-07 18:44:15 -0700
commit0d857b9028f2702ce439e13feccde8182d40e1e5 (patch)
treea20bf172e726bef479b16ab121ee5a348df6b613 /tests/HierarchyViewerTest
parent0a008049a21c5fbe36eac8047c5411c2e3aff41b (diff)
downloadframeworks_base-0d857b9028f2702ce439e13feccde8182d40e1e5.zip
frameworks_base-0d857b9028f2702ce439e13feccde8182d40e1e5.tar.gz
frameworks_base-0d857b9028f2702ce439e13feccde8182d40e1e5.tar.bz2
Improve hierarchy viewer dump hierarchy latency
Hierarchy Viewer obtains the properties for each view by using reflection and looking for fields and methods that have the @ExportedProperty annotation. Using reflection made it quite slow for large view hierarchies. This CL adds a new method (encode) to each class that wishes to export data to hiererachy viewer. Inside this method, the object can write a sequence of key, value pairs corresponding to the values it wants exported. With this change, the dump hierarchy operation that used to take more than 10 seconds can be performed in a few hundred milliseconds. Change-Id: I199ac2e7ca3c59ebcfec7e6bd201e134c41fd583
Diffstat (limited to 'tests/HierarchyViewerTest')
-rw-r--r--tests/HierarchyViewerTest/.gitignore6
-rw-r--r--tests/HierarchyViewerTest/Android.mk12
-rw-r--r--tests/HierarchyViewerTest/AndroidManifest.xml36
-rw-r--r--tests/HierarchyViewerTest/build.gradle31
-rw-r--r--tests/HierarchyViewerTest/res/layout/activity_main.xml12
-rw-r--r--tests/HierarchyViewerTest/res/menu/menu_main.xml5
-rw-r--r--tests/HierarchyViewerTest/res/values/strings.xml4
-rw-r--r--tests/HierarchyViewerTest/run_tests.sh4
-rw-r--r--tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/Decoder.java101
-rw-r--r--tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivity.java44
-rw-r--r--tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivityTest.java81
-rw-r--r--tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java73
12 files changed, 409 insertions, 0 deletions
diff --git a/tests/HierarchyViewerTest/.gitignore b/tests/HierarchyViewerTest/.gitignore
new file mode 100644
index 0000000..75eec98
--- /dev/null
+++ b/tests/HierarchyViewerTest/.gitignore
@@ -0,0 +1,6 @@
+.gradle
+.idea
+*.iml
+gradle*
+build
+local.properties
diff --git a/tests/HierarchyViewerTest/Android.mk b/tests/HierarchyViewerTest/Android.mk
new file mode 100644
index 0000000..07b90f0
--- /dev/null
+++ b/tests/HierarchyViewerTest/Android.mk
@@ -0,0 +1,12 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_PACKAGE_NAME := HierarchyViewerTest
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+include $(BUILD_PACKAGE)
diff --git a/tests/HierarchyViewerTest/AndroidManifest.xml b/tests/HierarchyViewerTest/AndroidManifest.xml
new file mode 100644
index 0000000..65f2fd3
--- /dev/null
+++ b/tests/HierarchyViewerTest/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.test.hierarchyviewer">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+
+ <activity
+ android:name=".MainActivity"
+ android:label="HvTest" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.test.hierarchyviewer" />
+</manifest>
diff --git a/tests/HierarchyViewerTest/build.gradle b/tests/HierarchyViewerTest/build.gradle
new file mode 100644
index 0000000..e8cdfa2
--- /dev/null
+++ b/tests/HierarchyViewerTest/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:1.1.0+'
+
+ }
+}
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 21
+ buildToolsVersion "22.0.0"
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 21
+ versionCode 1
+ versionName "1.0"
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest.xml'
+ java.srcDirs = ['src']
+ res.srcDirs = ['res']
+ }
+ }
+}
diff --git a/tests/HierarchyViewerTest/res/layout/activity_main.xml b/tests/HierarchyViewerTest/res/layout/activity_main.xml
new file mode 100644
index 0000000..410a776
--- /dev/null
+++ b/tests/HierarchyViewerTest/res/layout/activity_main.xml
@@ -0,0 +1,12 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleX="10"
+ android:text="@string/test" />
+
+</RelativeLayout>
diff --git a/tests/HierarchyViewerTest/res/menu/menu_main.xml b/tests/HierarchyViewerTest/res/menu/menu_main.xml
new file mode 100644
index 0000000..9b78a1e
--- /dev/null
+++ b/tests/HierarchyViewerTest/res/menu/menu_main.xml
@@ -0,0 +1,5 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
+ <item android:id="@+id/action_settings" android:title="Settings"
+ android:orderInCategory="100" android:showAsAction="never" />
+</menu>
diff --git a/tests/HierarchyViewerTest/res/values/strings.xml b/tests/HierarchyViewerTest/res/values/strings.xml
new file mode 100644
index 0000000..800ee1c
--- /dev/null
+++ b/tests/HierarchyViewerTest/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="test">Hello World</string>
+</resources> \ No newline at end of file
diff --git a/tests/HierarchyViewerTest/run_tests.sh b/tests/HierarchyViewerTest/run_tests.sh
new file mode 100644
index 0000000..094bb4c
--- /dev/null
+++ b/tests/HierarchyViewerTest/run_tests.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+# Runs the tests in this apk
+adb install $OUT/data/app/HierarchyViewerTest/HierarchyViewerTest.apk
+adb shell am instrument -w com.android.test.hierarchyviewer/android.test.InstrumentationTestRunner
diff --git a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/Decoder.java b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/Decoder.java
new file mode 100644
index 0000000..c6f1470
--- /dev/null
+++ b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/Decoder.java
@@ -0,0 +1,101 @@
+package com.android.test.hierarchyviewer;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Decoder {
+ // Prefixes for simple primitives. These match the JNI definitions.
+ public static final byte SIG_BOOLEAN = 'Z';
+ public static final byte SIG_BYTE = 'B';
+ public static final byte SIG_SHORT = 'S';
+ public static final byte SIG_INT = 'I';
+ public static final byte SIG_LONG = 'J';
+ public static final byte SIG_FLOAT = 'F';
+ public static final byte SIG_DOUBLE = 'D';
+
+ // Prefixes for some commonly used objects
+ public static final byte SIG_STRING = 'R';
+
+ public static final byte SIG_MAP = 'M'; // a map with an short key
+ public static final short SIG_END_MAP = 0;
+
+ private final ByteBuffer mBuf;
+
+ public Decoder(byte[] buf) {
+ this(ByteBuffer.wrap(buf));
+ }
+
+ public Decoder(ByteBuffer buf) {
+ mBuf = buf;
+ }
+
+ public boolean hasRemaining() {
+ return mBuf.hasRemaining();
+ }
+
+ public Object readObject() {
+ byte sig = mBuf.get();
+
+ switch (sig) {
+ case SIG_BOOLEAN:
+ return mBuf.get() == 0 ? Boolean.FALSE : Boolean.TRUE;
+ case SIG_BYTE:
+ return mBuf.get();
+ case SIG_SHORT:
+ return mBuf.getShort();
+ case SIG_INT:
+ return mBuf.getInt();
+ case SIG_LONG:
+ return mBuf.getLong();
+ case SIG_FLOAT:
+ return mBuf.getFloat();
+ case SIG_DOUBLE:
+ return mBuf.getDouble();
+ case SIG_STRING:
+ return readString();
+ case SIG_MAP:
+ return readMap();
+ default:
+ throw new DecoderException(sig, mBuf.position() - 1);
+ }
+ }
+
+ private String readString() {
+ short len = mBuf.getShort();
+ byte[] b = new byte[len];
+ mBuf.get(b, 0, len);
+ return new String(b, Charset.forName("utf-8"));
+ }
+
+ private Map<Short, Object> readMap() {
+ Map<Short, Object> m = new HashMap<Short, Object>();
+
+ while (true) {
+ Object o = readObject();
+ if (!(o instanceof Short)) {
+ throw new DecoderException("Expected short key, got " + o.getClass());
+ }
+
+ Short key = (Short)o;
+ if (key == SIG_END_MAP) {
+ break;
+ }
+
+ m.put(key, readObject());
+ }
+
+ return m;
+ }
+
+ public static class DecoderException extends RuntimeException {
+ public DecoderException(byte seen, int pos) {
+ super(String.format("Unexpected byte %c seen at position %d", (char)seen, pos));
+ }
+
+ public DecoderException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivity.java b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivity.java
new file mode 100644
index 0000000..3a67273
--- /dev/null
+++ b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivity.java
@@ -0,0 +1,44 @@
+package com.android.test.hierarchyviewer;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+
+public class MainActivity extends Activity {
+ private static final String TAG = "Main";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ View textView = findViewById(R.id.textView);
+ Log.d(TAG, "x, y = " + textView.getX() + ", " + textView.getY());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+
+ //noinspection SimplifiableIfStatement
+ if (id == R.id.action_settings) {
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivityTest.java b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivityTest.java
new file mode 100644
index 0000000..aae0ff5
--- /dev/null
+++ b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/MainActivityTest.java
@@ -0,0 +1,81 @@
+package com.android.test.hierarchyviewer;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.view.View;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+
+public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
+ private MainActivity mActivity;
+ private View mTextView;
+
+
+ public MainActivityTest() {
+ super(MainActivity.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mActivity = getActivity();
+ mTextView = mActivity.findViewById(R.id.textView);
+ }
+
+ private byte[] encode(View view) throws ClassNotFoundException, NoSuchMethodException,
+ IllegalAccessException, InstantiationException, InvocationTargetException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * 1024);
+
+ Object encoder = createEncoder(baos);
+ invokeMethod(View.class, view, "encode", encoder);
+ invokeMethod(encoder.getClass(), encoder, "endStream");
+
+ return baos.toByteArray();
+ }
+
+ private Object invokeMethod(Class targetClass, Object target, String methodName, Object... params)
+ throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ Class[] paramClasses = new Class[params.length];
+ for (int i = 0; i < params.length; i++) {
+ paramClasses[i] = params[i].getClass();
+ }
+ Method method = targetClass.getDeclaredMethod(methodName, paramClasses);
+ method.setAccessible(true);
+ return method.invoke(target, params);
+ }
+
+ private Object createEncoder(ByteArrayOutputStream baos) throws ClassNotFoundException,
+ NoSuchMethodException, IllegalAccessException, InvocationTargetException,
+ InstantiationException {
+ Class clazz = Class.forName("android.view.ViewHierarchyEncoder");
+ Constructor constructor = clazz.getConstructor(ByteArrayOutputStream.class);
+ return constructor.newInstance(baos);
+ }
+
+ public void testTextView() throws Exception {
+ byte[] data = encode(mTextView);
+ assertNotNull(data);
+ assertTrue(data.length > 0);
+
+ ViewDumpParser parser = new ViewDumpParser();
+ parser.parse(data);
+
+ List<Map<Short, Object>> views = parser.getViews();
+ Map<String, Short> propertyNameTable = parser.getIds();
+
+ assertEquals(1, views.size());
+ assertNotNull(propertyNameTable);
+
+ Map<Short, Object> textViewProperties = views.get(0);
+ assertEquals("android.widget.TextView",
+ textViewProperties.get(propertyNameTable.get("meta:__name__")));
+
+// assertEquals(mActivity.getString(R.string.test),
+// textViewProperties.get(propertyNameTable.get("text")));
+ }
+}
diff --git a/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java
new file mode 100644
index 0000000..0111bc6
--- /dev/null
+++ b/tests/HierarchyViewerTest/src/com/android/test/hierarchyviewer/ViewDumpParser.java
@@ -0,0 +1,73 @@
+package com.android.test.hierarchyviewer;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class ViewDumpParser {
+ private Map<String, Short> mIds;
+ private List<Map<Short,Object>> mViews;
+
+ public void parse(byte[] data) {
+ Decoder d = new Decoder(ByteBuffer.wrap(data));
+
+ mViews = new ArrayList<>(100);
+ while (d.hasRemaining()) {
+ Object o = d.readObject();
+ if (o instanceof Map) {
+ //noinspection unchecked
+ mViews.add((Map<Short, Object>) o);
+ }
+ }
+
+ if (mViews.isEmpty()) {
+ return;
+ }
+
+ // the last one is the property map
+ Map<Short,Object> idMap = mViews.remove(mViews.size() - 1);
+ mIds = reverse(idMap);
+ }
+
+ public String getFirstView() {
+ if (mViews.isEmpty()) {
+ return null;
+ }
+
+ Map<Short, Object> props = mViews.get(0);
+ Object name = getProperty(props, "__name__");
+ Object hash = getProperty(props, "__hash__");
+
+ if (name instanceof String && hash instanceof Integer) {
+ return String.format(Locale.US, "%s@%x", name, hash);
+ } else {
+ return null;
+ }
+ }
+
+ private Object getProperty(Map<Short, Object> props, String key) {
+ return props.get(mIds.get(key));
+ }
+
+ private static Map<String, Short> reverse(Map<Short, Object> m) {
+ Map<String, Short> r = new HashMap<String, Short>(m.size());
+
+ for (Map.Entry<Short, Object> e : m.entrySet()) {
+ r.put((String)e.getValue(), e.getKey());
+ }
+
+ return r;
+ }
+
+ public List<Map<Short, Object>> getViews() {
+ return mViews;
+ }
+
+ public Map<String, Short> getIds() {
+ return mIds;
+ }
+
+}