Bug 1339160 - 1. Allow GeckoThread to launch without being initialized; r=snorp
authorJim Chen <nchen@mozilla.com>
Wed, 15 Feb 2017 17:12:56 -0500
changeset 343199 c80d17b6ea4897766816f0f3ac9b005ce551cbd3
parent 343198 e0daea02c01fda3ec2a6ebbfc1fc11c8843c53c3
child 343200 26359cfd0d245637ff7efc288098d07f1652d417
push id31372
push usercbook@mozilla.com
push dateThu, 16 Feb 2017 12:16:10 +0000
treeherdermozilla-central@2737f66ad6ac [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1339160
milestone54.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1339160 - 1. Allow GeckoThread to launch without being initialized; r=snorp When GeckoThread is launched without being initialized, it will load all Gecko libs and then wait until it is initialized, before calling the Gecko entry point. This allows us to preload Gecko libs without actually running Gecko.
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoService.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1215,18 +1215,18 @@ public abstract class GeckoApp
             mIsRestoringActivity = true;
             Telemetry.addToHistogram("FENNEC_RESTORING_ACTIVITY", 1);
 
         } else {
             final String action = intent.getAction();
             final String args = intent.getStringExtra("args");
 
             sAlreadyLoaded = true;
-            GeckoThread.init(/* profile */ null, args, action,
-                             /* debugging */ ACTION_DEBUG.equals(action));
+            GeckoThread.initMainProcess(/* profile */ null, args,
+                                        /* debugging */ ACTION_DEBUG.equals(action));
 
             // Speculatively pre-fetch the profile in the background.
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     getProfile();
                 }
             });
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
@@ -145,17 +145,18 @@ public class GeckoService extends Servic
 
         final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
         final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
 
         if (profileName == null) {
             throw new IllegalArgumentException("Intent must specify profile.");
         }
 
