/* * Copyright (C) 2009 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.quicksearchbox.benchmarks; import android.app.Activity; import android.app.SearchManager; import android.app.SearchableInfo; import android.content.ComponentName; import android.content.ContentResolver; import android.database.ContentObserver; import android.database.Cursor; import android.database.DataSetObserver; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.util.Log; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public abstract class SourceLatency extends Activity { private static final String TAG = "SourceLatency"; private SearchManager mSearchManager; private ExecutorService mExecutorService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSearchManager = (SearchManager) getSystemService(SEARCH_SERVICE); mExecutorService = Executors.newSingleThreadExecutor(); } @Override protected void onResume() { super.onResume(); // TODO: call finish() when all tasks are done } private SearchableInfo getSearchable(ComponentName componentName) { SearchableInfo searchable = mSearchManager.getSearchableInfo(componentName); if (searchable == null || searchable.getSuggestAuthority() == null) { throw new RuntimeException("Component is not searchable: " + componentName.flattenToShortString()); } return searchable; } /** * Keeps track of timings in nanoseconds. */ private static class ElapsedTime { private long mTotal = 0; private int mCount = 0; public synchronized void addTime(long time) { mTotal += time; mCount++; } public synchronized long getTotal() { return mTotal; } public synchronized long getAverage() { return mTotal / mCount; } public synchronized int getCount() { return mCount; } } public void checkSourceConcurrent(final String src, final ComponentName componentName, String query, long delay) { final ElapsedTime time = new ElapsedTime(); final SearchableInfo searchable = getSearchable(componentName); int length = query.length(); for (int end = 0; end <= length; end++) { final String prefix = query.substring(0, end); (new Thread() { @Override public void run() { long t = checkSourceInternal(src, searchable, prefix); time.addTime(t); } }).start(); try { Thread.sleep(delay); } catch (InterruptedException ex) { Log.e(TAG, "sleep() in checkSourceConcurrent() interrupted."); } } int count = length + 1; // wait for all requests to finish while (time.getCount() < count) { try { Thread.sleep(1000); } catch (InterruptedException ex) { Log.e(TAG, "sleep() in checkSourceConcurrent() interrupted."); } } Log.d(TAG, src + "[DONE]: " + length + " queries in " + formatTime(time.getAverage()) + " (average), " + formatTime(time.getTotal()) + " (total)"); } public void checkSource(String src, ComponentName componentName, String[] queries) { ElapsedTime time = new ElapsedTime(); int count = queries.length; for (int i = 0; i < queries.length; i++) { long t = checkSource(src, componentName, queries[i]); time.addTime(t); } Log.d(TAG, src + "[DONE]: " + count + " queries in " + formatTime(time.getAverage()) + " (average), " + formatTime(time.getTotal()) + " (total)"); } public long checkSource(String src, ComponentName componentName, String query) { SearchableInfo searchable = getSearchable(componentName); return checkSourceInternal(src, searchable, query); } private long checkSourceInternal(String src, SearchableInfo searchable, String query) { Cursor cursor = null; try { final long start = System.nanoTime(); cursor = getSuggestions(searchable, query); long end = System.nanoTime(); long elapsed = end - start; if (cursor == null) { Log.d(TAG, src + ": null cursor in " + formatTime(elapsed) + " for '" + query + "'"); } else { Log.d(TAG, src + ": " + cursor.getCount() + " rows in " + formatTime(elapsed) + " for '" + query + "'"); } return elapsed; } finally { if (cursor != null) { cursor.close(); } } } public Cursor getSuggestions(SearchableInfo searchable, String query) { return getSuggestions(searchable, query, -1); } public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) { if (searchable == null) { return null; } String authority = searchable.getSuggestAuthority(); if (authority == null) { return null; } Uri.Builder uriBuilder = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(authority) .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() .fragment(""); // TODO: Remove, workaround for a bug in Uri.writeToParcel() // if content path provided, insert it now final String contentPath = searchable.getSuggestPath(); if (contentPath != null) { uriBuilder.appendEncodedPath(contentPath); } // append standard suggestion query path uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY); // get the query selection, may be null String selection = searchable.getSuggestSelection(); // inject query, either as selection args or inline String[] selArgs = null; if (selection != null) { // use selection if provided selArgs = new String[] { query }; } else { // no selection, use REST pattern uriBuilder.appendPath(query); } if (limit > 0) { uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT, String.valueOf(limit)); } Uri uri = uriBuilder.build(); // finally, make the query return getContentResolver().query(uri, null, selection, selArgs, null); } private static String formatTime(long ns) { return (ns / 1000000.0d) + " ms"; } public void checkLiveSource(String src, ComponentName componentName, String query) { mExecutorService.submit(new LiveSourceCheck(src, componentName, query)); } private class LiveSourceCheck implements Runnable { private String mSrc; private SearchableInfo mSearchable; private String mQuery; private Handler mHandler = new Handler(Looper.getMainLooper()); public LiveSourceCheck(String src, ComponentName componentName, String query) { mSrc = src; mSearchable = mSearchManager.getSearchableInfo(componentName); assert(mSearchable != null); assert(mSearchable.getSuggestAuthority() != null); mQuery = query; } public void run() { Cursor cursor = null; try { final long start = System.nanoTime(); cursor = getSuggestions(mSearchable, mQuery); long end = System.nanoTime(); long elapsed = (end - start); if (cursor == null) { Log.d(TAG, mSrc + ": null cursor in " + formatTime(elapsed) + " for '" + mQuery + "'"); } else { Log.d(TAG, mSrc + ": " + cursor.getCount() + " rows in " + formatTime(elapsed) + " for '" + mQuery + "'"); cursor.registerContentObserver(new ChangeObserver(cursor)); cursor.registerDataSetObserver(new MyDataSetObserver(mSrc, start, cursor)); try { Thread.sleep(2000); } catch (InterruptedException ex) { Log.d(TAG, mSrc + ": interrupted"); } } } finally { if (cursor != null) { cursor.close(); } } } private class ChangeObserver extends ContentObserver { private Cursor mCursor; public ChangeObserver(Cursor cursor) { super(mHandler); mCursor = cursor; } @Override public boolean deliverSelfNotifications() { return true; } @Override public void onChange(boolean selfChange) { mCursor.requery(); } } private class MyDataSetObserver extends DataSetObserver { private long mStart; private Cursor mCursor; private int mUpdateCount = 0; public MyDataSetObserver(String src, long start, Cursor cursor) { mSrc = src; mStart = start; mCursor = cursor; } @Override public void onChanged() { long end = System.nanoTime(); long elapsed = end - mStart; mUpdateCount++; Log.d(TAG, mSrc + ", update " + mUpdateCount + ": " + mCursor.getCount() + " rows in " + formatTime(elapsed)); } @Override public void onInvalidated() { Log.d(TAG, mSrc + ": invalidated"); } } } }