Improve settings cold start latency.
- Move view initialization from onViewCreated to onCreateView. This doesn't really improve anything, it simply aligns the code more with view's lifecycle management. - Move dashboard category init into background. The init contains logic invoking packageManager, which can be very expensive. - Remove any call to DashboardFeatureProvider from SummaryLoader, and delay the getCategory call until someone calls setListener(). - call updateCategory() from background thread. Test: rerun app launch test. Avg latency drops back to pre-suggestion-v2 level. Test: robotest Fixes: 68761512 Change-Id: I5ec85af08e7b610786e439bda93b3651f5975593
This commit is contained in:
@@ -24,6 +24,7 @@ import android.os.Bundle;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.service.settings.suggestions.Suggestion;
|
import android.service.settings.suggestions.Suggestion;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
|
import android.support.annotation.WorkerThread;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -50,6 +51,7 @@ import com.android.settingslib.drawer.SettingsDrawerActivity.CategoryListener;
|
|||||||
import com.android.settingslib.drawer.Tile;
|
import com.android.settingslib.drawer.Tile;
|
||||||
import com.android.settingslib.suggestions.SuggestionList;
|
import com.android.settingslib.suggestions.SuggestionList;
|
||||||
import com.android.settingslib.suggestions.SuggestionParser;
|
import com.android.settingslib.suggestions.SuggestionParser;
|
||||||
|
import com.android.settingslib.utils.ThreadUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -181,12 +183,6 @@ public class DashboardSummary extends InstrumentedFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
return inflater.inflate(R.layout.dashboard, container, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
@@ -198,9 +194,10 @@ public class DashboardSummary extends InstrumentedFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle bundle) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
mDashboard = view.findViewById(R.id.dashboard_container);
|
final View root = inflater.inflate(R.layout.dashboard, container, false);
|
||||||
|
mDashboard = root.findViewById(R.id.dashboard_container);
|
||||||
mLayoutManager = new LinearLayoutManager(getContext());
|
mLayoutManager = new LinearLayoutManager(getContext());
|
||||||
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||||
if (bundle != null) {
|
if (bundle != null) {
|
||||||
@@ -218,19 +215,19 @@ public class DashboardSummary extends InstrumentedFragment
|
|||||||
mSummaryLoader.setSummaryConsumer(mAdapter);
|
mSummaryLoader.setSummaryConsumer(mAdapter);
|
||||||
ActionBarShadowController.attachToRecyclerView(
|
ActionBarShadowController.attachToRecyclerView(
|
||||||
getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
|
getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
|
||||||
|
rebuildUI();
|
||||||
if (DEBUG_TIMING) {
|
if (DEBUG_TIMING) {
|
||||||
Log.d(TAG, "onViewCreated took "
|
Log.d(TAG, "onCreateView took "
|
||||||
+ (System.currentTimeMillis() - startTime) + " ms");
|
+ (System.currentTimeMillis() - startTime) + " ms");
|
||||||
}
|
}
|
||||||
rebuildUI();
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
void rebuildUI() {
|
void rebuildUI() {
|
||||||
if (!mSuggestionFeatureProvider.isSuggestionEnabled(getContext())) {
|
if (!mSuggestionFeatureProvider.isSuggestionEnabled(getContext())) {
|
||||||
Log.d(TAG, "Suggestion v1 feature is disabled, skipping suggestion v1");
|
Log.d(TAG, "Suggestion v1 feature is disabled, skipping suggestion v1");
|
||||||
updateCategory();
|
ThreadUtils.postOnBackgroundThread(() -> updateCategory());
|
||||||
} else {
|
} else {
|
||||||
new SuggestionLoader().execute();
|
new SuggestionLoader().execute();
|
||||||
// Set categories on their own if loading suggestions takes too long.
|
// Set categories on their own if loading suggestions takes too long.
|
||||||
@@ -340,11 +337,12 @@ public class DashboardSummary extends InstrumentedFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
void updateCategory() {
|
void updateCategory() {
|
||||||
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
|
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
|
||||||
CategoryKey.CATEGORY_HOMEPAGE);
|
CategoryKey.CATEGORY_HOMEPAGE);
|
||||||
mSummaryLoader.updateSummaryToCache(category);
|
mSummaryLoader.updateSummaryToCache(category);
|
||||||
mAdapter.setCategory(category);
|
ThreadUtils.postOnMainThread(() -> mAdapter.setCategory(category));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -60,23 +60,6 @@ public class SummaryLoader {
|
|||||||
private boolean mWorkerListening;
|
private boolean mWorkerListening;
|
||||||
private ArraySet<BroadcastReceiver> mReceivers = new ArraySet<>();
|
private ArraySet<BroadcastReceiver> mReceivers = new ArraySet<>();
|
||||||
|
|
||||||
public SummaryLoader(Activity activity, List<DashboardCategory> categories) {
|
|
||||||
mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
|
|
||||||
.getDashboardFeatureProvider(activity);
|
|
||||||
mCategoryKey = null;
|
|
||||||
mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
|
|
||||||
mWorkerThread.start();
|
|
||||||
mWorker = new Worker(mWorkerThread.getLooper());
|
|
||||||
mActivity = activity;
|
|
||||||
for (int i = 0; i < categories.size(); i++) {
|
|
||||||
List<Tile> tiles = categories.get(i).tiles;
|
|
||||||
for (int j = 0; j < tiles.size(); j++) {
|
|
||||||
Tile tile = tiles.get(j);
|
|
||||||
mWorker.obtainMessage(Worker.MSG_GET_PROVIDER, tile).sendToTarget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SummaryLoader(Activity activity, String categoryKey) {
|
public SummaryLoader(Activity activity, String categoryKey) {
|
||||||
mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
|
mDashboardFeatureProvider = FeatureFactory.getFactory(activity)
|
||||||
.getDashboardFeatureProvider(activity);
|
.getDashboardFeatureProvider(activity);
|
||||||
@@ -85,17 +68,6 @@ public class SummaryLoader {
|
|||||||
mWorkerThread.start();
|
mWorkerThread.start();
|
||||||
mWorker = new Worker(mWorkerThread.getLooper());
|
mWorker = new Worker(mWorkerThread.getLooper());
|
||||||
mActivity = activity;
|
mActivity = activity;
|
||||||
|
|
||||||
final DashboardCategory category =
|
|
||||||
mDashboardFeatureProvider.getTilesForCategory(categoryKey);
|
|
||||||
if (category == null || category.tiles == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Tile> tiles = category.tiles;
|
|
||||||
for (Tile tile : tiles) {
|
|
||||||
mWorker.obtainMessage(Worker.MSG_GET_PROVIDER, tile).sendToTarget();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void release() {
|
public void release() {
|
||||||
@@ -153,15 +125,32 @@ public class SummaryLoader {
|
|||||||
* Only call from the main thread.
|
* Only call from the main thread.
|
||||||
*/
|
*/
|
||||||
public void setListening(boolean listening) {
|
public void setListening(boolean listening) {
|
||||||
if (mListening == listening) return;
|
if (mListening == listening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mListening = listening;
|
mListening = listening;
|
||||||
// Unregister listeners immediately.
|
// Unregister listeners immediately.
|
||||||
for (int i = 0; i < mReceivers.size(); i++) {
|
for (int i = 0; i < mReceivers.size(); i++) {
|
||||||
mActivity.unregisterReceiver(mReceivers.valueAt(i));
|
mActivity.unregisterReceiver(mReceivers.valueAt(i));
|
||||||
}
|
}
|
||||||
mReceivers.clear();
|
mReceivers.clear();
|
||||||
|
|
||||||
mWorker.removeMessages(Worker.MSG_SET_LISTENING);
|
mWorker.removeMessages(Worker.MSG_SET_LISTENING);
|
||||||
mWorker.obtainMessage(Worker.MSG_SET_LISTENING, listening ? 1 : 0, 0).sendToTarget();
|
if (!listening) {
|
||||||
|
// Stop listen
|
||||||
|
mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 0 /* listening */).sendToTarget();
|
||||||
|
} else {
|
||||||
|
// Start listen
|
||||||
|
if (mSummaryProviderMap.isEmpty()) {
|
||||||
|
// Category not initialized yet, init before starting to listen
|
||||||
|
if (!mWorker.hasMessages(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING)) {
|
||||||
|
mWorker.sendEmptyMessage(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Category already initialized, start listening immediately
|
||||||
|
mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 1 /* listening */).sendToTarget();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SummaryProvider getSummaryProvider(Tile tile) {
|
private SummaryProvider getSummaryProvider(Tile tile) {
|
||||||
@@ -236,9 +225,13 @@ public class SummaryLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void setListeningW(boolean listening) {
|
private synchronized void setListeningW(boolean listening) {
|
||||||
if (mWorkerListening == listening) return;
|
if (mWorkerListening == listening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mWorkerListening = listening;
|
mWorkerListening = listening;
|
||||||
if (DEBUG) Log.d(TAG, "Listening " + listening);
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "Listening " + listening);
|
||||||
|
}
|
||||||
for (SummaryProvider p : mSummaryProviderMap.keySet()) {
|
for (SummaryProvider p : mSummaryProviderMap.keySet()) {
|
||||||
try {
|
try {
|
||||||
p.setListening(listening);
|
p.setListening(listening);
|
||||||
@@ -271,7 +264,6 @@ public class SummaryLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public interface SummaryProvider {
|
public interface SummaryProvider {
|
||||||
void setListening(boolean listening);
|
void setListening(boolean listening);
|
||||||
}
|
}
|
||||||
@@ -285,8 +277,9 @@ public class SummaryLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class Worker extends Handler {
|
private class Worker extends Handler {
|
||||||
private static final int MSG_GET_PROVIDER = 1;
|
private static final int MSG_GET_CATEGORY_TILES_AND_SET_LISTENING = 1;
|
||||||
private static final int MSG_SET_LISTENING = 2;
|
private static final int MSG_GET_PROVIDER = 2;
|
||||||
|
private static final int MSG_SET_LISTENING = 3;
|
||||||
|
|
||||||
public Worker(Looper looper) {
|
public Worker(Looper looper) {
|
||||||
super(looper);
|
super(looper);
|
||||||
@@ -295,6 +288,18 @@ public class SummaryLoader {
|
|||||||
@Override
|
@Override
|
||||||
public void handleMessage(Message msg) {
|
public void handleMessage(Message msg) {
|
||||||
switch (msg.what) {
|
switch (msg.what) {
|
||||||
|
case MSG_GET_CATEGORY_TILES_AND_SET_LISTENING:
|
||||||
|
final DashboardCategory category =
|
||||||
|
mDashboardFeatureProvider.getTilesForCategory(mCategoryKey);
|
||||||
|
if (category == null || category.tiles == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final List<Tile> tiles = category.tiles;
|
||||||
|
for (Tile tile : tiles) {
|
||||||
|
makeProviderW(tile);
|
||||||
|
}
|
||||||
|
setListeningW(true);
|
||||||
|
break;
|
||||||
case MSG_GET_PROVIDER:
|
case MSG_GET_PROVIDER:
|
||||||
Tile tile = (Tile) msg.obj;
|
Tile tile = (Tile) msg.obj;
|
||||||
makeProviderW(tile);
|
makeProviderW(tile);
|
||||||
|
@@ -16,6 +16,10 @@
|
|||||||
|
|
||||||
package com.android.settings.dashboard;
|
package com.android.settings.dashboard;
|
||||||
|
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -23,6 +27,7 @@ import android.content.Intent;
|
|||||||
import com.android.settings.TestConfig;
|
import com.android.settings.TestConfig;
|
||||||
import com.android.settings.testutils.FakeFeatureFactory;
|
import com.android.settings.testutils.FakeFeatureFactory;
|
||||||
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||||
|
import com.android.settingslib.drawer.CategoryKey;
|
||||||
import com.android.settingslib.drawer.DashboardCategory;
|
import com.android.settingslib.drawer.DashboardCategory;
|
||||||
import com.android.settingslib.drawer.Tile;
|
import com.android.settingslib.drawer.Tile;
|
||||||
|
|
||||||
@@ -35,12 +40,6 @@ import org.mockito.MockitoAnnotations;
|
|||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@RunWith(SettingsRobolectricTestRunner.class)
|
@RunWith(SettingsRobolectricTestRunner.class)
|
||||||
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
|
||||||
public class SummaryLoaderTest {
|
public class SummaryLoaderTest {
|
||||||
@@ -65,14 +64,14 @@ public class SummaryLoaderTest {
|
|||||||
mCallbackInvoked = false;
|
mCallbackInvoked = false;
|
||||||
|
|
||||||
final Activity activity = Robolectric.buildActivity(Activity.class).get();
|
final Activity activity = Robolectric.buildActivity(Activity.class).get();
|
||||||
final List<DashboardCategory> categories = new ArrayList<>();
|
|
||||||
mSummaryLoader = new SummaryLoader(activity, categories);
|
mSummaryLoader = new SummaryLoader(activity, CategoryKey.CATEGORY_HOMEPAGE);
|
||||||
mSummaryLoader.setSummaryConsumer(new SummaryLoader.SummaryConsumer() {
|
mSummaryLoader.setSummaryConsumer(tile -> mCallbackInvoked = true);
|
||||||
@Override
|
|
||||||
public void notifySummaryChanged(Tile tile) {
|
|
||||||
mCallbackInvoked = true;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
@Test
|
||||||
|
public void newInstance_shouldNotLoadCategory() {
|
||||||
|
verifyZeroInteractions(mFeatureFactory.dashboardFeatureProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
Reference in New Issue
Block a user