Merge "Add scaffolds for performance tests of DocumentsUI" into nyc-dev

This commit is contained in:
Tomasz Mikolajewski
2016-03-02 05:35:02 +00:00
committed by Android (Google) Code Review
11 changed files with 431 additions and 7 deletions

View File

@@ -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)

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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();