-        if (!GeckoThread.initWithProfile(profileName, profileDir != null ? new File(profileDir) : null)) {
+        if (!GeckoThread.initMainProcessWithProfile(
+                profileName, profileDir != null ? new File(profileDir) : null)) {
             Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
                           profileName + " [" + profileDir + ']');
 
             final GeckoProfile profile = GeckoThread.getActiveProfile();
             if (profile != null) {
                 Log.w(LOGTAG, "Current profile is " + profile.getName() +
                               " [" + profile.getDir().getAbsolutePath() + ']');
             }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java
@@ -78,17 +78,16 @@ public final class GeckoProfile {
 
     private static final ConcurrentHashMap<String, GeckoProfile> sProfileCache =
             new ConcurrentHashMap<String, GeckoProfile>(
                     /* capacity */ 4, /* load factor */ 0.75f, /* concurrency */ 2);
     private static String sDefaultProfileName;
 
     private final String mName;
     private final File mMozillaDir;
-    private final Context mApplicationContext;
 
     private Object mData;
 
     /**
      * Access to this member should be synchronized to avoid
      * races during creation -- particularly between getDir and GeckoView#init.
      *
      * Not final because this is lazily computed.
@@ -315,17 +314,16 @@ public final class GeckoProfile {
 
     private GeckoProfile(Context context, String profileName, File profileDir) throws NoMozillaDirectoryException {
         if (profileName == null) {
             throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
         } else if (CUSTOM_PROFILE.equals(profileName) && profileDir == null) {
             throw new IllegalArgumentException("Custom profile must have a directory");
         }
 
-        mApplicationContext = context.getApplicationContext();
         mName = profileName;
         mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
 
         mProfileDir = profileDir;
         if (profileDir != null && !profileDir.isDirectory()) {
             throw new IllegalArgumentException("Profile directory must exist if specified.");
         }
     }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -120,62 +120,74 @@ public class GeckoThread extends Thread 
             ThreadUtils.assertOnUiThread();
             long nextDelay = runUiThreadCallback();
             if (nextDelay >= 0) {
                 ThreadUtils.getUiHandler().postDelayed(this, nextDelay);
             }
         }
     };
 
-    private static GeckoThread sGeckoThread;
+    private static final GeckoThread INSTANCE = new GeckoThread();
 
     @WrapForJNI
     private static final ClassLoader clsLoader = GeckoThread.class.getClassLoader();
     @WrapForJNI
     private static MessageQueue msgQueue;
 
-    private GeckoProfile mProfile;
+    private boolean mInitialized;
+    private String[] mArgs;
 
-    private final String mArgs;
-    private final String mAction;
-    private final boolean mDebugging;
+    // Main process parameters
+    private GeckoProfile mProfile;
+    private String mExtraArgs;
+    private boolean mDebugging;
 
-    private String[] mChildProcessArgs;
+    // Child process parameters
     private int mCrashFileDescriptor;
     private int mIPCFileDescriptor;
 
-    GeckoThread(GeckoProfile profile, String args, String action, boolean debugging) {
-        mProfile = profile;
-        mArgs = args;
-        mAction = action;
-        mDebugging = debugging;
-        mChildProcessArgs = null;
-        mCrashFileDescriptor = -1;
-        mIPCFileDescriptor = -1;
-
+    GeckoThread() {
         setName("Gecko");
     }
 
-    public static boolean init(GeckoProfile profile, String args, String action, boolean debugging) {
-        ThreadUtils.assertOnUiThread();
-        if (isState(State.INITIAL) && sGeckoThread == null) {
-            sGeckoThread = new GeckoThread(profile, args, action, debugging);
-            return true;
-        }
-        return false;
+    private boolean isChildProcess() {
+        return mIPCFileDescriptor != -1;
     }
 
-    public static boolean initChildProcess(GeckoProfile profile, String[] args, int crashFd, int ipcFd, boolean debugging) {
-        if (init(profile, null, null, debugging)) {
-            sGeckoThread.mChildProcessArgs = args;
-            sGeckoThread.mCrashFileDescriptor = crashFd;
-            sGeckoThread.mIPCFileDescriptor = ipcFd;
-            return true;
+    private synchronized boolean init(final GeckoProfile profile, final String[] args,
+                                      final String extraArgs, final boolean debugging,
+                                      final int crashFd, final int ipcFd) {
+        ThreadUtils.assertOnUiThread();
+
+        if (mInitialized) {
+            return false;
         }
-        return false;
+
+        mProfile = profile;
+        mArgs = args;
+        mExtraArgs = extraArgs;
+        mDebugging = debugging;
+        mCrashFileDescriptor = crashFd;
+        mIPCFileDescriptor = ipcFd;
+
+        mInitialized = true;
+        notifyAll();
+        return true;
+    }
+
+    public static boolean initMainProcess(final GeckoProfile profile, final String extraArgs,
+                                          final boolean debugging) {
+        return INSTANCE.init(profile, /* args */ null, extraArgs, debugging,
+                                 /* crashFd */ -1, /* ipcFd */ -1);
+    }
+
+    public static boolean initChildProcess(final String[] args, final int crashFd,
+                                           final int ipcFd) {
+        return INSTANCE.init(/* profile */ null, args, /* extraArgs */ null,
+                                 /* debugging */ false, crashFd, ipcFd);
     }
 
     private static boolean canUseProfile(final Context context, final GeckoProfile profile,
                                          final String profileName, final File profileDir) {
         if (profileDir != null && !profileDir.isDirectory()) {
             return false;
         }
 
@@ -198,17 +210,18 @@ public class GeckoThread extends Thread 
     public static boolean canUseProfile(final String profileName, final File profileDir) {
         if (profileName == null) {
             throw new IllegalArgumentException("Null profile name");
         }
         return canUseProfile(GeckoAppShell.getApplicationContext(), getActiveProfile(),
                              profileName, profileDir);
     }
 
-    public static boolean initWithProfile(final String profileName, final File profileDir) {
+    public static boolean initMainProcessWithProfile(final String profileName,
+                                                     final File profileDir) {
         if (profileName == null) {
             throw new IllegalArgumentException("Null profile name");
         }
 
         final Context context = GeckoAppShell.getApplicationContext();
         final GeckoProfile profile = getActiveProfile();
 
         if (!canUseProfile(context, profile, profileName, profileDir)) {
@@ -217,24 +230,25 @@ public class GeckoThread extends Thread 
         }
 
         if (profile != null) {
             // We already have a compatible profile.
             return true;
         }
 
         // We haven't initialized yet; okay to initialize now.
-        return init(GeckoProfile.get(context, profileName, profileDir),
-                    /* args */ null, /* action */ null, /* debugging */ false);
+        return initMainProcess(GeckoProfile.get(context, profileName, profileDir),
+                               /* args */ null, /* debugging */ false);
     }
 
     public static boolean launch() {
         ThreadUtils.assertOnUiThread();
+
         if (checkAndSetState(State.INITIAL, State.LAUNCHED)) {
-            sGeckoThread.start();
+            INSTANCE.start();
             return true;
         }
         return false;
     }
 
     public static boolean isLaunched() {
         return !isState(State.INITIAL);
     }
@@ -392,17 +406,17 @@ public class GeckoThread extends Thread 
     }
 
     private static void loadGeckoLibs(final Context context, final String resourcePath) {
         GeckoLoader.loadSQLiteLibs(context, resourcePath);
         GeckoLoader.loadNSSLibs(context, resourcePath);
         GeckoLoader.loadGeckoLibs(context, resourcePath);
     }
 
-    private static String initGeckoEnvironment() {
+    private static void initGeckoEnvironment() {
         final Context context = GeckoAppShell.getApplicationContext();
         GeckoLoader.loadMozGlue(context);
         setState(State.MOZGLUE_READY);
 
         final Locale locale = Locale.getDefault();
         final Resources res = context.getResources();
         if (locale.toString().equalsIgnoreCase("zh_hk")) {
             final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
@@ -432,88 +446,81 @@ public class GeckoThread extends Thread 
                               new FileUtils.FilenameRegexFilter(".*\\.so(?:\\.crc)?$"),
                               /* recurse */ true);
 
             // Then try loading again. If this throws again, we actually crash.
             loadGeckoLibs(context, resourcePath);
         }
 
         setState(State.LIBS_READY);
-        return resourcePath;
     }
 
-    private void addCustomProfileArg(String args, ArrayList<String> list) {
-        // Make sure a profile exists.
-        final GeckoProfile profile = getProfile();
-        profile.getDir(); // call the lazy initializer
+    private String[] getMainProcessArgs() {
+        final Context context = GeckoAppShell.getApplicationContext();
+        final ArrayList<String> args = new ArrayList<String>();
 
-        boolean needsProfile = true;
+        // argv[0] is the program name, which for us is the package name.
+        args.add(context.getPackageName());
+        args.add("-greomni");
+        args.add(context.getPackageResourcePath());
 
-        if (args != null) {
-            StringTokenizer st = new StringTokenizer(args);
-            while (st.hasMoreTokens()) {
-                String token = st.nextToken();
-                if ("-P".equals(token) || "-profile".equals(token)) {
-                    needsProfile = false;
-                }
-                list.add(token);
-            }
+        final GeckoProfile profile = getProfile();
+        if (profile.isCustomProfile()) {
+            args.add("-profile");
+            args.add(profile.getDir().getAbsolutePath());
+        } else {
+            profile.getDir(); // Make sure the profile dir exists.
+            args.add("-P");
+            args.add(profile.getName());
         }
 
-        // If args don't include the profile, make sure it's included.
-        if (args == null || needsProfile) {
-            if (profile.isCustomProfile()) {
-                list.add("-profile");
-                list.add(profile.getDir().getAbsolutePath());
-            } else {
-                list.add("-P");
-                list.add(profile.getName());
+        if (mExtraArgs != null) {
+            final StringTokenizer st = new StringTokenizer(mExtraArgs);
+            while (st.hasMoreTokens()) {
+                final String token = st.nextToken();
+                if ("-P".equals(token) || "-profile".equals(token)) {
+                    // Skip -P and -profile arguments because we added them above.
+                    if (st.hasMoreTokens()) {
+                        st.nextToken();
+                    }
+                    continue;
+                }
+                args.add(token);
             }
         }
-    }
-
-    private String[] getGeckoArgs(final String apkPath) {
-        // argv[0] is the program name, which for us is the package name.
-        final Context context = GeckoAppShell.getApplicationContext();
-        final ArrayList<String> args = new ArrayList<String>();
-        args.add(context.getPackageName());
-        args.add("-greomni");
-        args.add(apkPath);
-
-        addCustomProfileArg(mArgs, args);
 
         // In un-official builds, we want to load Javascript resources fresh
         // with each build.  In official builds, the startup cache is purged by
         // the buildid mechanism, but most un-official builds don't bump the
         // buildid, so we purge here instead.
         final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
         if (gi == null || !gi.isOfficial()) {
             Log.w(LOGTAG, "STARTUP PERFORMANCE WARNING: un-official build: purging the " +
                           "startup (JavaScript) caches.");
             args.add("-purgecaches");
         }
 
         return args.toArray(new String[args.size()]);
     }
 
     public static GeckoProfile getActiveProfile() {
-        if (sGeckoThread == null) {
-            return null;
-        }
-        final GeckoProfile profile = sGeckoThread.mProfile;
-        if (profile != null) {
-            return profile;
-        }
-        return sGeckoThread.getProfile();
+        return INSTANCE.getProfile();
     }
 
     public synchronized GeckoProfile getProfile() {
+        if (!mInitialized) {
+            return null;
+        }
+        if (isChildProcess()) {
+            throw new UnsupportedOperationException(
+                    "Cannot access profile from child process");
+        }
         if (mProfile == null) {
             final Context context = GeckoAppShell.getApplicationContext();
-            mProfile = GeckoProfile.initFromArgs(context, mArgs);
+            mProfile = GeckoProfile.initFromArgs(context, mExtraArgs);
         }
         return mProfile;
     }
 
     @Override
     public void run() {
         Log.i(LOGTAG, "preparing to run Gecko");
 
@@ -531,44 +538,50 @@ public class GeckoThread extends Thread 
                 idleMsg.obj = geckoHandler;
                 geckoHandler.sendMessageAtFrontOfQueue(idleMsg);
                 // Keep this IdleHandler
                 return true;
             }
         };
         Looper.myQueue().addIdleHandler(idleHandler);
 
-        if (mDebugging) {
-            try {
-                Thread.sleep(5 * 1000 /* 5 seconds */);
-            } catch (final InterruptedException e) {
-            }
-        }
-
-        final String[] args;
-        if (mChildProcessArgs != null) {
-            initGeckoEnvironment();
-            args = mChildProcessArgs;
-        } else {
-            args = getGeckoArgs(initGeckoEnvironment());
-        }
+        initGeckoEnvironment();
 
         // This can only happen after the call to initGeckoEnvironment
         // above, because otherwise the JNI code hasn't been loaded yet.
         ThreadUtils.postToUiThread(new Runnable() {
             @Override public void run() {
                 registerUiThread();
             }
         });
 
+        // Wait until initialization before calling Gecko entry point.
+        synchronized (this) {
+            while (!mInitialized) {
+                try {
+                    wait();
+                } catch (final InterruptedException e) {
+                }
+            }
+        }
+
+        final String[] args = isChildProcess() ? mArgs : getMainProcessArgs();
+
+        if (mDebugging) {
+            try {
+                Thread.sleep(5 * 1000 /* 5 seconds */);
+            } catch (final InterruptedException e) {
+            }
+        }
+
         Log.w(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - runGecko");
 
         final GeckoAppShell.GeckoInterface gi = GeckoAppShell.getGeckoInterface();
         if (gi == null || !gi.isOfficial()) {
-            Log.i(LOGTAG, "RunGecko - args = " + args);
+            Log.i(LOGTAG, "RunGecko - args = " + TextUtils.join(" ", args));
         }
 
         // And go.
         GeckoLoader.nativeRun(args, mCrashFileDescriptor, mIPCFileDescriptor);
 
         // And... we're done.
         setState(State.EXITED);
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
@@ -69,17 +69,17 @@ public class GeckoServiceChildProcess ex
             serviceStarted = true;
             final int crashReporterFd = crashReporterPfd != null ? crashReporterPfd.detachFd() : -1;
             final int ipcFd = ipcPfd != null ? ipcPfd.detachFd() : -1;
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
                     GeckoAppShell.ensureCrashHandling();
                     GeckoAppShell.setApplicationContext(getApplicationContext());
-                    if (GeckoThread.initChildProcess(null, args, crashReporterFd, ipcFd, false)) {
+                    if (GeckoThread.initChildProcess(args, crashReporterFd, ipcFd)) {
                         GeckoThread.launch();
                     }
                 }
             });
         }
     };
 
     public IBinder onBind(final Intent intent) {