Commit d893d645 authored by Anna Malova's avatar Anna Malova Committed by Commit Bot

Separate CCT dynamic module loading and incrementation of its usage counter.

Remove the WeakReference in the LoadModuleCallback.
Add check to not create the second loading task if dynamic module if it is currently loading.

Bug: 853793
Change-Id: Ie02a7dd63e3f9e8a21e48d18affb3c25d90ab0ed
Reviewed-on: https://chromium-review.googlesource.com/c/1286661Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Reviewed-by: default avatarMichael van Ouwerkerk <mvanouwerkerk@chromium.org>
Commit-Queue: Anna Malova <amalova@chromium.org>
Cr-Commit-Position: refs/heads/master@{#601584}
parent 5e7fc52e
...@@ -72,6 +72,7 @@ import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsM ...@@ -72,6 +72,7 @@ import org.chromium.chrome.browser.contextual_suggestions.ContextualSuggestionsM
import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityDelegate; import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityDelegate;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityHostImpl; import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityHostImpl;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleEntryPoint; import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleEntryPoint;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleLoader;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleMetrics; import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleMetrics;
import org.chromium.chrome.browser.dependency_injection.ChromeActivityCommonsModule; import org.chromium.chrome.browser.dependency_injection.ChromeActivityCommonsModule;
import org.chromium.chrome.browser.dependency_injection.CustomTabActivityComponent; import org.chromium.chrome.browser.dependency_injection.CustomTabActivityComponent;
...@@ -117,7 +118,6 @@ import org.chromium.ui.base.PageTransition; ...@@ -117,7 +118,6 @@ import org.chromium.ui.base.PageTransition;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
...@@ -198,11 +198,12 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent ...@@ -198,11 +198,12 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
private ModuleEntryPoint mModuleEntryPoint; private ModuleEntryPoint mModuleEntryPoint;
@Nullable @Nullable
private ActivityDelegate mModuleActivityDelegate; private ActivityDelegate mModuleActivityDelegate;
@Nullable
private Runnable mLoadModuleCancelRunnable;
private boolean mModuleOnStartPending; private boolean mModuleOnStartPending;
private boolean mModuleOnResumePending; private boolean mModuleOnResumePending;
private boolean mHasSetOverlayView; private boolean mHasSetOverlayView;
@Nullable
private LoadModuleCallback mModuleCallback;
private ActivityTabTaskDescriptionHelper mTaskDescriptionHelper; private ActivityTabTaskDescriptionHelper mTaskDescriptionHelper;
...@@ -353,49 +354,40 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent ...@@ -353,49 +354,40 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
return; return;
} }
mLoadModuleCancelRunnable = ModuleLoader moduleLoader = mConnection.getModuleLoader(componentName);
mConnection.getModuleLoader(componentName).loadModule(new LoadModuleCallback(this)); moduleLoader.loadModule();
mModuleCallback = new LoadModuleCallback();
moduleLoader.addCallbackAndIncrementUseCount(mModuleCallback);
} }
private boolean isModuleLoading() { private boolean isModuleLoading() {
return mLoadModuleCancelRunnable != null; return mModuleCallback != null;
}
private static class LoadModuleCallback implements Callback<ModuleEntryPoint> {
private final WeakReference<CustomTabActivity> mActivity;
LoadModuleCallback(CustomTabActivity activity) {
mActivity = new WeakReference<>(activity);
}
@Override
public void onResult(@Nullable ModuleEntryPoint entryPoint) {
CustomTabActivity activity = mActivity.get();
if (activity == null || activity.isActivityDestroyed()) return;
activity.onModuleLoaded(entryPoint);
}
} }
/** /**
* Receives the entry point if it was loaded successfully, or null if there was a problem. This * Callback to receive the entry point if it was loaded successfully,
* is always called on the UI thread. * or null if there was a problem. This is always called on the UI thread.
*/ */
private void onModuleLoaded(@Nullable ModuleEntryPoint entryPoint) { private class LoadModuleCallback implements Callback<ModuleEntryPoint> {
mLoadModuleCancelRunnable = null; @Override
if (entryPoint == null) return; public void onResult(@Nullable ModuleEntryPoint entryPoint) {
if (entryPoint == null) return;
mModuleEntryPoint = entryPoint; mModuleEntryPoint = entryPoint;
long createActivityDelegateStartTime = ModuleMetrics.now(); long createActivityDelegateStartTime = ModuleMetrics.now();
mModuleActivityDelegate = entryPoint.createActivityDelegate(new ActivityHostImpl(this)); mModuleActivityDelegate = entryPoint.createActivityDelegate(
ModuleMetrics.recordCreateActivityDelegateTime(createActivityDelegateStartTime); new ActivityHostImpl(CustomTabActivity.this));
mModuleActivityDelegate.onCreate(getSavedInstanceState()); ModuleMetrics.recordCreateActivityDelegateTime(createActivityDelegateStartTime);
mModuleActivityDelegate.onCreate(getSavedInstanceState());
if (mModuleOnStartPending) startModule(); if (mModuleOnStartPending) startModule();
if (mModuleOnResumePending) resumeModule(); if (mModuleOnResumePending) resumeModule();
mConnection.setActivityDelegateForSession(mSession, mModuleActivityDelegate, mConnection.setActivityDelegateForSession(mSession, mModuleActivityDelegate,
mModuleEntryPoint.getModuleVersion()); mModuleEntryPoint.getModuleVersion());
mModuleCallback = null;
}
} }
private void startModule() { private void startModule() {
...@@ -997,10 +989,6 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent ...@@ -997,10 +989,6 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
@Override @Override
protected void onDestroyInternal() { protected void onDestroyInternal() {
super.onDestroyInternal(); super.onDestroyInternal();
if (mLoadModuleCancelRunnable != null) {
mLoadModuleCancelRunnable.run();
mLoadModuleCancelRunnable = null;
}
if (mModuleActivityDelegate != null) { if (mModuleActivityDelegate != null) {
mModuleActivityDelegate.onDestroy(); mModuleActivityDelegate.onDestroy();
mModuleActivityDelegate = null; mModuleActivityDelegate = null;
...@@ -1011,7 +999,8 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent ...@@ -1011,7 +999,8 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
moduleComponentName = mIntentDataProvider.getModuleComponentName(); moduleComponentName = mIntentDataProvider.getModuleComponentName();
} }
if (moduleComponentName != null) { if (moduleComponentName != null) {
mConnection.getModuleLoader(moduleComponentName).decrementModuleUseCount(); mConnection.getModuleLoader(moduleComponentName)
.removeCallbackAndDecrementUseCount(mModuleCallback);
} }
if (mIncognitoTabHost != null) { if (mIncognitoTabHost != null) {
IncognitoTabHostRegistry.getInstance().unregister(mIncognitoTabHost); IncognitoTabHostRegistry.getInstance().unregister(mIncognitoTabHost);
......
...@@ -16,6 +16,8 @@ import android.support.annotation.Nullable; ...@@ -16,6 +16,8 @@ import android.support.annotation.Nullable;
import org.chromium.base.Callback; import org.chromium.base.Callback;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.ChromeApplication;
import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeFeatureList;
...@@ -39,6 +41,10 @@ public class ModuleLoader { ...@@ -39,6 +41,10 @@ public class ModuleLoader {
*/ */
private int mModuleUseCount; private int mModuleUseCount;
private boolean mIsModuleLoading;
private final ObserverList<Callback<ModuleEntryPoint>> mCallbacks = new ObserverList<>();
/** /**
* The timestamp of the moment the module became unused. This is used to determine whether or * The timestamp of the moment the module became unused. This is used to determine whether or
* not to continue caching it. A value of -1 indicates there is no usable value. * not to continue caching it. A value of -1 indicates there is no usable value.
...@@ -86,46 +92,53 @@ public class ModuleLoader { ...@@ -86,46 +92,53 @@ public class ModuleLoader {
} }
/** /**
* If the module is not loaded yet, dynamically loads the module entry point class. If * If the module is not loaded yet, dynamically loads the module entry point class.
* successful, the callback will receive a {@link ModuleEntryPoint} asynchronously. If the */
* module fails to load, the callback will receive null. If the module was already loaded and a public void loadModule() {
* reference to it is still held, the callback will synchronously receive a if (mIsModuleLoading) return;
* {@link ModuleEntryPoint}.
// If module has been already loaded all callbacks must be notified synchronously.
// {@see #addCallbackAndIncrementUseCount}
if (mModuleEntryPoint != null) {
assert mCallbacks.isEmpty();
return;
}
Context moduleContext = getModuleContext(mComponentName.getPackageName());
if (moduleContext == null) {
runAndClearCallbacks();
return;
}
mIsModuleLoading = true;
new LoadClassTask(moduleContext).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Register a callback to receive a {@link ModuleEntryPoint} asynchronously.
* If the module fails to load, the callback will receive null.
* If the module was already loaded and a reference to it is still held,
* the callback will synchronously receive a {@link ModuleEntryPoint}.
*
* Module use count is incremented when a callback notified.
* *
* @param callback The callback to receive the result. * @param callback The callback to receive the result.
* @return If the module is not loaded yet, an {@link AsyncTask} will be used to load it and
* a {@link Runnable} returned to the caller for cancelling the task if necessary. If the
* module was already loaded, null is returned.
*/ */
@Nullable public void addCallbackAndIncrementUseCount(Callback<ModuleEntryPoint> callback) {
public Runnable loadModule(Callback<ModuleEntryPoint> callback) {
if (mModuleEntryPoint != null) { if (mModuleEntryPoint != null) {
mModuleUseCount++; mModuleUseCount++;
mModuleUnusedTimeMs = -1; mModuleUnusedTimeMs = -1;
ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.SUCCESS_CACHED); ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.SUCCESS_CACHED);
callback.onResult(mModuleEntryPoint); callback.onResult(mModuleEntryPoint);
return null; return;
}
Context moduleContext = getModuleContext(mComponentName.getPackageName());
if (moduleContext == null) {
callback.onResult(null);
return null;
} }
mCallbacks.addObserver(callback);
// TODO(crbug.com/864520): Define and return a CancellablePromise instead.
final LoadClassTask task = new LoadClassTask(moduleContext, callback);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return new Runnable() {
@Override
public void run() {
task.cancel(false);
}
};
} }
public void decrementModuleUseCount() { public void removeCallbackAndDecrementUseCount(Callback<ModuleEntryPoint> callback) {
if (mModuleEntryPoint == null) return; boolean isPendingCallback = mCallbacks.removeObserver(callback);
if (mModuleEntryPoint == null || isPendingCallback) return;
mModuleUseCount--; mModuleUseCount--;
if (mModuleUseCount == 0) { if (mModuleUseCount == 0) {
mModuleUnusedTimeMs = ModuleMetrics.now(); mModuleUnusedTimeMs = ModuleMetrics.now();
...@@ -172,22 +185,35 @@ public class ModuleLoader { ...@@ -172,22 +185,35 @@ public class ModuleLoader {
mModuleUnusedTimeMs = -1; mModuleUnusedTimeMs = -1;
} }
/**
* Notify all callbacks which are waiting for module loading. Each callback is needed to notify
* only once therefore all callbacks are cleared after call.
*/
private void runAndClearCallbacks() {
assert !mIsModuleLoading;
if (mModuleEntryPoint != null && mCallbacks.size() > 0) {
mModuleUseCount += mCallbacks.size();
mModuleUnusedTimeMs = -1;
}
for (Callback<ModuleEntryPoint> callback: mCallbacks) {
callback.onResult(mModuleEntryPoint);
}
mCallbacks.clear();
}
/** /**
* A task for loading the module entry point class on a background thread. * A task for loading the module entry point class on a background thread.
*/ */
private class LoadClassTask extends AsyncTask<Class<?>> { private class LoadClassTask extends AsyncTask<Class<?>> {
private final Context mModuleContext; private final Context mModuleContext;
private final Callback<ModuleEntryPoint> mCallback;
/** /**
* Constructs the task. * Constructs the task.
* @param moduleContext The context for the package to load the class from. * @param moduleContext The context for the package to load the class from.
* @param callback The callback to receive the result of the task. If there was a problem
* the result will be null.
*/ */
LoadClassTask(Context moduleContext, Callback<ModuleEntryPoint> callback) { LoadClassTask(Context moduleContext) {
mModuleContext = moduleContext; mModuleContext = moduleContext;
mCallback = callback;
} }
@Override @Override
...@@ -219,8 +245,9 @@ public class ModuleLoader { ...@@ -219,8 +245,9 @@ public class ModuleLoader {
@Override @Override
protected void onPostExecute(@Nullable Class<?> clazz) { protected void onPostExecute(@Nullable Class<?> clazz) {
mIsModuleLoading = false;
if (clazz == null) { if (clazz == null) {
mCallback.onResult(null); runAndClearCallbacks();
return; return;
} }
...@@ -242,7 +269,7 @@ public class ModuleLoader { ...@@ -242,7 +269,7 @@ public class ModuleLoader {
moduleHost.getHostVersion(), entryPoint.getMinimumHostVersion(), moduleHost.getHostVersion(), entryPoint.getMinimumHostVersion(),
entryPoint.getModuleVersion(), moduleHost.getMinimumModuleVersion()); entryPoint.getModuleVersion(), moduleHost.getMinimumModuleVersion());
ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.INCOMPATIBLE_VERSION); ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.INCOMPATIBLE_VERSION);
mCallback.onResult(null); runAndClearCallbacks();
return; return;
} }
...@@ -253,9 +280,8 @@ public class ModuleLoader { ...@@ -253,9 +280,8 @@ public class ModuleLoader {
entryPoint.init(moduleHost); entryPoint.init(moduleHost);
ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.SUCCESS_NEW); ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.SUCCESS_NEW);
mModuleEntryPoint = entryPoint; mModuleEntryPoint = entryPoint;
mModuleUseCount = 1; mModuleUnusedTimeMs = ModuleMetrics.now();
mModuleUnusedTimeMs = -1; runAndClearCallbacks();
mCallback.onResult(entryPoint);
return; return;
} catch (Exception e) { } catch (Exception e) {
// No multi-catch below API level 19 for reflection exceptions. // No multi-catch below API level 19 for reflection exceptions.
...@@ -263,7 +289,7 @@ public class ModuleLoader { ...@@ -263,7 +289,7 @@ public class ModuleLoader {
Log.e(TAG, "Could not instantiate class %s", mComponentName.getClassName(), e); Log.e(TAG, "Could not instantiate class %s", mComponentName.getClassName(), e);
ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.INSTANTIATION_EXCEPTION); ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.INSTANTIATION_EXCEPTION);
} }
mCallback.onResult(null); runAndClearCallbacks();
} }
} }
...@@ -289,4 +315,9 @@ public class ModuleLoader { ...@@ -289,4 +315,9 @@ public class ModuleLoader {
return entryPoint.getModuleVersion() >= moduleHost.getMinimumModuleVersion() return entryPoint.getModuleVersion() >= moduleHost.getMinimumModuleVersion()
&& moduleHost.getHostVersion() >= entryPoint.getMinimumHostVersion(); && moduleHost.getHostVersion() >= entryPoint.getMinimumHostVersion();
} }
@VisibleForTesting
public int getModuleUseCount() {
return mModuleUseCount;
}
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
package org.chromium.chrome.browser.customtabs; package org.chromium.chrome.browser.customtabs;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import android.content.ComponentName; import android.content.ComponentName;
...@@ -11,18 +12,26 @@ import android.support.test.InstrumentationRegistry; ...@@ -11,18 +12,26 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest; import android.support.test.filters.SmallTest;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.chromium.base.Callback;
import org.chromium.base.library_loader.LibraryLoader; import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType; import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.test.util.CallbackHelper; import org.chromium.base.test.util.CallbackHelper;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.customtabs.dynamicmodule.BaseModuleEntryPoint; import org.chromium.chrome.browser.customtabs.dynamicmodule.BaseModuleEntryPoint;
import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityDelegate; import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityDelegate;
import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityHost; import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityHost;
import org.chromium.chrome.browser.customtabs.dynamicmodule.IModuleHost; import org.chromium.chrome.browser.customtabs.dynamicmodule.IModuleHost;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleEntryPoint;
import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleLoader;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner; import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.Features;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
/** /**
...@@ -35,20 +44,78 @@ public class CustomTabsDynamicModuleTest { ...@@ -35,20 +44,78 @@ public class CustomTabsDynamicModuleTest {
InstrumentationRegistry.getInstrumentation().getContext().getPackageName(), InstrumentationRegistry.getInstrumentation().getContext().getPackageName(),
FakeCCTDynamicModule.class.getName()); FakeCCTDynamicModule.class.getName());
@Rule
public Features.JUnitProcessor mProcessor = new Features.JUnitProcessor();
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER); LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER);
} }
/**
* Test fake dynamic module is correctly loaded.
*/
@Test @Test
@SmallTest @SmallTest
public void testModuleLoading() throws TimeoutException, InterruptedException { public void testModuleLoading() throws TimeoutException, InterruptedException {
CallbackHelper loadingWaiter = new CallbackHelper(); ModuleLoader moduleLoader = new ModuleLoader(mModuleComponentName);
mConnection.getModuleLoader(mModuleComponentName).loadModule(result -> { moduleLoader.loadModule();
assertNotNull(result);
loadingWaiter.notifyCalled(); CallbackHelper onLoaded = new CallbackHelper();
}); moduleLoader.addCallbackAndIncrementUseCount(
loadingWaiter.waitForCallback(); result -> {
assertNotNull(result);
onLoaded.notifyCalled();
});
onLoaded.waitForCallback();
}
/**
* Test the ModuleLoader correctly update module usage counter.
*/
@Test
@SmallTest
@Features.EnableFeatures(ChromeFeatureList.CCT_MODULE_CACHE)
public void testModuleUseCounter() throws TimeoutException, InterruptedException {
final int callbacksNumber = 3;
ModuleLoader moduleLoader = new ModuleLoader(mModuleComponentName);
CallbackHelper onLoaded = new CallbackHelper();
// Test we correctly unregister callbacks which were never notified.
List<Callback<ModuleEntryPoint>> unusedCallbacks = new ArrayList<>();
for (int i = 0; i < callbacksNumber; i++) {
unusedCallbacks.add(result -> {});
moduleLoader.addCallbackAndIncrementUseCount(unusedCallbacks.get(i));
}
// module has not been loaded, therefore there is no usage
assertEquals(0, moduleLoader.getModuleUseCount());
// unregister all callbacks so they should not increment module usage
for (int i = 0; i < callbacksNumber; i++) {
moduleLoader.removeCallbackAndDecrementUseCount(unusedCallbacks.get(i));
}
assertEquals(0, moduleLoader.getModuleUseCount());
moduleLoader.loadModule();
List<Callback<ModuleEntryPoint>> callbacks = new ArrayList<>();
// register callbacks and wait for the notification when module is loaded
for (int i = 0; i < callbacksNumber; i++) {
callbacks.add(result -> onLoaded.notifyCalled());
moduleLoader.addCallbackAndIncrementUseCount(callbacks.get(i));
}
onLoaded.waitForCallback(0, callbacksNumber);
assertEquals(callbacksNumber, moduleLoader.getModuleUseCount());
// unregister already notified callbacks
for (int i = 0; i < callbacksNumber; i++) {
moduleLoader.removeCallbackAndDecrementUseCount(callbacks.get(i));
}
assertEquals(0, moduleLoader.getModuleUseCount());
} }
/** /**
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment