Dynamic Module: Separate creating of ClassLoader and module loading.

When loading a dynamic module from dex we need to create ClassLoader
in advance as part of warmup without loading a module.

See also: http://crrev.com/c/1479331

Bug: 924118
Change-Id: Ib7e7250eec8fe9c7f2ce27abe1c031f6dec8d270
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1506513
Commit-Queue: Anna Malova <amalova@chromium.org>
Reviewed-by: Peter Conn <peconn@chromium.org>
Reviewed-by: Michael van Ouwerkerk <mvanouwerkerk@chromium.org>
Cr-Commit-Position: refs/heads/master@{#638579}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java
index c1495a8..302f39f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java
@@ -49,12 +49,21 @@
 
     /** Specifies the module package name and entry point class name. */
     private final ComponentName mComponentName;
+    @Nullable
     private final String mDexAssetName;
     private final DexInputStreamProvider mDexInputStreamProvider;
     private final DexClassLoaderProvider mDexClassLoaderProvider;
     private final long mModuleLastUpdateTime;
     private final String mModuleId;
 
+    /** @param moduleContext The context for the package to load the class from. */
+    private Context mModuleContext;
+
+    @Nullable
+    private ClassLoader mClassLoader;
+    private boolean mIsClassLoaderCreating;
+    private boolean mNeedsToLoadModule;
+
     /**
      * Tracks the number of usages of the module. If it is no longer used, it may be destroyed, but
      * the time of destruction depends on the caching policy.
@@ -122,6 +131,9 @@
         }
         mModuleLastUpdateTime = lastUpdateTime;
         mModuleId = String.format("%s v%s (%s)", packageName, versionCode, versionName);
+
+        mModuleContext = createModuleContext(
+                mComponentName.getPackageName(), /* resourcesOnly = */ mDexAssetName != null);
     }
 
     public ComponentName getComponentName() {
@@ -137,6 +149,12 @@
      * If the module is not loaded yet, dynamically loads the module entry point class.
      */
     public void loadModule() {
+        if (mClassLoader == null) {
+            mNeedsToLoadModule = true;
+            if (!mIsClassLoaderCreating) createClassLoader();
+            return;
+        }
+
         if (mIsModuleLoading) return;
 
         // If module has been already loaded all callbacks must be notified synchronously.
@@ -146,9 +164,7 @@
             return;
         }
 
-        Context moduleContext = createModuleContext(
-                mComponentName.getPackageName(), /* resourcesOnly = */ mDexAssetName != null);
-        if (moduleContext == null) {
+        if (mModuleContext == null) {
             runAndClearCallbacks();
             return;
         }
@@ -156,9 +172,16 @@
         ModuleMetrics.registerLifecycleState(ModuleMetrics.LifecycleState.NOT_LOADED);
 
         mIsModuleLoading = true;
-        new LoadClassTask(moduleContext)
-                .executeWithTaskTraits(
-                        new TaskTraits().taskPriority(TaskPriority.USER_VISIBLE).mayBlock(true));
+        new LoadClassTask().executeWithTaskTraits(
+                new TaskTraits().taskPriority(TaskPriority.USER_VISIBLE).mayBlock(true));
+    }
+
+    public void createClassLoader() {
+        if (mClassLoader != null) return;
+
+        mIsClassLoaderCreating = true;
+        new ClassLoaderTask().executeWithTaskTraits(
+                new TaskTraits().taskPriority(TaskPriority.USER_VISIBLE).mayBlock(true));
     }
 
     /**
@@ -311,35 +334,19 @@
     }
 
     /**
-     * A task for loading the module entry point class on a background thread.
+     * A task for creating module {@link ClassLoader}.
      */
-    private class LoadClassTask extends AsyncTask<Class<?>> {
+    private class ClassLoaderTask extends AsyncTask<ClassLoader> {
         /** Buffer size to use while copying an input stream into the disk. */
         private static final int BUFFER_SIZE = 16 * 1024;
 
-        private final Context mModuleContext;
-
-        /**
-         * Constructs the task.
-         * @param moduleContext The context for the package to load the class from.
-         */
-        LoadClassTask(Context moduleContext) {
-            mModuleContext = moduleContext;
-        }
-
         @Override
         @Nullable
-        protected Class<?> doInBackground() {
+        protected ClassLoader doInBackground() {
             try {
                 boolean loadFromDex = updateModuleDexInDiskIfNeeded();
-                long entryPointLoadClassStartTime = ModuleMetrics.now();
-                Class<?> clazz =
-                        getModuleClassLoader(loadFromDex).loadClass(mComponentName.getClassName());
-                ModuleMetrics.recordLoadClassTime(entryPointLoadClassStartTime);
-                return clazz;
-            } catch (ClassNotFoundException e) {
-                Log.e(TAG, "Could not find class %s", mComponentName.getClassName(), e);
-                ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.CLASS_NOT_FOUND_EXCEPTION);
+                mClassLoader = getModuleClassLoader(loadFromDex);
+                return mClassLoader;
             } catch (IOException e) {
                 Log.e(TAG, "Could not copy dex to local storage", e);
                 ModuleMetrics.recordLoadResult(
@@ -348,6 +355,15 @@
             return null;
         }
 
+        @Override
+        protected void onPostExecute(ClassLoader classLoader) {
+            mIsClassLoaderCreating = false;
+            if (mNeedsToLoadModule) {
+                mNeedsToLoadModule = false;
+                loadModule();
+            }
+        }
+
         /**
          * Updates the local copy of the module dex file in disk if necessary.
          *
@@ -400,6 +416,27 @@
             }
             return mDexClassLoaderProvider.createClassLoader(getDexFile());
         }
+    }
+
+    /**
+     * A task for loading the module entry point class on a background thread.
+     */
+    private class LoadClassTask extends AsyncTask<Class<?>> {
+        @Override
+        @Nullable
+        protected Class<?> doInBackground() {
+            if (mClassLoader == null) return null;
+            try {
+                long entryPointLoadClassStartTime = ModuleMetrics.now();
+                Class<?> clazz = mClassLoader.loadClass(mComponentName.getClassName());
+                ModuleMetrics.recordLoadClassTime(entryPointLoadClassStartTime);
+                return clazz;
+            } catch (ClassNotFoundException e) {
+                Log.e(TAG, "Could not find class %s", mComponentName.getClassName(), e);
+                ModuleMetrics.recordLoadResult(ModuleMetrics.LoadResult.CLASS_NOT_FOUND_EXCEPTION);
+            }
+            return null;
+        }
 
         @Override
         protected void onPostExecute(@Nullable Class<?> clazz) {
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleLoaderTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleLoaderTest.java
index f0d90a8..7a6d65a 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleLoaderTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleLoaderTest.java
@@ -13,7 +13,7 @@
 import static org.chromium.base.ThreadUtils.runOnUiThreadBlocking;
 import static org.chromium.chrome.browser.customtabs.dynamicmodule.CustomTabsDynamicModuleTestUtils.FAKE_CLASS_LOADER_PROVIDER;
 import static org.chromium.chrome.browser.customtabs.dynamicmodule.CustomTabsDynamicModuleTestUtils.FAKE_MODULE_COMPONENT_NAME;
-import static org.chromium.chrome.browser.customtabs.dynamicmodule.CustomTabsDynamicModuleTestUtils.FAKE_MODULE_DEX_RESOURCE_ID;
+import static org.chromium.chrome.browser.customtabs.dynamicmodule.CustomTabsDynamicModuleTestUtils.FAKE_MODULE_DEX_ASSET_NAME;
 
 import android.support.test.filters.SmallTest;
 
@@ -54,12 +54,11 @@
         LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER);
         mDexInputStreamProvider = new FakeDexInputStreamProvider();
         mModuleLoaderFromApk = new ModuleLoader(FAKE_MODULE_COMPONENT_NAME,
-                /* dexAssetName = */ null,
-                mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
+                /* dexAssetName = */ null, mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
         mModuleLoaderFromDex = new ModuleLoader(FAKE_MODULE_COMPONENT_NAME,
-                FAKE_MODULE_DEX_RESOURCE_ID, mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
+                FAKE_MODULE_DEX_ASSET_NAME, mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
         mModuleLoaderFromDex2 = new ModuleLoader(FAKE_MODULE_COMPONENT_NAME,
-                FAKE_MODULE_DEX_RESOURCE_ID, mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
+                FAKE_MODULE_DEX_ASSET_NAME, mDexInputStreamProvider, FAKE_CLASS_LOADER_PROVIDER);
     }
 
     @After
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleTestUtils.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleTestUtils.java
index 9539de5..ff4bca4 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleTestUtils.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/dynamicmodule/CustomTabsDynamicModuleTestUtils.java
@@ -50,7 +50,7 @@
     /**
      * A asset name used to load {@link #FAKE_MODULE_DEX}.
      */
-    /* package */ final static String FAKE_MODULE_DEX_RESOURCE_ID = "42";
+    /* package */ final static String FAKE_MODULE_DEX_ASSET_NAME = "R.strings.forty_two";
 
     /**
      * A fake "dex file" that consists of couple of bytes.
@@ -308,7 +308,7 @@
 
         @Override
         public InputStream createInputStream(@Nullable String dexAssetName, Context moduleContext) {
-            if (!FAKE_MODULE_DEX_RESOURCE_ID.equals(dexAssetName)) {
+            if (!FAKE_MODULE_DEX_ASSET_NAME.equals(dexAssetName)) {
                 throw new RuntimeException("Unknown resource ID: " + dexAssetName);
             }