1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.quicksearchbox.benchmarks;
18
19import android.app.Activity;
20import android.app.SearchManager;
21import android.app.SearchableInfo;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.database.ContentObserver;
25import android.database.Cursor;
26import android.database.DataSetObserver;
27import android.net.Uri;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.Looper;
31import android.util.Log;
32
33import java.util.concurrent.ExecutorService;
34import java.util.concurrent.Executors;
35
36public abstract class SourceLatency extends Activity {
37
38    private static final String TAG = "SourceLatency";
39
40    private SearchManager mSearchManager;
41
42    private ExecutorService mExecutorService;
43
44    @Override
45    protected void onCreate(Bundle savedInstanceState) {
46        super.onCreate(savedInstanceState);
47
48        mSearchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
49        mExecutorService = Executors.newSingleThreadExecutor();
50    }
51
52    @Override
53    protected void onResume() {
54        super.onResume();
55
56        // TODO: call finish() when all tasks are done
57    }
58
59    private SearchableInfo getSearchable(ComponentName componentName) {
60        SearchableInfo searchable = mSearchManager.getSearchableInfo(componentName);
61        if (searchable == null || searchable.getSuggestAuthority() == null) {
62            throw new RuntimeException("Component is not searchable: "
63                    + componentName.flattenToShortString());
64        }
65        return searchable;
66    }
67
68    /**
69     * Keeps track of timings in nanoseconds.
70     */
71    private static class ElapsedTime {
72        private long mTotal = 0;
73        private int mCount = 0;
74        public synchronized void addTime(long time) {
75            mTotal += time;
76            mCount++;
77        }
78        public synchronized long getTotal() {
79            return mTotal;
80        }
81        public synchronized long getAverage() {
82            return mTotal / mCount;
83        }
84        public synchronized int getCount() {
85            return mCount;
86        }
87    }
88
89    public void checkSourceConcurrent(final String src, final ComponentName componentName,
90            String query, long delay) {
91        final ElapsedTime time = new ElapsedTime();
92        final SearchableInfo searchable = getSearchable(componentName);
93        int length = query.length();
94        for (int end = 0; end <= length; end++) {
95            final String prefix = query.substring(0, end);
96            (new Thread() {
97                @Override
98                public void run() {
99                    long t = checkSourceInternal(src, searchable, prefix);
100                    time.addTime(t);
101                }
102            }).start();
103            try {
104                Thread.sleep(delay);
105            } catch (InterruptedException ex) {
106                Log.e(TAG, "sleep() in checkSourceConcurrent() interrupted.");
107            }
108        }
109        int count = length + 1;
110        // wait for all requests to finish
111        while (time.getCount() < count) {
112            try {
113                Thread.sleep(1000);
114            } catch (InterruptedException ex) {
115                Log.e(TAG, "sleep() in checkSourceConcurrent() interrupted.");
116            }
117        }
118        Log.d(TAG, src + "[DONE]: " + length + " queries in " + formatTime(time.getAverage())
119                + " (average), " + formatTime(time.getTotal()) + " (total)");
120    }
121
122    public void checkSource(String src, ComponentName componentName, String[] queries) {
123        ElapsedTime time = new ElapsedTime();
124        int count = queries.length;
125        for (int i = 0; i < queries.length; i++) {
126            long t = checkSource(src, componentName, queries[i]);
127            time.addTime(t);
128        }
129        Log.d(TAG, src + "[DONE]: " + count + " queries in " + formatTime(time.getAverage())
130                + " (average), " + formatTime(time.getTotal()) + " (total)");
131    }
132
133    public long checkSource(String src, ComponentName componentName, String query) {
134        SearchableInfo searchable = getSearchable(componentName);
135        return checkSourceInternal(src, searchable, query);
136    }
137
138    private long checkSourceInternal(String src, SearchableInfo searchable, String query) {
139        Cursor cursor = null;
140        try {
141            final long start = System.nanoTime();
142            cursor = getSuggestions(searchable, query);
143            long end = System.nanoTime();
144            long elapsed = end - start;
145            if (cursor == null) {
146                Log.d(TAG, src + ": null cursor in " + formatTime(elapsed)
147                        + " for '" + query + "'");
148            } else {
149                Log.d(TAG, src + ": " + cursor.getCount() + " rows in " + formatTime(elapsed)
150                        + " for '" + query + "'");
151            }
152            return elapsed;
153        } finally {
154            if (cursor != null) {
155                cursor.close();
156            }
157        }
158    }
159
160    public Cursor getSuggestions(SearchableInfo searchable, String query) {
161        return getSuggestions(searchable, query, -1);
162    }
163
164    public Cursor getSuggestions(SearchableInfo searchable, String query, int limit) {
165        if (searchable == null) {
166            return null;
167        }
168
169        String authority = searchable.getSuggestAuthority();
170        if (authority == null) {
171            return null;
172        }
173
174        Uri.Builder uriBuilder = new Uri.Builder()
175                .scheme(ContentResolver.SCHEME_CONTENT)
176                .authority(authority)
177                .query("")  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
178                .fragment("");  // TODO: Remove, workaround for a bug in Uri.writeToParcel()
179
180        // if content path provided, insert it now
181        final String contentPath = searchable.getSuggestPath();
182        if (contentPath != null) {
183            uriBuilder.appendEncodedPath(contentPath);
184        }
185
186        // append standard suggestion query path
187        uriBuilder.appendPath(SearchManager.SUGGEST_URI_PATH_QUERY);
188
189        // get the query selection, may be null
190        String selection = searchable.getSuggestSelection();
191        // inject query, either as selection args or inline
192        String[] selArgs = null;
193        if (selection != null) {    // use selection if provided
194            selArgs = new String[] { query };
195        } else {                    // no selection, use REST pattern
196            uriBuilder.appendPath(query);
197        }
198
199        if (limit > 0) {
200            uriBuilder.appendQueryParameter(SearchManager.SUGGEST_PARAMETER_LIMIT,
201                    String.valueOf(limit));
202        }
203
204        Uri uri = uriBuilder.build();
205
206        // finally, make the query
207        return getContentResolver().query(uri, null, selection, selArgs, null);
208    }
209
210    private static String formatTime(long ns) {
211        return (ns / 1000000.0d) + " ms";
212    }
213
214    public void checkLiveSource(String src, ComponentName componentName, String query) {
215        mExecutorService.submit(new LiveSourceCheck(src, componentName, query));
216    }
217
218    private class LiveSourceCheck implements Runnable {
219
220        private String mSrc;
221        private SearchableInfo mSearchable;
222        private String mQuery;
223        private Handler mHandler = new Handler(Looper.getMainLooper());
224
225        public LiveSourceCheck(String src, ComponentName componentName, String query) {
226            mSrc = src;
227            mSearchable = mSearchManager.getSearchableInfo(componentName);
228            assert(mSearchable != null);
229            assert(mSearchable.getSuggestAuthority() != null);
230            mQuery = query;
231        }
232
233        public void run() {
234            Cursor cursor = null;
235            try {
236                final long start = System.nanoTime();
237                cursor = getSuggestions(mSearchable, mQuery);
238                long end = System.nanoTime();
239                long elapsed = (end - start);
240                if (cursor == null) {
241                    Log.d(TAG, mSrc + ": null cursor in " + formatTime(elapsed)
242                            + " for '" + mQuery + "'");
243                } else {
244                    Log.d(TAG, mSrc + ": " + cursor.getCount() + " rows in " + formatTime(elapsed)
245                            + " for '" + mQuery + "'");
246                    cursor.registerContentObserver(new ChangeObserver(cursor));
247                    cursor.registerDataSetObserver(new MyDataSetObserver(mSrc, start, cursor));
248                    try {
249                        Thread.sleep(2000);
250                    } catch (InterruptedException ex) {
251                        Log.d(TAG, mSrc + ": interrupted");
252                    }
253                }
254            } finally {
255                if (cursor != null) {
256                    cursor.close();
257                }
258            }
259        }
260
261        private class ChangeObserver extends ContentObserver {
262            private Cursor mCursor;
263
264            public ChangeObserver(Cursor cursor) {
265                super(mHandler);
266                mCursor = cursor;
267            }
268
269            @Override
270            public boolean deliverSelfNotifications() {
271                return true;
272            }
273
274            @Override
275            public void onChange(boolean selfChange) {
276                mCursor.requery();
277            }
278        }
279
280        private class MyDataSetObserver extends DataSetObserver {
281            private long mStart;
282            private Cursor mCursor;
283            private int mUpdateCount = 0;
284
285            public MyDataSetObserver(String src, long start, Cursor cursor) {
286                mSrc = src;
287                mStart = start;
288                mCursor = cursor;
289            }
290
291            @Override
292            public void onChanged() {
293                long end = System.nanoTime();
294                long elapsed = end - mStart;
295                mUpdateCount++;
296                Log.d(TAG, mSrc + ", update " + mUpdateCount + ": " + mCursor.getCount()
297                        + " rows in " + formatTime(elapsed));
298            }
299
300            @Override
301            public void onInvalidated() {
302                Log.d(TAG, mSrc + ": invalidated");
303            }
304        }
305    }
306
307
308}
309