diff options
author | 2016-02-24 12:53:44 +0900 | |
---|---|---|
committer | 2016-03-02 13:01:11 +0900 | |
commit | e29e3416fddc34f72234a7ece191289ad3ba3a21 (patch) | |
tree | 95db2480b02f282a2a040030956fc0281e149f16 | |
parent | 191af919bb063c85544b72053e1eb33cc1daa5a0 (diff) |
Add scaffolds for performance tests of DocumentsUI
Bug: 27370274
Change-Id: I14dea1b85cd84c8bb3c0eee27b2954108bfa4f8b
-rw-r--r-- | perf-tests/Android.mk | 22 | ||||
-rw-r--r-- | perf-tests/AndroidManifest.xml | 24 | ||||
-rw-r--r-- | perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java | 144 | ||||
-rw-r--r-- | perf-tests/src/com/android/documentsui/StressProvider.java | 149 | ||||
-rw-r--r-- | src/com/android/documentsui/BaseActivity.java | 26 | ||||
-rw-r--r-- | src/com/android/documentsui/DirectoryLoader.java | 2 | ||||
-rw-r--r-- | src/com/android/documentsui/DirectoryResult.java | 3 | ||||
-rw-r--r-- | src/com/android/documentsui/EventListener.java | 32 | ||||
-rw-r--r-- | src/com/android/documentsui/dirlist/DirectoryFragment.java | 5 | ||||
-rw-r--r-- | src/com/android/documentsui/dirlist/Model.java | 3 | ||||
-rw-r--r-- | tests/src/com/android/documentsui/ActivityTest.java | 28 |
11 files changed, 431 insertions, 7 deletions
diff --git a/perf-tests/Android.mk b/perf-tests/Android.mk new file mode 100644 index 000000000..c83094e35 --- /dev/null +++ b/perf-tests/Android.mk @@ -0,0 +1,22 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests +#LOCAL_SDK_VERSION := current + +LOCAL_SRC_FILES := $(call all-java-files-under, src) \ + $(call all-java-files-under, ../tests/src/com/android/documentsui/bots) \ + ../tests/src/com/android/documentsui/ActivityTest.java \ + ../tests/src/com/android/documentsui/DocumentsProviderHelper.java \ + ../tests/src/com/android/documentsui/StubProvider.java + +LOCAL_JAVA_LIBRARIES := android.test.runner +LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 mockito-target ub-uiautomator + +LOCAL_PACKAGE_NAME := DocumentsUIPerfTests +LOCAL_INSTRUMENTATION_FOR := DocumentsUI + +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) + diff --git a/perf-tests/AndroidManifest.xml b/perf-tests/AndroidManifest.xml new file mode 100644 index 000000000..97353e7df --- /dev/null +++ b/perf-tests/AndroidManifest.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.documentsui.perftests"> + + <application> + <uses-library android:name="android.test.runner" /> + <provider + android:name="com.android.documentsui.StressProvider" + android:authorities="com.android.documentsui.stressprovider" + android:exported="true" + android:grantUriPermissions="true" + android:permission="android.permission.MANAGE_DOCUMENTS" + android:enabled="true"> + <intent-filter> + <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> + </intent-filter> + </provider> + </application> + + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.documentsui" + android:label="Performance tests for DocumentsUI" /> + +</manifest> diff --git a/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java b/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java new file mode 100644 index 000000000..8c39aac43 --- /dev/null +++ b/perf-tests/src/com/android/documentsui/FilesActivityPerfTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2016 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.android.documentsui; + +import static com.android.documentsui.StressProvider.DEFAULT_AUTHORITY; +import static com.android.documentsui.StressProvider.STRESS_ROOT_0_ID; +import static com.android.documentsui.StressProvider.STRESS_ROOT_1_ID; + +import android.app.Activity; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.test.suitebuilder.annotation.LargeTest; +import android.util.Log; +import android.view.KeyEvent; + +import com.android.documentsui.model.RootInfo; +import com.android.documentsui.EventListener; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.concurrent.CountDownLatch; + +@LargeTest +public class FilesActivityPerfTest extends ActivityTest<FilesActivity> { + + // Constants starting with KEY_ are used to report metrics to APCT. + private static final String KEY_FILES_LISTED_PERFORMANCE_FIRST = + "files-listed-performance-first"; + + private static final String KEY_FILES_LISTED_PERFORMANCE_MEDIAN = + "files-listed-performance-median"; + + private static final String TESTED_URI = + "content://com.android.documentsui.stressprovider/document/STRESS_ROOT_1_DOC"; + + private static final int NUM_MEASUREMENTS = 10; + + public FilesActivityPerfTest() { + super(FilesActivity.class); + } + + @Override + protected RootInfo getInitialRoot() { + return rootDir0; + } + + @Override + protected String getTestingProviderAuthority() { + return DEFAULT_AUTHORITY; + } + + @Override + protected void setupTestingRoots() throws RemoteException { + rootDir0 = mDocsHelper.getRoot(STRESS_ROOT_0_ID); + rootDir1 = mDocsHelper.getRoot(STRESS_ROOT_1_ID); + } + + @Override + public void initTestFiles() throws RemoteException { + // Nothing to create, already done by StressProvider. + } + + public void testFilesListedPerformance() throws Exception { + final BaseActivity activity = getActivity(); + + final List<Long> measurements = new ArrayList<Long>(); + CountDownLatch signal; + EventListener listener; + for (int i = 0; i < 10; i++) { + signal = new CountDownLatch(1); + listener = new EventListener() { + @Override + public void onDirectoryNavigated(Uri uri) { + if (uri != null && TESTED_URI.equals(uri.toString())) { + mStartTime = System.currentTimeMillis(); + } else { + mStartTime = -1; + } + } + + @Override + public void onDirectoryLoaded(Uri uri) { + if (uri == null || !TESTED_URI.equals(uri.toString())) { + return; + } + assertTrue(mStartTime != -1); + getInstrumentation().waitForIdle(new Runnable() { + @Override + public void run() { + assertTrue(mStartTime != -1); + measurements.add(System.currentTimeMillis() - mStartTime); + signal.countDown(); + } + }); + } + + private long mStartTime = -1; + }; + + try { + activity.addEventListener(listener); + bots.roots.openRoot(STRESS_ROOT_1_ID); + signal.await(); + } finally { + activity.removeEventListener(listener); + } + + assertEquals(i, measurements.size()); + + // Go back to the empty root. + bots.roots.openRoot(STRESS_ROOT_0_ID); + } + + assertEquals(NUM_MEASUREMENTS, measurements.size()); + + final Bundle status = new Bundle(); + status.putDouble(KEY_FILES_LISTED_PERFORMANCE_FIRST, measurements.get(0)); + + final Long[] rawMeasurements = measurements.toArray(new Long[NUM_MEASUREMENTS]); + Arrays.sort(rawMeasurements); + + final long median = rawMeasurements[NUM_MEASUREMENTS / 2 - 1]; + status.putDouble(KEY_FILES_LISTED_PERFORMANCE_MEDIAN, median); + + getInstrumentation().sendStatus(Activity.RESULT_OK, status); + } +} diff --git a/perf-tests/src/com/android/documentsui/StressProvider.java b/perf-tests/src/com/android/documentsui/StressProvider.java new file mode 100644 index 000000000..1bc802a00 --- /dev/null +++ b/perf-tests/src/com/android/documentsui/StressProvider.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2016 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.android.documentsui; + +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor.RowBuilder; +import android.database.MatrixCursor; +import android.os.CancellationSignal; +import android.os.FileUtils; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsContract; +import android.provider.DocumentsProvider; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Provider with thousands of files for testing loading time of directories in DocumentsUI. + * It doesn't support any file operations. + */ +public class StressProvider extends DocumentsProvider { + + public static final String DEFAULT_AUTHORITY = "com.android.documentsui.stressprovider"; + + // Empty root. + public static final String STRESS_ROOT_0_ID = "STRESS_ROOT_0"; + + // Root with thousands of items. + public static final String STRESS_ROOT_1_ID = "STRESS_ROOT_1"; + + private static final String STRESS_ROOT_0_DOC_ID = "STRESS_ROOT_0_DOC"; + private static final String STRESS_ROOT_1_DOC_ID = "STRESS_ROOT_1_DOC"; + + private static final String[] DEFAULT_ROOT_PROJECTION = new String[] { + Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES + }; + private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] { + Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE, + }; + + private String mAuthority = DEFAULT_AUTHORITY; + private ArrayList<String> mIds = new ArrayList<>(); + + @Override + public void attachInfo(Context context, ProviderInfo info) { + mAuthority = info.authority; + super.attachInfo(context, info); + } + + @Override + public boolean onCreate() { + mIds = new ArrayList(); + for (int i = 0; i < 10000; i++) { + mIds.add(createRandomId(i)); + } + mIds.add(STRESS_ROOT_0_DOC_ID); + mIds.add(STRESS_ROOT_1_DOC_ID); + return true; + } + + @Override + public Cursor queryRoots(String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(DEFAULT_ROOT_PROJECTION); + includeRoot(result, STRESS_ROOT_0_ID, STRESS_ROOT_0_DOC_ID); + includeRoot(result, STRESS_ROOT_1_ID, STRESS_ROOT_1_DOC_ID); + return result; + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION); + includeDocument(result, documentId); + return result; + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) + throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(DEFAULT_DOCUMENT_PROJECTION); + if (STRESS_ROOT_1_DOC_ID.equals(parentDocumentId)) { + for (String id : mIds) { + includeDocument(result, id); + } + } + return result; + } + + @Override + public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal) + throws FileNotFoundException { + throw new UnsupportedOperationException(); + } + + private void includeRoot(MatrixCursor result, String rootId, String docId) { + final RowBuilder row = result.newRow(); + row.add(Root.COLUMN_ROOT_ID, rootId); + row.add(Root.COLUMN_FLAGS, 0); + row.add(Root.COLUMN_TITLE, rootId); + row.add(Root.COLUMN_DOCUMENT_ID, docId); + } + + private void includeDocument(MatrixCursor result, String id) { + final RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, id); + row.add(Document.COLUMN_DISPLAY_NAME, id); + row.add(Document.COLUMN_SIZE, 0); + row.add(Document.COLUMN_MIME_TYPE, DocumentsContract.Document.MIME_TYPE_DIR); + row.add(Document.COLUMN_FLAGS, 0); + row.add(Document.COLUMN_LAST_MODIFIED, null); + } + + private static String getDocumentIdForFile(File file) { + return file.getAbsolutePath(); + } + + private String createRandomId(int index) { + final Random random = new Random(index); + final StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 20; i++) { + builder.append((char) (random.nextInt(96) + 32)); + } + builder.append(index); // Append a number to guarantee uniqueness. + return builder.toString(); + } +} diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index 46cbbdf81..699700bac 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -39,6 +39,7 @@ import android.provider.DocumentsContract.Root; import android.support.annotation.CallSuper; import android.support.annotation.LayoutRes; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.KeyEvent; import android.view.Menu; @@ -67,6 +68,7 @@ public abstract class BaseActivity extends Activity SearchViewManager mSearchManager; DrawerController mDrawer; NavigationView mNavigator; + List<EventListener> mEventListeners = new ArrayList<>(); private final String mTag; @@ -329,6 +331,8 @@ public abstract class BaseActivity extends Activity void openContainerDocument(DocumentInfo doc) { assert(doc.isContainer()); + notifyDirectoryNavigated(doc.derivedUri); + mState.pushDocument(doc); // Show an opening animation only if pressing "back" would get us back to the // previous directory. Especially after opening a root document, pressing @@ -594,6 +598,28 @@ public abstract class BaseActivity extends Activity return super.onKeyDown(keyCode, event); } + @VisibleForTesting + public void addEventListener(EventListener listener) { + mEventListeners.add(listener); + } + + @VisibleForTesting + public void removeEventListener(EventListener listener) { + mEventListeners.remove(listener); + } + + public void notifyDirectoryLoaded(Uri uri) { + for (EventListener listener : mEventListeners) { + listener.onDirectoryLoaded(uri); + } + } + + void notifyDirectoryNavigated(Uri uri) { + for (EventListener listener : mEventListeners) { + listener.onDirectoryNavigated(uri); + } + } + /** * Toggles focus between the navigation drawer and the directory listing. If the drawer isn't * locked, open/close it as appropriate. diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java index 13b7b1460..d2e918c91 100644 --- a/src/com/android/documentsui/DirectoryLoader.java +++ b/src/com/android/documentsui/DirectoryLoader.java @@ -59,7 +59,6 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { private CancellationSignal mSignal; private DirectoryResult mResult; - public DirectoryLoader(Context context, int type, RootInfo root, DocumentInfo doc, Uri uri, int userSortOrder, boolean inSearchMode) { super(context, ProviderExecutor.forAuthority(root.authority)); @@ -84,6 +83,7 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { final String authority = mUri.getAuthority(); final DirectoryResult result = new DirectoryResult(); + result.doc = mDoc; // Use default document when searching if (mSearchMode) { diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java index 22e438add..62686439c 100644 --- a/src/com/android/documentsui/DirectoryResult.java +++ b/src/com/android/documentsui/DirectoryResult.java @@ -22,12 +22,15 @@ import static com.android.documentsui.State.SORT_ORDER_UNKNOWN; import android.content.ContentProviderClient; import android.database.Cursor; +import com.android.documentsui.model.DocumentInfo; + import libcore.io.IoUtils; public class DirectoryResult implements AutoCloseable { ContentProviderClient client; public Cursor cursor; public Exception exception; + public DocumentInfo doc; public int sortOrder = SORT_ORDER_UNKNOWN; diff --git a/src/com/android/documentsui/EventListener.java b/src/com/android/documentsui/EventListener.java new file mode 100644 index 000000000..c15e9a641 --- /dev/null +++ b/src/com/android/documentsui/EventListener.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016 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.android.documentsui; + +import android.net.Uri; +import android.support.annotation.Nullable; + +public interface EventListener { + /** + * @param uri Uri navigated to. If recents, then null. + */ + void onDirectoryNavigated(@Nullable Uri uri); + + /** + * @param uri Uri of the loaded directory. If recents, then null. + */ + void onDirectoryLoaded(@Nullable Uri uri); +} diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index dfceff8d2..1348a58f4 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -1290,6 +1290,11 @@ public class DirectoryFragment extends Fragment showDirectory(); mAdapter.notifyDataSetChanged(); } + + if (!model.isLoading()) { + ((BaseActivity) getActivity()).notifyDirectoryLoaded( + model.doc != null ? model.doc.derivedUri : null); + } } @Override diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java index 5e55e1a8b..8170e2ab8 100644 --- a/src/com/android/documentsui/dirlist/Model.java +++ b/src/com/android/documentsui/dirlist/Model.java @@ -64,6 +64,7 @@ public class Model { @Nullable String info; @Nullable String error; + @Nullable DocumentInfo doc; /** * Generates a Model ID for a cursor entry that refers to a document. The Model ID is a unique @@ -111,6 +112,7 @@ public class Model { mPositions.clear(); info = null; error = null; + doc = null; mIsLoading = false; notifyUpdateListeners(); return; @@ -125,6 +127,7 @@ public class Model { mCursor = result.cursor; mCursorCount = mCursor.getCount(); mSortOrder = result.sortOrder; + doc = result.doc; updateModelData(); diff --git a/tests/src/com/android/documentsui/ActivityTest.java b/tests/src/com/android/documentsui/ActivityTest.java index 4b98aaf94..bad21886f 100644 --- a/tests/src/com/android/documentsui/ActivityTest.java +++ b/tests/src/com/android/documentsui/ActivityTest.java @@ -32,8 +32,11 @@ import android.support.test.uiautomator.Configurator; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObjectNotFoundException; import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; import android.view.MotionEvent; +import com.android.documentsui.BaseActivity; +import com.android.documentsui.EventListener; import com.android.documentsui.bots.DirectoryListBot; import com.android.documentsui.bots.KeyboardBot; import com.android.documentsui.bots.RootsListBot; @@ -64,7 +67,6 @@ public abstract class ActivityTest<T extends Activity> extends ActivityInstrumen public RootInfo rootDir0; public RootInfo rootDir1; - ContentResolver mResolver; DocumentsProviderHelper mDocsHelper; ContentProviderClient mClient; @@ -84,6 +86,23 @@ public abstract class ActivityTest<T extends Activity> extends ActivityInstrumen return rootDir0; } + /** + * Returns the authority of the testing provider begin used. + * By default it's StubProvider's authority. + * @return Authority of the provider. + */ + protected String getTestingProviderAuthority() { + return DEFAULT_AUTHORITY; + } + + /** + * Resolves testing roots. + */ + protected void setupTestingRoots() throws RemoteException { + rootDir0 = mDocsHelper.getRoot(ROOT_0_ID); + rootDir1 = mDocsHelper.getRoot(ROOT_1_ID); + } + @Override public void setUp() throws Exception { device = UiDevice.getInstance(getInstrumentation()); @@ -95,11 +114,8 @@ public abstract class ActivityTest<T extends Activity> extends ActivityInstrumen Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE); mResolver = context.getContentResolver(); - mClient = mResolver.acquireUnstableContentProviderClient(DEFAULT_AUTHORITY); - mDocsHelper = new DocumentsProviderHelper(DEFAULT_AUTHORITY, mClient); - - rootDir0 = mDocsHelper.getRoot(ROOT_0_ID); - rootDir1 = mDocsHelper.getRoot(ROOT_1_ID); + mClient = mResolver.acquireUnstableContentProviderClient(getTestingProviderAuthority()); + mDocsHelper = new DocumentsProviderHelper(getTestingProviderAuthority(), mClient); launchActivity(); resetStorage(); |