diff --git a/shell/common/switches.cc b/shell/common/switches.cc index 553e77361c6bc..a2726fea343e8 100644 --- a/shell/common/switches.cc +++ b/shell/common/switches.cc @@ -404,6 +404,12 @@ Settings SettingsFromCommandLine(const fml::CommandLine& command_line) { settings.purge_persistent_cache = command_line.HasOption(FlagForSwitch(Switch::PurgePersistentCache)); + if (command_line.HasOption(FlagForSwitch(Switch::OldGenHeapSize))) { + std::string old_gen_heap_size; + command_line.GetOptionValue(FlagForSwitch(Switch::OldGenHeapSize), + &old_gen_heap_size); + settings.old_gen_heap_size = std::stoi(old_gen_heap_size); + } return settings; } diff --git a/shell/common/switches.h b/shell/common/switches.h index 9a9dea9401d8f..6f6e2a34b8f1c 100644 --- a/shell/common/switches.h +++ b/shell/common/switches.h @@ -199,6 +199,9 @@ DEF_SWITCH( "Uses separate threads for the platform, UI, GPU and IO task runners. " "By default, a single thread is used for all task runners. Only available " "in the flutter_tester.") +DEF_SWITCH(OldGenHeapSize, + "old-gen-heap-size", + "The size limit in megabytes for the Dart VM old gen heap space.") DEF_SWITCHES_END diff --git a/shell/platform/android/io/flutter/FlutterInjector.java b/shell/platform/android/io/flutter/FlutterInjector.java index 7d944785c4f45..0fe80e058e17a 100644 --- a/shell/platform/android/io/flutter/FlutterInjector.java +++ b/shell/platform/android/io/flutter/FlutterInjector.java @@ -62,23 +62,12 @@ public static void reset() { instance = null; } - private FlutterInjector(boolean shouldLoadNative, @NonNull FlutterLoader flutterLoader) { - this.shouldLoadNative = shouldLoadNative; + private FlutterInjector(@NonNull FlutterLoader flutterLoader) { this.flutterLoader = flutterLoader; } - private boolean shouldLoadNative; private FlutterLoader flutterLoader; - /** - * Returns whether the Flutter Android engine embedding should load the native C++ engine. - * - *

Useful for testing since JVM tests via Robolectric can't load native libraries. - */ - public boolean shouldLoadNative() { - return shouldLoadNative; - } - /** Returns the {@link FlutterLoader} instance to use for the Flutter Android engine embedding. */ @NonNull public FlutterLoader flutterLoader() { @@ -92,20 +81,6 @@ public FlutterLoader flutterLoader() { *

Non-overriden values have reasonable defaults. */ public static final class Builder { - - private boolean shouldLoadNative = true; - /** - * Sets whether the Flutter Android engine embedding should load the native C++ engine. - * - *

Useful for testing since JVM tests via Robolectric can't load native libraries. - * - *

Defaults to true. - */ - public Builder setShouldLoadNative(boolean shouldLoadNative) { - this.shouldLoadNative = shouldLoadNative; - return this; - } - private FlutterLoader flutterLoader; /** * Sets a {@link FlutterLoader} override. @@ -130,8 +105,7 @@ private void fillDefaults() { public FlutterInjector build() { fillDefaults(); - System.out.println("should load native is " + shouldLoadNative); - return new FlutterInjector(shouldLoadNative, flutterLoader); + return new FlutterInjector(flutterLoader); } } } diff --git a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java index 5e9864c345bdc..604bb0ebc4e2d 100644 --- a/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java +++ b/shell/platform/android/io/flutter/embedding/engine/FlutterJNI.java @@ -97,6 +97,50 @@ public class FlutterJNI { private static final String TAG = "FlutterJNI"; + // BEGIN Methods related to loading for FlutterLoader. + /** + * Loads the libflutter.so C++ library. + * + *

This must be called before any other native methods, and can be overridden by tests to avoid + * loading native libraries. + */ + public void loadLibrary() { + System.loadLibrary("flutter"); + } + + /** + * Prefetch the default font manager provided by SkFontMgr::RefDefault() which is a process-wide + * singleton owned by Skia. Note that, the first call to SkFontMgr::RefDefault() will take + * noticeable time, but later calls will return a reference to the preexisting font manager. + */ + public void prefetchDefaultFontManager() { + FlutterJNI.nativePrefetchDefaultFontManager(); + } + + /** + * Perform one time initialization of the Dart VM and Flutter engine. + * + *

This method must be called only once. + * + * @param context The application context. + * @param args Arguments to the Dart VM/Flutter engine. + * @param bundlePath For JIT runtimes, the path to the Dart kernel file for the application. + * @param appStoragePath The path to the application data directory. + * @param engineCachesPath The path to the application cache directory. + * @param initTimeMillis The time, in milliseconds, taken for initialization. + */ + public void init( + @NonNull Context context, + @NonNull String[] args, + @Nullable String bundlePath, + @NonNull String appStoragePath, + @NonNull String engineCachesPath, + long initTimeMillis) { + FlutterJNI.nativeInit( + context, args, bundlePath, appStoragePath, engineCachesPath, initTimeMillis); + } + // END methods related to FlutterLoader + @Nullable private static AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate; // This should also be updated by FlutterView when it is attached to a Display. // The initial value of 0.0 indicates unknown refresh rate. @@ -105,7 +149,8 @@ public class FlutterJNI { // This is set from native code via JNI. @Nullable private static String observatoryUri; - // TODO(mattcarroll): add javadocs + /** @deprecated Use {@link #init(Context, String[], String, String, String, long)} instead. */ + @Deprecated public static native void nativeInit( @NonNull Context context, @NonNull String[] args, @@ -114,11 +159,8 @@ public static native void nativeInit( @NonNull String engineCachesPath, long initTimeMillis); - /** - * Prefetch the default font manager provided by SkFontMgr::RefDefault() which is a process-wide - * singleton owned by Skia. Note that, the first call to SkFontMgr::RefDefault() will take - * noticeable time, but later calls will return a reference to the preexisting font manager. - */ + /** @deprecated Use {@link #prefetchDefaultFontManager()} instead. */ + @Deprecated public static native void nativePrefetchDefaultFontManager(); private native boolean nativeGetIsSoftwareRenderingEnabled(); diff --git a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java index e2d97c40ee174..95e96d3473e05 100644 --- a/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java +++ b/shell/platform/android/io/flutter/embedding/engine/loader/FlutterLoader.java @@ -4,9 +4,12 @@ package io.flutter.embedding.engine.loader; +import android.app.ActivityManager; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -14,7 +17,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import io.flutter.BuildConfig; -import io.flutter.FlutterInjector; import io.flutter.Log; import io.flutter.embedding.engine.FlutterJNI; import io.flutter.util.PathUtils; @@ -29,6 +31,9 @@ public class FlutterLoader { private static final String TAG = "FlutterLoader"; + private static final String OLD_GEN_HEAP_SIZE_META_DATA_KEY = + "io.flutter.embedding.android.OldGenHeapSize"; + // Must match values in flutter::switches static final String AOT_SHARED_LIBRARY_NAME = "aot-shared-library-name"; static final String SNAPSHOT_ASSET_PATH_KEY = "snapshot-asset-path"; @@ -60,10 +65,26 @@ public static FlutterLoader getInstance() { return instance; } + /** Creates a {@code FlutterLoader} that uses a default constructed {@link FlutterJNI}. */ + public FlutterLoader() { + this(new FlutterJNI()); + } + + /** + * Creates a {@code FlutterLoader} with the specified {@link FlutterJNI}. + * + * @param flutterJNI The {@link FlutterJNI} instance to use for loading the libflutter.so C++ + * library, setting up the font manager, and calling into C++ initalization. + */ + public FlutterLoader(@NonNull FlutterJNI flutterJNI) { + this.flutterJNI = flutterJNI; + } + private boolean initialized = false; @Nullable private Settings settings; private long initStartTimestampMillis; private FlutterApplicationInfo flutterApplicationInfo; + private FlutterJNI flutterJNI; private static class InitResult { final String appStoragePath; @@ -125,9 +146,7 @@ public void startInitialization(@NonNull Context applicationContext, @NonNull Se public InitResult call() { ResourceExtractor resourceExtractor = initResources(appContext); - if (FlutterInjector.instance().shouldLoadNative()) { - System.loadLibrary("flutter"); - } + flutterJNI.loadLibrary(); // Prefetch the default font manager as soon as possible on a background thread. // It helps to reduce time cost of engine setup that blocks the platform thread. @@ -136,7 +155,7 @@ public InitResult call() { new Runnable() { @Override public void run() { - FlutterJNI.nativePrefetchDefaultFontManager(); + flutterJNI.prefetchDefaultFontManager(); } }); @@ -225,17 +244,34 @@ public void ensureInitializationComplete( shellArgs.add("--log-tag=" + settings.getLogTag()); } + ApplicationInfo applicationInfo = + applicationContext + .getPackageManager() + .getApplicationInfo( + applicationContext.getPackageName(), PackageManager.GET_META_DATA); + Bundle metaData = applicationInfo.metaData; + int oldGenHeapSizeMegaBytes = + metaData != null ? metaData.getInt(OLD_GEN_HEAP_SIZE_META_DATA_KEY) : 0; + if (oldGenHeapSizeMegaBytes == 0) { + // default to half of total memory. + ActivityManager activityManager = + (ActivityManager) applicationContext.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memInfo); + oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2); + } + + shellArgs.add("--old-gen-heap-size=" + oldGenHeapSizeMegaBytes); + long initTimeMillis = SystemClock.uptimeMillis() - initStartTimestampMillis; - if (FlutterInjector.instance().shouldLoadNative()) { - FlutterJNI.nativeInit( - applicationContext, - shellArgs.toArray(new String[0]), - kernelPath, - result.appStoragePath, - result.engineCachesPath, - initTimeMillis); - } + flutterJNI.init( + applicationContext, + shellArgs.toArray(new String[0]), + kernelPath, + result.appStoragePath, + result.engineCachesPath, + initTimeMillis); initialized = true; } catch (Exception e) { diff --git a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java index 225c24959e136..9809095a0746d 100644 --- a/shell/platform/android/test/io/flutter/FlutterInjectorTest.java +++ b/shell/platform/android/test/io/flutter/FlutterInjectorTest.java @@ -7,7 +7,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; import io.flutter.embedding.engine.loader.FlutterLoader; import org.junit.Before; @@ -35,7 +34,6 @@ public void itHasSomeReasonableDefaults() { // Implicitly builds when first accessed. FlutterInjector injector = FlutterInjector.instance(); assertNotNull(injector.flutterLoader()); - assertTrue(injector.shouldLoadNative()); } @Test @@ -44,7 +42,6 @@ public void canPartiallyOverride() { new FlutterInjector.Builder().setFlutterLoader(mockFlutterLoader).build()); FlutterInjector injector = FlutterInjector.instance(); assertEquals(injector.flutterLoader(), mockFlutterLoader); - assertTrue(injector.shouldLoadNative()); } @Test() diff --git a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java index 71f47890245bc..138e36c766f0c 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/PluginComponentTest.java @@ -32,11 +32,13 @@ public void setUp() { @Test public void pluginsCanAccessFlutterAssetPaths() { // Setup test. - FlutterInjector.setInstance(new FlutterInjector.Builder().setShouldLoadNative(false).build()); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterInjector.setInstance( + new FlutterInjector.Builder().setFlutterLoader(new FlutterLoader(mockFlutterJNI)).build()); FlutterJNI flutterJNI = mock(FlutterJNI.class); when(flutterJNI.isAttached()).thenReturn(true); - FlutterLoader flutterLoader = new FlutterLoader(); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); // Execute behavior under test. FlutterEngine flutterEngine = diff --git a/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java b/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java index 4ffd848d855b7..c7c6c3afd39ec 100644 --- a/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java +++ b/shell/platform/android/test/io/flutter/embedding/engine/loader/FlutterLoaderTest.java @@ -4,13 +4,25 @@ package io.flutter.embedding.engine.loader; +import static android.os.Looper.getMainLooper; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; -import io.flutter.FlutterInjector; -import org.junit.Before; +import android.app.ActivityManager; +import android.content.Context; +import io.flutter.embedding.engine.FlutterJNI; +import java.util.Arrays; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -18,10 +30,6 @@ @Config(manifest = Config.NONE) @RunWith(RobolectricTestRunner.class) public class FlutterLoaderTest { - @Before - public void setUp() { - FlutterInjector.reset(); - } @Test public void itReportsUninitializedAfterCreating() { @@ -31,13 +39,43 @@ public void itReportsUninitializedAfterCreating() { @Test public void itReportsInitializedAfterInitializing() { - FlutterInjector.setInstance(new FlutterInjector.Builder().setShouldLoadNative(false).build()); - FlutterLoader flutterLoader = new FlutterLoader(); + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); assertFalse(flutterLoader.initialized()); flutterLoader.startInitialization(RuntimeEnvironment.application); flutterLoader.ensureInitializationComplete(RuntimeEnvironment.application, null); + shadowOf(getMainLooper()).idle(); assertTrue(flutterLoader.initialized()); - FlutterInjector.reset(); + verify(mockFlutterJNI, times(1)).loadLibrary(); + } + + @Test + public void itDefaultsTheOldGenHeapSizeAppropriately() { + FlutterJNI mockFlutterJNI = mock(FlutterJNI.class); + FlutterLoader flutterLoader = new FlutterLoader(mockFlutterJNI); + + assertFalse(flutterLoader.initialized()); + flutterLoader.startInitialization(RuntimeEnvironment.application); + flutterLoader.ensureInitializationComplete(RuntimeEnvironment.application, null); + shadowOf(getMainLooper()).idle(); + + ActivityManager activityManager = + (ActivityManager) RuntimeEnvironment.application.getSystemService(Context.ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memInfo); + int oldGenHeapSizeMegaBytes = (int) (memInfo.totalMem / 1e6 / 2); + final String oldGenHeapArg = "--old-gen-heap-size=" + oldGenHeapSizeMegaBytes; + ArgumentCaptor shellArgsCaptor = ArgumentCaptor.forClass(String[].class); + verify(mockFlutterJNI, times(1)) + .init( + eq(RuntimeEnvironment.application), + shellArgsCaptor.capture(), + anyString(), + anyString(), + anyString(), + anyLong()); + List arguments = Arrays.asList(shellArgsCaptor.getValue()); + assertTrue(arguments.contains(oldGenHeapArg)); } }